diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..9b0a64ba --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [jendib] diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml new file mode 100644 index 00000000..2c38750a --- /dev/null +++ b/.github/workflows/build-deploy.yml @@ -0,0 +1,84 @@ +name: Maven CI/CD + +on: + push: + branches: [master] + tags: [v*] + workflow_dispatch: + +jobs: + build_and_publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: "11" + distribution: "temurin" + cache: maven + - name: Install test dependencies + run: sudo apt-get update && sudo apt-get -y -q --no-install-recommends install ffmpeg mediainfo tesseract-ocr tesseract-ocr-deu + - name: Build with Maven + run: mvn -Pprod clean install + - name: Upload war artifact + uses: actions/upload-artifact@v2 + with: + name: docs-web-ci.war + path: docs-web/target/docs*.war + + build_docker_image: + name: Publish to Docker Hub + runs-on: ubuntu-latest + needs: [build_and_publish] + + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Download war artifact + uses: actions/download-artifact@v2 + with: + name: docs-web-ci.war + path: docs-web/target + - + name: Setup up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Populate Docker metadata + id: metadata + uses: docker/metadata-action@v3 + with: + images: sismics/docs + flavor: | + latest=false + tags: | + type=ref,event=tag + type=raw,value=latest,enable=${{ github.ref_type != 'tag' }} + labels: | + org.opencontainers.image.title = Teedy + org.opencontainers.image.description = Teedy is an open source, lightweight document management system for individuals and businesses. + org.opencontainers.image.created = ${{ github.event_created_at }} + org.opencontainers.image.author = Sismics + org.opencontainers.image.url = https://teedy.io/ + org.opencontainers.image.vendor = Sismics + org.opencontainers.image.license = GPLv2 + org.opencontainers.image.version = ${{ github.event_head_commit.id }} + - + name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} diff --git a/.gitignore b/.gitignore index 06a670ab..ef52ccd0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,11 @@ *.iml node_modules import_test -docs-importer-linux -docs-importer-macos -docs-importer-win.exe \ No newline at end of file +teedy-importer-linux +teedy-importer-macos +teedy-importer-win.exe +docs/* +!docs/.gitkeep + +#macos +.DS_Store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9663b0db..00000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: required -dist: trusty -language: java -before_install: - - sudo add-apt-repository -y ppa:mc3man/trusty-media - - sudo apt-get -qq update - - sudo apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld tesseract-ocr-tur - - sudo apt-get -y -q install haveged && sudo service haveged start -after_success: - - | - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then - mvn -Pprod -DskipTests clean install - docker login -u $DOCKER_USER -p $DOCKER_PASS - export REPO=sismics/docs - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` - docker build -f Dockerfile -t $REPO:$COMMIT . - docker tag $REPO:$COMMIT $REPO:$TAG - docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER - docker push $REPO - fi -env: - global: - - secure: LRGpjWORb0qy6VuypZjTAfA8uRHlFUMTwb77cenS9PPRBxuSnctC531asS9Xg3DqC5nsRxBBprgfCKotn5S8nBSD1ceHh84NASyzLSBft3xSMbg7f/2i7MQ+pGVwLncusBU6E/drnMFwZBleo+9M8Tf96axY5zuUp90MUTpSgt0= - - secure: bCDDR6+I7PmSkuTYZv1HF/z98ANX/SFEESUCqxVmV5Gs0zFC0vQXaPJQ2xaJNRop1HZBFMZLeMMPleb0iOs985smpvK2F6Rbop9Tu+Vyo0uKqv9tbZ7F8Nfgnv9suHKZlL84FNeUQZJX6vsFIYPEJ/r7K5P/M0PdUy++fEwxEhU= - - secure: ewXnzbkgCIHpDWtaWGMa1OYZJ/ki99zcIl4jcDPIC0eB3njX/WgfcC6i0Ke9mLqDqwXarWJ6helm22sNh+xtQiz6isfBtBX+novfRt9AANrBe3koCMUemMDy7oh5VflBaFNP0DVb8LSCnwf6dx6ZB5E9EB8knvk40quc/cXpGjY= - - COMMIT=${TRAVIS_COMMIT::8} diff --git a/Dockerfile b/Dockerfile index d4dced9f..a6a1a2e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,48 @@ -FROM sismics/ubuntu-jetty:9.4.12-2 -MAINTAINER b.gamard@sismics.com +FROM sismics/ubuntu-jetty:11.0.14 +LABEL maintainer="b.gamard@sismics.com" -RUN apt-get update && apt-get -y -q install ffmpeg mediainfo tesseract-ocr tesseract-ocr-fra tesseract-ocr-ita tesseract-ocr-kor tesseract-ocr-rus tesseract-ocr-ukr tesseract-ocr-spa tesseract-ocr-ara tesseract-ocr-hin tesseract-ocr-deu tesseract-ocr-pol tesseract-ocr-jpn tesseract-ocr-por tesseract-ocr-tha tesseract-ocr-jpn tesseract-ocr-chi-sim tesseract-ocr-chi-tra tesseract-ocr-nld tesseract-ocr-tur && \ - apt-get clean && rm -rf /var/lib/apt/lists/* +RUN apt-get update && \ + apt-get -y -q --no-install-recommends install \ + ffmpeg \ + mediainfo \ + tesseract-ocr \ + tesseract-ocr-ara \ + tesseract-ocr-ces \ + tesseract-ocr-chi-sim \ + tesseract-ocr-chi-tra \ + tesseract-ocr-dan \ + tesseract-ocr-deu \ + tesseract-ocr-fin \ + tesseract-ocr-fra \ + tesseract-ocr-heb \ + tesseract-ocr-hin \ + tesseract-ocr-hun \ + tesseract-ocr-ita \ + tesseract-ocr-jpn \ + tesseract-ocr-kor \ + tesseract-ocr-lav \ + tesseract-ocr-nld \ + tesseract-ocr-nor \ + tesseract-ocr-pol \ + tesseract-ocr-por \ + tesseract-ocr-rus \ + tesseract-ocr-spa \ + tesseract-ocr-swe \ + tesseract-ocr-tha \ + tesseract-ocr-tur \ + tesseract-ocr-ukr \ + tesseract-ocr-vie \ + tesseract-ocr-sqi && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + mkdir /app && \ + cd /app && \ + java -jar /opt/jetty/start.jar --add-modules=server,http,webapp,deploy -# Remove the embedded javax.mail jar from Jetty -RUN rm -f /opt/jetty/lib/mail/javax.mail.glassfish-*.jar +ADD docs.xml /app/webapps/docs.xml +ADD docs-web/target/docs-web-*.war /app/webapps/docs.war + +ENV JAVA_OPTIONS -Xmx1g + +WORKDIR /app +CMD ["java", "-jar", "/opt/jetty/start.jar"] -ADD docs.xml /opt/jetty/webapps/docs.xml -ADD docs-web/target/docs-web-*.war /opt/jetty/webapps/docs.war diff --git a/README.md b/README.md index 9e455ae8..69461826 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,38 @@ Teedy -[![Twitter: @teedyio](https://img.shields.io/badge/contact-@teedyio-blue.svg?style=flat)](https://twitter.com/teedyio) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -[![Build Status](https://secure.travis-ci.org/sismics/docs.png)](http://travis-ci.org/sismics/docs) +[![Maven CI/CD](https://github.com/sismics/docs/actions/workflows/build-deploy.yml/badge.svg)](https://github.com/sismics/docs/actions/workflows/build-deploy.yml) Teedy is an open source, lightweight document management system for individuals and businesses. -**Discuss it on [Product Hunt](https://www.producthunt.com/posts/sismics-docs) 🦄** -

- ✨ Sismics Docs is now called Teedy! You can still find our cloud and support offer on teedy.io ✨ + ✨ Sponsor this project if you use and appreciate it!


![New!](https://teedy.io/img/laptop-demo.png?20180301) -Demo ----- +# Demo A demo is available at [demo.teedy.io](https://demo.teedy.io) + - Guest login is enabled with read access on all documents - "admin" login with "admin" password - "demo" login with "password" password -Features --------- +# Features - Responsive user interface - Optical character recognition +- LDAP authentication ![New!](https://www.sismics.com/public/img/new.png) - Support image, PDF, ODT, DOCX, PPTX files - Video file support - Flexible search engine with suggestions and highlighting - Full text search in all supported files - All [Dublin Core](http://dublincore.org/) metadata +- Custom user-defined metadata ![New!](https://www.sismics.com/public/img/new.png) - Workflow system ![New!](https://www.sismics.com/public/img/new.png) - 256-bit AES encryption of stored files - File versioning ![New!](https://www.sismics.com/public/img/new.png) @@ -55,86 +53,192 @@ Features - [Bulk files importer](https://github.com/sismics/docs/tree/master/docs-importer) (single or scan mode) - Tested to one million documents -Install with Docker -------------------- +# Install with Docker + +A preconfigured Docker image is available, including OCR and media conversion tools, listening on port 8080. If no PostgreSQL config is provided, the database is an embedded H2 database. The H2 embedded database should only be used for testing. For production usage use the provided PostgreSQL configuration (check the Docker Compose example) -From a Docker host, run this command to download and install Teedy. The server will run on . **The default admin password is "admin". Don't forget to change it before going to production.** - docker run --rm --name teedy_latest -d -e DOCS_BASE_URL='http://[your-docker-host-ip]:8100' -p 8100:8080 -v teedy_latest:/data sismics/docs:latest - **Note:** You will need to change [your-docker-host-ip] with the IP address or FQDN of your docker host e.g. - - FQDN: http://docs.mycompany.com - IP: http://192.168.100.10 +- Master branch, can be unstable. Not recommended for production use: `sismics/docs:latest` +- Latest stable version: `sismics/docs:v1.11` -Manual installation -------------------- +The data directory is `/data`. Don't forget to mount a volume on it. -#### Requirements -- Java 8 with the [Java Cryptography Extension](http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html) -- Tesseract 3 or 4 for OCR +To build external URL, the server is expecting a `DOCS_BASE_URL` environment variable (for example https://teedy.mycompany.com) + +## Available environment variables + +- General + - `DOCS_BASE_URL`: The base url used by the application. Generated url's will be using this as base. + - `DOCS_GLOBAL_QUOTA`: Defines the default quota applying to all users. + - `DOCS_BCRYPT_WORK`: Defines the work factor which is used for password hashing. The default is `10`. This value may be `4...31` including `4` and `31`. The specified value will be used for all new users and users changing their password. Be aware that setting this factor to high can heavily impact login and user creation performance. + +- Admin + - `DOCS_ADMIN_EMAIL_INIT`: Defines the e-mail-address the admin user should have upon initialization. + - `DOCS_ADMIN_PASSWORD_INIT`: Defines the password the admin user should have upon initialization. Needs to be a bcrypt hash. **Be aware that `$` within the hash have to be escaped with a second `$`.** + +- Database + - `DATABASE_URL`: The jdbc connection string to be used by `hibernate`. + - `DATABASE_USER`: The user which should be used for the database connection. + - `DATABASE_PASSWORD`: The password to be used for the database connection. + +- Language + - `DOCS_DEFAULT_LANGUAGE`: The language which will be used as default. Currently supported values are: + - `eng`, `fra`, `ita`, `deu`, `spa`, `por`, `pol`, `rus`, `ukr`, `ara`, `hin`, `chi_sim`, `chi_tra`, `jpn`, `tha`, `kor`, `nld`, `tur`, `heb`, `hun`, `fin`, `swe`, `lav`, `dan` + +- E-Mail + - `DOCS_SMTP_HOSTNAME`: Hostname of the SMTP-Server to be used by Teedy. + - `DOCS_SMTP_PORT`: The port which should be used. + - `DOCS_SMTP_USERNAME`: The username to be used. + - `DOCS_SMTP_PASSWORD`: The password to be used. + +## Examples + +In the following examples some passwords are exposed in cleartext. This was done in order to keep the examples simple. We strongly encourage you to use variables with an `.env` file or other means to securely store your passwords. + + +### Default, using PostgreSQL + +```yaml +version: '3' +services: +# Teedy Application + teedy-server: + image: sismics/docs:v1.11 + restart: unless-stopped + ports: + # Map internal port to host + - 8080:8080 + environment: + # Base url to be used + DOCS_BASE_URL: "https://docs.example.com" + # Set the admin email + DOCS_ADMIN_EMAIL_INIT: "admin@example.com" + # Set the admin password (in this example: "superSecure") + DOCS_ADMIN_PASSWORD_INIT: "$$2a$$05$$PcMNUbJvsk7QHFSfEIDaIOjk1VI9/E7IPjTKx.jkjPxkx2EOKSoPS" + # Setup the database connection. "teedy-db" is the hostname + # and "teedy" is the name of the database the application + # will connect to. + DATABASE_URL: "jdbc:postgresql://teedy-db:5432/teedy" + DATABASE_USER: "teedy_db_user" + DATABASE_PASSWORD: "teedy_db_password" + volumes: + - ./docs/data:/data + networks: + - docker-internal + - internet + depends_on: + - teedy-db + +# DB for Teedy + teedy-db: + image: postgres:13.1-alpine + restart: unless-stopped + expose: + - 5432 + environment: + POSTGRES_USER: "teedy_db_user" + POSTGRES_PASSWORD: "teedy_db_password" + POSTGRES_DB: "teedy" + volumes: + - ./docs/db:/var/lib/postgresql/data + networks: + - docker-internal + +networks: + # Network without internet access. The db does not need + # access to the host network. + docker-internal: + driver: bridge + internal: true + internet: + driver: bridge +``` + +### Using the internal database (only for testing) + +```yaml +version: '3' +services: +# Teedy Application + teedy-server: + image: sismics/docs:v1.11 + restart: unless-stopped + ports: + # Map internal port to host + - 8080:8080 + environment: + # Base url to be used + DOCS_BASE_URL: "https://docs.example.com" + # Set the admin email + DOCS_ADMIN_EMAIL_INIT: "admin@example.com" + # Set the admin password (in this example: "superSecure") + DOCS_ADMIN_PASSWORD_INIT: "$$2a$$05$$PcMNUbJvsk7QHFSfEIDaIOjk1VI9/E7IPjTKx.jkjPxkx2EOKSoPS" + volumes: + - ./docs/data:/data +``` + +# Manual installation + +## Requirements + +- Java 11 +- Tesseract 4 for OCR - ffmpeg for video thumbnails - mediainfo for video metadata extraction - A webapp server like [Jetty](http://eclipse.org/jetty/) or [Tomcat](http://tomcat.apache.org/) -#### Download +## Download + The latest release is downloadable here: in WAR format. **The default admin password is "admin". Don't forget to change it before going to production.** -How to build Teedy from the sources ----------------------------------- +## How to build Teedy from the sources -Prerequisites: JDK 8 with JCE, Maven 3, Tesseract 3 or 4 +Prerequisites: JDK 11, Maven 3, NPM, Grunt, Tesseract 4 Teedy is organized in several Maven modules: - - docs-core - - docs-web - - docs-web-common +- docs-core +- docs-web +- docs-web-common First off, clone the repository: `git clone git://github.com/sismics/docs.git` or download the sources from GitHub. -#### Launch the build +### Launch the build From the root directory: - mvn clean -DskipTests install +```console +mvn clean -DskipTests install +``` -#### Run a stand-alone version +### Run a stand-alone version From the `docs-web` directory: - mvn jetty:run +```console +mvn jetty:run +``` -#### Build a .war to deploy to your servlet container +### Build a .war to deploy to your servlet container From the `docs-web` directory: - mvn -Pprod -DskipTests clean install +```console +mvn -Pprod -DskipTests clean install +``` You will get your deployable WAR in the `docs-web/target` directory. -Contributing ------------- +# Contributing All contributions are more than welcomed. Contributions may close an issue, fix a bug (reported or not reported), improve the existing code, add new feature, and so on. The `master` branch is the default and base branch for the project. It is used for development and all Pull Requests should go there. - -Community ---------- - -Get updates on Teedy's development and chat with the project maintainers: - -- Follow [@teedyio on Twitter](https://twitter.com/teedyio) -- Read and subscribe to [The Official Teedy Blog](https://blog.teedy.io/) -- Check the [Official Website](https://teedy.io) -- Join us [on Facebook](https://www.facebook.com/teedyio) - -License -------- +# License Teedy is released under the terms of the GPL license. See `COPYING` for more information or see . diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..8315aa50 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' +services: +# Teedy Application + teedy-server: + image: sismics/docs:v1.10 + restart: unless-stopped + ports: + # Map internal port to host + - 8080:8080 + environment: + # Base url to be used + DOCS_BASE_URL: "https://docs.example.com" + # Set the admin email + DOCS_ADMIN_EMAIL_INIT: "admin@example.com" + # Set the admin password (in this example: "superSecure") + DOCS_ADMIN_PASSWORD_INIT: "$$2a$$05$$PcMNUbJvsk7QHFSfEIDaIOjk1VI9/E7IPjTKx.jkjPxkx2EOKSoPS" + volumes: + - ./docs/data:/data diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle index a6238e78..1e5664eb 100644 --- a/docs-android/app/build.gradle +++ b/docs-android/app/build.gradle @@ -4,7 +4,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.4.0' } } apply plugin: 'com.android.application' diff --git a/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java b/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java index 80797651..2f14a754 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java +++ b/docs-android/app/src/main/java/com/sismics/docs/adapter/LanguageAdapter.java @@ -34,6 +34,7 @@ public class LanguageAdapter extends BaseAdapter { languageList.add(new Language("fra", R.string.language_french, R.drawable.fra)); languageList.add(new Language("eng", R.string.language_english, R.drawable.eng)); languageList.add(new Language("deu", R.string.language_german, R.drawable.deu)); + languageList.add(new Language("pol", R.string.language_polish, R.drawable.pol)); } @Override diff --git a/docs-android/app/src/main/java/com/sismics/docs/util/SearchQueryBuilder.java b/docs-android/app/src/main/java/com/sismics/docs/util/SearchQueryBuilder.java index 42522174..107d289f 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/util/SearchQueryBuilder.java +++ b/docs-android/app/src/main/java/com/sismics/docs/util/SearchQueryBuilder.java @@ -39,7 +39,9 @@ public class SearchQueryBuilder { */ public SearchQueryBuilder simpleSearch(String simpleSearch) { if (isValid(simpleSearch)) { - query.append(SEARCH_SEPARATOR).append(simpleSearch); + query.append(SEARCH_SEPARATOR) + .append("simple:") + .append(simpleSearch); } return this; } diff --git a/docs-android/app/src/main/res/drawable-xhdpi/pol.png b/docs-android/app/src/main/res/drawable-xhdpi/pol.png new file mode 100644 index 00000000..60e45d1f Binary files /dev/null and b/docs-android/app/src/main/res/drawable-xhdpi/pol.png differ diff --git a/docs-android/app/src/main/res/values-pl/strings.xml b/docs-android/app/src/main/res/values-pl/strings.xml new file mode 100644 index 00000000..287caf7b --- /dev/null +++ b/docs-android/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,164 @@ + + + + + Nieprawidłowy email + Za krótki (min. %d) + Za długi (max. %d) + Wymagany + Tylko litery i cyfry + + + Teedy + Otwórz szufladę nawigacji + Zamknij szufladę nawigacji + github.com/sismics/docs i poniżej wprowadzić adres]]> + Serwer + Użytkownik + Hasło + Zaloguj + OK + Anuluj + Błąd logowania + Nieprawidłowa nazwa użytkownika lub hasło + Błąd sieci + Błąd sieci, sprawdź połączenie z interneterm oraz adres URL serwera + Nieprawidłowy adres URL + Sprawdź adres URL serwera i spróbuj ponownie + Wystąpiła awaria, wysłano raport w celu rozwiązania tego problemu + Data utworzenia + Pobierz bieżący plik + Pobierz + Znadź dokumenty + Wszystkie dokumenty + Udostępnione dokumenty + Wszystkie etykiety + Brak etykiet + Błąd ładowania etykiet + Brak dokumentów + Błąd ładowania dokumentów + Brak plików + Błąd ładowania plików + Nowy dokument + Udostępnij + Zamknij + Dodaj + Nazwa udostępnienia (opcjonalnie) + Ten dokument nie jest obecnie udostępniony + Usuń udostępnienie + Wyślij link udostępnienia + Błąd ładowania udostępnień + Błąd dodawania udostępnienia + Udostępnij link + Błąd usuwania udostępnienia + Wyślij link udostępnienia do + dodaj plik + Przeslij plik z + ustawienia + Wyloguj + Wersja + Kompilacja + Ustawienia zaawansowane + O programie + GitHub + Zgłoś błąd + Wyczyść cache + Wyczyść podręczne pliki + Cache wyczyszczony + Wyczyść historię wyszukiwania + Opróżnij ostatnie sugestie wyszukiwania + Historia wyszukiwania wyczyszczona + Rozmiar cache + Francuski + Angielski + Niemiecki + Polski + Zapisz + Edytuj + Błąd sieci, spróbuj ponownie + Proszę czekać + Wysyłam twoje dane + Usuń + Usuń dokument + Naprawdę chcesz usunąć dokument i powiązane z nim pliki? + Błąd sieci w czasie usuwania tego dokumentu + Usuwanie dokumentu + Usuń plik + Naprawdę chcesz usunąć ten plik? + Błąd sieci w czasie usuwania bieżącego pliku + Usuwanie pliku + Błąd podczas odczytu pliku + Teedy + Przesyłanie nowego pliku do dokumentu + Błąd przsyłania nowego pliku + Usuń bieżący plik + Zaawansowane wyszukiwanie + Znajdź + Dodaj eytkiety + Data utworzenia + Opis + Tytuł + Proste wyszukiwanie + Wyszukiwanie pełnotekstowe + Autor + Po dacie + Przed datą + Znajdź etykiety + Wszystkie języki + Przełącz informacje + Kto ma dostęp + Komentarze + Brak komentarzy + Błąd ładowania komentarzy + Wyślij + Dodaj komentarz + Błąd dodawania komentarza + Dodawanie komentarza + Usuń komentarz + Usuwanie komentarza + Błąd usuwania komentarza + PDF + Pobierz + Margines + Dostosuj obraz do strony + Eksport komentarzy + Eksport metadanych + mm + Eksport plików Teedy + Eksport dokumentu Teedy + Eksport Teedy jako PDF + Ostatnie aktywności + Aktywności + E-mail + Limit magazynu + %1$d/%2$d MB + Kod weryfikujący + Udostępnienie + Język + Zakres + Rodzaj + Źródło + Format + Udostępniający + Identifikator + temat + Prawa + Współtwórcy + Powiązania + + + ACL + Komentarz + Dokument + Plik + Grupa + Przepływ + Model przepływu + Etykieta + Użytkownik + Webhook + utworzony + zaktualizowany + usunięty + + diff --git a/docs-android/app/src/main/res/values/strings.xml b/docs-android/app/src/main/res/values/strings.xml index 65dd4d83..3b7e5403 100644 --- a/docs-android/app/src/main/res/values/strings.xml +++ b/docs-android/app/src/main/res/values/strings.xml @@ -72,6 +72,7 @@ Français English Deutsch + Polski Save Edit Network error, please try again diff --git a/docs-android/gradle/wrapper/gradle-wrapper.properties b/docs-android/gradle/wrapper/gradle-wrapper.properties index b0546596..b3b5f4b7 100644 --- a/docs-android/gradle/wrapper/gradle-wrapper.properties +++ b/docs-android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jan 30 16:31:31 CET 2019 +#Tue May 07 11:49:13 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/docs-core/pom.xml b/docs-core/pom.xml index ac17fcb4..4decd254 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -5,8 +5,8 @@ com.sismics.docs docs-parent - 1.6-SNAPSHOT - .. + 1.12-SNAPSHOT + ../pom.xml 4.0.0 @@ -18,17 +18,7 @@ org.hibernate - hibernate-core - - - - org.hibernate - hibernate-entitymanager - - - - org.hibernate - hibernate-c3p0 + hibernate-core-jakarta @@ -48,8 +38,8 @@ - commons-lang - commons-lang + org.apache.commons + commons-lang3 @@ -63,8 +53,8 @@ - org.glassfish - javax.json + jakarta.json + jakarta.json-api @@ -91,10 +81,10 @@ org.slf4j jcl-over-slf4j - + - org.mindrot - jbcrypt + at.favre.lib + bcrypt @@ -122,16 +112,16 @@ lucene-highlighter - - com.sun.mail - javax.mail - - com.squareup.okhttp3 okhttp - + + + org.apache.directory.api + api-all + + org.apache.lucene @@ -189,7 +179,7 @@ org.postgresql postgresql - + junit diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java index e4e41db5..bf51a703 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/ConfigType.java @@ -40,7 +40,25 @@ public enum ConfigType { INBOX_ENABLED, INBOX_HOSTNAME, INBOX_PORT, + INBOX_STARTTLS, INBOX_USERNAME, INBOX_PASSWORD, - INBOX_TAG + INBOX_FOLDER, + INBOX_TAG, + INBOX_AUTOMATIC_TAGS, + INBOX_DELETE_IMPORTED, + + /** + * LDAP connection. + */ + LDAP_ENABLED, + LDAP_HOST, + LDAP_PORT, + LDAP_USESSL, + LDAP_ADMIN_DN, + LDAP_ADMIN_PASSWORD, + LDAP_BASE_DN, + LDAP_FILTER, + LDAP_DEFAULT_EMAIL, + LDAP_DEFAULT_STORAGE } diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index 5c70635b..4121573a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -18,13 +18,18 @@ public class Constants { /** * Administrator's default password ("admin"). */ - public static final String DEFAULT_ADMIN_PASSWORD = "$2a$05$6Ny3TjrW3aVAL1or2SlcR.fhuDgPKp5jp.P9fBXwVNePgeLqb4i3C"; + public static final String DEFAULT_ADMIN_PASSWORD = "$2y$10$xg0EEKVUehutDI1m6qQhVeFz7SMQMl1jQzjf2KkVsR2c7aV2vyyjK"; /** * Administrator's default email. */ public static final String DEFAULT_ADMIN_EMAIL = "admin@localhost"; + /** + * Bcrypt default work factor + */ + public static final int DEFAULT_BCRYPT_WORK = 10; + /** * Guest user ID. */ @@ -38,7 +43,7 @@ public class Constants { /** * Supported document languages. */ - public static final List SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur"); + public static final List SUPPORTED_LANGUAGES = Lists.newArrayList("eng", "fra", "ita", "deu", "spa", "por", "pol", "rus", "ukr", "ara", "hin", "chi_sim", "chi_tra", "jpn", "tha", "kor", "nld", "tur", "heb", "hun", "fin", "swe", "lav", "dan", "nor", "vie", "ces", "sqi"); /** * Base URL environment variable. @@ -73,6 +78,11 @@ public class Constants { */ public static final String ADMIN_EMAIL_INIT_ENV = "DOCS_ADMIN_EMAIL_INIT"; + /** + * Work factor to be used by Bcrypt + */ + public static final String BCRYPT_WORK_ENV = "DOCS_BCRYPT_WORK"; + /** * Expiration time of the password recovery in hours. */ diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/MetadataType.java b/docs-core/src/main/java/com/sismics/docs/core/constant/MetadataType.java new file mode 100644 index 00000000..5a7ef12e --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/MetadataType.java @@ -0,0 +1,14 @@ +package com.sismics.docs.core.constant; + +/** + * Metadata type. + * + * @author bgamard + */ +public enum MetadataType { + STRING, + INTEGER, + FLOAT, + DATE, + BOOLEAN +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java index 328bf1ac..ae44bc1f 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AclDao.java @@ -10,8 +10,8 @@ import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.SecurityUtil; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -128,6 +128,9 @@ public class AclDao { if (SecurityUtil.skipAclCheck(targetIdList)) { return true; } + if (targetIdList.isEmpty()) { + return false; + } EntityManager em = ThreadLocalContext.get().getEntityManager(); StringBuilder sb = new StringBuilder("select a.ACL_ID_C from T_ACL a "); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java index cb3d54e9..955883bd 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AuditLogDao.java @@ -12,7 +12,7 @@ import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.sql.Timestamp; import java.util.*; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/AuthenticationTokenDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/AuthenticationTokenDao.java index 8455e6a8..27bc5533 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/AuthenticationTokenDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/AuthenticationTokenDao.java @@ -4,8 +4,8 @@ import com.sismics.docs.core.model.jpa.AuthenticationToken; import com.sismics.util.context.ThreadLocalContext; import org.joda.time.DateTime; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.Date; import java.util.List; import java.util.UUID; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java index 42309b7d..9bb5fc3c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/CommentDao.java @@ -6,9 +6,9 @@ import com.sismics.docs.core.model.jpa.Comment; import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; @@ -27,7 +27,6 @@ public class CommentDao { * @param comment Comment * @param userId User ID * @return New ID - * @throws Exception */ public String create(Comment comment, String userId) { // Create the UUID @@ -99,7 +98,7 @@ public class CommentDao { @SuppressWarnings("unchecked") List l = q.getResultList(); - List commentDtoList = new ArrayList(); + List commentDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; CommentDto commentDto = new CommentDto(); @@ -107,7 +106,7 @@ public class CommentDao { commentDto.setContent((String) o[i++]); commentDto.setCreateTimestamp(((Timestamp) o[i++]).getTime()); commentDto.setCreatorName((String) o[i++]); - commentDto.setCreatorEmail((String) o[i++]); + commentDto.setCreatorEmail((String) o[i]); commentDtoList.add(commentDto); } return commentDtoList; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/ConfigDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/ConfigDao.java index 3db3b7d7..1e744679 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/ConfigDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/ConfigDao.java @@ -4,8 +4,8 @@ import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.model.jpa.Config; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; /** * Configuration parameter DAO. diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/ContributorDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/ContributorDao.java index 22e392a7..a6cd6d83 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/ContributorDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/ContributorDao.java @@ -4,8 +4,8 @@ import com.sismics.docs.core.dao.dto.ContributorDto; import com.sismics.docs.core.model.jpa.Contributor; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -56,7 +56,7 @@ public class ContributorDao { @SuppressWarnings("unchecked") public List getByDocumentId(String documentId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - StringBuilder sb = new StringBuilder("select u.USE_USERNAME_C, u.USE_EMAIL_C from T_CONTRIBUTOR c "); + StringBuilder sb = new StringBuilder("select distinct u.USE_USERNAME_C, u.USE_EMAIL_C from T_CONTRIBUTOR c "); sb.append(" join T_USER u on u.USE_ID_C = c.CTR_IDUSER_C "); sb.append(" where c.CTR_IDDOC_C = :documentId "); Query q = em.createNativeQuery(sb.toString()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java index 35c30a51..dd28032a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentDao.java @@ -7,9 +7,10 @@ import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; import java.sql.Timestamp; import java.util.Date; import java.util.List; @@ -50,10 +51,9 @@ public class DocumentDao { * @param limit Limit * @return List of documents */ - @SuppressWarnings("unchecked") public List findAll(int offset, int limit) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select d from Document d where d.deleteDate is null"); + TypedQuery q = em.createQuery("select d from Document d where d.deleteDate is null", Document.class); q.setFirstResult(offset); q.setMaxResults(limit); return q.getResultList(); @@ -65,10 +65,9 @@ public class DocumentDao { * @param userId User ID * @return List of documents */ - @SuppressWarnings("unchecked") public List findByUserId(String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select d from Document d where d.userId = :userId and d.deleteDate is null"); + TypedQuery q = em.createQuery("select d from Document d where d.userId = :userId and d.deleteDate is null", Document.class); q.setParameter("userId", userId); return q.getResultList(); } @@ -138,16 +137,16 @@ public class DocumentDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Get the document - Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null"); - q.setParameter("id", id); - Document documentDb = (Document) q.getSingleResult(); + TypedQuery dq = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null", Document.class); + dq.setParameter("id", id); + Document documentDb = dq.getSingleResult(); // Delete the document Date dateNow = new Date(); documentDb.setDeleteDate(dateNow); // Delete linked data - q = em.createQuery("update File f set f.deleteDate = :dateNow where f.documentId = :documentId and f.deleteDate is null"); + Query q = em.createQuery("update File f set f.deleteDate = :dateNow where f.documentId = :documentId and f.deleteDate is null"); q.setParameter("documentId", id); q.setParameter("dateNow", dateNow); q.executeUpdate(); @@ -179,10 +178,10 @@ public class DocumentDao { */ public Document getById(String id) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null"); + TypedQuery q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null", Document.class); q.setParameter("id", id); try { - return (Document) q.getSingleResult(); + return q.getSingleResult(); } catch (NoResultException e) { return null; } @@ -199,9 +198,9 @@ public class DocumentDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Get the document - Query q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null"); + TypedQuery q = em.createQuery("select d from Document d where d.id = :id and d.deleteDate is null", Document.class); q.setParameter("id", document.getId()); - Document documentDb = (Document) q.getSingleResult(); + Document documentDb = q.getSingleResult(); // Update the document documentDb.setTitle(document.getTitle()); @@ -232,12 +231,11 @@ public class DocumentDao { */ public void updateFileId(Document document) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query query = em.createNativeQuery("update T_DOCUMENT d set d.DOC_IDFILE_C = :fileId, d.DOC_UPDATEDATE_D = :updateDate where d.DOC_ID_C = :id"); + Query query = em.createNativeQuery("update T_DOCUMENT d set DOC_IDFILE_C = :fileId, DOC_UPDATEDATE_D = :updateDate where d.DOC_ID_C = :id"); query.setParameter("updateDate", new Date()); query.setParameter("fileId", document.getFileId()); query.setParameter("id", document.getId()); query.executeUpdate(); - } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java new file mode 100644 index 00000000..9736efaa --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/DocumentMetadataDao.java @@ -0,0 +1,89 @@ +package com.sismics.docs.core.dao; + +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.dto.DocumentMetadataDto; +import com.sismics.docs.core.model.jpa.DocumentMetadata; +import com.sismics.util.context.ThreadLocalContext; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Document metadata DAO. + * + * @author bgamard + */ +public class DocumentMetadataDao { + /** + * Creates a new document metadata. + * + * @param documentMetadata Document metadata + * @return New ID + */ + public String create(DocumentMetadata documentMetadata) { + // Create the UUID + documentMetadata.setId(UUID.randomUUID().toString()); + + // Create the document metadata + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.persist(documentMetadata); + + return documentMetadata.getId(); + } + + /** + * Updates a document metadata. + * + * @param documentMetadata Document metadata + * @return Updated document metadata + */ + public DocumentMetadata update(DocumentMetadata documentMetadata) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the document metadata + Query q = em.createQuery("select u from DocumentMetadata u where u.id = :id"); + q.setParameter("id", documentMetadata.getId()); + DocumentMetadata documentMetadataDb = (DocumentMetadata) q.getSingleResult(); + + // Update the document metadata + documentMetadataDb.setValue(documentMetadata.getValue()); + + return documentMetadata; + } + + /** + * Returns the list of all metadata values on a document. + * + * @param documentId Document ID + * @return List of metadata + */ + @SuppressWarnings("unchecked") + public List getByDocumentId(String documentId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + StringBuilder sb = new StringBuilder("select dm.DME_ID_C, dm.DME_IDDOCUMENT_C, dm.DME_IDMETADATA_C, dm.DME_VALUE_C, m.MET_TYPE_C"); + sb.append(" from T_DOCUMENT_METADATA dm, T_METADATA m "); + sb.append(" where dm.DME_IDMETADATA_C = m.MET_ID_C and dm.DME_IDDOCUMENT_C = :documentId and m.MET_DELETEDATE_D is null"); + + // Perform the search + Query q = em.createNativeQuery(sb.toString()); + q.setParameter("documentId", documentId); + List l = q.getResultList(); + + // Assemble results + List dtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + DocumentMetadataDto dto = new DocumentMetadataDto(); + dto.setId((String) o[i++]); + dto.setDocumentId((String) o[i++]); + dto.setMetadataId((String) o[i++]); + dto.setValue((String) o[i++]); + dto.setType(MetadataType.valueOf((String) o[i])); + dtoList.add(dto); + } + return dtoList; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java index 473511c9..b66fbaf8 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/FileDao.java @@ -4,12 +4,16 @@ import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.util.context.ThreadLocalContext; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; +import jakarta.persistence.TypedQuery; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; /** @@ -47,10 +51,9 @@ public class FileDao { * @param limit Limit * @return List of files */ - @SuppressWarnings("unchecked") public List findAll(int offset, int limit) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.deleteDate is null", File.class); q.setFirstResult(offset); q.setMaxResults(limit); return q.getResultList(); @@ -62,28 +65,38 @@ public class FileDao { * @param userId User ID * @return List of files */ - @SuppressWarnings("unchecked") public List findByUserId(String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.userId = :userId and f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.userId = :userId and f.deleteDate is null", File.class); q.setParameter("userId", userId); return q.getResultList(); } + + /** + * Returns a list of active files. + * + * @param ids Files IDs + * @return List of files + */ + public List getFiles(List ids) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + TypedQuery q = em.createQuery("select f from File f where f.id in :ids and f.deleteDate is null", File.class); + q.setParameter("ids", ids); + return q.getResultList(); + } /** - * Returns an active file. + * Returns an active file or null. * * @param id File ID - * @return Document + * @return File */ public File getFile(String id) { - EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); - q.setParameter("id", id); - try { - return (File) q.getSingleResult(); - } catch (NoResultException e) { + List files = getFiles(List.of(id)); + if (files.isEmpty()) { return null; + } else { + return files.get(0); } } @@ -92,15 +105,15 @@ public class FileDao { * * @param id File ID * @param userId User ID - * @return Document + * @return File */ public File getFile(String id, String userId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.id = :id and f.userId = :userId and f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.id = :id and f.userId = :userId and f.deleteDate is null", File.class); q.setParameter("id", id); q.setParameter("userId", userId); try { - return (File) q.getSingleResult(); + return q.getSingleResult(); } catch (NoResultException e) { return null; } @@ -116,9 +129,9 @@ public class FileDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Get the file - Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null", File.class); q.setParameter("id", id); - File fileDb = (File) q.getSingleResult(); + File fileDb = q.getSingleResult(); // Delete the file Date dateNow = new Date(); @@ -138,9 +151,9 @@ public class FileDao { EntityManager em = ThreadLocalContext.get().getEntityManager(); // Get the file - Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null", File.class); q.setParameter("id", file.getId()); - File fileDb = (File) q.getSingleResult(); + File fileDb = q.getSingleResult(); // Update the file fileDb.setDocumentId(file.getDocumentId()); @@ -150,10 +163,11 @@ public class FileDao { fileDb.setMimeType(file.getMimeType()); fileDb.setVersionId(file.getVersionId()); fileDb.setLatestVersion(file.isLatestVersion()); + fileDb.setSize(file.getSize()); return file; } - + /** * Gets a file by its ID. * @@ -162,46 +176,82 @@ public class FileDao { */ public File getActiveById(String id) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); + TypedQuery q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null", File.class); q.setParameter("id", id); try { - return (File) q.getSingleResult(); + return q.getSingleResult(); } catch (NoResultException e) { return null; } } /** - * Get files by document ID or all orphan files of an user. + * Get files by document ID or all orphan files of a user. * * @param userId User ID * @param documentId Document ID * @return List of files */ - @SuppressWarnings("unchecked") public List getByDocumentId(String userId, String documentId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); if (documentId == null) { - Query q = em.createQuery("select f from File f where f.documentId is null and f.deleteDate is null and f.latestVersion = true and f.userId = :userId order by f.createDate asc"); + TypedQuery q = em.createQuery("select f from File f where f.documentId is null and f.deleteDate is null and f.latestVersion = true and f.userId = :userId order by f.createDate asc", File.class); q.setParameter("userId", userId); return q.getResultList(); + } else { + return getByDocumentsIds(Collections.singleton(documentId)); } - Query q = em.createQuery("select f from File f where f.documentId = :documentId and f.latestVersion = true and f.deleteDate is null order by f.order asc"); - q.setParameter("documentId", documentId); + } + + /** + * Get files by documents IDs. + * + * @param documentIds Documents IDs + * @return List of files + */ + public List getByDocumentsIds(Iterable documentIds) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + TypedQuery q = em.createQuery("select f from File f where f.documentId in :documentIds and f.latestVersion = true and f.deleteDate is null order by f.order asc", File.class); + q.setParameter("documentIds", documentIds); return q.getResultList(); } + /** + * Get files count by documents IDs. + * + * @param documentIds Documents IDs + * @return the number of files per document id + */ + public Map countByDocumentsIds(Iterable documentIds) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + Query q = em.createQuery("select f.documentId, count(*) from File f where f.documentId in :documentIds and f.latestVersion = true and f.deleteDate is null group by (f.documentId)"); + q.setParameter("documentIds", documentIds); + Map result = new HashMap<>(); + q.getResultList().forEach(o -> { + Object[] resultLine = (Object[]) o; + result.put((String) resultLine[0], (Long) resultLine[1]); + }); + return result; + } + /** * Get all files from a version. * * @param versionId Version ID * @return List of files */ - @SuppressWarnings("unchecked") public List getByVersionId(String versionId) { EntityManager em = ThreadLocalContext.get().getEntityManager(); - Query q = em.createQuery("select f from File f where f.versionId = :versionId and f.deleteDate is null order by f.order asc"); + TypedQuery q = em.createQuery("select f from File f where f.versionId = :versionId and f.deleteDate is null order by f.order asc", File.class); q.setParameter("versionId", versionId); return q.getResultList(); } + + public List getFilesWithUnknownSize(int limit) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + TypedQuery q = em.createQuery("select f from File f where f.size = :size and f.deleteDate is null order by f.order asc", File.class); + q.setParameter("size", File.UNKNOWN_SIZE); + q.setMaxResults(limit); + return q.getResultList(); + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/GroupDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/GroupDao.java index 2647a5c7..5f67d78b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/GroupDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/GroupDao.java @@ -12,9 +12,9 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.util.*; /** @@ -183,12 +183,10 @@ public class GroupDao { } criteriaList.add("g.GRP_DELETEDATE_D is null"); - - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } - + + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); @SuppressWarnings("unchecked") diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/MetadataDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/MetadataDao.java new file mode 100644 index 00000000..2867e2ad --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/MetadataDao.java @@ -0,0 +1,146 @@ +package com.sismics.docs.core.dao; + +import com.google.common.base.Joiner; +import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.criteria.MetadataCriteria; +import com.sismics.docs.core.dao.dto.MetadataDto; +import com.sismics.docs.core.model.jpa.Metadata; +import com.sismics.docs.core.util.AuditLogUtil; +import com.sismics.docs.core.util.jpa.QueryParam; +import com.sismics.docs.core.util.jpa.QueryUtil; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.util.context.ThreadLocalContext; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; +import java.util.*; + +/** + * Metadata DAO. + * + * @author bgamard + */ +public class MetadataDao { + /** + * Creates a new metdata. + * + * @param metadata Metadata + * @param userId User ID + * @return New ID + */ + public String create(Metadata metadata, String userId) { + // Create the UUID + metadata.setId(UUID.randomUUID().toString()); + + // Create the metadata + EntityManager em = ThreadLocalContext.get().getEntityManager(); + em.persist(metadata); + + // Create audit log + AuditLogUtil.create(metadata, AuditLogType.CREATE, userId); + + return metadata.getId(); + } + + /** + * Update a metadata. + * + * @param metadata Metadata to update + * @param userId User ID + * @return Updated metadata + */ + public Metadata update(Metadata metadata, String userId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the metadata + Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null"); + q.setParameter("id", metadata.getId()); + Metadata metadataDb = (Metadata) q.getSingleResult(); + + // Update the metadata + metadataDb.setName(metadata.getName()); + + // Create audit log + AuditLogUtil.create(metadataDb, AuditLogType.UPDATE, userId); + + return metadataDb; + } + + /** + * Gets an active metadata by its ID. + * + * @param id Metadata ID + * @return Metadata + */ + public Metadata getActiveById(String id) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + try { + Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null"); + q.setParameter("id", id); + return (Metadata) q.getSingleResult(); + } catch (NoResultException e) { + return null; + } + } + + /** + * Deletes a metadata. + * + * @param id Metadata ID + * @param userId User ID + */ + public void delete(String id, String userId) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the metadata + Query q = em.createQuery("select r from Metadata r where r.id = :id and r.deleteDate is null"); + q.setParameter("id", id); + Metadata metadataDb = (Metadata) q.getSingleResult(); + + // Delete the metadata + Date dateNow = new Date(); + metadataDb.setDeleteDate(dateNow); + + // Create audit log + AuditLogUtil.create(metadataDb, AuditLogType.DELETE, userId); + } + + /** + * Returns the list of all metadata. + * + * @param criteria Search criteria + * @param sortCriteria Sort criteria + * @return List of metadata + */ + public List findByCriteria(MetadataCriteria criteria, SortCriteria sortCriteria) { + Map parameterMap = new HashMap<>(); + List criteriaList = new ArrayList<>(); + + StringBuilder sb = new StringBuilder("select m.MET_ID_C c0, m.MET_NAME_C c1, m.MET_TYPE_C c2"); + sb.append(" from T_METADATA m "); + + criteriaList.add("m.MET_DELETEDATE_D is null"); + + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + + // Perform the search + QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); + @SuppressWarnings("unchecked") + List l = QueryUtil.getNativeQuery(queryParam).getResultList(); + + // Assemble results + List dtoList = new ArrayList<>(); + for (Object[] o : l) { + int i = 0; + MetadataDto dto = new MetadataDto(); + dto.setId((String) o[i++]); + dto.setName((String) o[i++]); + dto.setType(MetadataType.valueOf((String) o[i])); + dtoList.add(dto); + } + return dtoList; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/PasswordRecoveryDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/PasswordRecoveryDao.java index ba94bf6e..71213343 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/PasswordRecoveryDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/PasswordRecoveryDao.java @@ -6,9 +6,9 @@ import com.sismics.util.context.ThreadLocalContext; import org.joda.time.DateTime; import org.joda.time.DurationFieldType; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.util.Date; import java.util.UUID; diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java index 11e8163a..11194630 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RelationDao.java @@ -4,8 +4,8 @@ import com.sismics.docs.core.dao.dto.RelationDto; import com.sismics.docs.core.model.jpa.Relation; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.*; /** @@ -36,13 +36,13 @@ public class RelationDao { List l = q.getResultList(); // Assemble results - List relationDtoList = new ArrayList(); + List relationDtoList = new ArrayList<>(); for (Object[] o : l) { int i = 0; RelationDto relationDto = new RelationDto(); relationDto.setId((String) o[i++]); relationDto.setTitle((String) o[i++]); - String fromDocId = (String) o[i++]; + String fromDocId = (String) o[i]; relationDto.setSource(documentId.equals(fromDocId)); relationDtoList.add(relationDto); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RoleBaseFunctionDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RoleBaseFunctionDao.java index 718779e5..32536900 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RoleBaseFunctionDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RoleBaseFunctionDao.java @@ -3,8 +3,8 @@ package com.sismics.docs.core.dao; import com.google.common.collect.Sets; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.Set; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteDao.java index f9feaa50..3cadd1c6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteDao.java @@ -11,7 +11,7 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.sql.Timestamp; import java.util.*; @@ -64,10 +64,8 @@ public class RouteDao { } criteriaList.add("r.RTE_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java index 63952aaa..dac826f0 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteModelDao.java @@ -4,7 +4,6 @@ import com.google.common.base.Joiner; import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.dao.criteria.RouteModelCriteria; import com.sismics.docs.core.dao.dto.RouteModelDto; -import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.SecurityUtil; @@ -13,9 +12,9 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.sql.Timestamp; import java.util.*; @@ -62,7 +61,7 @@ public class RouteModelDao { q.setParameter("id", routeModel.getId()); RouteModel routeModelDb = (RouteModel) q.getSingleResult(); - // Update the group + // Update the route model routeModelDb.setName(routeModel.getName()); routeModelDb.setSteps(routeModel.getSteps()); @@ -146,10 +145,8 @@ public class RouteModelDao { criteriaList.add("rm.RTM_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteStepDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteStepDao.java index 81e66596..acca0d37 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/RouteStepDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/RouteStepDao.java @@ -12,8 +12,8 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.sql.Timestamp; import java.util.*; @@ -90,10 +90,8 @@ public class RouteStepDao { } criteriaList.add("rs.RTP_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/ShareDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/ShareDao.java index a5ba66ba..de5dbee7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/ShareDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/ShareDao.java @@ -3,8 +3,8 @@ package com.sismics.docs.core.dao; import com.sismics.docs.core.model.jpa.Share; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import java.util.Date; import java.util.UUID; @@ -19,7 +19,6 @@ public class ShareDao { * * @param share Share * @return New ID - * @throws Exception */ public String create(Share share) { // Create the UUID diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java index 7a51071e..62869a85 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/TagDao.java @@ -13,9 +13,9 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.util.*; /** @@ -199,10 +199,8 @@ public class TagDao { criteriaList.add("t.TAG_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java index 2b10f59c..1107a8b9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/UserDao.java @@ -1,7 +1,14 @@ package com.sismics.docs.core.dao; import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import at.favre.lib.crypto.bcrypt.BCrypt; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.sismics.docs.core.constant.AuditLogType; +import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.criteria.UserCriteria; import com.sismics.docs.core.dao.dto.UserDto; import com.sismics.docs.core.model.jpa.User; @@ -11,12 +18,10 @@ import com.sismics.docs.core.util.jpa.QueryParam; import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import org.joda.time.DateTime; -import org.mindrot.jbcrypt.BCrypt; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.sql.Timestamp; import java.util.*; @@ -26,6 +31,11 @@ import java.util.*; * @author jtremeaux */ public class UserDao { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(UserDao.class); + /** * Authenticates an user. * @@ -39,7 +49,8 @@ public class UserDao { q.setParameter("username", username); try { User user = (User) q.getSingleResult(); - if (!BCrypt.checkpw(password, user.getPassword()) || user.getDisableDate() != null) { + BCrypt.Result result = BCrypt.verifyer().verify(password.toCharArray(), user.getPassword()); + if (!result.verified || user.getDisableDate() != null) { return null; } return user; @@ -277,7 +288,21 @@ public class UserDao { * @return Hashed password */ private String hashPassword(String password) { - return BCrypt.hashpw(password, BCrypt.gensalt()); + int bcryptWork = Constants.DEFAULT_BCRYPT_WORK; + String envBcryptWork = System.getenv(Constants.BCRYPT_WORK_ENV); + if (!Strings.isNullOrEmpty(envBcryptWork)) { + try { + int envBcryptWorkInt = Integer.parseInt(envBcryptWork); + if (envBcryptWorkInt >= 4 && envBcryptWorkInt <= 31) { + bcryptWork = envBcryptWorkInt; + } else { + log.warn(Constants.BCRYPT_WORK_ENV + " needs to be in range 4...31. Falling back to " + Constants.DEFAULT_BCRYPT_WORK + "."); + } + } catch (NumberFormatException e) { + log.warn(Constants.BCRYPT_WORK_ENV + " needs to be a number in range 4...31. Falling back to " + Constants.DEFAULT_BCRYPT_WORK + "."); + } + } + return BCrypt.withDefaults().hashToString(bcryptWork, password.toCharArray()); } /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/VocabularyDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/VocabularyDao.java index e467eb04..1cb54e3c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/VocabularyDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/VocabularyDao.java @@ -3,9 +3,9 @@ package com.sismics.docs.core.dao; import com.sismics.docs.core.model.jpa.Vocabulary; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.util.List; import java.util.UUID; @@ -20,7 +20,6 @@ public class VocabularyDao { * * @param vocabulary Vocabulary * @return New ID - * @throws Exception */ public String create(Vocabulary vocabulary) { // Create the UUID diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java index cfd16ccb..9f28266c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/WebhookDao.java @@ -9,9 +9,9 @@ import com.sismics.docs.core.util.jpa.QueryUtil; import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.Query; import java.sql.Timestamp; import java.util.*; @@ -42,11 +42,9 @@ public class WebhookDao { } criteriaList.add("w.WHK_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } - + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); + // Perform the search QueryParam queryParam = QueryUtil.getSortedQueryParam(new QueryParam(sb.toString(), parameterMap), sortCriteria); @SuppressWarnings("unchecked") diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java index 4e8ace08..8d69f381 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/DocumentCriteria.java @@ -1,5 +1,6 @@ package com.sismics.docs.core.dao.criteria; +import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -49,13 +50,13 @@ public class DocumentCriteria { * Tag IDs. * The first level list will be AND'ed and the second level list will be OR'ed. */ - private List> tagIdList; + private List> tagIdList = new ArrayList<>(); /** - * Tag IDs to excluded. + * Tag IDs to exclude. * The first and second level list will be excluded. */ - private List> excludedTagIdList; + private List> excludedTagIdList = new ArrayList<>(); /** * Shared status. @@ -76,7 +77,17 @@ public class DocumentCriteria { * A route is active. */ private Boolean activeRoute; - + + /** + * MIME type of a file. + */ + private String mimeType; + + /** + * Titles to include. + */ + private List titleList = new ArrayList<>(); + public List getTargetIdList() { return targetIdList; } @@ -121,19 +132,10 @@ public class DocumentCriteria { return tagIdList; } - public void setTagIdList(List> tagIdList) { - this.tagIdList = tagIdList; - } - public List> getExcludedTagIdList() { return excludedTagIdList; } - public DocumentCriteria setExcludedTagIdList(List> excludedTagIdList) { - this.excludedTagIdList = excludedTagIdList; - return this; - } - public Boolean getShared() { return shared; } @@ -157,11 +159,7 @@ public class DocumentCriteria { public void setCreatorId(String creatorId) { this.creatorId = creatorId; } - - public Boolean getActiveRoute() { - return activeRoute; - } - + public Date getUpdateDateMin() { return updateDateMin; } @@ -178,7 +176,23 @@ public class DocumentCriteria { this.updateDateMax = updateDateMax; } + public Boolean getActiveRoute() { + return activeRoute; + } + public void setActiveRoute(Boolean activeRoute) { this.activeRoute = activeRoute; } + + public String getMimeType() { + return mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public List getTitleList() { + return titleList; + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/MetadataCriteria.java b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/MetadataCriteria.java new file mode 100644 index 00000000..5717df09 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/criteria/MetadataCriteria.java @@ -0,0 +1,9 @@ +package com.sismics.docs.core.dao.criteria; + +/** + * Metadata criteria. + * + * @author bgamard + */ +public class MetadataCriteria { +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java new file mode 100644 index 00000000..accfdd4c --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/DocumentMetadataDto.java @@ -0,0 +1,94 @@ +package com.sismics.docs.core.dao.dto; + +import com.sismics.docs.core.constant.MetadataType; + +/** + * Document metadata DTO. + * + * @author bgamard + */ +public class DocumentMetadataDto { + /** + * Document metadata ID. + */ + private String id; + + /** + * Document ID. + */ + private String documentId; + + /** + * Metadata ID. + */ + private String metadataId; + + /** + * Name. + */ + private String name; + + /** + * Value. + */ + private String value; + + /** + * Type. + */ + private MetadataType type; + + public String getId() { + return id; + } + + public DocumentMetadataDto setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public DocumentMetadataDto setName(String name) { + this.name = name; + return this; + } + + public MetadataType getType() { + return type; + } + + public DocumentMetadataDto setType(MetadataType type) { + this.type = type; + return this; + } + + public String getDocumentId() { + return documentId; + } + + public DocumentMetadataDto setDocumentId(String documentId) { + this.documentId = documentId; + return this; + } + + public String getMetadataId() { + return metadataId; + } + + public DocumentMetadataDto setMetadataId(String metadataId) { + this.metadataId = metadataId; + return this; + } + + public String getValue() { + return value; + } + + public DocumentMetadataDto setValue(String value) { + this.value = value; + return this; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/MetadataDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/MetadataDto.java new file mode 100644 index 00000000..b5cefaa1 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/MetadataDto.java @@ -0,0 +1,52 @@ +package com.sismics.docs.core.dao.dto; + +import com.sismics.docs.core.constant.MetadataType; + +/** + * Metadata DTO. + * + * @author bgamard + */ +public class MetadataDto { + /** + * Metadata ID. + */ + private String id; + + /** + * Name. + */ + private String name; + + /** + * Type. + */ + private MetadataType type; + + public String getId() { + return id; + } + + public MetadataDto setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public MetadataDto setName(String name) { + this.name = name; + return this; + } + + public MetadataType getType() { + return type; + } + + public MetadataDto setType(MetadataType type) { + this.type = type; + return this; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/RouteStepDto.java b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/RouteStepDto.java index 09de1be9..0f4763f9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/dto/RouteStepDto.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/dto/RouteStepDto.java @@ -3,8 +3,8 @@ package com.sismics.docs.core.dao.dto; import com.sismics.docs.core.constant.RouteStepType; import com.sismics.util.JsonUtil; -import javax.json.Json; -import javax.json.JsonObjectBuilder; +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; /** * Route step DTO. diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/DocumentCreatedAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/DocumentCreatedAsyncEvent.java index f43575ff..e5394bbb 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/DocumentCreatedAsyncEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/DocumentCreatedAsyncEvent.java @@ -1,7 +1,6 @@ package com.sismics.docs.core.event; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.model.jpa.Document; /** * Document created event. @@ -10,32 +9,22 @@ import com.sismics.docs.core.model.jpa.Document; */ public class DocumentCreatedAsyncEvent extends UserEvent { /** - * Created document. + * Document ID. */ - private Document document; - - /** - * Getter of document. - * - * @return the document - */ - public Document getDocument() { - return document; + private String documentId; + + public String getDocumentId() { + return documentId; } - /** - * Setter of document. - * - * @param document document - */ - public void setDocument(Document document) { - this.document = document; + public void setDocumentId(String documentId) { + this.documentId = documentId; } @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("document", document) - .toString(); + .add("documentId", documentId) + .toString(); } } \ No newline at end of file diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/FileDeletedAsyncEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/FileDeletedAsyncEvent.java index 05835ae5..34cf0cd0 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/FileDeletedAsyncEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/FileDeletedAsyncEvent.java @@ -1,7 +1,6 @@ package com.sismics.docs.core.event; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.model.jpa.File; /** * File deleted event. @@ -10,22 +9,33 @@ import com.sismics.docs.core.model.jpa.File; */ public class FileDeletedAsyncEvent extends UserEvent { /** - * Deleted file. + * File ID. */ - private File file; - - public File getFile() { - return file; + private String fileId; + + private Long fileSize; + + public String getFileId() { + return fileId; } - public void setFile(File file) { - this.file = file; + public void setFileId(String fileId) { + this.fileId = fileId; } - + + public Long getFileSize() { + return fileSize; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("file", file) + .add("fileId", fileId) + .add("fileSize", fileSize) .toString(); } -} \ No newline at end of file +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/event/FileEvent.java b/docs-core/src/main/java/com/sismics/docs/core/event/FileEvent.java index 9337e84b..08caf296 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/event/FileEvent.java +++ b/docs-core/src/main/java/com/sismics/docs/core/event/FileEvent.java @@ -1,7 +1,6 @@ package com.sismics.docs.core.event; import com.google.common.base.MoreObjects; -import com.sismics.docs.core.model.jpa.File; import java.nio.file.Path; @@ -12,9 +11,9 @@ import java.nio.file.Path; */ public abstract class FileEvent extends UserEvent { /** - * Created file. + * File ID. */ - private File file; + private String fileId; /** * Language of the file. @@ -25,15 +24,15 @@ public abstract class FileEvent extends UserEvent { * Unencrypted original file. */ private Path unencryptedFile; - - public File getFile() { - return file; + + public String getFileId() { + return fileId; } - public void setFile(File file) { - this.file = file; + public void setFileId(String fileId) { + this.fileId = fileId; } - + public String getLanguage() { return language; } @@ -54,7 +53,7 @@ public abstract class FileEvent extends UserEvent { @Override public String toString() { return MoreObjects.toStringHelper(this) - .add("file", file) + .add("fileId", fileId) .add("language", language) .toString(); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java index 036d7ece..003eaa9e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/DocumentCreatedAsyncListener.java @@ -3,9 +3,11 @@ package com.sismics.docs.core.listener.async; import com.google.common.eventbus.AllowConcurrentEvents; import com.google.common.eventbus.Subscribe; import com.sismics.docs.core.dao.ContributorDao; +import com.sismics.docs.core.dao.DocumentDao; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.Contributor; +import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.util.TransactionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,15 +36,22 @@ public class DocumentCreatedAsyncListener { } TransactionUtil.handle(() -> { + // Fetch a fresh document + Document document = new DocumentDao().getById(event.getDocumentId()); + if (document == null) { + // The document has been deleted since + return; + } + // Add the first contributor (the creator of the document) ContributorDao contributorDao = new ContributorDao(); Contributor contributor = new Contributor(); - contributor.setDocumentId(event.getDocument().getId()); + contributor.setDocumentId(event.getDocumentId()); contributor.setUserId(event.getUserId()); contributorDao.create(contributor); // Update index - AppContext.getInstance().getIndexingHandler().createDocument(event.getDocument()); + AppContext.getInstance().getIndexingHandler().createDocument(document); }); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java index d1897c8b..3c6fea3a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListener.java @@ -2,9 +2,11 @@ package com.sismics.docs.core.listener.async; import com.google.common.eventbus.AllowConcurrentEvents; import com.google.common.eventbus.Subscribe; +import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.event.FileDeletedAsyncEvent; import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.util.FileUtil; import com.sismics.docs.core.util.TransactionUtil; import org.slf4j.Logger; @@ -12,7 +14,7 @@ import org.slf4j.LoggerFactory; /** * Listener on file deleted. - * + * * @author bgamard */ public class FileDeletedAsyncListener { @@ -23,7 +25,7 @@ public class FileDeletedAsyncListener { /** * File deleted. - * + * * @param event File deleted event * @throws Exception e */ @@ -33,14 +35,31 @@ public class FileDeletedAsyncListener { if (log.isInfoEnabled()) { log.info("File deleted event: " + event.toString()); } + TransactionUtil.handle(() -> { + // Update the user quota + UserDao userDao = new UserDao(); + User user = userDao.getById(event.getUserId()); + if (user != null) { + Long fileSize = event.getFileSize(); + + if (fileSize.equals(File.UNKNOWN_SIZE)) { + // The file size was not in the database, in this case we need to get from the unencrypted size. + fileSize = FileUtil.getFileSize(event.getFileId(), user); + } + + if (! fileSize.equals(File.UNKNOWN_SIZE)) { + user.setStorageCurrent(user.getStorageCurrent() - fileSize); + userDao.updateQuota(user); + } + } + }); // Delete the file from storage - File file = event.getFile(); - FileUtil.delete(file); + FileUtil.delete(event.getFileId()); TransactionUtil.handle(() -> { // Update index - AppContext.getInstance().getIndexingHandler().deleteDocument(file.getId()); + AppContext.getInstance().getIndexingHandler().deleteDocument(event.getFileId()); }); } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java index cdcb20a8..fcddddc6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/FileProcessingAsyncListener.java @@ -28,6 +28,7 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicReference; /** * Listener on file processing. @@ -52,15 +53,7 @@ public class FileProcessingAsyncListener { log.info("File created event: " + event.toString()); } - TransactionUtil.handle(() -> { - // Generate thumbnail, extract content - processFile(event); - - // Update index - AppContext.getInstance().getIndexingHandler().createFile(event.getFile()); - }); - - FileUtil.endProcessingFile(event.getFile().getId()); + processFile(event, true); } /** @@ -71,43 +64,84 @@ public class FileProcessingAsyncListener { @Subscribe @AllowConcurrentEvents public void on(final FileUpdatedAsyncEvent event) { - if (log.isInfoEnabled()) { - log.info("File updated event: " + event.toString()); - } + log.info("File updated event: " + event.toString()); - TransactionUtil.handle(() -> { - // Generate thumbnail, extract content - processFile(event); - - // Update index - AppContext.getInstance().getIndexingHandler().updateFile(event.getFile()); - }); - - FileUtil.endProcessingFile(event.getFile().getId()); + processFile(event, false); } /** - * Process the file (create/update). + * Process a file : + * Generate thumbnails + * Extract and save text content * * @param event File event + * @param isFileCreated True if the file was just created */ - private void processFile(FileEvent event) { + private void processFile(FileEvent event, boolean isFileCreated) { + AtomicReference file = new AtomicReference<>(); + AtomicReference user = new AtomicReference<>(); + + // Open a first transaction to get what we need to start the processing + TransactionUtil.handle(() -> { + // Generate thumbnail, extract content + file.set(new FileDao().getActiveById(event.getFileId())); + if (file.get() == null) { + // The file has been deleted since + return; + } + + // Get the creating user from the database for its private key + UserDao userDao = new UserDao(); + user.set(userDao.getById(file.get().getUserId())); + }); + + // Process the file outside of a transaction + if (user.get() == null || file.get() == null) { + // The user or file has been deleted + FileUtil.endProcessingFile(event.getFileId()); + return; + } + String content = extractContent(event, user.get(), file.get()); + + // Open a new transaction to save the file content + TransactionUtil.handle(() -> { + // Save the file to database + FileDao fileDao = new FileDao(); + File freshFile = fileDao.getActiveById(event.getFileId()); + if (freshFile == null) { + // The file has been deleted since the text extraction started, ignore the result + return; + } + + freshFile.setContent(content); + fileDao.update(freshFile); + + // Update index with the updated file + if (isFileCreated) { + AppContext.getInstance().getIndexingHandler().createFile(freshFile); + } else { + AppContext.getInstance().getIndexingHandler().updateFile(freshFile); + } + }); + + FileUtil.endProcessingFile(event.getFileId()); + } + + /** + * Extract text content from a file. + * This is executed outside of a transaction. + * + * @param event File event + * @param user User whom created the file + * @param file Fresh file + * @return Text content + */ + private String extractContent(FileEvent event, User user, File file) { // Find a format handler - final File file = event.getFile(); FormatHandler formatHandler = FormatHandlerUtil.find(file.getMimeType()); if (formatHandler == null) { log.info("Format unhandled: " + file.getMimeType()); - FileUtil.endProcessingFile(file.getId()); - return; - } - - // Get the user from the database - UserDao userDao = new UserDao(); - User user = userDao.getById(event.getUserId()); - if (user == null) { - // The user has been deleted meanwhile - FileUtil.endProcessingFile(file.getId()); - return; + return null; } // Generate file variations @@ -132,28 +166,21 @@ public class FileProcessingAsyncListener { ImageUtil.writeJpeg(thumbnail, outputStream); } } - } catch (Exception e) { - log.error("Unable to generate thumbnails", e); + } catch (Throwable e) { + log.error("Unable to generate thumbnails for: " + file, e); } // Extract text content from the file long startTime = System.currentTimeMillis(); String content = null; + log.info("Start extracting content from: " + file); try { content = formatHandler.extractContent(event.getLanguage(), event.getUnencryptedFile()); - } catch (Exception e) { - log.error("Error extracting content from: " + event.getFile(), e); + } catch (Throwable e) { + log.error("Error extracting content from: " + file, e); } - log.info(MessageFormat.format("File content extracted in {0}ms", System.currentTimeMillis() - startTime)); + log.info(MessageFormat.format("File content extracted in {0}ms: " + file.getId(), System.currentTimeMillis() - startTime)); - // Save the file to database - FileDao fileDao = new FileDao(); - if (fileDao.getActiveById(file.getId()) == null) { - // The file has been deleted since the text extraction started, ignore the result - return; - } - - file.setContent(content); - fileDao.update(file); + return content; } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java b/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java index 5ad81ae1..f45f6278 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java +++ b/docs-core/src/main/java/com/sismics/docs/core/listener/async/WebhookAsyncListener.java @@ -36,7 +36,7 @@ public class WebhookAsyncListener { @Subscribe @AllowConcurrentEvents public void on(final DocumentCreatedAsyncEvent event) { - triggerWebhook(WebhookEvent.DOCUMENT_CREATED, event.getDocument().getId()); + triggerWebhook(WebhookEvent.DOCUMENT_CREATED, event.getDocumentId()); } @Subscribe @@ -54,19 +54,19 @@ public class WebhookAsyncListener { @Subscribe @AllowConcurrentEvents public void on(final FileCreatedAsyncEvent event) { - triggerWebhook(WebhookEvent.FILE_CREATED, event.getFile().getId()); + triggerWebhook(WebhookEvent.FILE_CREATED, event.getFileId()); } @Subscribe @AllowConcurrentEvents public void on(final FileUpdatedAsyncEvent event) { - triggerWebhook(WebhookEvent.FILE_UPDATED, event.getFile().getId()); + triggerWebhook(WebhookEvent.FILE_UPDATED, event.getFileId()); } @Subscribe @AllowConcurrentEvents public void on(final FileDeletedAsyncEvent event) { - triggerWebhook(WebhookEvent.FILE_DELETED, event.getFile().getId()); + triggerWebhook(WebhookEvent.FILE_DELETED, event.getFileId()); } /** @@ -86,7 +86,7 @@ public class WebhookAsyncListener { } }); - RequestBody body = RequestBody.create(JSON, "{\"event\": \"" + event.name() + "\", \"id\": \"" + id + "\"}"); + RequestBody body = RequestBody.create("{\"event\": \"" + event.name() + "\", \"id\": \"" + id + "\"}", JSON); for (String webhookUrl : webhookUrlList) { Request request = new Request.Builder() diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java index 0b191b82..bc277038 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/context/AppContext.java @@ -1,14 +1,15 @@ package com.sismics.docs.core.model.context; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.EventBus; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.UserDao; -import com.sismics.docs.core.event.RebuildIndexAsyncEvent; import com.sismics.docs.core.listener.async.*; import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.service.FileService; +import com.sismics.docs.core.service.FileSizeService; import com.sismics.docs.core.service.InboxService; import com.sismics.docs.core.util.PdfUtil; import com.sismics.docs.core.util.indexing.IndexingHandler; @@ -65,6 +66,11 @@ public class AppContext { */ private FileService fileService; + /** + * File size service. + */ + private FileSizeService fileSizeService; + /** * Asynchronous executors. */ @@ -81,7 +87,7 @@ public class AppContext { List> indexingHandlerList = Lists.newArrayList( new ClasspathScanner().findClasses(IndexingHandler.class, "com.sismics.docs.core.util.indexing")); for (Class handlerClass : indexingHandlerList) { - IndexingHandler handler = handlerClass.newInstance(); + IndexingHandler handler = handlerClass.getDeclaredConstructor().newInstance(); if (handler.accept()) { indexingHandler = handler; break; @@ -102,12 +108,17 @@ public class AppContext { inboxService.startAsync(); inboxService.awaitRunning(); + // Start file size service + fileSizeService = new FileSizeService(); + fileSizeService.startAsync(); + fileSizeService.awaitRunning(); + // Register fonts PdfUtil.registerFonts(); // Change the admin password if needed String envAdminPassword = System.getenv(Constants.ADMIN_PASSWORD_INIT_ENV); - if (envAdminPassword != null) { + if (!Strings.isNullOrEmpty(envAdminPassword)) { UserDao userDao = new UserDao(); User adminUser = userDao.getById("admin"); if (Constants.DEFAULT_ADMIN_PASSWORD.equals(adminUser.getPassword())) { @@ -118,7 +129,7 @@ public class AppContext { // Change the admin email if needed String envAdminEmail = System.getenv(Constants.ADMIN_EMAIL_INIT_ENV); - if (envAdminEmail != null) { + if (!Strings.isNullOrEmpty(envAdminEmail)) { UserDao userDao = new UserDao(); User adminUser = userDao.getById("admin"); if (Constants.DEFAULT_ADMIN_EMAIL.equals(adminUser.getEmail())) { @@ -172,7 +183,8 @@ public class AppContext { if (EnvironmentUtil.isUnitTest()) { return new EventBus(); } else { - ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, + int threadCount = Math.max(Runtime.getRuntime().availableProcessors() / 2, 2); + ThreadPoolExecutor executor = new ThreadPoolExecutor(threadCount, threadCount, 1L, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); asyncExecutorList.add(executor); @@ -237,6 +249,10 @@ public class AppContext { fileService.stopAsync(); } + if (fileSizeService != null) { + fileSizeService.stopAsync(); + } + instance = null; } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java index ba5baeec..7c0055d7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Acl.java @@ -4,7 +4,7 @@ import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.AclType; import com.sismics.docs.core.constant.PermType; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java index 3b831891..95a4f181 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuditLog.java @@ -2,12 +2,12 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.AuditLogType; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java index 3b592bf2..8052bc92 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/AuthenticationToken.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/BaseFunction.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/BaseFunction.java index 530d9fe8..7ca303a8 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/BaseFunction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/BaseFunction.java @@ -1,9 +1,9 @@ package com.sismics.docs.core.model.jpa; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java index 574eedc7..a34c97da 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Comment.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Config.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Config.java index 65c196d6..a4ef6a56 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Config.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Config.java @@ -1,11 +1,11 @@ package com.sismics.docs.core.model.jpa; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.ConfigType; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java index b583a7b2..20ca1525 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Contributor.java @@ -1,9 +1,9 @@ package com.sismics.docs.core.model.jpa; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java index 9fc76692..2bed3e14 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Document.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java new file mode 100644 index 00000000..120935e9 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentMetadata.java @@ -0,0 +1,91 @@ +package com.sismics.docs.core.model.jpa; + +import com.google.common.base.MoreObjects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serializable; + +/** + * Link between a document and a metadata, holding the value. + * + * @author bgamard + */ +@Entity +@Table(name = "T_DOCUMENT_METADATA") +public class DocumentMetadata implements Serializable { + /** + * Serial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Document metadata ID. + */ + @Id + @Column(name = "DME_ID_C", length = 36) + private String id; + + /** + * Document ID. + */ + @Column(name = "DME_IDDOCUMENT_C", nullable = false, length = 36) + private String documentId; + + /** + * Metadata ID. + */ + @Column(name = "DME_IDMETADATA_C", nullable = false, length = 36) + private String metadataId; + + /** + * Value. + */ + @Column(name = "DME_VALUE_C", length = 4000) + private String value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDocumentId() { + return documentId; + } + + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + public String getMetadataId() { + return metadataId; + } + + public DocumentMetadata setMetadataId(String metadataId) { + this.metadataId = metadataId; + return this; + } + + public String getValue() { + return value; + } + + public DocumentMetadata setValue(String value) { + this.value = value; + return this; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("documentId", documentId) + .add("metadataId", metadataId) + .toString(); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java index bacc47df..7eeff3cb 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/DocumentTag.java @@ -3,10 +3,10 @@ package com.sismics.docs.core.model.jpa; import java.io.Serializable; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java index c3e7064a..c67f85e1 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/File.java @@ -4,7 +4,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.sismics.util.mime.MimeTypeUtil; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; /** @@ -49,7 +49,6 @@ public class File implements Loggable { /** * OCR-ized content. */ - @Lob @Column(name = "FIL_CONTENT_C") private String content; @@ -89,6 +88,14 @@ public class File implements Loggable { @Column(name = "FIL_LATESTVERSION_B", nullable = false) private boolean latestVersion; + public static final Long UNKNOWN_SIZE = -1L; + + /** + * Can be {@link File#UNKNOWN_SIZE} if the size has not been stored in the database when the file has been uploaded + */ + @Column(name = "FIL_SIZE_N", nullable = false) + private Long size; + /** * Private key to decrypt the file. * Not saved to database, of course. @@ -205,6 +212,18 @@ public class File implements Loggable { return this; } + /** + * Can return {@link File#UNKNOWN_SIZE} if the file size is not stored in the database. + */ + public Long getSize() { + return size; + } + + public File setSize(Long size) { + this.size = size; + return this; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Group.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Group.java index 56c7fdea..4c516f5e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Group.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Group.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Metadata.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Metadata.java new file mode 100644 index 00000000..b06fd331 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Metadata.java @@ -0,0 +1,92 @@ +package com.sismics.docs.core.model.jpa; + +import com.google.common.base.MoreObjects; +import com.sismics.docs.core.constant.MetadataType; + +import jakarta.persistence.*; +import java.util.Date; + +/** + * Metadata entity. + * + * @author bgamard + */ +@Entity +@Table(name = "T_METADATA") +public class Metadata implements Loggable { + /** + * Metadata ID. + */ + @Id + @Column(name = "MET_ID_C", length = 36) + private String id; + + /** + * Name. + */ + @Column(name = "MET_NAME_C", length = 50, nullable = false) + private String name; + + /** + * Type. + */ + @Column(name = "MET_TYPE_C", length = 20, nullable = false) + @Enumerated(EnumType.STRING) + private MetadataType type; + + /** + * Deletion date. + */ + @Column(name = "MET_DELETEDATE_D") + private Date deleteDate; + + public String getId() { + return id; + } + + public Metadata setId(String id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public Metadata setName(String name) { + this.name = name; + return this; + } + + public MetadataType getType() { + return type; + } + + public Metadata setType(MetadataType type) { + this.type = type; + return this; + } + + @Override + public Date getDeleteDate() { + return deleteDate; + } + + public void setDeleteDate(Date deleteDate) { + this.deleteDate = deleteDate; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("name", name) + .add("type", type) + .toString(); + } + + @Override + public String toMessage() { + return name; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java index 6306c807..ec337f45 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/PasswordRecovery.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Relation.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Relation.java index 9eeba90f..0ff25dbc 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Relation.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Relation.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Role.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Role.java index 287401d6..bff4bbd9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Role.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Role.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RoleBaseFunction.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RoleBaseFunction.java index d4600244..5c793212 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RoleBaseFunction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RoleBaseFunction.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java index e597a077..e7173126 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Route.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteModel.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteModel.java index 89295739..e714ffe2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteModel.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteModel.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java index f591fe34..fd31643e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/RouteStep.java @@ -4,7 +4,7 @@ import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.RouteStepTransition; import com.sismics.docs.core.constant.RouteStepType; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java index f27b6a65..e5813b22 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Share.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java index 82a2f2cd..dcfb7be2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Tag.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java index a7a7c382..0c48c91c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/User.java @@ -2,10 +2,10 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/UserGroup.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/UserGroup.java index 3ae2592a..d0af30de 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/UserGroup.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/UserGroup.java @@ -3,10 +3,10 @@ package com.sismics.docs.core.model.jpa; import java.io.Serializable; import java.util.Date; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Vocabulary.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Vocabulary.java index 8e484a28..1639c57b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Vocabulary.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Vocabulary.java @@ -1,9 +1,9 @@ package com.sismics.docs.core.model.jpa; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import com.google.common.base.MoreObjects; diff --git a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java index a1db95d2..00ae9610 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java +++ b/docs-core/src/main/java/com/sismics/docs/core/model/jpa/Webhook.java @@ -3,7 +3,7 @@ package com.sismics.docs.core.model.jpa; import com.google.common.base.MoreObjects; import com.sismics.docs.core.constant.WebhookEvent; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/service/FileService.java b/docs-core/src/main/java/com/sismics/docs/core/service/FileService.java index 5b9aa402..ad295ef7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/service/FileService.java +++ b/docs-core/src/main/java/com/sismics/docs/core/service/FileService.java @@ -69,13 +69,18 @@ public class FileService extends AbstractScheduledService { return Scheduler.newFixedDelaySchedule(0, 5, TimeUnit.SECONDS); } + public Path createTemporaryFile() throws IOException { + return createTemporaryFile(null); + } + /** * Create a temporary file. * + * @param name Wanted file name * @return New temporary file */ - public Path createTemporaryFile() throws IOException { - Path path = Files.createTempFile("sismics_docs", null); + public Path createTemporaryFile(String name) throws IOException { + Path path = Files.createTempFile("sismics_docs", name); referenceSet.add(new TemporaryPathReference(path, referenceQueue)); return path; } @@ -85,7 +90,7 @@ public class FileService extends AbstractScheduledService { * * @author bgamard */ - class TemporaryPathReference extends PhantomReference { + static class TemporaryPathReference extends PhantomReference { String path; TemporaryPathReference(Path referent, ReferenceQueue q) { super(referent, q); diff --git a/docs-core/src/main/java/com/sismics/docs/core/service/FileSizeService.java b/docs-core/src/main/java/com/sismics/docs/core/service/FileSizeService.java new file mode 100644 index 00000000..21cafc63 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/service/FileSizeService.java @@ -0,0 +1,78 @@ +package com.sismics.docs.core.service; + +import com.google.common.util.concurrent.AbstractScheduledService; +import com.sismics.docs.core.dao.FileDao; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.FileUtil; +import com.sismics.docs.core.util.TransactionUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Service that retrieve files sizes when they are not in the database. + */ +public class FileSizeService extends AbstractScheduledService { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(FileSizeService.class); + + public FileSizeService() { + } + + @Override + protected void startUp() { + log.info("File size service starting up"); + } + + @Override + protected void shutDown() { + log.info("File size service shutting down"); + } + + private static final int BATCH_SIZE = 30; + + @Override + protected void runOneIteration() { + try { + TransactionUtil.handle(() -> { + FileDao fileDao = new FileDao(); + List files = fileDao.getFilesWithUnknownSize(BATCH_SIZE); + for(File file : files) { + processFile(file); + } + if(files.size() < BATCH_SIZE) { + log.info("No more file to process, stopping the service"); + stopAsync(); + } + }); + } catch (Throwable e) { + log.error("Exception during file service iteration", e); + } + } + + void processFile(File file) { + UserDao userDao = new UserDao(); + User user = userDao.getById(file.getUserId()); + if(user == null) { + return; + } + + long fileSize = FileUtil.getFileSize(file.getId(), user); + if(fileSize != File.UNKNOWN_SIZE){ + FileDao fileDao = new FileDao(); + file.setSize(fileSize); + fileDao.update(file); + } + } + + @Override + protected Scheduler scheduler() { + return Scheduler.newFixedDelaySchedule(0, 1, TimeUnit.MINUTES); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java index ae0b7500..d3bcdf5b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java +++ b/docs-core/src/main/java/com/sismics/docs/core/service/InboxService.java @@ -1,9 +1,10 @@ package com.sismics.docs.core.service; -import com.google.common.collect.Sets; import com.google.common.util.concurrent.AbstractScheduledService; import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.dao.TagDao; +import com.sismics.docs.core.dao.criteria.TagCriteria; +import com.sismics.docs.core.dao.dto.TagDto; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.Tag; @@ -11,17 +12,19 @@ import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.DocumentUtil; import com.sismics.docs.core.util.FileUtil; import com.sismics.docs.core.util.TransactionUtil; +import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.util.EmailUtil; import com.sismics.util.context.ThreadLocalContext; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.mail.*; import javax.mail.search.FlagTerm; -import java.util.Date; -import java.util.Properties; +import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Inbox scanning service. @@ -79,22 +82,25 @@ public class InboxService extends AbstractScheduledService { lastSyncDate = new Date(); lastSyncMessageCount = 0; try { + Map tagsNameToId = getAllTags(); + inbox = openInbox(); Message[] messages = inbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)); log.info(messages.length + " messages found"); for (Message message : messages) { - importMessage(message); + importMessage(message, tagsNameToId); lastSyncMessageCount++; } } catch (FolderClosedException e) { // Ignore this, we will just continue importing on the next cycle } catch (Exception e) { - log.error("Error synching the inbox", e); + log.error("Error syncing the inbox", e); lastSyncError = e.getMessage(); } finally { try { if (inbox != null) { - inbox.close(false); + // The parameter controls if the messages flagged to be deleted, should actually get deleted. + inbox.close(ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_DELETE_IMPORTED)); inbox.getStore().close(); } } catch (Exception e) { @@ -150,6 +156,7 @@ public class InboxService extends AbstractScheduledService { String port = ConfigUtil.getConfigStringValue(ConfigType.INBOX_PORT); properties.put("mail.imap.host", ConfigUtil.getConfigStringValue(ConfigType.INBOX_HOSTNAME)); properties.put("mail.imap.port", port); + properties.setProperty("mail.imap.starttls.enable", ConfigUtil.getConfigStringValue(ConfigType.INBOX_STARTTLS).toString()); boolean isSsl = "993".equals(port); properties.put("mail.imap.ssl.enable", String.valueOf(isSsl)); properties.setProperty("mail.imap.socketFactory.class", @@ -172,7 +179,7 @@ public class InboxService extends AbstractScheduledService { store.connect(ConfigUtil.getConfigStringValue(ConfigType.INBOX_USERNAME), ConfigUtil.getConfigStringValue(ConfigType.INBOX_PASSWORD)); - Folder inbox = store.getFolder("INBOX"); + Folder inbox = store.getFolder(ConfigUtil.getConfigStringValue(ConfigType.INBOX_FOLDER)); inbox.open(Folder.READ_WRITE); return inbox; } @@ -183,7 +190,7 @@ public class InboxService extends AbstractScheduledService { * @param message Message * @throws Exception e */ - private void importMessage(Message message) throws Exception { + private void importMessage(Message message, Map tags) throws Exception { log.info("Importing message: " + message.getSubject()); // Parse the mail @@ -194,12 +201,27 @@ public class InboxService extends AbstractScheduledService { // Create the document Document document = new Document(); - document.setUserId("admin"); - if (mailContent.getSubject() == null) { - document.setTitle("Imported email from EML file"); - } else { - document.setTitle(StringUtils.abbreviate(mailContent.getSubject(), 100)); + String subject = mailContent.getSubject(); + if (subject == null) { + subject = "Imported email from EML file"; } + + HashSet tagsFound = new HashSet<>(); + if (tags != null) { + Pattern pattern = Pattern.compile("#([^\\s:#]+)"); + Matcher matcher = pattern.matcher(subject); + while (matcher.find()) { + if (tags.containsKey(matcher.group(1)) && tags.get(matcher.group(1)) != null) { + tagsFound.add(tags.get(matcher.group(1))); + subject = subject.replaceFirst("#" + matcher.group(1), ""); + } + } + log.debug("Tags found: " + String.join(", ", tagsFound)); + subject = subject.trim().replaceAll(" +", " "); + } + + document.setUserId("admin"); + document.setTitle(StringUtils.abbreviate(subject, 100)); document.setDescription(StringUtils.abbreviate(mailContent.getMessage(), 4000)); document.setSubject(StringUtils.abbreviate(mailContent.getSubject(), 500)); document.setFormat("EML"); @@ -220,14 +242,19 @@ public class InboxService extends AbstractScheduledService { TagDao tagDao = new TagDao(); Tag tag = tagDao.getById(tagId); if (tag != null) { - tagDao.updateTagList(document.getId(), Sets.newHashSet(tagId)); + tagsFound.add(tagId); } } + // Update tags + if (!tagsFound.isEmpty()) { + new TagDao().updateTagList(document.getId(), tagsFound); + } + // Raise a document created event DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent(); documentCreatedAsyncEvent.setUserId("admin"); - documentCreatedAsyncEvent.setDocument(document); + documentCreatedAsyncEvent.setDocumentId(document.getId()); ThreadLocalContext.get().addAsyncEvent(documentCreatedAsyncEvent); // Add files to the document @@ -235,6 +262,29 @@ public class InboxService extends AbstractScheduledService { FileUtil.createFile(fileContent.getName(), null, fileContent.getFile(), fileContent.getSize(), document.getLanguage(), "admin", document.getId()); } + + if (ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_DELETE_IMPORTED)) { + message.setFlag(Flags.Flag.DELETED, true); + } + } + + /** + * Fetches a HashMap with all tag names as keys and their respective ids as values. + * + * @return Map with all tags or null if not enabled + */ + private Map getAllTags() { + if (!ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_AUTOMATIC_TAGS)) { + return null; + } + TagDao tagDao = new TagDao(); + List tags = tagDao.findByCriteria(new TagCriteria().setTargetIdList(null), new SortCriteria(1, true)); + + Map tagsNameToId = new HashMap<>(); + for (TagDto tagDto : tags) { + tagsNameToId.put(tagDto.getName(), tagDto.getId()); + } + return tagsNameToId; } public Date getLastSyncDate() { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java index 9d00b4b7..3425e403 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/ActionUtil.java @@ -9,7 +9,7 @@ import com.sismics.docs.core.util.action.RemoveTagAction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.json.JsonObject; +import jakarta.json.JsonObject; /** * Action utilities. diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java index 5197a844..7168503e 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/AuditLogUtil.java @@ -6,7 +6,7 @@ import com.sismics.docs.core.model.jpa.AuditLog; import com.sismics.docs.core.model.jpa.Loggable; import com.sismics.util.context.ThreadLocalContext; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; /** * Audit log utilities. diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/ConfigUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/ConfigUtil.java index 5f77a201..d9c6754b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/ConfigUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/ConfigUtil.java @@ -50,6 +50,19 @@ public class ConfigUtil { return Integer.parseInt(value); } + /** + * Returns the long value of a configuration parameter. + * + * @param configType Type of the configuration parameter + * @return Long value of the configuration parameter + * @throws IllegalStateException Configuration parameter undefined + */ + public static long getConfigLongValue(ConfigType configType) { + String value = getConfigStringValue(configType); + + return Long.parseLong(value); + } + /** * Returns the boolean value of a configuration parameter. * diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/DirectoryUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/DirectoryUtil.java index be941c28..1c335ed6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/DirectoryUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/DirectoryUtil.java @@ -5,7 +5,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import com.sismics.util.EnvironmentUtil; diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java index 49357e33..e77e3480 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/FileUtil.java @@ -1,6 +1,5 @@ package com.sismics.docs.core.util; -import com.google.common.base.Charsets; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.CharStreams; @@ -17,7 +16,12 @@ import com.sismics.util.Scalr; import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.io.InputStreamReaderThread; import com.sismics.util.mime.MimeTypeUtil; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.CountingInputStream; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; @@ -26,6 +30,7 @@ import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -36,10 +41,15 @@ import java.util.*; * @author bgamard */ public class FileUtil { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(FileUtil.class); + /** * File ID of files currently being processed. */ - private static Set processingFileSet = Collections.synchronizedSet(new HashSet<>()); + private static final Set processingFileSet = Collections.synchronizedSet(new HashSet<>()); /** * Optical character recognition on an image. @@ -69,19 +79,19 @@ public class FileUtil { // Consume the data as text try (InputStream is = process.getInputStream()) { - return CharStreams.toString(new InputStreamReader(is, Charsets.UTF_8)); + return CharStreams.toString(new InputStreamReader(is, StandardCharsets.UTF_8)); } } /** * Remove a file from the storage filesystem. * - * @param file File to delete + * @param fileId ID of file to delete */ - public static void delete(File file) throws IOException { - Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId()); - Path webFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_web"); - Path thumbnailFile = DirectoryUtil.getStorageDirectory().resolve(file.getId() + "_thumb"); + public static void delete(String fileId) throws IOException { + Path storedFile = DirectoryUtil.getStorageDirectory().resolve(fileId); + Path webFile = DirectoryUtil.getStorageDirectory().resolve(fileId + "_web"); + Path thumbnailFile = DirectoryUtil.getStorageDirectory().resolve(fileId + "_thumb"); if (Files.exists(storedFile)) { Files.delete(storedFile); @@ -126,7 +136,7 @@ public class FileUtil { // Validate global quota String globalStorageQuotaStr = System.getenv(Constants.GLOBAL_QUOTA_ENV); if (!Strings.isNullOrEmpty(globalStorageQuotaStr)) { - long globalStorageQuota = Long.valueOf(globalStorageQuotaStr); + long globalStorageQuota = Long.parseLong(globalStorageQuotaStr); long globalStorageCurrent = userDao.getGlobalStorageCurrent(); if (globalStorageCurrent + fileSize > globalStorageQuota) { throw new IOException("QuotaReached"); @@ -142,6 +152,7 @@ public class FileUtil { file.setName(StringUtils.abbreviate(name, 200)); file.setMimeType(mimeType); file.setUserId(userId); + file.setSize(fileSize); // Get files of this document FileDao fileDao = new FileDao(); @@ -190,7 +201,7 @@ public class FileUtil { FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); fileCreatedAsyncEvent.setUserId(userId); fileCreatedAsyncEvent.setLanguage(language); - fileCreatedAsyncEvent.setFile(file); + fileCreatedAsyncEvent.setFileId(file.getId()); fileCreatedAsyncEvent.setUnencryptedFile(unencryptedFile); ThreadLocalContext.get().addAsyncEvent(fileCreatedAsyncEvent); @@ -211,6 +222,7 @@ public class FileUtil { */ public static void startProcessingFile(String fileId) { processingFileSet.add(fileId); + log.info("Processing started for file: " + fileId); } /** @@ -220,6 +232,7 @@ public class FileUtil { */ public static void endProcessingFile(String fileId) { processingFileSet.remove(fileId); + log.info("Processing ended for file: " + fileId); } /** @@ -231,4 +244,31 @@ public class FileUtil { public static boolean isProcessingFile(String fileId) { return processingFileSet.contains(fileId); } + + /** + * Get the size of a file on disk. + * + * @param fileId the file id + * @param user the file owner + * @return the size or -1 if something went wrong + */ + public static long getFileSize(String fileId, User user) { + // To get the size we copy the decrypted content into a null output stream + // and count the copied byte size. + Path storedFile = DirectoryUtil.getStorageDirectory().resolve(fileId); + if (! Files.exists(storedFile)) { + log.debug("File does not exist " + fileId); + return File.UNKNOWN_SIZE; + } + try (InputStream fileInputStream = Files.newInputStream(storedFile); + InputStream inputStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey()); + CountingInputStream countingInputStream = new CountingInputStream(inputStream); + ) { + IOUtils.copy(countingInputStream, NullOutputStream.NULL_OUTPUT_STREAM); + return countingInputStream.getByteCount(); + } catch (Exception e) { + log.debug("Can't find size of file " + fileId, e); + return File.UNKNOWN_SIZE; + } + } } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java new file mode 100644 index 00000000..99c61537 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/MetadataUtil.java @@ -0,0 +1,196 @@ +package com.sismics.docs.core.util; + +import com.google.common.collect.Maps; +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.DocumentMetadataDao; +import com.sismics.docs.core.dao.MetadataDao; +import com.sismics.docs.core.dao.criteria.MetadataCriteria; +import com.sismics.docs.core.dao.dto.DocumentMetadataDto; +import com.sismics.docs.core.dao.dto.MetadataDto; +import com.sismics.docs.core.model.jpa.DocumentMetadata; +import com.sismics.docs.core.util.jpa.SortCriteria; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; + +/** + * Metadata utilities. + * + * @author bgamard + */ +public class MetadataUtil { + /** + * Update custom metadata on a document. + * + * @param documentId Document ID + * @param metadataIdList Metadata ID list + * @param metadataValueList Metadata value list + */ + public static void updateMetadata(String documentId, List metadataIdList, List metadataValueList) throws Exception { + if (metadataIdList == null || metadataValueList == null || metadataIdList.isEmpty()) { + return; + } + if (metadataIdList.size() != metadataValueList.size()) { + throw new Exception("metadata_id and metadata_value must have the same length"); + } + + Map newValues = Maps.newHashMap(); + for (int i = 0; i < metadataIdList.size(); i++) { + newValues.put(metadataIdList.get(i), metadataValueList.get(i)); + } + + MetadataDao metadataDao = new MetadataDao(); + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + List metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), null); + List documentMetadataDtoList = documentMetadataDao.getByDocumentId(documentId); + + // Update existing values + for (DocumentMetadataDto documentMetadataDto : documentMetadataDtoList) { + if (newValues.containsKey(documentMetadataDto.getMetadataId())) { + // Update the value + String value = newValues.get(documentMetadataDto.getMetadataId()); + validateValue(documentMetadataDto.getType(), value); + updateValue(documentMetadataDto.getId(), value); + newValues.remove(documentMetadataDto.getMetadataId()); + } else { + // Remove the value + updateValue(documentMetadataDto.getId(), null); + } + } + + // Create new values + for (Map.Entry entry : newValues.entrySet()) { + // Search the metadata definition + MetadataDto metadata = null; + for (MetadataDto metadataDto : metadataDtoList) { + if (metadataDto.getId().equals(entry.getKey())) { + metadata = metadataDto; + break; + } + } + + if (metadata == null) { + throw new Exception(MessageFormat.format("Metadata not found: {0}", entry.getKey())); + } + + // Add the value + validateValue(metadata.getType(), entry.getValue()); + createValue(documentId, entry.getKey(), entry.getValue()); + } + } + + /** + * Validate a custom metadata value. + * + * @param type Metadata type + * @param value Value + * @throws Exception In case of validation error + */ + private static void validateValue(MetadataType type, String value) throws Exception { + switch (type) { + case STRING: + case BOOLEAN: + return; + case DATE: + try { + Long.parseLong(value); + } catch (NumberFormatException e) { + throw new Exception("Date value not parsable as timestamp"); + } + break; + case FLOAT: + try { + Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new Exception("Float value not parsable"); + } + break; + case INTEGER: + try { + Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new Exception("Integer value not parsable"); + } + break; + } + } + + /** + * Create a custom metadata value on a document. + * + * @param documentId Document ID + * @param metadataId Metadata ID + * @param value Value + */ + private static void createValue(String documentId, String metadataId, String value) { + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + DocumentMetadata documentMetadata = new DocumentMetadata(); + documentMetadata.setDocumentId(documentId); + documentMetadata.setMetadataId(metadataId); + documentMetadata.setValue(value); + documentMetadataDao.create(documentMetadata); + } + + /** + * Update a custom metadata value. + * + * @param documentMetadataId Document metadata ID + * @param value Value + */ + private static void updateValue(String documentMetadataId, String value) { + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + DocumentMetadata documentMetadata = new DocumentMetadata(); + documentMetadata.setId(documentMetadataId); + documentMetadata.setValue(value); + documentMetadataDao.update(documentMetadata); + } + + /** + * Add custom metadata to a JSON response. + * + * @param json JSON + * @param documentId Document ID + */ + public static void addMetadata(JsonObjectBuilder json, String documentId) { + DocumentMetadataDao documentMetadataDao = new DocumentMetadataDao(); + MetadataDao metadataDao = new MetadataDao(); + List metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), new SortCriteria(1, true)); + List documentMetadataDtoList = documentMetadataDao.getByDocumentId(documentId); + JsonArrayBuilder metadata = Json.createArrayBuilder(); + for (MetadataDto metadataDto : metadataDtoList) { + JsonObjectBuilder meta = Json.createObjectBuilder() + .add("id", metadataDto.getId()) + .add("name", metadataDto.getName()) + .add("type", metadataDto.getType().name()); + for (DocumentMetadataDto documentMetadataDto : documentMetadataDtoList) { + if (documentMetadataDto.getMetadataId().equals(metadataDto.getId())) { + if (documentMetadataDto.getValue() != null) { + switch (metadataDto.getType()) { + case STRING: + meta.add("value", documentMetadataDto.getValue()); + break; + case BOOLEAN: + meta.add("value", Boolean.parseBoolean(documentMetadataDto.getValue())); + break; + case DATE: + meta.add("value", Long.parseLong(documentMetadataDto.getValue())); + break; + case FLOAT: + meta.add("value", Double.parseDouble(documentMetadataDto.getValue())); + break; + case INTEGER: + meta.add("value", Integer.parseInt(documentMetadataDto.getValue())); + break; + } + } + } + } + metadata.add(meta); + } + json.add("metadata", metadata); + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java index 990cfc14..dc59e58b 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/RoutingUtil.java @@ -19,10 +19,10 @@ import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.util.context.ThreadLocalContext; -import javax.json.Json; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.json.JsonReader; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import java.io.StringReader; import java.util.List; diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java index e6b04612..dad74d18 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/TagUtil.java @@ -1,8 +1,8 @@ package com.sismics.docs.core.util; -import com.google.common.collect.Lists; import com.sismics.docs.core.dao.dto.TagDto; +import java.util.ArrayList; import java.util.List; /** @@ -12,14 +12,14 @@ import java.util.List; */ public class TagUtil { /** - * Recursively find children of a tags. + * Recursively find children of a tag. * * @param parentTagDto Parent tag * @param allTagDtoList List of all tags * @return Children tags */ public static List findChildren(TagDto parentTagDto, List allTagDtoList) { - List childrenTagDtoList = Lists.newArrayList(); + List childrenTagDtoList = new ArrayList<>(); for (TagDto tagDto : allTagDtoList) { if (parentTagDto.getId().equals(tagDto.getParentId())) { @@ -32,15 +32,15 @@ public class TagUtil { } /** - * Find tags by name (start with). + * Find tags by name (start with, ignore case). * * @param name Name * @param allTagDtoList List of all tags * @return List of filtered tags */ public static List findByName(String name, List allTagDtoList) { - List tagDtoList = Lists.newArrayList(); - if (name == null || name.isEmpty()) { + List tagDtoList = new ArrayList<>(); + if (name.isEmpty()) { return tagDtoList; } name = name.toLowerCase(); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/TransactionUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/TransactionUtil.java index 20a12f0d..312903be 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/TransactionUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/TransactionUtil.java @@ -5,8 +5,8 @@ import com.sismics.util.jpa.EMF; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; /** * Database transaction utils. diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java index 9e6090e2..597533e9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/Action.java @@ -2,7 +2,7 @@ package com.sismics.docs.core.util.action; import com.sismics.docs.core.dao.dto.DocumentDto; -import javax.json.JsonObject; +import jakarta.json.JsonObject; /** * Base action interface. diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java index 174d6d2d..2892beba 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/AddTagAction.java @@ -6,7 +6,7 @@ import com.sismics.docs.core.dao.criteria.TagCriteria; import com.sismics.docs.core.dao.dto.DocumentDto; import com.sismics.docs.core.dao.dto.TagDto; -import javax.json.JsonObject; +import jakarta.json.JsonObject; import java.util.List; import java.util.Set; diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/ProcessFilesAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/ProcessFilesAction.java index 920e228f..4ff3c6e5 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/ProcessFilesAction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/ProcessFilesAction.java @@ -13,7 +13,7 @@ import com.sismics.util.context.ThreadLocalContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.json.JsonObject; +import jakarta.json.JsonObject; import java.nio.file.Path; import java.util.List; @@ -48,7 +48,7 @@ public class ProcessFilesAction implements Action { FileUpdatedAsyncEvent event = new FileUpdatedAsyncEvent(); event.setUserId("admin"); event.setLanguage(documentDto.getLanguage()); - event.setFile(file); + event.setFileId(file.getId()); event.setUnencryptedFile(unencryptedFile); ThreadLocalContext.get().addAsyncEvent(event); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java index cc2a9630..ff2dc131 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/RemoveTagAction.java @@ -6,7 +6,7 @@ import com.sismics.docs.core.dao.criteria.TagCriteria; import com.sismics.docs.core.dao.dto.DocumentDto; import com.sismics.docs.core.dao.dto.TagDto; -import javax.json.JsonObject; +import jakarta.json.JsonObject; import java.util.List; import java.util.Set; diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java b/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java index e6fc8cf0..1deab2a3 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/action/TagAction.java @@ -4,7 +4,7 @@ import com.sismics.docs.core.dao.TagDao; import com.sismics.docs.core.dao.criteria.TagCriteria; import com.sismics.docs.core.dao.dto.TagDto; -import javax.json.JsonObject; +import jakarta.json.JsonObject; import java.util.List; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/authentication/AuthenticationUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/AuthenticationUtil.java index bf023d5b..d2325eee 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/authentication/AuthenticationUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/AuthenticationUtil.java @@ -20,7 +20,7 @@ public class AuthenticationUtil { .map(clazz -> { try { - return clazz.newInstance(); + return clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java new file mode 100644 index 00000000..65d0afc8 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/authentication/LdapAuthenticationHandler.java @@ -0,0 +1,107 @@ +package com.sismics.docs.core.util.authentication; + +import com.sismics.docs.core.constant.ConfigType; +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.ConfigDao; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.Config; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.ConfigUtil; +import com.sismics.util.ClasspathScanner; +import org.apache.directory.api.ldap.model.cursor.EntryCursor; +import org.apache.directory.api.ldap.model.entry.Attribute; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.entry.Value; +import org.apache.directory.api.ldap.model.message.SearchScope; +import org.apache.directory.ldap.client.api.LdapConnection; +import org.apache.directory.ldap.client.api.LdapConnectionConfig; +import org.apache.directory.ldap.client.api.LdapNetworkConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; + +/** + * LDAP authentication handler. + * + * @author bgamard + */ +@ClasspathScanner.Priority(50) // Before the internal database +public class LdapAuthenticationHandler implements AuthenticationHandler { + /** + * Logger. + */ + private static final Logger log = LoggerFactory.getLogger(LdapAuthenticationHandler.class); + + /** + * Get a LDAP connection. + * @return LdapConnection + */ + private LdapConnection getConnection() { + ConfigDao configDao = new ConfigDao(); + Config ldapEnabled = configDao.getById(ConfigType.LDAP_ENABLED); + if (ldapEnabled == null || !Boolean.parseBoolean(ldapEnabled.getValue())) { + return null; + } + + LdapConnectionConfig config = new LdapConnectionConfig(); + config.setLdapHost(ConfigUtil.getConfigStringValue(ConfigType.LDAP_HOST)); + config.setLdapPort(ConfigUtil.getConfigIntegerValue(ConfigType.LDAP_PORT)); + config.setUseSsl(ConfigUtil.getConfigBooleanValue(ConfigType.LDAP_USESSL)); + config.setName(ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_DN)); + config.setCredentials(ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_PASSWORD)); + + return new LdapNetworkConnection(config); + } + + @Override + public User authenticate(String username, String password) { + // Fetch and authenticate the user + Entry userEntry; + try (LdapConnection ldapConnection = getConnection()) { + if (ldapConnection == null) { + return null; + } + + EntryCursor cursor = ldapConnection.search(ConfigUtil.getConfigStringValue(ConfigType.LDAP_BASE_DN), + ConfigUtil.getConfigStringValue(ConfigType.LDAP_FILTER).replace("USERNAME", username), SearchScope.SUBTREE); + if (cursor.next()) { + userEntry = cursor.get(); + ldapConnection.bind(userEntry.getDn(), password); + } else { + // User not found + return null; + } + } catch (Exception e) { + log.error("Error authenticating \"" + username + "\" using the LDAP", e); + return null; + } + + UserDao userDao = new UserDao(); + User user = userDao.getActiveByUsername(username); + if (user == null) { + // The user is valid but never authenticated, create the user now + log.info("\"" + username + "\" authenticated for the first time, creating the internal user"); + user = new User(); + user.setRoleId(Constants.DEFAULT_USER_ROLE); + user.setUsername(username); + user.setPassword(UUID.randomUUID().toString()); // No authentication using the internal database + Attribute mailAttribute = userEntry.get("mail"); + if (mailAttribute == null || mailAttribute.get() == null) { + user.setEmail(ConfigUtil.getConfigStringValue(ConfigType.LDAP_DEFAULT_EMAIL)); + } else { + Value value = mailAttribute.get(); + user.setEmail(value.getString()); + } + user.setStorageQuota(ConfigUtil.getConfigLongValue(ConfigType.LDAP_DEFAULT_STORAGE)); + try { + userDao.create(user, "admin"); + } catch (Exception e) { + log.error("Error while creating the internal user", e); + return null; + } + } + + return user; + } +} diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/format/FormatHandlerUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/format/FormatHandlerUtil.java index 787739b4..04abb6d4 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/format/FormatHandlerUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/format/FormatHandlerUtil.java @@ -26,12 +26,12 @@ public class FormatHandlerUtil { public static FormatHandler find(String mimeType) { try { for (Class formatHandlerClass : FORMAT_HANDLERS) { - FormatHandler formatHandler = formatHandlerClass.newInstance(); + FormatHandler formatHandler = formatHandlerClass.getDeclaredConstructor().newInstance(); if (formatHandler.accept(mimeType)) { return formatHandler; } } - } catch (InstantiationException | IllegalAccessException e) { + } catch (Exception e) { return null; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/format/PdfFormatHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/format/PdfFormatHandler.java index 08c698a1..670358b9 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/format/PdfFormatHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/format/PdfFormatHandler.java @@ -6,6 +6,7 @@ import com.sismics.util.mime.MimeType; import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; import org.slf4j.Logger; @@ -60,7 +61,7 @@ public class PdfFormatHandler implements FormatHandler { for (int pageIndex = 0; pageIndex < pdfDocument.getNumberOfPages(); pageIndex++) { log.info("OCR page " + (pageIndex + 1) + "/" + pdfDocument.getNumberOfPages() + " of PDF file containing only images"); sb.append(" "); - sb.append(FileUtil.ocrFile(language, renderer.renderImage(pageIndex))); + sb.append(FileUtil.ocrFile(language, renderer.renderImageWithDPI(pageIndex, 300, ImageType.GRAY))); } return sb.toString(); } catch (Exception e) { diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/format/PptxFormatHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/format/PptxFormatHandler.java index db3074bd..c41dcfa7 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/format/PptxFormatHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/format/PptxFormatHandler.java @@ -9,7 +9,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.poi.xslf.extractor.XSLFPowerPointExtractor; +import org.apache.poi.sl.extractor.SlideShowExtractor; import org.apache.poi.xslf.usermodel.XMLSlideShow; import org.apache.poi.xslf.usermodel.XSLFSlide; @@ -50,7 +50,7 @@ public class PptxFormatHandler implements FormatHandler { @Override public String extractContent(String language, Path file) throws Exception { XMLSlideShow pptx = loadPPtxFile(file); - return new XSLFPowerPointExtractor(pptx).getText(); + return new SlideShowExtractor<>(pptx).getText(); } @Override diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/format/TextPlainFormatHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/format/TextPlainFormatHandler.java index 8b505ab5..eb2441c6 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/format/TextPlainFormatHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/format/TextPlainFormatHandler.java @@ -1,6 +1,5 @@ package com.sismics.docs.core.util.format; -import com.google.common.base.Charsets; import com.google.common.io.Closer; import com.lowagie.text.*; import com.lowagie.text.pdf.PdfWriter; @@ -11,6 +10,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import java.awt.image.BufferedImage; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -33,7 +33,7 @@ public class TextPlainFormatHandler implements FormatHandler { PdfWriter.getInstance(output, pdfOutputStream); output.open(); - String content = new String(Files.readAllBytes(file), Charsets.UTF_8); + String content = Files.readString(file, StandardCharsets.UTF_8); Font font = FontFactory.getFont("LiberationMono-Regular"); Paragraph paragraph = new Paragraph(content, font); paragraph.setAlignment(Element.ALIGN_LEFT); @@ -46,7 +46,7 @@ public class TextPlainFormatHandler implements FormatHandler { @Override public String extractContent(String language, Path file) throws Exception { - return new String(Files.readAllBytes(file), "UTF-8"); + return Files.readString(file, StandardCharsets.UTF_8); } @Override diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/format/VideoFormatHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/format/VideoFormatHandler.java index 1365aca5..c02ea764 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/format/VideoFormatHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/format/VideoFormatHandler.java @@ -1,6 +1,5 @@ package com.sismics.docs.core.util.format; -import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; @@ -13,6 +12,7 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -65,7 +65,7 @@ public class VideoFormatHandler implements FormatHandler { // Consume the data as a string try (InputStream is = process.getInputStream()) { - return new String(ByteStreams.toByteArray(is), Charsets.UTF_8); + return new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8); } catch (Exception e) { return null; } diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java index 5a190c58..27a33547 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/indexing/LuceneIndexingHandler.java @@ -26,9 +26,18 @@ import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; -import org.apache.lucene.index.*; +import org.apache.lucene.index.CheckIndex; +import org.apache.lucene.index.ConcurrentMergeScheduler; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.simple.SimpleQueryParser; -import org.apache.lucene.search.*; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLEncoder; @@ -37,18 +46,22 @@ import org.apache.lucene.search.spell.LuceneDictionary; import org.apache.lucene.search.suggest.Lookup; import org.apache.lucene.search.suggest.analyzing.FuzzySuggester; import org.apache.lucene.store.Directory; +import org.apache.lucene.store.NIOFSDirectory; import org.apache.lucene.store.NoLockFactory; import org.apache.lucene.store.RAMDirectory; -import org.apache.lucene.store.SimpleFSDirectory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.sql.Timestamp; -import java.util.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; /** * Lucene indexing handler. @@ -117,7 +130,7 @@ public class LuceneIndexingHandler implements IndexingHandler { } else if (luceneStorage.equals("FILE")) { Path luceneDirectory = DirectoryUtil.getLuceneDirectory(); log.info("Using file Lucene storage: {}", luceneDirectory); - directory = new SimpleFSDirectory(luceneDirectory, NoLockFactory.INSTANCE); + directory = new NIOFSDirectory(luceneDirectory, NoLockFactory.INSTANCE); } // Create an index writer @@ -243,32 +256,27 @@ public class LuceneIndexingHandler implements IndexingHandler { 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, d.DOC_IDFILE_C, "); sb.append(" s.count c5, "); - sb.append(" f.count c6, "); sb.append(" rs2.RTP_ID_C c7, rs2.RTP_NAME_C, d.DOC_UPDATEDATE_D c8 "); sb.append(" from T_DOCUMENT d "); sb.append(" left join (SELECT count(s.SHA_ID_C) count, ac.ACL_SOURCEID_C " + " FROM T_SHARE s, T_ACL ac " + " WHERE ac.ACL_TARGETID_C = s.SHA_ID_C AND ac.ACL_DELETEDATE_D IS NULL AND " + - " s.SHA_DELETEDATE_D IS NULL group by ac.ACL_SOURCEID_C) s on s.ACL_SOURCEID_C = d.DOC_ID_C " + - " left join (SELECT count(f.FIL_ID_C) count, f.FIL_IDDOC_C " + - " FROM T_FILE f " + - " WHERE f.FIL_DELETEDATE_D IS NULL group by f.FIL_IDDOC_C) f on f.FIL_IDDOC_C = d.DOC_ID_C "); + " s.SHA_DELETEDATE_D IS NULL group by ac.ACL_SOURCEID_C) s on s.ACL_SOURCEID_C = d.DOC_ID_C "); sb.append(" left join (select rs.*, rs3.idDocument " + "from T_ROUTE_STEP rs " + "join (select r.RTE_IDDOCUMENT_C idDocument, rs.RTP_IDROUTE_C idRoute, min(rs.RTP_ORDER_N) minOrder from T_ROUTE_STEP rs join T_ROUTE r on r.RTE_ID_C = rs.RTP_IDROUTE_C and r.RTE_DELETEDATE_D is null where rs.RTP_DELETEDATE_D is null and rs.RTP_ENDDATE_D is null group by rs.RTP_IDROUTE_C, r.RTE_IDDOCUMENT_C) rs3 on rs.RTP_IDROUTE_C = rs3.idRoute and rs.RTP_ORDER_N = rs3.minOrder " + "where rs.RTP_IDTARGET_C in (:targetIdList)) rs2 on rs2.idDocument = d.DOC_ID_C "); // Add search criterias - if (criteria.getTargetIdList() != null) { - if (!SecurityUtil.skipAclCheck(criteria.getTargetIdList())) { - // Read permission is enough for searching - sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); - sb.append(" left join T_DOCUMENT_TAG dta on dta.DOT_IDDOCUMENT_C = d.DOC_ID_C and dta.DOT_DELETEDATE_D is null "); - sb.append(" left join T_ACL a2 on a2.ACL_TARGETID_C in (:targetIdList) and a2.ACL_SOURCEID_C = dta.DOT_IDTAG_C and a2.ACL_PERM_C = 'READ' and a2.ACL_DELETEDATE_D is null "); - criteriaList.add("(a.ACL_ID_C is not null or a2.ACL_ID_C is not null)"); - } - parameterMap.put("targetIdList", criteria.getTargetIdList()); + if (!SecurityUtil.skipAclCheck(criteria.getTargetIdList())) { + // Read permission is enough for searching + sb.append(" left join T_ACL a on a.ACL_TARGETID_C in (:targetIdList) and a.ACL_SOURCEID_C = d.DOC_ID_C and a.ACL_PERM_C = 'READ' and a.ACL_DELETEDATE_D is null "); + sb.append(" left join T_DOCUMENT_TAG dta on dta.DOT_IDDOCUMENT_C = d.DOC_ID_C and dta.DOT_DELETEDATE_D is null "); + sb.append(" left join T_ACL a2 on a2.ACL_TARGETID_C in (:targetIdList) and a2.ACL_SOURCEID_C = dta.DOT_IDTAG_C and a2.ACL_PERM_C = 'READ' and a2.ACL_DELETEDATE_D is null "); + criteriaList.add("(a.ACL_ID_C is not null or a2.ACL_ID_C is not null)"); } + parameterMap.put("targetIdList", criteria.getTargetIdList()); + if (!Strings.isNullOrEmpty(criteria.getSearch()) || !Strings.isNullOrEmpty(criteria.getFullSearch())) { documentSearchMap = search(criteria.getSearch(), criteria.getFullSearch()); if (documentSearchMap.isEmpty()) { @@ -278,7 +286,7 @@ public class LuceneIndexingHandler implements IndexingHandler { criteriaList.add("d.DOC_ID_C in :documentIdList"); parameterMap.put("documentIdList", documentSearchMap.keySet()); - suggestSearchTerms(criteria.getSearch(), suggestionList); + suggestSearchTerms(criteria.getFullSearch(), suggestionList); } if (criteria.getCreateDateMin() != null) { criteriaList.add("d.DOC_CREATEDATE_D >= :createDateMin"); @@ -296,7 +304,11 @@ public class LuceneIndexingHandler implements IndexingHandler { criteriaList.add("d.DOC_UPDATEDATE_D <= :updateDateMax"); parameterMap.put("updateDateMax", criteria.getUpdateDateMax()); } - if (criteria.getTagIdList() != null && !criteria.getTagIdList().isEmpty()) { + if (!criteria.getTitleList().isEmpty()) { + criteriaList.add("d.DOC_TITLE_C in :title"); + parameterMap.put("title", criteria.getTitleList()); + } + if (!criteria.getTagIdList().isEmpty()) { int index = 0; for (List tagIdList : criteria.getTagIdList()) { List tagCriteriaList = Lists.newArrayList(); @@ -309,7 +321,7 @@ public class LuceneIndexingHandler implements IndexingHandler { criteriaList.add("(" + Joiner.on(" OR ").join(tagCriteriaList) + ")"); } } - if (criteria.getExcludedTagIdList() != null && !criteria.getExcludedTagIdList().isEmpty()) { + if (!criteria.getExcludedTagIdList().isEmpty()) { int index = 0; for (List tagIdList : criteria.getExcludedTagIdList()) { List tagCriteriaList = Lists.newArrayList(); @@ -325,6 +337,11 @@ public class LuceneIndexingHandler implements IndexingHandler { if (criteria.getShared() != null && criteria.getShared()) { criteriaList.add("s.count > 0"); } + if (criteria.getMimeType() != null) { + sb.append("left join T_FILE f0 on f0.FIL_IDDOC_C = d.DOC_ID_C and f0.FIL_MIMETYPE_C = :mimeType and f0.FIL_DELETEDATE_D is null"); + parameterMap.put("mimeType", criteria.getMimeType()); + criteriaList.add("f0.FIL_ID_C is not null"); + } if (criteria.getLanguage() != null) { criteriaList.add("d.DOC_LANGUAGE_C = :language"); parameterMap.put("language", criteria.getLanguage()); @@ -339,10 +356,8 @@ public class LuceneIndexingHandler implements IndexingHandler { criteriaList.add("d.DOC_DELETEDATE_D is null"); - if (!criteriaList.isEmpty()) { - sb.append(" where "); - sb.append(Joiner.on(" and ").join(criteriaList)); - } + sb.append(" where "); + sb.append(Joiner.on(" and ").join(criteriaList)); // Perform the search QueryParam queryParam = new QueryParam(sb.toString(), parameterMap); @@ -361,8 +376,6 @@ public class LuceneIndexingHandler implements IndexingHandler { documentDto.setFileId((String) o[i++]); Number shareCount = (Number) o[i++]; documentDto.setShared(shareCount != null && shareCount.intValue() > 0); - Number fileCount = (Number) o[i++]; - documentDto.setFileCount(fileCount == null ? 0 : fileCount.intValue()); documentDto.setActiveRoute(o[i++] != null); documentDto.setCurrentStepName((String) o[i++]); documentDto.setUpdateTimestamp(((Timestamp) o[i]).getTime()); @@ -390,7 +403,7 @@ public class LuceneIndexingHandler implements IndexingHandler { LuceneDictionary dictionary = new LuceneDictionary(directoryReader, "title"); suggester.build(dictionary); int lastIndex = search.lastIndexOf(' '); - String suggestQuery = search.substring(lastIndex < 0 ? 0 : lastIndex); + String suggestQuery = search.substring(Math.max(lastIndex, 0)); List lookupResultList = suggester.lookup(suggestQuery, false, 10); for (Lookup.LookupResult lookupResult : lookupResultList) { suggestionList.add(lookupResult.key.toString()); diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java index 15b51ad7..08748609 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/PaginatedLists.java @@ -1,6 +1,6 @@ package com.sismics.docs.core.util.jpa; -import javax.persistence.Query; +import jakarta.persistence.Query; import java.util.List; /** diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java index 3575636e..365cec4a 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/jpa/QueryUtil.java @@ -2,8 +2,8 @@ package com.sismics.docs.core.util.jpa; import java.util.Map.Entry; -import javax.persistence.EntityManager; -import javax.persistence.Query; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; import com.sismics.util.context.ThreadLocalContext; diff --git a/docs-core/src/main/java/com/sismics/util/EmailUtil.java b/docs-core/src/main/java/com/sismics/util/EmailUtil.java index 7448313f..8767ee04 100644 --- a/docs-core/src/main/java/com/sismics/util/EmailUtil.java +++ b/docs-core/src/main/java/com/sismics/util/EmailUtil.java @@ -17,9 +17,9 @@ import org.jsoup.Jsoup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -87,29 +88,34 @@ public class EmailUtil { try { // Build email headers HtmlEmail email = new HtmlEmail(); - email.setCharset("UTF-8"); + email.setCharset(StandardCharsets.UTF_8.name()); ConfigDao configDao = new ConfigDao(); // Hostname String envHostname = System.getenv(Constants.SMTP_HOSTNAME_ENV); - if (envHostname == null) { + if (Strings.isNullOrEmpty(envHostname)) { email.setHostName(ConfigUtil.getConfigStringValue(ConfigType.SMTP_HOSTNAME)); } else { email.setHostName(envHostname); } // Port + int port = ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT); String envPort = System.getenv(Constants.SMTP_PORT_ENV); - if (envPort == null) { - email.setSmtpPort(ConfigUtil.getConfigIntegerValue(ConfigType.SMTP_PORT)); - } else { - email.setSmtpPort(Integer.valueOf(envPort)); + if (!Strings.isNullOrEmpty(envPort)) { + port = Integer.valueOf(envPort); + } + email.setSmtpPort(port); + if (port == 465) { + email.setSSLOnConnect(true); + } else if (port == 587) { + email.setStartTLSRequired(true); } // Username and password String envUsername = System.getenv(Constants.SMTP_USERNAME_ENV); String envPassword = System.getenv(Constants.SMTP_PASSWORD_ENV); - if (envUsername == null || envPassword == null) { + if (Strings.isNullOrEmpty(envUsername) || Strings.isNullOrEmpty(envPassword)) { Config usernameConfig = configDao.getById(ConfigType.SMTP_USERNAME); Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); if (usernameConfig != null && passwordConfig != null) { diff --git a/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java b/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java index 39bce3b5..c99ef0f9 100644 --- a/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java +++ b/docs-core/src/main/java/com/sismics/util/HtmlToPlainText.java @@ -1,6 +1,6 @@ package com.sismics.util; -import org.jsoup.helper.StringUtil; +import org.jsoup.internal.StringUtil; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; @@ -28,7 +28,7 @@ public class HtmlToPlainText { } // the formatting rules, implemented in a breadth-first DOM traverse - private class FormattingVisitor implements NodeVisitor { + static private class FormattingVisitor implements NodeVisitor { private static final int maxWidth = 80; private int width = 0; private StringBuilder accum = new StringBuilder(); // holds the accumulated text @@ -64,7 +64,7 @@ public class HtmlToPlainText { return; // don't accumulate long runs of empty spaces if (text.length() + width > maxWidth) { // won't fit, needs to wrap - String words[] = text.split("\\s+"); + String[] words = text.split("\\s+"); for (int i = 0; i < words.length; i++) { String word = words[i]; boolean last = i == words.length - 1; diff --git a/docs-core/src/main/java/com/sismics/util/ImageUtil.java b/docs-core/src/main/java/com/sismics/util/ImageUtil.java index 0d54718d..5990f331 100644 --- a/docs-core/src/main/java/com/sismics/util/ImageUtil.java +++ b/docs-core/src/main/java/com/sismics/util/ImageUtil.java @@ -1,6 +1,5 @@ package com.sismics.util; -import com.google.common.base.Charsets; import com.google.common.hash.Hashing; import javax.imageio.IIOImage; @@ -13,6 +12,7 @@ import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Iterator; /** @@ -80,7 +80,7 @@ public class ImageUtil { } return Hashing.md5().hashString( - email.trim().toLowerCase(), Charsets.UTF_8) + email.trim().toLowerCase(), StandardCharsets.UTF_8) .toString(); } diff --git a/docs-core/src/main/java/com/sismics/util/JsonUtil.java b/docs-core/src/main/java/com/sismics/util/JsonUtil.java index 945c1b74..36548c06 100644 --- a/docs-core/src/main/java/com/sismics/util/JsonUtil.java +++ b/docs-core/src/main/java/com/sismics/util/JsonUtil.java @@ -1,7 +1,7 @@ package com.sismics.util; -import javax.json.Json; -import javax.json.JsonValue; +import jakarta.json.Json; +import jakarta.json.JsonValue; /** * JSON utilities. diff --git a/docs-core/src/main/java/com/sismics/util/ResourceUtil.java b/docs-core/src/main/java/com/sismics/util/ResourceUtil.java index 4a3c8f72..8427e946 100644 --- a/docs-core/src/main/java/com/sismics/util/ResourceUtil.java +++ b/docs-core/src/main/java/com/sismics/util/ResourceUtil.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.*; import java.util.jar.JarEntry; @@ -53,7 +54,7 @@ public class ResourceUtil { // Extract the JAR path String jarPath = dirUrl.getPath().substring(5, dirUrl.getPath().indexOf("!")); - JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); + JarFile jar = new JarFile(URLDecoder.decode(jarPath, StandardCharsets.UTF_8)); Set fileSet = new HashSet(); try { diff --git a/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java b/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java index 266ff944..db1e3770 100644 --- a/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java +++ b/docs-core/src/main/java/com/sismics/util/context/ThreadLocalContext.java @@ -3,7 +3,7 @@ package com.sismics.util.context; import com.google.common.collect.Lists; import com.sismics.docs.core.model.context.AppContext; -import javax.persistence.EntityManager; +import jakarta.persistence.EntityManager; import java.util.Iterator; import java.util.List; diff --git a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java index 74c198d5..401bb424 100644 --- a/docs-core/src/main/java/com/sismics/util/jpa/EMF.java +++ b/docs-core/src/main/java/com/sismics/util/jpa/EMF.java @@ -1,15 +1,15 @@ package com.sismics.util.jpa; +import com.google.common.base.Strings; import com.sismics.docs.core.util.DirectoryUtil; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; -import org.hibernate.cfg.Environment; import org.hibernate.internal.util.config.ConfigurationHelper; import org.hibernate.service.ServiceRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -34,7 +34,6 @@ public final class EMF { try { properties = getEntityManagerProperties(); - Environment.verifyProperties(properties); ConfigurationHelper.resolvePlaceHolders(properties); ServiceRegistry reg = new StandardServiceRegistryBuilder().applySettings(properties).build(); @@ -85,7 +84,8 @@ public final class EMF { Map props = new HashMap<>(); Path dbDirectory = DirectoryUtil.getDbDirectory(); String dbFile = dbDirectory.resolve("docs").toAbsolutePath().toString(); - if (databaseUrl == null) { + if (Strings.isNullOrEmpty(databaseUrl)) { + log.warn("Using an embedded H2 database. Only suitable for testing purpose, not for production!"); props.put("hibernate.connection.driver_class", "org.h2.Driver"); props.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); props.put("hibernate.connection.url", "jdbc:h2:file:" + dbFile + ";CACHE_SIZE=65536;LOCK_TIMEOUT=10000"); @@ -102,12 +102,9 @@ public final class EMF { props.put("hibernate.format_sql", "false"); props.put("hibernate.max_fetch_depth", "5"); props.put("hibernate.cache.use_second_level_cache", "false"); - props.put("hibernate.c3p0.min_size", "1"); - props.put("hibernate.c3p0.max_size", "10"); - props.put("hibernate.c3p0.timeout", "5000"); - props.put("hibernate.c3p0.max_statements", "0"); - props.put("hibernate.c3p0.acquire_increment", "1"); - props.put("hibernate.c3p0.idle_test_period", "10"); + props.put("hibernate.connection.initial_pool_size", "1"); + props.put("hibernate.connection.pool_size", "10"); + props.put("hibernate.connection.pool_validation_interval", "5"); return props; } diff --git a/docs-core/src/main/java/com/sismics/util/log4j/LogCriteria.java b/docs-core/src/main/java/com/sismics/util/log4j/LogCriteria.java index d8e4a90e..c28a31a6 100644 --- a/docs-core/src/main/java/com/sismics/util/log4j/LogCriteria.java +++ b/docs-core/src/main/java/com/sismics/util/log4j/LogCriteria.java @@ -1,6 +1,6 @@ package com.sismics.util.log4j; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Level; /** diff --git a/docs-core/src/main/java/com/sismics/util/mime/MimeType.java b/docs-core/src/main/java/com/sismics/util/mime/MimeType.java index 1ea316b2..f45e1f96 100644 --- a/docs-core/src/main/java/com/sismics/util/mime/MimeType.java +++ b/docs-core/src/main/java/com/sismics/util/mime/MimeType.java @@ -13,7 +13,7 @@ public class MimeType { public static final String IMAGE_GIF = "image/gif"; public static final String APPLICATION_ZIP = "application/zip"; - + public static final String APPLICATION_PDF = "application/pdf"; public static final String OPEN_DOCUMENT_TEXT = "application/vnd.oasis.opendocument.text"; diff --git a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java index aa014cbb..546efcb4 100644 --- a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java +++ b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java @@ -1,15 +1,9 @@ package com.sismics.util.mime; -import com.google.common.base.Charsets; -import org.apache.commons.compress.utils.IOUtils; - import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; /** * Utility to check MIME types. @@ -18,7 +12,7 @@ import java.util.zip.ZipInputStream; */ public class MimeTypeUtil { /** - * Try to guess the MIME type of a file by its magic number (header). + * Try to guess the MIME type of a file. * * @param file File to inspect * @param name File name @@ -26,57 +20,17 @@ public class MimeTypeUtil { * @throws IOException e */ public static String guessMimeType(Path file, String name) throws IOException { - String mimeType; - try (InputStream is = Files.newInputStream(file)) { - byte[] headerBytes = new byte[64]; - is.read(headerBytes); - mimeType = guessMimeType(headerBytes, name); + String mimeType = Files.probeContentType(file); + + if (mimeType == null && name != null) { + mimeType = URLConnection.getFileNameMap().getContentTypeFor(name); } - return guessOpenDocumentFormat(mimeType, file); - } - - /** - * Try to guess the MIME type of a file by its magic number (header). - * - * @param headerBytes File header (first bytes) - * @param name File name - * @return MIME type - * @throws UnsupportedEncodingException e - */ - public static String guessMimeType(byte[] headerBytes, String name) throws UnsupportedEncodingException { - String header = new String(headerBytes, "US-ASCII"); - - // Detect by header bytes - if (header.startsWith("PK")) { - return MimeType.APPLICATION_ZIP; - } else if (header.startsWith("GIF87a") || header.startsWith("GIF89a")) { - return MimeType.IMAGE_GIF; - } else if (headerBytes[0] == ((byte) 0xff) && headerBytes[1] == ((byte) 0xd8)) { - return MimeType.IMAGE_JPEG; - } else if (headerBytes[0] == ((byte) 0x89) && headerBytes[1] == ((byte) 0x50) && headerBytes[2] == ((byte) 0x4e) && headerBytes[3] == ((byte) 0x47) && - headerBytes[4] == ((byte) 0x0d) && headerBytes[5] == ((byte) 0x0a) && headerBytes[6] == ((byte) 0x1a) && headerBytes[7] == ((byte) 0x0a)) { - return MimeType.IMAGE_PNG; - } else if (headerBytes[0] == ((byte) 0x25) && headerBytes[1] == ((byte) 0x50) && headerBytes[2] == ((byte) 0x44) && headerBytes[3] == ((byte) 0x46)) { - return MimeType.APPLICATION_PDF; - } else if (headerBytes[0] == ((byte) 0x00) && headerBytes[1] == ((byte) 0x00) && headerBytes[2] == ((byte) 0x00) - && (headerBytes[3] == ((byte) 0x14) || headerBytes[3] == ((byte) 0x18) || headerBytes[3] == ((byte) 0x20)) - && headerBytes[4] == ((byte) 0x66) && headerBytes[5] == ((byte) 0x74) && headerBytes[6] == ((byte) 0x79) && headerBytes[7] == ((byte) 0x70)) { - return MimeType.VIDEO_MP4; - } else if (headerBytes[0] == ((byte) 0x1a) && headerBytes[1] == ((byte) 0x45) && headerBytes[2] == ((byte) 0xdf) && headerBytes[3] == ((byte) 0xa3)) { - return MimeType.VIDEO_WEBM; + if (mimeType == null) { + return MimeType.DEFAULT; } - // Detect by file extension - if (name != null) { - if (name.endsWith(".txt")) { - return MimeType.TEXT_PLAIN; - } else if (name.endsWith(".csv")) { - return MimeType.TEXT_CSV; - } - } - - return MimeType.DEFAULT; + return mimeType; } /** @@ -113,52 +67,4 @@ public class MimeTypeUtil { return "bin"; } } - - /** - * Guess the MIME type of open document formats (docx and odt). - * It's more costly than the simple header check, but needed because open document formats - * are simple ZIP files on the outside and much bigger on the inside. - * - * @param mimeType Currently detected MIME type - * @param file File on disk - * @return MIME type - */ - private static String guessOpenDocumentFormat(String mimeType, Path file) { - if (!MimeType.APPLICATION_ZIP.equals(mimeType)) { - // open document formats are ZIP files - return mimeType; - } - - try (InputStream inputStream = Files.newInputStream(file); - ZipInputStream zipInputStream = new ZipInputStream(inputStream, Charsets.ISO_8859_1)) { - ZipEntry archiveEntry = zipInputStream.getNextEntry(); - while (archiveEntry != null) { - if (archiveEntry.getName().equals("mimetype")) { - // Maybe it's an ODT file - String content = new String(IOUtils.toByteArray(zipInputStream), Charsets.ISO_8859_1); - if (MimeType.OPEN_DOCUMENT_TEXT.equals(content.trim())) { - mimeType = MimeType.OPEN_DOCUMENT_TEXT; - break; - } - } else if (archiveEntry.getName().equals("[Content_Types].xml")) { - // Maybe it's a DOCX file - String content = new String(IOUtils.toByteArray(zipInputStream), Charsets.ISO_8859_1); - if (content.contains(MimeType.OFFICE_DOCUMENT)) { - mimeType = MimeType.OFFICE_DOCUMENT; - break; - } else if (content.contains(MimeType.OFFICE_PRESENTATION)) { - mimeType = MimeType.OFFICE_PRESENTATION; - break; - } - } - - archiveEntry = zipInputStream.getNextEntry(); - } - } catch (Exception e) { - // In case of any error, just give up and keep the ZIP MIME type - return mimeType; - } - - return mimeType; - } } diff --git a/docs-core/src/main/resources/META-INF/persistence.xml b/docs-core/src/main/resources/META-INF/persistence.xml index b62e7679..a4edc706 100644 --- a/docs-core/src/main/resources/META-INF/persistence.xml +++ b/docs-core/src/main/resources/META-INF/persistence.xml @@ -1,8 +1,8 @@ - + org.hibernate.jpa.HibernatePersistenceProvider diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index 0ac161b7..435fb302 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=23 \ No newline at end of file +db.version=30 diff --git a/docs-core/src/main/resources/db/update/dbupdate-000-0.sql b/docs-core/src/main/resources/db/update/dbupdate-000-0.sql index a28cb0d8..19b70b8d 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-000-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-000-0.sql @@ -41,4 +41,4 @@ insert into T_LOCALE(LOC_ID_C) values('fr'); insert into T_ROLE(ROL_ID_C, ROL_NAME_C, ROL_CREATEDATE_D) values('admin', 'Admin', NOW()); insert into T_ROLE(ROL_ID_C, ROL_NAME_C, ROL_CREATEDATE_D) values('user', 'User', NOW()); insert into T_ROLE_BASE_FUNCTION(RBF_ID_C, RBF_IDROLE_C, RBF_IDBASEFUNCTION_C, RBF_CREATEDATE_D) values('admin_ADMIN', 'admin', 'ADMIN', NOW()); -insert into T_USER(USE_ID_C, USE_IDLOCALE_C, USE_IDROLE_C, USE_USERNAME_C, USE_PASSWORD_C, USE_EMAIL_C, USE_THEME_C, USE_FIRSTCONNECTION_B, USE_CREATEDATE_D, USE_PRIVATEKEY_C) values('admin', 'en', 'admin', 'admin', '$2a$05$6Ny3TjrW3aVAL1or2SlcR.fhuDgPKp5jp.P9fBXwVNePgeLqb4i3C', 'admin@localhost', 'default.less', true, NOW(), 'AdminPk'); +insert into T_USER(USE_ID_C, USE_IDLOCALE_C, USE_IDROLE_C, USE_USERNAME_C, USE_PASSWORD_C, USE_EMAIL_C, USE_THEME_C, USE_FIRSTCONNECTION_B, USE_CREATEDATE_D, USE_PRIVATEKEY_C) values('admin', 'en', 'admin', 'admin', '$2y$10$xg0EEKVUehutDI1m6qQhVeFz7SMQMl1jQzjf2KkVsR2c7aV2vyyjK', 'admin@localhost', 'default.less', true, NOW(), 'AdminPk'); diff --git a/docs-core/src/main/resources/db/update/dbupdate-024-0.sql b/docs-core/src/main/resources/db/update/dbupdate-024-0.sql new file mode 100644 index 00000000..672f7f58 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-024-0.sql @@ -0,0 +1,5 @@ +create cached table T_METADATA ( MET_ID_C varchar(36) not null, MET_NAME_C varchar(50) not null, MET_TYPE_C varchar(20) not null, MET_DELETEDATE_D datetime, primary key (MET_ID_C) ); +create cached table T_DOCUMENT_METADATA ( DME_ID_C varchar(36) not null, DME_IDDOCUMENT_C varchar(36) not null, DME_IDMETADATA_C varchar(36) not null, DME_VALUE_C varchar(4000) null, primary key (DME_ID_C) ); +alter table T_DOCUMENT_METADATA add constraint FK_DME_IDDOCUMENT_C foreign key (DME_IDDOCUMENT_C) references T_DOCUMENT (DOC_ID_C) on delete restrict on update restrict; +alter table T_DOCUMENT_METADATA add constraint FK_DME_IDMETADATA_C foreign key (DME_IDMETADATA_C) references T_METADATA (MET_ID_C) on delete restrict on update restrict; +update T_CONFIG set CFG_VALUE_C = '24' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-025-0.sql b/docs-core/src/main/resources/db/update/dbupdate-025-0.sql new file mode 100644 index 00000000..bde9adf3 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-025-0.sql @@ -0,0 +1,3 @@ +insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_AUTOMATIC_TAGS', 'false'); +insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_DELETE_IMPORTED', 'false'); +update T_CONFIG set CFG_VALUE_C = '25' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-026-0.sql b/docs-core/src/main/resources/db/update/dbupdate-026-0.sql new file mode 100644 index 00000000..67a4e6b6 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-026-0.sql @@ -0,0 +1,2 @@ +!PGSQL!UPDATE t_file SET fil_content_c = convert_from(loread(lo_open(fil_content_c::int, CAST( x'20000' AS integer)), 999999999), 'UNICODE')::TEXT WHERE fil_content_c IS NOT NULL; +update T_CONFIG set CFG_VALUE_C = '26' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-027-0.sql b/docs-core/src/main/resources/db/update/dbupdate-027-0.sql new file mode 100644 index 00000000..cafa4f69 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-027-0.sql @@ -0,0 +1,2 @@ +insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_FOLDER', 'INBOX'); +update T_CONFIG set CFG_VALUE_C = '27' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-028-0.sql b/docs-core/src/main/resources/db/update/dbupdate-028-0.sql new file mode 100644 index 00000000..3c37e035 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-028-0.sql @@ -0,0 +1,2 @@ +insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('INBOX_STARTTLS', 'false'); +update T_CONFIG set CFG_VALUE_C = '28' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-029-0.sql b/docs-core/src/main/resources/db/update/dbupdate-029-0.sql new file mode 100644 index 00000000..6e455d85 --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-029-0.sql @@ -0,0 +1,2 @@ +alter table T_FILE add column FIL_SIZE_N bigint not null default -1; +update T_CONFIG set CFG_VALUE_C = '29' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/db/update/dbupdate-030-0.sql b/docs-core/src/main/resources/db/update/dbupdate-030-0.sql new file mode 100644 index 00000000..be80c0ef --- /dev/null +++ b/docs-core/src/main/resources/db/update/dbupdate-030-0.sql @@ -0,0 +1,2 @@ +create index IDX_FIL_IDDOC_C ON T_FILE (FIL_IDDOC_C ASC); +update T_CONFIG set CFG_VALUE_C = '30' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-core/src/main/resources/messages.properties.de b/docs-core/src/main/resources/messages.properties.de deleted file mode 100644 index 7c45de50..00000000 --- a/docs-core/src/main/resources/messages.properties.de +++ /dev/null @@ -1,10 +0,0 @@ -email.template.password_recovery.subject=Bitte setzen Sie ihr Passwort zur\u00FCck -email.template.password_recovery.hello=Hallo {0}. -email.template.password_recovery.instruction1=Wir haben eine Anfrage zum Zur\u00FCcksetzen Ihres Passworts erhalten.
Wenn Sie keine Hilfe angefordert haben, k\u00F6nnen Sie diese E-Mail einfach ignorieren. -email.template.password_recovery.instruction2=Um Ihr Passwort zur\u00FCckzusetzen, besuchen Sie bitte den folgenden Link: -email.template.password_recovery.click_here=Klicken Sie hier, um Ihr Passwort zur\u00FCckzusetzen -email.template.route_step_validate.subject=Ein Dokument braucht Ihre Aufmerksamkeit -email.template.route_step_validate.hello=Hallo {0}. -email.template.route_step_validate.instruction1=Ihnen wurde ein Workflow-Schritt zugewiesen, der Ihre Aufmerksamkeit erfordert. -email.template.route_step_validate.instruction2=Um das Dokument anzuzeigen und den Workflow zu \u00FCberpr\u00FCfen, besuchen Sie bitte den folgenden Link: -email.no_html.error=Ihr E-Mail-Client unterst\u00FCtzt keine HTML-Nachrichten diff --git a/docs-core/src/main/resources/messages.properties.fr b/docs-core/src/main/resources/messages.properties.fr deleted file mode 100644 index e80dd012..00000000 --- a/docs-core/src/main/resources/messages.properties.fr +++ /dev/null @@ -1,10 +0,0 @@ -email.template.password_recovery.subject=R\u00E9initialiser votre mot de passe -email.template.password_recovery.hello=Bonjour {0}. -email.template.password_recovery.instruction1=Nous avons re\u00E7u une demande de r\u00E9initialisation de mot de passe.
Si vous n'avez rien demand\u00E9, vous pouvez ignorer cet mail. -email.template.password_recovery.instruction2=Pour r\u00E9initialiser votre mot de passe, cliquez sur le lien ci-dessous : -email.template.password_recovery.click_here=Cliquez ici pour r\u00E9initialiser votre mot de passe. -email.template.route_step_validate.subject=Un document n\u00E9cessite votre attention -email.template.route_step_validate.hello=Bonjour {0}. -email.template.route_step_validate.instruction1=Une \u00E9tape de workflow vous a \u00E9t\u00E9 attribu\u00E9e et n\u00E9cessite votre attention. -email.template.route_step_validate.instruction2=Pour voir le document et valider le workflow, veuillez visiter le lien ci-dessous : -email.no_html.error=Votre client mail ne supporte pas les messages HTML diff --git a/docs-core/src/main/resources/messages.properties.zh_CN b/docs-core/src/main/resources/messages.properties.zh_CN deleted file mode 100644 index a0627b0d..00000000 --- a/docs-core/src/main/resources/messages.properties.zh_CN +++ /dev/null @@ -1,10 +0,0 @@ -email.template.password_recovery.subject=\u8BF7\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801 -email.template.password_recovery.hello=\u60A8\u597D {0}. -email.template.password_recovery.instruction1=\u6211\u4EEC\u6536\u5230\u4E86\u4E00\u4E2A\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801\u7684\u8BF7\u6C42\u3002
\u5982\u679C\u60A8\u6CA1\u6709\u53D1\u9001\u8BE5\u8BF7\u6C42\uFF0C\u8BF7\u5FFD\u7565\u6B64\u7535\u5B50\u90AE\u4EF6 -email.template.password_recovery.instruction2=\u8981\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801\uFF0C\u8BF7\u8BBF\u95EE\u4EE5\u4E0B\u94FE\u63A5\uFF1A -email.template.password_recovery.click_here=\u8BF7\u70B9\u51FB\u6B64\u5904\u91CD\u7F6E\u60A8\u7684\u5BC6\u7801 -email.template.route_step_validate.subject=\u4E00\u4EFD\u6587\u4EF6\u9700\u8981\u4F60\u7684\u5173\u6CE8 -email.template.route_step_validate.hello={0}\uFF0C\u60A8\u597D. -email.template.route_step_validate.instruction1=\u5DE5\u4F5C\u6D41\u6B65\u9AA4\u5DF2\u7ECF\u5206\u914D\u7ED9\u60A8\uFF0C\u9700\u8981\u60A8\u7684\u5173\u6CE8\u3002 -email.template.route_step_validate.instruction2=\u8981\u67E5\u770B\u6587\u6863\u5E76\u9A8C\u8BC1\u5DE5\u4F5C\u6D41\u7A0B\uFF0C\u8BF7\u8BBF\u95EE\u4EE5\u4E0B\u94FE\u63A5\uFF1A -email.no_html.error=\u60A8\u7684\u7535\u5B50\u90AE\u4EF6\u5BA2\u6237\u7AEF\u4E0D\u652F\u6301HTML\u683C\u5F0F\u90AE\u4EF6 diff --git a/docs-core/src/main/resources/messages.properties.zh_TW b/docs-core/src/main/resources/messages.properties.zh_TW deleted file mode 100644 index da961212..00000000 --- a/docs-core/src/main/resources/messages.properties.zh_TW +++ /dev/null @@ -1,10 +0,0 @@ -email.template.password_recovery.subject=\u8ACB\u91CD\u65B0\u8A2D\u7F6E\u60A8\u7684\u5BC6\u78BC -email.template.password_recovery.hello=\u60A8\u597D{0}\uFF01 -email.template.password_recovery.instruction1=\u6211\u5011\u6536\u5230\u4E86\u91CD\u7F6E\u5BC6\u78BC\u7684\u8ACB\u6C42\u3002
\u5982\u679C\u60A8\u6C92\u6709\u8ACB\u6C42\u5E6B\u52A9\uFF0C\u8ACB\u5FFD\u7565\u6B64\u96FB\u5B50\u90F5\u4EF6\u3002 -email.template.password_recovery.instruction2=\u8981\u91CD\u7F6E\u60A8\u7684\u5BC6\u78BC\uFF0C\u8ACB\u8A2A\u554F\u4EE5\u4E0B\u93C8\u63A5\uFF1A -email.template.password_recovery.click_here=\u9EDE\u64CA\u9019\u88E1\u91CD\u7F6E\u60A8\u7684\u5BC6\u78BC -email.template.route_step_validate.subject=\u4E00\u4EFD\u6587\u4EF6\u9700\u8981\u4F60\u7684\u95DC\u6CE8 -email.template.route_step_validate.hello={0}\uFF0C\u60A8\u597D. -email.template.route_step_validate.instruction1=\u5DE5\u4F5C\u6D41\u6B65\u9A5F\u5DF2\u7D93\u5206\u914D\u7D66\u60A8\uFF0C\u9700\u8981\u60A8\u7684\u95DC\u6CE8\u3002 -email.template.route_step_validate.instruction2=\u8981\u67E5\u770B\u6587\u6A94\u4E26\u9A57\u8B49\u5DE5\u4F5C\u6D41\u7A0B\uFF0C\u8ACB\u8A2A\u554F\u4EE5\u4E0B\u93C8\u63A5\uFF1A -email.no_html.error=\u60A8\u7684\u96FB\u5B50\u90F5\u4EF6\u5BA2\u6236\u7AEF\u4E0D\u652F\u6301HTML\u683C\u5F0F\u90F5\u4EF6 diff --git a/docs-core/src/main/resources/messages_pl.properties b/docs-core/src/main/resources/messages_pl.properties new file mode 100644 index 00000000..66eef3ac --- /dev/null +++ b/docs-core/src/main/resources/messages_pl.properties @@ -0,0 +1,10 @@ +email.template.password_recovery.subject=Proszę zresetować swoje hasło +email.template.password_recovery.hello=Witaj {0}. +email.template.password_recovery.instruction1=Otrzymaliśmy żądanie zresetowania twojego hasła.
Jeśli to nie ty potrzebujesz pomocy, moóżesz zignorować ten email. +email.template.password_recovery.instruction2=Aby zresetować swoje hasło, proszę naciśnij link poniżej: +email.template.password_recovery.click_here=Naciśnij, aby zresetować swoje hasło +email.template.route_step_validate.subject=Dokument potrzebuje twojej uwagi +email.template.route_step_validate.hello=Witaj {0}. +email.template.route_step_validate.instruction1=Został Ci przypisany etap przepływu i wymaga Twojej uwagi. +email.template.route_step_validate.instruction2=Aby wyświetlić dokument i zweryfikować przepływ pracy, kliknij poniższy link: +email.no_html.error=Twój klient poczty e-mail nie obsługuje wiadomości HTML \ No newline at end of file diff --git a/docs-core/src/test/java/com/sismics/BaseTest.java b/docs-core/src/test/java/com/sismics/BaseTest.java new file mode 100644 index 00000000..384bc164 --- /dev/null +++ b/docs-core/src/test/java/com/sismics/BaseTest.java @@ -0,0 +1,49 @@ +package com.sismics; + +import java.io.InputStream; +import java.net.URL; + +public abstract class BaseTest { + + protected static final String FILE_CSV = "document.csv"; + + protected static final String FILE_DOCX = "document.docx"; + + protected static final String FILE_GIF = "image.gif"; + + protected static final String FILE_JPG = "apollo_portrait.jpg"; + + protected static final Long FILE_JPG_SIZE = 7_907L; + + protected static final String FILE_JPG2 = "apollo_landscape.jpg"; + + protected static final String FILE_MP4 = "video.mp4"; + + protected static final String FILE_ODT = "document.odt"; + + protected static final String FILE_PDF = "udhr.pdf"; + + protected static final String FILE_PDF_ENCRYPTED = "udhr_encrypted.pdf"; + + protected static final String FILE_PDF_SCANNED = "scanned.pdf"; + + protected static final String FILE_PNG = "image.png"; + + protected static final String FILE_PPTX = "apache.pptx"; + + protected static final String FILE_TXT = "document.txt"; + + protected static final String FILE_WEBM = "video.webm"; + + protected static final String FILE_XLSX = "document.xlsx"; + + protected static final String FILE_ZIP = "document.zip"; + + protected static URL getResource(String fileName) { + return ClassLoader.getSystemResource("file/" + fileName); + } + + protected static InputStream getSystemResourceAsStream(String fileName) { + return ClassLoader.getSystemResourceAsStream("file/" + fileName); + } +} diff --git a/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java b/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java index 8de90b68..974c0deb 100644 --- a/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java +++ b/docs-core/src/test/java/com/sismics/docs/BaseTransactionalTest.java @@ -1,19 +1,34 @@ package com.sismics.docs; +import com.sismics.BaseTest; +import com.sismics.docs.core.dao.FileDao; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.DirectoryUtil; +import com.sismics.docs.core.util.EncryptionUtil; import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.jpa.EMF; +import com.sismics.util.mime.MimeType; import org.junit.After; import org.junit.Before; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import java.io.InputStream; +import java.nio.file.Files; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; /** * Base class of tests with a transactional context. * * @author jtremeaux */ -public abstract class BaseTransactionalTest { +public abstract class BaseTransactionalTest extends BaseTest { @Before public void setUp() throws Exception { // Initialize the entity manager @@ -27,4 +42,32 @@ public abstract class BaseTransactionalTest { @After public void tearDown() throws Exception { } + + protected User createUser(String userName) throws Exception { + UserDao userDao = new UserDao(); + User user = new User(); + user.setUsername(userName); + user.setPassword("12345678"); + user.setEmail("toto@docs.com"); + user.setRoleId("admin"); + user.setStorageQuota(100_000L); + userDao.create(user, userName); + return user; + } + + protected File createFile(User user, long fileSize) throws Exception { + FileDao fileDao = new FileDao(); + try(InputStream inputStream = getSystemResourceAsStream(FILE_JPG)) { + File file = new File(); + file.setId("apollo_portrait"); + file.setUserId(user.getId()); + file.setVersion(0); + file.setMimeType(MimeType.IMAGE_JPEG); + file.setSize(fileSize); + String fileId = fileDao.create(file, user.getId()); + Cipher cipher = EncryptionUtil.getEncryptionCipher(user.getPrivateKey()); + Files.copy(new CipherInputStream(inputStream, cipher), DirectoryUtil.getStorageDirectory().resolve(fileId), REPLACE_EXISTING); + return file; + } + } } diff --git a/docs-core/src/test/java/com/sismics/docs/core/dao/jpa/TestJpa.java b/docs-core/src/test/java/com/sismics/docs/core/dao/jpa/TestJpa.java index 0adbfe35..04c41b32 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/dao/jpa/TestJpa.java +++ b/docs-core/src/test/java/com/sismics/docs/core/dao/jpa/TestJpa.java @@ -18,22 +18,16 @@ public class TestJpa extends BaseTransactionalTest { public void testJpa() throws Exception { // Create a user UserDao userDao = new UserDao(); - User user = new User(); - user.setUsername("username"); - user.setPassword("12345678"); - user.setEmail("toto@docs.com"); - user.setRoleId("admin"); - user.setStorageQuota(10L); - String id = userDao.create(user, "me"); - + User user = createUser("testJpa"); + TransactionUtil.commit(); // Search a user by his ID - user = userDao.getById(id); + user = userDao.getById(user.getId()); Assert.assertNotNull(user); Assert.assertEquals("toto@docs.com", user.getEmail()); // Authenticate using the database - Assert.assertNotNull(new InternalAuthenticationHandler().authenticate("username", "12345678")); + Assert.assertNotNull(new InternalAuthenticationHandler().authenticate("testJpa", "12345678")); } } diff --git a/docs-core/src/test/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListenerTest.java b/docs-core/src/test/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListenerTest.java new file mode 100644 index 00000000..0cd2e6e3 --- /dev/null +++ b/docs-core/src/test/java/com/sismics/docs/core/listener/async/FileDeletedAsyncListenerTest.java @@ -0,0 +1,52 @@ +package com.sismics.docs.core.listener.async; + +import com.sismics.docs.BaseTransactionalTest; +import com.sismics.docs.core.dao.UserDao; +import com.sismics.docs.core.event.FileDeletedAsyncEvent; +import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.docs.core.util.TransactionUtil; +import org.junit.Assert; +import org.junit.Test; + +public class FileDeletedAsyncListenerTest extends BaseTransactionalTest { + + @Test + public void updateQuotaSizeKnown() throws Exception { + User user = createUser("updateQuotaSizeKnown"); + File file = createFile(user, FILE_JPG_SIZE); + UserDao userDao = new UserDao(); + user = userDao.getById(user.getId()); + user.setStorageCurrent(10_000L); + userDao.updateQuota(user); + + FileDeletedAsyncListener fileDeletedAsyncListener = new FileDeletedAsyncListener(); + TransactionUtil.commit(); + FileDeletedAsyncEvent event = new FileDeletedAsyncEvent(); + event.setFileSize(FILE_JPG_SIZE); + event.setFileId(file.getId()); + event.setUserId(user.getId()); + fileDeletedAsyncListener.on(event); + Assert.assertEquals(userDao.getById(user.getId()).getStorageCurrent(), Long.valueOf(10_000 - FILE_JPG_SIZE)); + } + + @Test + public void updateQuotaSizeUnknown() throws Exception { + User user = createUser("updateQuotaSizeUnknown"); + File file = createFile(user, File.UNKNOWN_SIZE); + UserDao userDao = new UserDao(); + user = userDao.getById(user.getId()); + user.setStorageCurrent(10_000L); + userDao.updateQuota(user); + + FileDeletedAsyncListener fileDeletedAsyncListener = new FileDeletedAsyncListener(); + TransactionUtil.commit(); + FileDeletedAsyncEvent event = new FileDeletedAsyncEvent(); + event.setFileSize(FILE_JPG_SIZE); + event.setFileId(file.getId()); + event.setUserId(user.getId()); + fileDeletedAsyncListener.on(event); + Assert.assertEquals(userDao.getById(user.getId()).getStorageCurrent(), Long.valueOf(10_000 - FILE_JPG_SIZE)); + } + +} diff --git a/docs-core/src/test/java/com/sismics/docs/core/service/TestFileSizeService.java b/docs-core/src/test/java/com/sismics/docs/core/service/TestFileSizeService.java new file mode 100644 index 00000000..f70ba75b --- /dev/null +++ b/docs-core/src/test/java/com/sismics/docs/core/service/TestFileSizeService.java @@ -0,0 +1,22 @@ +package com.sismics.docs.core.service; + +import com.sismics.docs.BaseTransactionalTest; +import com.sismics.docs.core.dao.FileDao; +import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.model.jpa.User; +import org.junit.Assert; +import org.junit.Test; + +public class TestFileSizeService extends BaseTransactionalTest { + + @Test + public void processFileTest() throws Exception { + User user = createUser("processFileTest"); + + FileDao fileDao = new FileDao(); + File file = createFile(user, File.UNKNOWN_SIZE); + FileSizeService fileSizeService = new FileSizeService(); + fileSizeService.processFile(file); + Assert.assertEquals(fileDao.getFile(file.getId()).getSize(), Long.valueOf(FILE_JPG_SIZE)); + } +} diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestEncryptUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestEncryptUtil.java index 197626df..ad06efcc 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestEncryptUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestEncryptUtil.java @@ -2,6 +2,7 @@ package com.sismics.docs.core.util; import com.google.common.base.Strings; import com.google.common.io.ByteStreams; +import com.sismics.BaseTest; import org.junit.Assert; import org.junit.Test; @@ -14,7 +15,7 @@ import java.io.InputStream; * * @author bgamard */ -public class TestEncryptUtil { +public class TestEncryptUtil extends BaseTest { @Test public void generatePrivateKeyTest() { String key = EncryptionUtil.generatePrivateKey(); @@ -31,9 +32,9 @@ public class TestEncryptUtil { // NOP } Cipher cipher = EncryptionUtil.getEncryptionCipher("OnceUponATime"); - InputStream inputStream = new CipherInputStream(this.getClass().getResourceAsStream("/file/udhr.pdf"), cipher); + InputStream inputStream = new CipherInputStream(getSystemResourceAsStream(FILE_PDF), cipher); byte[] encryptedData = ByteStreams.toByteArray(inputStream); - byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf")); + byte[] assertData = ByteStreams.toByteArray(getSystemResourceAsStream(FILE_PDF_ENCRYPTED)); Assert.assertEquals(encryptedData.length, assertData.length); } @@ -41,9 +42,9 @@ public class TestEncryptUtil { @Test public void decryptStreamTest() throws Exception { InputStream inputStream = EncryptionUtil.decryptInputStream( - this.getClass().getResourceAsStream("/file/udhr_encrypted.pdf"), "OnceUponATime"); + getSystemResourceAsStream(FILE_PDF_ENCRYPTED), "OnceUponATime"); byte[] encryptedData = ByteStreams.toByteArray(inputStream); - byte[] assertData = ByteStreams.toByteArray(this.getClass().getResourceAsStream("/file/udhr.pdf")); + byte[] assertData = ByteStreams.toByteArray(getSystemResourceAsStream(FILE_PDF)); Assert.assertEquals(encryptedData.length, assertData.length); } diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java index 1a1fddfb..c7ccd0d3 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java @@ -2,6 +2,7 @@ package com.sismics.docs.core.util; import com.google.common.collect.Lists; import com.google.common.io.Resources; +import com.sismics.BaseTest; import com.sismics.docs.core.dao.dto.DocumentDto; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.util.format.*; @@ -23,11 +24,11 @@ import java.util.Date; * * @author bgamard */ -public class TestFileUtil { +public class TestFileUtil extends BaseTest { @Test public void extractContentOpenDocumentTextTest() throws Exception { - Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI()); - FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "document.odt")); + Path path = Paths.get(getResource(FILE_ODT).toURI()); + FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_ODT)); Assert.assertNotNull(formatHandler); Assert.assertTrue(formatHandler instanceof OdtFormatHandler); String content = formatHandler.extractContent("eng", path); @@ -36,8 +37,8 @@ public class TestFileUtil { @Test public void extractContentOfficeDocumentTest() throws Exception { - Path path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI()); - FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "document.docx")); + Path path = Paths.get(getResource(FILE_DOCX).toURI()); + FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_DOCX)); Assert.assertNotNull(formatHandler); Assert.assertTrue(formatHandler instanceof DocxFormatHandler); String content = formatHandler.extractContent("eng", path); @@ -46,8 +47,8 @@ public class TestFileUtil { @Test public void extractContentPowerpointTest() throws Exception { - Path path = Paths.get(ClassLoader.getSystemResource("file/apache.pptx").toURI()); - FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "apache.pptx")); + Path path = Paths.get(getResource(FILE_PPTX).toURI()); + FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PPTX)); Assert.assertNotNull(formatHandler); Assert.assertTrue(formatHandler instanceof PptxFormatHandler); String content = formatHandler.extractContent("eng", path); @@ -56,8 +57,8 @@ public class TestFileUtil { @Test public void extractContentPdf() throws Exception { - Path path = Paths.get(ClassLoader.getSystemResource("file/udhr.pdf").toURI()); - FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "udhr.pdf")); + Path path = Paths.get(getResource(FILE_PDF).toURI()); + FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PDF)); Assert.assertNotNull(formatHandler); Assert.assertTrue(formatHandler instanceof PdfFormatHandler); String content = formatHandler.extractContent("eng", path); @@ -66,8 +67,8 @@ public class TestFileUtil { @Test public void extractContentScannedPdf() throws Exception { - Path path = Paths.get(ClassLoader.getSystemResource("file/scanned.pdf").toURI()); - FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, "scanned.pdf")); + Path path = Paths.get(getResource("scanned.pdf").toURI()); + FormatHandler formatHandler = FormatHandlerUtil.find(MimeTypeUtil.guessMimeType(path, FILE_PDF_SCANNED)); Assert.assertNotNull(formatHandler); Assert.assertTrue(formatHandler instanceof PdfFormatHandler); String content = formatHandler.extractContent("eng", path); @@ -76,12 +77,12 @@ public class TestFileUtil { @Test public void convertToPdfTest() throws Exception { - try (InputStream inputStream0 = Resources.getResource("file/apollo_landscape.jpg").openStream(); - InputStream inputStream1 = Resources.getResource("file/apollo_portrait.jpg").openStream(); - InputStream inputStream2 = Resources.getResource("file/udhr_encrypted.pdf").openStream(); - InputStream inputStream3 = Resources.getResource("file/document.docx").openStream(); - InputStream inputStream4 = Resources.getResource("file/document.odt").openStream(); - InputStream inputStream5 = Resources.getResource("file/apache.pptx").openStream()) { + try (InputStream inputStream0 = getSystemResourceAsStream(FILE_JPG2); + InputStream inputStream1 = getSystemResourceAsStream(FILE_JPG); + InputStream inputStream2 = getSystemResourceAsStream(FILE_PDF_ENCRYPTED); + InputStream inputStream3 = getSystemResourceAsStream(FILE_DOCX); + InputStream inputStream4 = getSystemResourceAsStream(FILE_ODT); + InputStream inputStream5 = getSystemResourceAsStream(FILE_PPTX)) { // Document DocumentDto documentDto = new DocumentDto(); documentDto.setTitle("My super document 1"); @@ -137,7 +138,6 @@ public class TestFileUtil { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PdfUtil.convertToPdf(documentDto, Lists.newArrayList(file0, file1, file2, file3, file4, file5), true, true, 10, outputStream); Assert.assertTrue(outputStream.toByteArray().length > 0); - com.google.common.io.Files.write(outputStream.toByteArray(), new java.io.File("C:\\Users\\Jendib\\Downloads\\test.pdf")); } } } diff --git a/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java b/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java index b6e64299..b941ef16 100644 --- a/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java +++ b/docs-core/src/test/java/com/sismics/util/TestMimeTypeUtil.java @@ -1,5 +1,6 @@ package com.sismics.util; +import com.sismics.BaseTest; import com.sismics.util.mime.MimeType; import com.sismics.util.mime.MimeTypeUtil; import org.junit.Assert; @@ -13,19 +14,59 @@ import java.nio.file.Paths; * * @author bgamard */ -public class TestMimeTypeUtil { +public class TestMimeTypeUtil extends BaseTest { @Test - public void guessOpenDocumentFormatTest() throws Exception { + public void test() throws Exception { // Detect ODT files - Path path = Paths.get(ClassLoader.getSystemResource("file/document.odt").toURI()); - Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessMimeType(path, "document.odt")); + Path path = Paths.get(getResource(FILE_ODT).toURI()); + Assert.assertEquals(MimeType.OPEN_DOCUMENT_TEXT, MimeTypeUtil.guessMimeType(path, FILE_ODT)); // Detect DOCX files - path = Paths.get(ClassLoader.getSystemResource("file/document.docx").toURI()); - Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessMimeType(path, "document.odt")); + path = Paths.get(getResource(FILE_DOCX).toURI()); + Assert.assertEquals(MimeType.OFFICE_DOCUMENT, MimeTypeUtil.guessMimeType(path, FILE_ODT)); // Detect PPTX files - path = Paths.get(ClassLoader.getSystemResource("file/apache.pptx").toURI()); - Assert.assertEquals(MimeType.OFFICE_PRESENTATION, MimeTypeUtil.guessMimeType(path, "apache.pptx")); + path = Paths.get(getResource(FILE_PPTX).toURI()); + Assert.assertEquals(MimeType.OFFICE_PRESENTATION, MimeTypeUtil.guessMimeType(path, FILE_PPTX)); + + // Detect XLSX files + path = Paths.get(getResource(FILE_XLSX).toURI()); + Assert.assertEquals(MimeType.OFFICE_SHEET, MimeTypeUtil.guessMimeType(path, FILE_XLSX)); + + // Detect TXT files + path = Paths.get(getResource(FILE_TXT).toURI()); + Assert.assertEquals(MimeType.TEXT_PLAIN, MimeTypeUtil.guessMimeType(path, FILE_TXT)); + + // Detect CSV files + path = Paths.get(getResource(FILE_CSV).toURI()); + Assert.assertEquals(MimeType.TEXT_CSV, MimeTypeUtil.guessMimeType(path, FILE_CSV)); + + // Detect PDF files + path = Paths.get(getResource(FILE_PDF).toURI()); + Assert.assertEquals(MimeType.APPLICATION_PDF, MimeTypeUtil.guessMimeType(path, FILE_PDF)); + + // Detect JPEG files + path = Paths.get(getResource(FILE_JPG).toURI()); + Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(path, FILE_JPG)); + + // Detect GIF files + path = Paths.get(getResource(FILE_GIF).toURI()); + Assert.assertEquals(MimeType.IMAGE_GIF, MimeTypeUtil.guessMimeType(path, FILE_GIF)); + + // Detect PNG files + path = Paths.get(getResource(FILE_PNG).toURI()); + Assert.assertEquals(MimeType.IMAGE_PNG, MimeTypeUtil.guessMimeType(path, FILE_PNG)); + + // Detect ZIP files + path = Paths.get(getResource(FILE_ZIP).toURI()); + Assert.assertEquals(MimeType.APPLICATION_ZIP, MimeTypeUtil.guessMimeType(path, FILE_ZIP)); + + // Detect WEBM files + path = Paths.get(getResource(FILE_WEBM).toURI()); + Assert.assertEquals(MimeType.VIDEO_WEBM, MimeTypeUtil.guessMimeType(path, FILE_WEBM)); + + // Detect MP4 files + path = Paths.get(getResource(FILE_MP4).toURI()); + Assert.assertEquals(MimeType.VIDEO_MP4, MimeTypeUtil.guessMimeType(path, FILE_MP4)); } } diff --git a/docs-core/src/test/java/com/sismics/util/format/TestPdfFormatHandler.java b/docs-core/src/test/java/com/sismics/util/format/TestPdfFormatHandler.java new file mode 100644 index 00000000..42f99398 --- /dev/null +++ b/docs-core/src/test/java/com/sismics/util/format/TestPdfFormatHandler.java @@ -0,0 +1,28 @@ +package com.sismics.util.format; + +import com.sismics.BaseTest; +import com.sismics.docs.core.util.format.PdfFormatHandler; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.file.Paths; + +/** + * Test of {@link PdfFormatHandler} + * + * @author bgamard + */ +public class TestPdfFormatHandler extends BaseTest { + /** + * Test related to https://github.com/sismics/docs/issues/373. + */ + @Test + public void testIssue373() throws Exception { + PdfFormatHandler formatHandler = new PdfFormatHandler(); + String content = formatHandler.extractContent("deu", Paths.get(getResource("issue373.pdf").toURI())); + Assert.assertTrue(content.contains("Aufrechterhaltung")); + Assert.assertTrue(content.contains("Außentemperatur")); + Assert.assertTrue(content.contains("Grundumsatzmessungen")); + Assert.assertTrue(content.contains("ermitteln")); + } +} diff --git a/docs-core/src/test/resources/file/document.csv b/docs-core/src/test/resources/file/document.csv new file mode 100644 index 00000000..f26e670a --- /dev/null +++ b/docs-core/src/test/resources/file/document.csv @@ -0,0 +1,2 @@ +col1,col2 +test,me \ No newline at end of file diff --git a/docs-core/src/test/resources/file/document.txt b/docs-core/src/test/resources/file/document.txt new file mode 100644 index 00000000..c076d962 --- /dev/null +++ b/docs-core/src/test/resources/file/document.txt @@ -0,0 +1 @@ +test me. \ No newline at end of file diff --git a/docs-core/src/test/resources/file/document.xlsx b/docs-core/src/test/resources/file/document.xlsx new file mode 100644 index 00000000..c67d3776 Binary files /dev/null and b/docs-core/src/test/resources/file/document.xlsx differ diff --git a/docs-core/src/test/resources/file/document.zip b/docs-core/src/test/resources/file/document.zip new file mode 100644 index 00000000..f9d8d7de Binary files /dev/null and b/docs-core/src/test/resources/file/document.zip differ diff --git a/docs-core/src/test/resources/file/image.gif b/docs-core/src/test/resources/file/image.gif new file mode 100644 index 00000000..6d302dc6 Binary files /dev/null and b/docs-core/src/test/resources/file/image.gif differ diff --git a/docs-core/src/test/resources/file/image.png b/docs-core/src/test/resources/file/image.png new file mode 100644 index 00000000..48b92ecc Binary files /dev/null and b/docs-core/src/test/resources/file/image.png differ diff --git a/docs-core/src/test/resources/file/issue373.pdf b/docs-core/src/test/resources/file/issue373.pdf new file mode 100644 index 00000000..180fc9b7 Binary files /dev/null and b/docs-core/src/test/resources/file/issue373.pdf differ diff --git a/docs-core/src/test/resources/file/video.mp4 b/docs-core/src/test/resources/file/video.mp4 new file mode 100644 index 00000000..3355ae77 Binary files /dev/null and b/docs-core/src/test/resources/file/video.mp4 differ diff --git a/docs-core/src/test/resources/file/video.webm b/docs-core/src/test/resources/file/video.webm new file mode 100644 index 00000000..0757a975 Binary files /dev/null and b/docs-core/src/test/resources/file/video.webm differ diff --git a/docs-core/src/test/resources/hibernate.properties b/docs-core/src/test/resources/hibernate.properties index 2c068af9..b47cd667 100644 --- a/docs-core/src/test/resources/hibernate.properties +++ b/docs-core/src/test/resources/hibernate.properties @@ -8,7 +8,6 @@ hibernate.show_sql=true hibernate.format_sql=false hibernate.max_fetch_depth=5 hibernate.cache.use_second_level_cache=false -hibernate.c3p0.min_size=1 -hibernate.c3p0.max_size=10 -hibernate.c3p0.timeout=0 -hibernate.c3p0.max_statements=0 \ No newline at end of file +hibernate.connection.initial_pool_size=1 +hibernate.connection.pool_size=10 +hibernate.connection.pool_validation_interval=5 \ No newline at end of file diff --git a/docs-core/src/test/resources/log4j.properties b/docs-core/src/test/resources/log4j.properties index b13ac1dc..7fc5f2b5 100644 --- a/docs-core/src/test/resources/log4j.properties +++ b/docs-core/src/test/resources/log4j.properties @@ -6,4 +6,5 @@ log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender log4j.appender.MEMORY.size=1000 log4j.logger.com.sismics=INFO -log4j.logger.org.hibernate=ERROR \ No newline at end of file +log4j.logger.org.hibernate=ERROR +log4j.logger.org.apache.directory=ERROR \ No newline at end of file diff --git a/docs-importer/Dockerfile b/docs-importer/Dockerfile new file mode 100644 index 00000000..d83d981e --- /dev/null +++ b/docs-importer/Dockerfile @@ -0,0 +1,16 @@ +FROM node:14.2-alpine AS builder +WORKDIR /build +COPY main.js package-lock.json package.json ./ +RUN npm install && npm install -g pkg +RUN pkg -t node14-alpine-x64 . + +FROM alpine +ENV TEEDY_TAG= TEEDY_ADDTAGS=false TEEDY_LANG=eng TEEDY_URL='http://localhost:8080' TEEDY_USERNAME=username TEEDY_PASSWORD=password TEEDY_COPYFOLDER= TEEDY_FILEFILTER=* +RUN apk add --no-cache \ + libc6-compat \ + libstdc++ +ADD pref /root/.config/preferences/com.sismics.docs.importer.pref +ADD env.sh / +COPY --from=builder /build/teedy-importer ./ + +CMD ["/bin/ash","-c","/env.sh && /teedy-importer -d"] diff --git a/docs-importer/README.md b/docs-importer/README.md index 68afac71..3dae004d 100644 --- a/docs-importer/README.md +++ b/docs-importer/README.md @@ -1,35 +1,53 @@ -File Importer -============= +# File Importer This tool can be used to do a single import of files or to periodically scan for files in an input folder. -Downloads ---------- +## Downloads + Built binaries for Windows/Linux/MacOSX can be found at -Usage ------ +## Usage + ```console ./docs-importer-macos (for MacOSX) ./docs-importer-linux (for Linux) docs-importer-win.exe (for Windows) ``` -A wizard will ask you for the import configuration and write it in `~/.config/preferences/com.sismics.docs.importer.pref` +A wizard will ask you for the import configuration and write it in `~/.config/preferences/com.sismics.docs.importer.pref`. +Words following a `#` in the filename will be added as tags to the document, if there is a tag with the same name on the Server. For the next start, pass the `-d` argument to skip the wizard: + ```console ./docs-importer-linux -d ``` -Daemon mode ------------ -The daemon mode scan the input directory every 30 seconds for new files. Once a file is found and imported, it is **deleted**. +## Daemon mode + +The daemon mode scan the input directory every 30 seconds for new files. Once a file is found and imported, it is **deleted**. You can set a `copyFolder` to copy the file to before deletion. + +## Docker + +The docker image needs a volume mounted from a previously generated preference file at `/root/.config/preferences/com.sismics.docs.importer.pref`. The container will start the importer in daemon mode. It will look for files in `/import`. +Example usage: + +``` +docker run --name teedy-import -d -v /path/to/preferencefile:/root/.config/preferences/com.sismics.docs.importer.pref -v /path/to/import/folder:/import sismics/docs-importer:latest +``` +### Environment variables +Instead of mounting the preferences file, the options can also be set by setting the environment variables `TEEDY_TAG`, `TEEDY_ADDTAGS`, `TEEDY_LANG`, `TEEDY_COPYFOLDER`, `TEEDY_FILEFILTER`, `TEEDY_URL`, `TEEDY_USERNAME` and `TEEDY_PASSWORD`. +The latter three have to be set for the importer to work. The value of `TEEDY_TAG` has to be set to the UUID of the tag, not the name (The UUID can be found by visiting `baseUrl/api/tag/list` in your browser). +Example usage: + +``` +docker run --name teedy-import -d -e TEEDY_TAG=2071fdf7-0e26-409d-b53d-f25823a5eb9e -e TEEDY_ADDTAGS=false -e TEEDY_LANG=eng -e TEEDY_URL='http://teedy.example.com:port' -e TEEDY_USERNAME=username -e TEEDY_PASSWORD=superSecretPassword -v /path/to/import/folder:/import sismics/docs-importer:latest +``` + +## Build from sources -Build from sources ------------------- ```console npm install npm install -g pkg pkg . -``` \ No newline at end of file +``` diff --git a/docs-importer/env.sh b/docs-importer/env.sh new file mode 100755 index 00000000..0e8771f7 --- /dev/null +++ b/docs-importer/env.sh @@ -0,0 +1,11 @@ +#!/bin/ash +file=/root/.config/preferences/com.sismics.docs.importer.pref +sed -i "s/env1/$TEEDY_TAG/g" $file +sed -i "s/env2/$TEEDY_ADDTAGS/g" $file +sed -i "s/env3/$TEEDY_LANG/g" $file +sed -i "s,env4,$TEEDY_URL,g" $file +sed -i "s/env5/$TEEDY_USERNAME/g" $file +sed -i "s/env6/$TEEDY_PASSWORD/g" $file +sed -i "s,env7,$TEEDY_COPYFOLDER,g" $file +sed -i "s,env8,$TEEDY_FILEFILTER,g" $file +echo "Environment variables replaced" \ No newline at end of file diff --git a/docs-importer/main.js b/docs-importer/main.js index 0e64542b..813d02f7 100644 --- a/docs-importer/main.js +++ b/docs-importer/main.js @@ -1,6 +1,7 @@ 'use strict'; const recursive = require('recursive-readdir'); +const minimatch = require("minimatch"); const ora = require('ora'); const inquirer = require('inquirer'); const preferences = require('preferences'); @@ -10,6 +11,7 @@ const _ = require('underscore'); const request = require('request').defaults({ jar: true }); +const qs = require('querystring'); // Load preferences const prefs = new preferences('com.sismics.docs.importer',{ @@ -22,7 +24,7 @@ const prefs = new preferences('com.sismics.docs.importer',{ }); // Welcome message -console.log('Teedy Importer 1.0.0, https://teedy.io' + +console.log('Teedy Importer 1.9, https://teedy.io' + '\n\n' + 'This program let you import files from your system to Teedy' + '\n'); @@ -141,13 +143,32 @@ const askPath = () => { recursive(answers.path, function (error, files) { spinner.succeed(files.length + ' files in this directory'); - askTag(); + askFileFilter(); }); }); }); }); }; +// Ask for the file filter +const askFileFilter = () => { + console.log(''); + + inquirer.prompt([ + { + type: 'input', + name: 'fileFilter', + message: 'What pattern do you want to use to match files? (eg. *.+(pdf|txt|jpg))', + default: prefs.importer.fileFilter || "*" + } + ]).then(answers => { + // Save fileFilter + prefs.importer.fileFilter = answers.fileFilter; + + askTag(); + }); +}; + // Ask for the tag to add const askTag = () => { console.log(''); @@ -176,7 +197,7 @@ const askTag = () => { { type: 'list', name: 'tag', - message: 'Which tag to add on imported documents?', + message: 'Which tag to add to all imported documents?', default: defaultTagName, choices: [ 'No tag' ].concat(_.pluck(tags, 'name')) } @@ -184,11 +205,109 @@ const askTag = () => { // Save tag prefs.importer.tag = answers.tag === 'No tag' ? '' : _.findWhere(tags, { name: answers.tag }).id; - askDaemon(); + askAddTag(); }); }); }; + +const askAddTag = () => { + console.log(''); + + inquirer.prompt([ + { + type: 'confirm', + name: 'addtags', + message: 'Do you want to add tags from the filename given with # ?', + default: prefs.importer.addtags === true + } + ]).then(answers => { + // Save daemon + prefs.importer.addtags = answers.addtags; + + // Save all preferences in case the program is sig-killed + askLang(); + }); +} + + +const askLang = () => { + console.log(''); + + // Load tags + const spinner = ora({ + text: 'Loading default language', + spinner: 'flips' + }).start(); + + request.get({ + url: prefs.importer.baseUrl + '/api/app', + }, function (error, response, body) { + if (error || !response || response.statusCode !== 200) { + spinner.fail('Connection to Teedy failed: ' + error); + askLang(); + return; + } + spinner.succeed('Language loaded'); + const defaultLang = prefs.importer.lang ? prefs.importer.lang : JSON.parse(body).default_language; + + inquirer.prompt([ + { + type: 'input', + name: 'lang', + message: 'Which should be the default language of the document?', + default: defaultLang + } + ]).then(answers => { + // Save tag + prefs.importer.lang = answers.lang + askCopyFolder(); + }); + }); +}; + +const askCopyFolder = () => { + console.log(''); + + inquirer.prompt([ + { + type: 'input', + name: 'copyFolder', + message: 'Enter a path to copy files before they are deleted or leave empty to disable. The path must end with a \'/\' on MacOS and Linux or with a \'\\\' on Windows. Entering \'undefined\' will disable this again after setting the folder.', + default: prefs.importer.copyFolder + } + ]).then(answers => { + // Save path + prefs.importer.copyFolder = answers.copyFolder=='undefined' ? '' : answers.copyFolder; + + if (prefs.importer.copyFolder) { + // Test path + const spinner = ora({ + text: 'Checking copy folder path', + spinner: 'flips' + }).start(); + fs.lstat(answers.copyFolder, (error, stats) => { + if (error || !stats.isDirectory()) { + spinner.fail('Please enter a valid directory path'); + askCopyFolder(); + return; + } + + fs.access(answers.copyFolder, fs.W_OK | fs.R_OK, (error) => { + if (error) { + spinner.fail('This directory is not writable'); + askCopyFolder(); + return; + } + spinner.succeed('Copy folder set!'); + askDaemon(); + }); + }); + } + else {askDaemon();} + }); +}; + // Ask for daemon mode const askDaemon = () => { console.log(''); @@ -245,6 +364,8 @@ const start = () => { // Import the files const importFiles = (remove, filesImported) => { recursive(prefs.importer.path, function (error, files) { + + files = files.filter(minimatch.filter(prefs.importer.fileFilter || '*', { matchBase: true })); if (files.length === 0) { filesImported(); return; @@ -270,37 +391,94 @@ const importFile = (file, remove, resolve) => { spinner: 'flips' }).start(); - request.put({ - url: prefs.importer.baseUrl + '/api/document', - form: { - title: file.replace(/^.*[\\\/]/, ''), - language: 'eng', - tags: prefs.importer.tag === '' ? undefined : prefs.importer.tag - } - }, function (error, response, body) { + // Remove path of file + let filename = file.replace(/^.*[\\\/]/, ''); + + // Get Tags given as hashtags from filename + let taglist = filename.match(/#[^\s:#]+/mg); + taglist = taglist ? taglist.map(s => s.substr(1)) : []; + + // Get available tags and UUIDs from server + request.get({ + url: prefs.importer.baseUrl + '/api/tag/list', + }, function (error, response, body) { if (error || !response || response.statusCode !== 200) { - spinner.fail('Upload failed for ' + file + ': ' + error); - resolve(); + spinner.fail('Error loading tags'); return; } + + let tagsarray = {}; + for (let l of JSON.parse(body).tags) { + tagsarray[l.name] = l.id; + } - request.put({ - url: prefs.importer.baseUrl + '/api/file', - formData: { - id: JSON.parse(body).id, - file: fs.createReadStream(file) + // Intersect tags from filename with existing tags on server + let foundtags = []; + for (let j of taglist) { + // If the tag is last in the filename it could include a file extension and would not be recognized + if (j.includes('.') && !tagsarray.hasOwnProperty(j) && !foundtags.includes(tagsarray[j])) { + while (j.includes('.') && !tagsarray.hasOwnProperty(j)) { + j = j.replace(/\.[^.]*$/,''); + } } - }, function (error, response) { + if (tagsarray.hasOwnProperty(j) && !foundtags.includes(tagsarray[j])) { + foundtags.push(tagsarray[j]); + filename = filename.split('#'+j).join(''); + } + } + if (prefs.importer.tag !== '' && !foundtags.includes(prefs.importer.tag)){ + foundtags.push(prefs.importer.tag); + } + + let data = {} + if (prefs.importer.addtags) { + data = { + title: prefs.importer.addtags ? filename : file.replace(/^.*[\\\/]/, '').substring(0, 100), + language: prefs.importer.lang || 'eng', + tags: foundtags + } + } + else { + data = { + title: prefs.importer.addtags ? filename : file.replace(/^.*[\\\/]/, '').substring(0, 100), + language: prefs.importer.lang || 'eng', + tags: prefs.importer.tag === '' ? undefined : prefs.importer.tag + } + } + // Create document + request.put({ + url: prefs.importer.baseUrl + '/api/document', + form: qs.stringify(data) + }, function (error, response, body) { if (error || !response || response.statusCode !== 200) { spinner.fail('Upload failed for ' + file + ': ' + error); resolve(); return; } - spinner.succeed('Upload successful for ' + file); - if (remove) { - fs.unlinkSync(file); - } - resolve(); + + // Upload file + request.put({ + url: prefs.importer.baseUrl + '/api/file', + formData: { + id: JSON.parse(body).id, + file: fs.createReadStream(file) + } + }, function (error, response) { + if (error || !response || response.statusCode !== 200) { + spinner.fail('Upload failed for ' + file + ': ' + error); + resolve(); + return; + } + spinner.succeed('Upload successful for ' + file); + if (remove) { + if (prefs.importer.copyFolder) { + fs.copyFileSync(file, prefs.importer.copyFolder + file.replace(/^.*[\\\/]/, '')); + fs.unlinkSync(file); + } + else {fs.unlinkSync(file);} + } + resolve(); + }); }); }); }; @@ -312,7 +490,12 @@ if (argv.hasOwnProperty('d')) { 'Username: ' + prefs.importer.username + '\n' + 'Password: ***********\n' + 'Tag: ' + prefs.importer.tag + '\n' + - 'Daemon mode: ' + prefs.importer.daemon); + 'Add tags given #: ' + prefs.importer.addtags + '\n' + + 'Language: ' + prefs.importer.lang + '\n' + + 'Daemon mode: ' + prefs.importer.daemon + '\n' + + 'Copy folder: ' + prefs.importer.copyFolder + '\n' + + 'File filter: ' + prefs.importer.fileFilter + ); start(); } else { askBaseUrl(); diff --git a/docs-importer/package-lock.json b/docs-importer/package-lock.json index f2b988fd..6584665e 100644 --- a/docs-importer/package-lock.json +++ b/docs-importer/package-lock.json @@ -1,6 +1,6 @@ { - "name": "docs-importer", - "version": "1.5.1", + "name": "teedy-importer", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9,10 +9,10 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" } }, "ansi-escapes": { @@ -30,7 +30,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", "requires": { - "color-convert": "1.9.1" + "color-convert": "^1.9.0" } }, "argparse": { @@ -38,7 +38,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "asn1": { @@ -75,9 +75,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "boom": { @@ -85,7 +84,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } }, "brace-expansion": { @@ -93,7 +92,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -107,9 +106,9 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "5.2.0" + "ansi-styles": "^3.2.0", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.2.0" } }, "chardet": { @@ -122,7 +121,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "requires": { - "restore-cursor": "2.0.0" + "restore-cursor": "^2.0.0" } }, "cli-spinners": { @@ -150,7 +149,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "requires": { - "color-name": "1.1.3" + "color-name": "^1.1.1" } }, "color-name": { @@ -163,7 +162,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "concat-map": { @@ -181,7 +180,7 @@ "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "requires": { - "boom": "5.2.0" + "boom": "5.x.x" }, "dependencies": { "boom": { @@ -189,7 +188,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } } } @@ -199,7 +198,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "defaults": { @@ -207,7 +206,7 @@ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", "requires": { - "clone": "1.0.3" + "clone": "^1.0.2" } }, "delayed-stream": { @@ -219,9 +218,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "escape-string-regexp": { @@ -244,9 +242,9 @@ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", "requires": { - "chardet": "0.4.2", - "iconv-lite": "0.4.19", - "tmp": "0.0.33" + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" } }, "extsprintf": { @@ -269,7 +267,7 @@ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", "requires": { - "escape-string-regexp": "1.0.5" + "escape-string-regexp": "^1.0.5" } }, "forever-agent": { @@ -282,9 +280,9 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { - "asynckit": "0.4.0", + "asynckit": "^0.4.0", "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "mime-types": "^2.1.12" } }, "getpass": { @@ -292,7 +290,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "graceful-fs": { @@ -310,8 +308,8 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" + "ajv": "^5.1.0", + "har-schema": "^2.0.0" } }, "has-flag": { @@ -324,10 +322,10 @@ "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" + "boom": "4.x.x", + "cryptiles": "3.x.x", + "hoek": "4.x.x", + "sntp": "2.x.x" } }, "hoek": { @@ -340,9 +338,9 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -360,19 +358,19 @@ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.1.0.tgz", "integrity": "sha512-kn7N70US1MSZHZHSGJLiZ7iCwwncc7b0gc68YtlX29OjI3Mp0tSVV+snVXpZ1G+ONS3Ac9zd1m6hve2ibLDYfA==", "requires": { - "ansi-escapes": "3.0.0", - "chalk": "2.3.1", - "cli-cursor": "2.1.0", - "cli-width": "2.2.0", - "external-editor": "2.1.0", - "figures": "2.0.0", - "lodash": "4.17.5", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", "mute-stream": "0.0.7", - "run-async": "2.3.0", - "rxjs": "5.5.6", - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "through": "2.3.8" + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" } }, "is-fullwidth-code-point": { @@ -396,19 +394,18 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "requires": { - "argparse": "1.0.10", - "esprima": "4.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "json-schema": { "version": "0.2.3", @@ -437,16 +434,16 @@ } }, "lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "requires": { - "chalk": "2.3.1" + "chalk": "^2.0.1" } }, "mime-db": { @@ -459,7 +456,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.33.0" } }, "mimic-fn": { @@ -472,27 +469,20 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } + "minimist": "^1.2.5" } }, "mute-stream": { @@ -510,7 +500,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "requires": { - "mimic-fn": "1.2.0" + "mimic-fn": "^1.0.0" } }, "ora": { @@ -518,12 +508,12 @@ "resolved": "https://registry.npmjs.org/ora/-/ora-2.0.0.tgz", "integrity": "sha512-g+IR0nMUXq1k4nE3gkENbN4wkF0XsVZFyxznTF6CdmwQ9qeTGONGpSR9LM5//1l0TVvJoJF3MkMtJp6slUsWFg==", "requires": { - "chalk": "2.3.1", - "cli-cursor": "2.1.0", - "cli-spinners": "1.1.0", - "log-symbols": "2.2.0", - "strip-ansi": "4.0.0", - "wcwidth": "1.0.1" + "chalk": "^2.3.1", + "cli-cursor": "^2.1.0", + "cli-spinners": "^1.1.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^4.0.0", + "wcwidth": "^1.0.1" } }, "os-homedir": { @@ -546,11 +536,11 @@ "resolved": "https://registry.npmjs.org/preferences/-/preferences-1.0.2.tgz", "integrity": "sha512-cRjA8Galk1HDDBOKjx6DhTwfy5+FVZtH7ogg6rgTLX8Ak4wi55RaS4uRztJuVPd+md1jZo99bH/h1Q9bQQK8bg==", "requires": { - "graceful-fs": "4.1.11", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "os-homedir": "1.0.2", - "write-file-atomic": "1.3.4" + "graceful-fs": "^4.1.2", + "js-yaml": "^3.10.0", + "mkdirp": "^0.5.1", + "os-homedir": "^1.0.1", + "write-file-atomic": "^1.1.3" } }, "punycode": { @@ -559,9 +549,9 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" }, "recursive-readdir": { "version": "2.2.2", @@ -576,28 +566,35 @@ "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "aws-sign2": "~0.7.0", + "aws4": "^1.6.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.1", + "forever-agent": "~0.6.1", + "form-data": "~2.3.1", + "har-validator": "~5.0.3", + "hawk": "~6.0.2", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.17", + "oauth-sign": "~0.8.2", + "performance-now": "^2.1.0", + "qs": "~6.5.1", + "safe-buffer": "^5.1.1", + "stringstream": "~0.0.5", + "tough-cookie": "~2.3.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.1.0" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + } } }, "restore-cursor": { @@ -605,8 +602,8 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "requires": { - "onetime": "2.0.1", - "signal-exit": "3.0.2" + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" } }, "run-async": { @@ -614,7 +611,7 @@ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", "requires": { - "is-promise": "2.1.0" + "is-promise": "^2.1.0" } }, "rxjs": { @@ -630,6 +627,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -645,7 +647,7 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "requires": { - "hoek": "4.2.1" + "hoek": "4.x.x" } }, "sprintf-js": { @@ -654,18 +656,19 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" } }, "string-width": { @@ -673,8 +676,8 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "stringstream": { @@ -687,7 +690,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" } }, "supports-color": { @@ -695,7 +698,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } }, "symbol-observable": { @@ -713,7 +716,7 @@ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "requires": { - "os-tmpdir": "1.0.2" + "os-tmpdir": "~1.0.2" } }, "tough-cookie": { @@ -721,7 +724,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" } }, "tunnel-agent": { @@ -729,14 +732,13 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "underscore": { "version": "1.8.3", @@ -753,9 +755,9 @@ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "wcwidth": { @@ -763,7 +765,7 @@ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", "requires": { - "defaults": "1.0.3" + "defaults": "^1.0.3" } }, "write-file-atomic": { @@ -771,9 +773,9 @@ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "slide": "1.1.6" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" } } } diff --git a/docs-importer/package.json b/docs-importer/package.json index f3b662d6..360a9d4b 100644 --- a/docs-importer/package.json +++ b/docs-importer/package.json @@ -1,6 +1,6 @@ { "name": "teedy-importer", - "version": "1.5.1", + "version": "1.9.0", "description": "Import files to Teedy", "bin": "main.js", "scripts": { @@ -11,6 +11,9 @@ "url": "git+https://github.com/sismics/docs.git" }, "author": "Benjamin Gamard", + "contributors": [ + "Cornelius Hoffmann " + ], "license": "GPL-2.0", "bugs": { "url": "https://github.com/sismics/docs/issues" @@ -18,10 +21,12 @@ "homepage": "https://github.com/sismics/docs#readme", "dependencies": { "inquirer": "^5.1.0", - "minimist": "^1.2.0", + "minimist": "^1.2.5", "ora": "^2.0.0", "preferences": "^1.0.2", + "qs": "^6.9.4", "recursive-readdir": "^2.2.2", + "minimatch": "^3.0.4", "request": "^2.83.0", "underscore": "^1.8.3" } diff --git a/docs-importer/pref b/docs-importer/pref new file mode 100644 index 00000000..99830062 --- /dev/null +++ b/docs-importer/pref @@ -0,0 +1,11 @@ +importer: + daemon: true + path: import + tag: 'env1' + addtags: 'env2' + lang: 'env3' + baseUrl: 'env4' + username: 'env5' + password: 'env6' + copyFolder: 'env7' + fileFilter: 'env8' \ No newline at end of file diff --git a/docs-stress/pom.xml b/docs-stress/pom.xml deleted file mode 100644 index 1c3a6d93..00000000 --- a/docs-stress/pom.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - com.sismics.docs - docs-parent - 1.6-SNAPSHOT - .. - - - 4.0.0 - docs-stress - jar - Docs Stress - - - - - org.glassfish.jersey.core - jersey-client - - - - org.glassfish.jersey.media - jersey-media-multipart - - - - - com.sismics.docs - docs-web-common - - - - com.sismics.docs - docs-web-common - test-jar - - - - - com.google.guava - guava - - - - log4j - log4j - - - - org.slf4j - slf4j-log4j12 - - - - org.slf4j - slf4j-api - - - - org.slf4j - jcl-over-slf4j - - - - junit - junit - - - - - - - - src/main/resources - - - - diff --git a/docs-stress/src/main/java/com/sismics/docs/stress/Main.java b/docs-stress/src/main/java/com/sismics/docs/stress/Main.java deleted file mode 100644 index 3a7f78fd..00000000 --- a/docs-stress/src/main/java/com/sismics/docs/stress/Main.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.sismics.docs.stress; - -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; -import com.google.common.io.Resources; -import com.sismics.docs.rest.util.ClientUtil; -import com.sismics.util.filter.TokenBasedSecurityFilter; -import org.glassfish.jersey.client.ClientResponse; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; -import org.glassfish.jersey.media.multipart.MultiPartFeature; -import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; -import org.junit.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.json.JsonObject; -import javax.ws.rs.client.*; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response.Status; -import java.io.InputStream; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -/** - * Stress app for Teedy. - * - * @author bgamard - */ -public class Main { - /** - * Logger. - */ - private static final Logger log = LoggerFactory.getLogger(Main.class); - - private static final String API_URL = "http://localhost:9999/docs-web/api/"; - private static final int USER_COUNT = 50; - private static final int DOCUMENT_PER_USER_COUNT = 2000; - private static final int TAG_PER_USER_COUNT = 20; - private static final int FILE_PER_DOCUMENT_COUNT = 10; - - private static Client client = ClientBuilder.newClient(); - - private static Set userSet = Sets.newHashSet(); - - /** - * Entry point. - * - * @param args Args - * @throws Exception - */ - public static void main(String[] args) throws Exception { - log.info("Starting stress test..."); - - WebTarget resource = client.target(API_URL); - ClientUtil clientUtil = new ClientUtil(resource); - - // Create users - for (int i = 0; i < USER_COUNT; i++) { - String username = generateString(); - clientUtil.createUser(username); - userSet.add(new User(username, (clientUtil.login(username)))); - log.info("Created user " + (i + 1) + "/" + USER_COUNT); - } - - // Create tags for each user - int tagCreatedCount = 1; - for (User user : userSet) { - Invocation.Builder tagResource = resource.path("/tag").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, user.authToken); - - for (int j = 0; j < TAG_PER_USER_COUNT; j++) { - Form form = new Form(); - String name = generateString(); - form.param("name", name); - form.param("color", "#ff0000"); - JsonObject json = tagResource.put(Entity.form(form), JsonObject.class); - user.tagList.add(json.getString("id")); - log.info("Created tag " + (tagCreatedCount++) + "/" + TAG_PER_USER_COUNT * USER_COUNT); - } - } - - // Create documents for each user - int documentCreatedCount = 1; - for (User user : userSet) { - for (int i = 0; i < DOCUMENT_PER_USER_COUNT; i++) { - long createDate = new Date().getTime(); - Form form = new Form() - .param("title", generateString()) - .param("description", generateString()) - .param("tags", user.tagList.get(ThreadLocalRandom.current().nextInt(user.tagList.size()))) // Random tag - .param("language", "eng") - .param("create_date", Long.toString(createDate)); - JsonObject json = resource.path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, user.authToken) - .put(Entity.form(form), JsonObject.class); - String documentId = json.getString("id"); - log.info("Created document " + (documentCreatedCount++) + "/" + DOCUMENT_PER_USER_COUNT * USER_COUNT + " for user: " + user.username); - - // Add files for each document - for (int j = 0; j < FILE_PER_DOCUMENT_COUNT; j++) { - try (InputStream is = Resources.getResource("empty.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "empty.png"); - @SuppressWarnings("resource") - ClientResponse response = resource - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, user.authToken) - .put(Entity.entity(new FormDataMultiPart().field("id", documentId).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), ClientResponse.class); - Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); - } - } - } - } - } - - private static String generateString() { - return UUID.randomUUID().toString().replace("-", ""); - } - - private static class User { - String username; - List tagList = Lists.newArrayList(); - String authToken; - - User(String username, String authToken) { - this.username = username; - this.authToken = authToken; - } - } -} diff --git a/docs-stress/src/main/resources/empty.png b/docs-stress/src/main/resources/empty.png deleted file mode 100644 index cee81591..00000000 Binary files a/docs-stress/src/main/resources/empty.png and /dev/null differ diff --git a/docs-stress/src/main/resources/log4j.properties b/docs-stress/src/main/resources/log4j.properties deleted file mode 100644 index d7ea887b..00000000 --- a/docs-stress/src/main/resources/log4j.properties +++ /dev/null @@ -1,6 +0,0 @@ -log4j.rootCategory=WARN, CONSOLE -log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender -log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout -log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n - -log4j.logger.com.sismics=DEBUG diff --git a/docs-web-common/pom.xml b/docs-web-common/pom.xml index 7d277514..c1ff6b9e 100644 --- a/docs-web-common/pom.xml +++ b/docs-web-common/pom.xml @@ -5,8 +5,8 @@ com.sismics.docs docs-parent - 1.6-SNAPSHOT - .. + 1.12-SNAPSHOT + ../pom.xml 4.0.0 @@ -34,8 +34,8 @@ - commons-lang - commons-lang + org.apache.commons + commons-lang3 @@ -54,20 +54,20 @@ - commons-dbcp - commons-dbcp + jakarta.servlet + jakarta.servlet-api + provided - - javax.servlet - javax.servlet-api - provided - - joda-time joda-time + + + org.slf4j + jul-to-slf4j + diff --git a/docs-web-common/src/main/java/com/sismics/rest/exception/ClientException.java b/docs-web-common/src/main/java/com/sismics/rest/exception/ClientException.java index 9a1a233d..e8c7b69d 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/exception/ClientException.java +++ b/docs-web-common/src/main/java/com/sismics/rest/exception/ClientException.java @@ -1,12 +1,11 @@ package com.sismics.rest.exception; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.json.Json; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.Json; /** * Jersey exception encapsulating an error from the client (BAD_REQUEST). @@ -43,7 +42,7 @@ public class ClientException extends WebApplicationException { * @param message Human readable error message */ public ClientException(String type, String message) { - super(Response.status(Status.BAD_REQUEST).entity(Json.createObjectBuilder() + super(Response.status(Response.Status.BAD_REQUEST).entity(Json.createObjectBuilder() .add("type", type) .add("message", message).build()).build()); } diff --git a/docs-web-common/src/main/java/com/sismics/rest/exception/ForbiddenClientException.java b/docs-web-common/src/main/java/com/sismics/rest/exception/ForbiddenClientException.java index 7f92b32b..74596037 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/exception/ForbiddenClientException.java +++ b/docs-web-common/src/main/java/com/sismics/rest/exception/ForbiddenClientException.java @@ -1,9 +1,9 @@ package com.sismics.rest.exception; -import javax.json.Json; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.Json; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; /** * Unauthorized access to the resource exception. diff --git a/docs-web-common/src/main/java/com/sismics/rest/exception/ServerException.java b/docs-web-common/src/main/java/com/sismics/rest/exception/ServerException.java index 76b80d47..c4a16e76 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/exception/ServerException.java +++ b/docs-web-common/src/main/java/com/sismics/rest/exception/ServerException.java @@ -1,9 +1,9 @@ package com.sismics.rest.exception; -import javax.json.Json; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.Json; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/docs-web-common/src/main/java/com/sismics/rest/util/AclUtil.java b/docs-web-common/src/main/java/com/sismics/rest/util/AclUtil.java index c6bda473..61edae91 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/util/AclUtil.java +++ b/docs-web-common/src/main/java/com/sismics/rest/util/AclUtil.java @@ -6,9 +6,9 @@ import com.sismics.docs.core.dao.AclDao; import com.sismics.docs.core.dao.dto.AclDto; import com.sismics.util.JsonUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; import java.util.List; /** diff --git a/docs-web-common/src/main/java/com/sismics/rest/util/RestUtil.java b/docs-web-common/src/main/java/com/sismics/rest/util/RestUtil.java new file mode 100644 index 00000000..3a3cae46 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/rest/util/RestUtil.java @@ -0,0 +1,44 @@ +package com.sismics.rest.util; + +import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.util.DirectoryUtil; +import com.sismics.docs.core.util.FileUtil; +import com.sismics.rest.exception.ServerException; +import com.sismics.util.JsonUtil; + +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; + +import java.io.IOException; +import java.nio.file.Files; + +/** + * Rest utilities. + * + * @author bgamard + */ +public class RestUtil { + /** + * Transform a File into its JSON representation. + * If the file size it is not stored in the database the size can be wrong + * because the encrypted file size is used. + * @param fileDb a file + * @return the JSON + */ + public static JsonObjectBuilder fileToJsonObjectBuilder(File fileDb) { + try { + long fileSize = fileDb.getSize().equals(File.UNKNOWN_SIZE) ? Files.size(DirectoryUtil.getStorageDirectory().resolve(fileDb.getId())) : fileDb.getSize(); + return Json.createObjectBuilder() + .add("id", fileDb.getId()) + .add("processing", FileUtil.isProcessingFile(fileDb.getId())) + .add("name", JsonUtil.nullable(fileDb.getName())) + .add("version", fileDb.getVersion()) + .add("mimetype", fileDb.getMimeType()) + .add("document_id", JsonUtil.nullable(fileDb.getDocumentId())) + .add("create_date", fileDb.getCreateDate().getTime()) + .add("size", fileSize); + } catch (IOException e) { + throw new ServerException("FileError", "Unable to get the size of " + fileDb.getId(), e); + } + } +} diff --git a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java index ce4b9883..a1fc6831 100644 --- a/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java +++ b/docs-web-common/src/main/java/com/sismics/rest/util/ValidationUtil.java @@ -2,7 +2,7 @@ package com.sismics.rest.util; import com.google.common.base.Strings; import com.sismics.rest.exception.ClientException; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import java.text.MessageFormat; @@ -21,6 +21,8 @@ public class ValidationUtil { private static Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9_]+"); + private static Pattern USERNAME_PATTERN = Pattern.compile("[a-zA-Z0-9_@\\.]+"); + /** * Checks that the argument is not null. * @@ -111,7 +113,18 @@ public class ValidationUtil { public static void validateHexColor(String s, String name, boolean nullable) throws ClientException { ValidationUtil.validateLength(s, name, 7, 7, nullable); } - + + /** + * Validate a tag name. + * + * @param name Name of the tag + */ + public static void validateTagName(String name) throws ClientException { + if (name.contains(" ") || name.contains(":")) { + throw new ClientException("IllegalTagName", "Spaces and colons are not allowed in tag name"); + } + } + /** * Validates that the provided string matches an URL with HTTP or HTTPS scheme. * @@ -141,6 +154,12 @@ public class ValidationUtil { } } + public static void validateUsername(String s, String name) throws ClientException { + if (!USERNAME_PATTERN.matcher(s).matches()) { + throw new ClientException("ValidationError", MessageFormat.format("{0} must have only alphanumeric, underscore characters or @ and .", name)); + } + } + public static void validateRegex(String s, String name, String regex) throws ClientException { if (!Pattern.compile(regex).matcher(s).matches()) { throw new ClientException("ValidationError", MessageFormat.format("{0} must match {1}", name, regex)); diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/CorsFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/CorsFilter.java index 83c0c8d6..efa5a306 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/CorsFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/CorsFilter.java @@ -2,9 +2,9 @@ package com.sismics.util.filter; import com.sismics.util.EnvironmentUtil; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; /** diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java index d0a99e27..538381ec 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java @@ -4,8 +4,8 @@ import com.google.common.base.Strings; import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.model.jpa.User; -import javax.servlet.FilterConfig; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.http.HttpServletRequest; /** * A header-based security filter that authenticates an user using the "X-Authenticated-User" request header as the user ID. diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java index 224998c4..6775338e 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java @@ -6,17 +6,18 @@ import com.sismics.docs.core.util.TransactionUtil; import com.sismics.util.EnvironmentUtil; import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.jpa.EMF; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.core.HttpHeaders; import org.apache.log4j.Level; import org.apache.log4j.PatternLayout; import org.apache.log4j.RollingFileAppender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; -import javax.persistence.EntityManager; -import javax.persistence.EntityTransaction; -import javax.servlet.*; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.HttpHeaders; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityTransaction; import java.io.IOException; import java.text.MessageFormat; @@ -57,6 +58,8 @@ public class RequestContextFilter implements Filter { fileAppender.setMaxBackupIndex(5); fileAppender.activateOptions(); org.apache.log4j.Logger.getRootLogger().addAppender(fileAppender); + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); // Initialize the application context TransactionUtil.handle(AppContext::getInstance); diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java index 1aacbb9b..5e5f7b41 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java @@ -13,8 +13,8 @@ import org.joda.time.DateTimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.HashSet; import java.util.List; diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java index bea91929..ae8a84ba 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java @@ -5,8 +5,8 @@ import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.model.jpa.AuthenticationToken; import com.sismics.docs.core.model.jpa.User; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import java.text.MessageFormat; import java.util.Date; diff --git a/docs-web-common/src/main/java/com/sismics/util/listener/IIOProviderContextListener.java b/docs-web-common/src/main/java/com/sismics/util/listener/IIOProviderContextListener.java new file mode 100644 index 00000000..2b96f7c4 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/util/listener/IIOProviderContextListener.java @@ -0,0 +1,78 @@ +package com.sismics.util.listener; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ServiceRegistry; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; + +/** + * Takes care of registering and de-registering local ImageIO plugins (service providers) for the servlet context. + *

+ * Registers all available plugins on {@code contextInitialized} event, using {@code ImageIO.scanForPlugins()}, to make + * sure they are available to the current servlet context. + * De-registers all plugins which have the {@link Thread#getContextClassLoader() current thread's context class loader} + * as its class loader on {@code contextDestroyed} event, to avoid class/resource leak. + *

+ * Copied from: https://github.com/haraldk/TwelveMonkeys + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: IIOProviderContextListener.java,v 1.0 14.02.12 21:53 haraldk Exp$ + * @see ImageIO#scanForPlugins() + */ +public final class IIOProviderContextListener implements ServletContextListener { + + public void contextInitialized(final ServletContextEvent event) { + event.getServletContext().log("Scanning for locally installed ImageIO plugin providers"); + + // Registers all locally available IIO plugins. + ImageIO.scanForPlugins(); + } + + public void contextDestroyed(final ServletContextEvent event) { + ServletContext servletContext = event.getServletContext(); + + // De-register any locally registered IIO plugins. Relies on each web app having its own context class loader. + LocalFilter localFilter = new LocalFilter(Thread.currentThread().getContextClassLoader()); // scanForPlugins uses context class loader + + IIORegistry registry = IIORegistry.getDefaultInstance(); + Iterator> categories = registry.getCategories(); + + while (categories.hasNext()) { + deregisterLocalProvidersForCategory(registry, localFilter, categories.next(), servletContext); + } + } + + private static void deregisterLocalProvidersForCategory(IIORegistry registry, LocalFilter localFilter, Class category, ServletContext context) { + Iterator providers = registry.getServiceProviders(category, localFilter, false); + + // Copy the providers, as de-registering while iterating over providers will lead to ConcurrentModificationExceptions. + List providersCopy = new ArrayList<>(); + while (providers.hasNext()) { + providersCopy.add(providers.next()); + } + + for (T provider : providersCopy) { + registry.deregisterServiceProvider(provider, category); + context.log(String.format("Unregistered locally installed provider class: %s", provider.getClass())); + } + } + + static class LocalFilter implements ServiceRegistry.Filter { + private final ClassLoader loader; + + public LocalFilter(ClassLoader loader) { + this.loader = loader; + } + + public boolean filter(Object provider) { + return provider.getClass().getClassLoader() == loader; + } + } +} \ No newline at end of file diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java index cce2f744..96a63244 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java @@ -20,12 +20,16 @@ import org.subethamail.wiser.WiserMessage; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; -import javax.ws.rs.core.Application; -import javax.ws.rs.core.UriBuilder; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.UriBuilder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.Objects; /** * Base class of integration tests with Jersey. @@ -33,6 +37,18 @@ import java.util.List; * @author jtremeaux */ public abstract class BaseJerseyTest extends JerseyTest { + protected static final String FILE_APACHE_PPTX = "file/apache.pptx"; + protected static final String FILE_DOCUMENT_DOCX = "file/document.docx"; + protected static final String FILE_DOCUMENT_ODT = "file/document.odt"; + protected static final String FILE_DOCUMENT_TXT = "file/document.txt"; + protected static final String FILE_EINSTEIN_ROOSEVELT_LETTER_PNG = "file/Einstein-Roosevelt-letter.png"; + protected static final long FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE = 292641L; + protected static final String FILE_PIA_00452_JPG = "file/PIA00452.jpg"; + protected static final long FILE_PIA_00452_JPG_SIZE = 163510L; + protected static final String FILE_VIDEO_WEBM = "file/video.webm"; + protected static final String FILE_WIKIPEDIA_PDF = "file/wikipedia.pdf"; + protected static final String FILE_WIKIPEDIA_ZIP = "file/wikipedia.zip"; + /** * Test HTTP server. */ @@ -56,7 +72,7 @@ public abstract class BaseJerseyTest extends JerseyTest { @Override protected Application configure() { String travisEnv = System.getenv("TRAVIS"); - if (travisEnv == null || !travisEnv.equals("true")) { + if (!Objects.equals(travisEnv, "true")) { // Travis doesn't like big logs enable(TestProperties.LOG_TRAFFIC); enable(TestProperties.DUMP_ENTITY); @@ -79,7 +95,7 @@ public abstract class BaseJerseyTest extends JerseyTest { httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort()); WebappContext context = new WebappContext("GrizzlyContext", "/docs"); - context.addListener("com.twelvemonkeys.servlet.image.IIOProviderContextListener"); + context.addListener("com.sismics.util.listener.IIOProviderContextListener"); context.addFilter("requestContextFilter", RequestContextFilter.class) .addMappingForUrlPatterns(null, "/*"); context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class) diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java index 2213160e..6b7bb414 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/util/ClientUtil.java @@ -3,18 +3,26 @@ package com.sismics.docs.rest.util; import com.google.common.io.Resources; import com.sismics.util.filter.TokenBasedSecurityFilter; import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.MultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; +import org.junit.Assert; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Date; /** * REST client utilities. @@ -113,7 +121,8 @@ public class ClientUtil { .param("username", username) .param("password", password) .param("remember", remember.toString()))); - + Assert.assertEquals(200, response.getStatus()); + return getAuthenticationCookie(response); } @@ -154,27 +163,58 @@ public class ClientUtil { return authToken; } + /** + * Create a document + * + * @param token Authentication token + * @return Document ID + */ + public String createDocument(String token) { + JsonObject json = this.resource.path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, token) + .put(Entity.form(new Form() + .param("title", "Document Title") + .param("description", "Document description") + .param("language", "eng") + .param("create_date", Long.toString(new Date().getTime()))), JsonObject.class); + String documentId = json.getString("id"); + Assert.assertNotNull(documentId); + return documentId; + } + /** * Add a file to a document. * * @param file File path - * @param filename Filename * @param token Authentication token * @param documentId Document ID * @return File ID * @throws IOException e + * @throws URISyntaxException e */ - public String addFileToDocument(String file, String filename, String token, String documentId) throws IOException { - try (InputStream is = Resources.getResource(file).openStream()) { + public String addFileToDocument(String file, String token, String documentId) throws IOException, URISyntaxException { + URL fileResource = Resources.getResource(file); + Path filePath = Paths.get(fileResource.toURI()); + String filename = filePath.getFileName().toString(); + try (InputStream is = fileResource.openStream()) { StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, filename); try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - JsonObject json = resource + MultiPart formContent; + if (documentId != null) { + formContent = multiPart.field("id", documentId).bodyPart(streamDataBodyPart); + } else { + formContent = multiPart.bodyPart(streamDataBodyPart); + } + JsonObject json = this.resource .register(MultiPartFeature.class) .path("/file").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, token) - .put(Entity.entity(multiPart.field("id", documentId).bodyPart(streamDataBodyPart), + .put(Entity.entity(formContent, MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - return json.getString("id"); + String fileId = json.getString("id"); + Assert.assertNotNull(fileId); + Assert.assertEquals(Files.size(filePath), json.getJsonNumber("size").longValue()); + return fileId; } } } diff --git a/docs-web/pom.xml b/docs-web/pom.xml index 32aa4aa7..20b06e16 100644 --- a/docs-web/pom.xml +++ b/docs-web/pom.xml @@ -5,8 +5,8 @@ com.sismics.docs docs-parent - 1.6-SNAPSHOT - .. + 1.12-SNAPSHOT + ../pom.xml 4.0.0 @@ -26,25 +26,6 @@ docs-web-common
- - - javax.xml.bind - jaxb-api - 2.3.0 - - - - com.sun.xml.bind - jaxb-core - 2.3.0 - - - - com.sun.xml.bind - jaxb-impl - 2.3.0 - - org.glassfish.jersey.containers @@ -68,8 +49,8 @@ - commons-lang - commons-lang + org.apache.commons + commons-lang3 @@ -87,17 +68,6 @@ log4j - - commons-dbcp - commons-dbcp - - - - javax.servlet - javax.servlet-api - provided - - joda-time joda-time @@ -109,8 +79,9 @@ - com.twelvemonkeys.servlet - servlet + jakarta.servlet + jakarta.servlet-api + provided @@ -157,7 +128,7 @@ greenmail test - + @@ -228,54 +199,6 @@ - - - stress - - - env - stress - - - - - - - src/stress/resources - false - - **/config.properties - - - - src/stress/resources - true - - **/config.properties - - - - - - - org.eclipse.jetty - jetty-maven-plugin - - - - application.mode - dev - - - - /docs-web - - - - - - - prod diff --git a/docs-web/src/dev/main/webapp/web-override.xml b/docs-web/src/dev/main/webapp/web-override.xml index 1dea0dae..4e250a10 100644 --- a/docs-web/src/dev/main/webapp/web-override.xml +++ b/docs-web/src/dev/main/webapp/web-override.xml @@ -1,11 +1,10 @@ - + xmlns="https://jakarta.ee/xml/ns/jakartaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" + version="5.0" + metadata-complete="true"> default diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 8e983362..37e03ad0 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=23 \ No newline at end of file +db.version=30 diff --git a/docs-web/src/dev/resources/hibernate.properties b/docs-web/src/dev/resources/hibernate.properties index 8b092865..33a61d08 100644 --- a/docs-web/src/dev/resources/hibernate.properties +++ b/docs-web/src/dev/resources/hibernate.properties @@ -8,9 +8,6 @@ hibernate.show_sql=false hibernate.format_sql=false hibernate.max_fetch_depth=5 hibernate.cache.use_second_level_cache=false -hibernate.c3p0.min_size=1 -hibernate.c3p0.max_size=10 -hibernate.c3p0.timeout=5000 -hibernate.c3p0.max_statements=0 -hibernate.c3p0.acquire_increment=1 -hibernate.c3p0.idle_test_period=10 +hibernate.connection.initial_pool_size=1 +hibernate.connection.pool_size=10 +hibernate.connection.pool_validation_interval=5 diff --git a/docs-web/src/dev/resources/log4j.properties b/docs-web/src/dev/resources/log4j.properties index 0b05e8e9..ad707aff 100644 --- a/docs-web/src/dev/resources/log4j.properties +++ b/docs-web/src/dev/resources/log4j.properties @@ -6,3 +6,6 @@ log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender log4j.appender.MEMORY.size=1000 log4j.logger.com.sismics=DEBUG +log4j.logger.org.apache.pdfbox=ERROR +log4j.logger.org.glassfish.jersey.servlet.WebComponent=ERROR +log4j.logger.org.apache.directory=ERROR \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java index 60ebebe9..bdb48f97 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AclResource.java @@ -21,11 +21,11 @@ import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.context.ThreadLocalContext; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.text.MessageFormat; import java.util.List; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java index ff871d9e..bb1e8edd 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AppResource.java @@ -26,19 +26,19 @@ import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.log4j.LogCriteria; import com.sismics.util.log4j.LogEntry; import com.sismics.util.log4j.MemoryAppender; -import org.apache.commons.lang.StringUtils; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Appender; import org.apache.log4j.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.persistence.EntityManager; -import javax.persistence.Query; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -204,28 +204,28 @@ public class AppResource extends BaseResource { Config passwordConfig = configDao.getById(ConfigType.SMTP_PASSWORD); Config fromConfig = configDao.getById(ConfigType.SMTP_FROM); JsonObjectBuilder response = Json.createObjectBuilder(); - if (System.getenv(Constants.SMTP_HOSTNAME_ENV) == null) { + if (Strings.isNullOrEmpty(System.getenv(Constants.SMTP_HOSTNAME_ENV))) { if (hostnameConfig == null) { response.addNull("hostname"); } else { response.add("hostname", hostnameConfig.getValue()); } } - if (System.getenv(Constants.SMTP_PORT_ENV) == null) { + if (Strings.isNullOrEmpty(System.getenv(Constants.SMTP_PORT_ENV))) { if (portConfig == null) { response.addNull("port"); } else { response.add("port", Integer.valueOf(portConfig.getValue())); } } - if (System.getenv(Constants.SMTP_USERNAME_ENV) == null) { + if (Strings.isNullOrEmpty(System.getenv(Constants.SMTP_USERNAME_ENV))) { if (usernameConfig == null) { response.addNull("username"); } else { response.add("username", usernameConfig.getValue()); } } - if (System.getenv(Constants.SMTP_PASSWORD_ENV) == null) { + if (Strings.isNullOrEmpty(System.getenv(Constants.SMTP_PASSWORD_ENV))) { if (passwordConfig == null) { response.addNull("password"); } else { @@ -311,6 +311,7 @@ public class AppResource extends BaseResource { * @apiSuccess {String} port IMAP port * @apiSuccess {String} username IMAP username * @apiSuccess {String} password IMAP password + * @apiSuccess {String} folder IMAP folder * @apiSuccess {String} tag Tag for created documents * @apiError (client) ForbiddenError Access denied * @apiPermission admin @@ -328,14 +329,20 @@ public class AppResource extends BaseResource { ConfigDao configDao = new ConfigDao(); Boolean enabled = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_ENABLED); + Boolean autoTags = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_AUTOMATIC_TAGS); + Boolean deleteImported = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_DELETE_IMPORTED); Config hostnameConfig = configDao.getById(ConfigType.INBOX_HOSTNAME); Config portConfig = configDao.getById(ConfigType.INBOX_PORT); + Boolean starttls = ConfigUtil.getConfigBooleanValue(ConfigType.INBOX_STARTTLS); Config usernameConfig = configDao.getById(ConfigType.INBOX_USERNAME); Config passwordConfig = configDao.getById(ConfigType.INBOX_PASSWORD); + Config folderConfig = configDao.getById(ConfigType.INBOX_FOLDER); Config tagConfig = configDao.getById(ConfigType.INBOX_TAG); JsonObjectBuilder response = Json.createObjectBuilder(); response.add("enabled", enabled); + response.add("autoTagsEnabled", autoTags); + response.add("deleteImported", deleteImported); if (hostnameConfig == null) { response.addNull("hostname"); } else { @@ -346,6 +353,7 @@ public class AppResource extends BaseResource { } else { response.add("port", Integer.valueOf(portConfig.getValue())); } + response.add("starttls", starttls); if (usernameConfig == null) { response.addNull("username"); } else { @@ -356,6 +364,11 @@ public class AppResource extends BaseResource { } else { response.add("password", passwordConfig.getValue()); } + if (folderConfig == null) { + response.addNull("folder"); + } else { + response.add("folder", folderConfig.getValue()); + } if (tagConfig == null) { response.addNull("tag"); } else { @@ -384,10 +397,13 @@ public class AppResource extends BaseResource { * @apiName PostAppConfigInbox * @apiGroup App * @apiParam {Boolean} enabled True if the inbox scanning is enabled + * @apiParam {Boolean} autoTagsEnabled If true automatically add tags to document (prefixed by #) + * @apiParam {Boolean} deleteImported If true delete message from mailbox after import * @apiParam {String} hostname IMAP hostname * @apiParam {Integer} port IMAP port * @apiParam {String} username IMAP username * @apiParam {String} password IMAP password + * @apiParam {String} folder IMAP folder * @apiParam {String} tag Tag for created documents * @apiError (client) ForbiddenError Access denied * @apiError (client) ValidationError Validation error @@ -399,41 +415,55 @@ public class AppResource extends BaseResource { * @param portStr IMAP port * @param username IMAP username * @param password IMAP password + * @param folder IMAP folder * @param tag Tag for created documents * @return Response */ @POST @Path("config_inbox") public Response configInbox(@FormParam("enabled") Boolean enabled, + @FormParam("autoTagsEnabled") Boolean autoTagsEnabled, + @FormParam("deleteImported") Boolean deleteImported, @FormParam("hostname") String hostname, @FormParam("port") String portStr, + @FormParam("starttls") Boolean starttls, @FormParam("username") String username, @FormParam("password") String password, + @FormParam("folder") String folder, @FormParam("tag") String tag) { if (!authenticate()) { throw new ForbiddenClientException(); } checkBaseFunction(BaseFunction.ADMIN); ValidationUtil.validateRequired(enabled, "enabled"); + ValidationUtil.validateRequired(autoTagsEnabled, "autoTagsEnabled"); + ValidationUtil.validateRequired(deleteImported, "deleteImported"); if (!Strings.isNullOrEmpty(portStr)) { ValidationUtil.validateInteger(portStr, "port"); } + ValidationUtil.validateRequired(starttls, "starttls"); // Just update the changed configuration ConfigDao configDao = new ConfigDao(); configDao.update(ConfigType.INBOX_ENABLED, enabled.toString()); + configDao.update(ConfigType.INBOX_AUTOMATIC_TAGS, autoTagsEnabled.toString()); + configDao.update(ConfigType.INBOX_DELETE_IMPORTED, deleteImported.toString()); if (!Strings.isNullOrEmpty(hostname)) { configDao.update(ConfigType.INBOX_HOSTNAME, hostname); } if (!Strings.isNullOrEmpty(portStr)) { configDao.update(ConfigType.INBOX_PORT, portStr); } + configDao.update(ConfigType.INBOX_STARTTLS, starttls.toString()); if (!Strings.isNullOrEmpty(username)) { configDao.update(ConfigType.INBOX_USERNAME, username); } if (!Strings.isNullOrEmpty(password)) { configDao.update(ConfigType.INBOX_PASSWORD, password); } + if (!Strings.isNullOrEmpty(folder)) { + configDao.update(ConfigType.INBOX_FOLDER, folder); + } if (!Strings.isNullOrEmpty(tag)) { configDao.update(ConfigType.INBOX_TAG, tag); } @@ -486,7 +516,7 @@ public class AppResource extends BaseResource { * @apiSuccess {String} logs.message Message * @apiError (client) ForbiddenError Access denied * @apiError (server) ServerError MEMORY appender not configured - * @apiPermission user + * @apiPermission admin * @apiVersion 1.5.0 * * @param minLevel Filter on logging level @@ -507,6 +537,7 @@ public class AppResource extends BaseResource { if (!authenticate()) { throw new ForbiddenClientException(); } + checkBaseFunction(BaseFunction.ADMIN); // Get the memory appender org.apache.log4j.Logger logger = org.apache.log4j.Logger.getRootLogger(); @@ -643,49 +674,185 @@ public class AppResource extends BaseResource { log.info("Deleting {} orphan ACLs", q.executeUpdate()); // Soft delete orphan comments - q = em.createNativeQuery("update T_COMMENT c set c.COM_DELETEDATE_D = :dateNow where c.COM_ID_C in (select c.COM_ID_C from T_COMMENT c left join T_DOCUMENT d on d.DOC_ID_C = c.COM_IDDOC_C and d.DOC_DELETEDATE_D is null where d.DOC_ID_C is null)"); + q = em.createNativeQuery("update T_COMMENT set COM_DELETEDATE_D = :dateNow where COM_ID_C in (select c.COM_ID_C from T_COMMENT c left join T_DOCUMENT d on d.DOC_ID_C = c.COM_IDDOC_C and d.DOC_DELETEDATE_D is null where d.DOC_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan comments", q.executeUpdate()); // Soft delete orphan document tag links - q = em.createNativeQuery("update T_DOCUMENT_TAG dt set dt.DOT_DELETEDATE_D = :dateNow where dt.DOT_ID_C in (select dt.DOT_ID_C from T_DOCUMENT_TAG dt left join T_DOCUMENT d on dt.DOT_IDDOCUMENT_C = d.DOC_ID_C and d.DOC_DELETEDATE_D is null left join T_TAG t on t.TAG_ID_C = dt.DOT_IDTAG_C and t.TAG_DELETEDATE_D is null where d.DOC_ID_C is null or t.TAG_ID_C is null)"); + q = em.createNativeQuery("update T_DOCUMENT_TAG set DOT_DELETEDATE_D = :dateNow where DOT_ID_C in (select dt.DOT_ID_C from T_DOCUMENT_TAG dt left join T_DOCUMENT d on dt.DOT_IDDOCUMENT_C = d.DOC_ID_C and d.DOC_DELETEDATE_D is null left join T_TAG t on t.TAG_ID_C = dt.DOT_IDTAG_C and t.TAG_DELETEDATE_D is null where d.DOC_ID_C is null or t.TAG_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan document tag links", q.executeUpdate()); // Soft delete orphan shares - q = em.createNativeQuery("update T_SHARE s set s.SHA_DELETEDATE_D = :dateNow where s.SHA_ID_C in (select s.SHA_ID_C from T_SHARE s left join T_ACL a on a.ACL_TARGETID_C = s.SHA_ID_C and a.ACL_DELETEDATE_D is null where a.ACL_ID_C is null)"); + q = em.createNativeQuery("update T_SHARE set SHA_DELETEDATE_D = :dateNow where SHA_ID_C in (select s.SHA_ID_C from T_SHARE s left join T_ACL a on a.ACL_TARGETID_C = s.SHA_ID_C and a.ACL_DELETEDATE_D is null where a.ACL_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan shares", q.executeUpdate()); // Soft delete orphan tags - q = em.createNativeQuery("update T_TAG t set t.TAG_DELETEDATE_D = :dateNow where t.TAG_ID_C in (select t.TAG_ID_C from T_TAG t left join T_USER u on u.USE_ID_C = t.TAG_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); + q = em.createNativeQuery("update T_TAG set TAG_DELETEDATE_D = :dateNow where TAG_ID_C in (select t.TAG_ID_C from T_TAG t left join T_USER u on u.USE_ID_C = t.TAG_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan tags", q.executeUpdate()); // Soft delete orphan documents - q = em.createNativeQuery("update T_DOCUMENT d set d.DOC_DELETEDATE_D = :dateNow where d.DOC_ID_C in (select d.DOC_ID_C from T_DOCUMENT d left join T_USER u on u.USE_ID_C = d.DOC_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); + q = em.createNativeQuery("update T_DOCUMENT set DOC_DELETEDATE_D = :dateNow where DOC_ID_C in (select d.DOC_ID_C from T_DOCUMENT d left join T_USER u on u.USE_ID_C = d.DOC_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan documents", q.executeUpdate()); // Soft delete orphan files - q = em.createNativeQuery("update T_FILE f set f.FIL_DELETEDATE_D = :dateNow where f.FIL_ID_C in (select f.FIL_ID_C from T_FILE f left join T_USER u on u.USE_ID_C = f.FIL_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); + q = em.createNativeQuery("update T_FILE set FIL_DELETEDATE_D = :dateNow where FIL_ID_C in (select f.FIL_ID_C from T_FILE f left join T_USER u on u.USE_ID_C = f.FIL_IDUSER_C and u.USE_DELETEDATE_D is null where u.USE_ID_C is null)"); q.setParameter("dateNow", new Date()); log.info("Deleting {} orphan files", q.executeUpdate()); // Hard delete softly deleted data - log.info("Deleting {} soft deleted document tag links", em.createQuery("delete DocumentTag dt where dt.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted ACLs", em.createQuery("delete Acl a where a.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted shares", em.createQuery("delete Share s where s.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted tags", em.createQuery("delete Tag t where t.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted comments", em.createQuery("delete Comment c where c.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted files", em.createQuery("delete File f where f.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted documents", em.createQuery("delete Document d where d.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted users", em.createQuery("delete User u where u.deleteDate is not null").executeUpdate()); - log.info("Deleting {} soft deleted groups", em.createQuery("delete Group g where g.deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted document tag links", em.createQuery("delete DocumentTag where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted ACLs", em.createQuery("delete Acl where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted shares", em.createQuery("delete Share where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted tags", em.createQuery("delete Tag where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted comments", em.createQuery("delete Comment where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted files", em.createQuery("delete File where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted documents", em.createQuery("delete Document where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted users", em.createQuery("delete User where deleteDate is not null").executeUpdate()); + log.info("Deleting {} soft deleted groups", em.createQuery("delete Group where deleteDate is not null").executeUpdate()); // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() .add("status", "ok"); return Response.ok().entity(response.build()).build(); } + + /** + * Get the LDAP authentication configuration. + * + * @api {get} /app/config_ldap Get the LDAP authentication configuration + * @apiName GetAppConfigLdap + * @apiGroup App + * @apiSuccess {Boolean} enabled LDAP authentication enabled + * @apiSuccess {String} host LDAP server host + * @apiSuccess {Integer} port LDAP server port + * @apiSuccess {String} admin_dn Admin DN + * @apiSuccess {String} admin_password Admin password + * @apiSuccess {String} base_dn Base DN + * @apiSuccess {String} filter LDAP filter + * @apiSuccess {String} default_email LDAP default email + * @apiSuccess {Integer} default_storage LDAP default storage + * @apiError (client) ForbiddenError Access denied + * @apiPermission admin + * @apiVersion 1.9.0 + * + * @return Response + */ + @GET + @Path("config_ldap") + public Response getConfigLdap() { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + ConfigDao configDao = new ConfigDao(); + Config enabled = configDao.getById(ConfigType.LDAP_ENABLED); + + JsonObjectBuilder response = Json.createObjectBuilder(); + if (enabled != null && Boolean.parseBoolean(enabled.getValue())) { + // LDAP enabled + response.add("enabled", true) + .add("host", ConfigUtil.getConfigStringValue(ConfigType.LDAP_HOST)) + .add("port", ConfigUtil.getConfigIntegerValue(ConfigType.LDAP_PORT)) + .add("usessl", ConfigUtil.getConfigBooleanValue(ConfigType.LDAP_USESSL)) + .add("admin_dn", ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_DN)) + .add("admin_password", ConfigUtil.getConfigStringValue(ConfigType.LDAP_ADMIN_PASSWORD)) + .add("base_dn", ConfigUtil.getConfigStringValue(ConfigType.LDAP_BASE_DN)) + .add("filter", ConfigUtil.getConfigStringValue(ConfigType.LDAP_FILTER)) + .add("default_email", ConfigUtil.getConfigStringValue(ConfigType.LDAP_DEFAULT_EMAIL)) + .add("default_storage", ConfigUtil.getConfigLongValue(ConfigType.LDAP_DEFAULT_STORAGE)); + } else { + // LDAP disabled + response.add("enabled", false); + } + + return Response.ok().entity(response.build()).build(); + } + + /** + * Configure the LDAP authentication. + * + * @api {post} /app/config_ldap Configure the LDAP authentication + * @apiName PostAppConfigLdap + * @apiGroup App + * @apiParam {Boolean} enabled LDAP authentication enabled + * @apiParam {String} host LDAP server host + * @apiParam {Integer} port LDAP server port + * @apiParam {Boolean} use SSL (ldaps) + * @apiParam {String} admin_dn Admin DN + * @apiParam {String} admin_password Admin password + * @apiParam {String} base_dn Base DN + * @apiParam {String} filter LDAP filter + * @apiParam {String} default_email LDAP default email + * @apiParam {Integer} default_storage LDAP default storage + * @apiError (client) ForbiddenError Access denied + * @apiError (client) ValidationError Validation error + * @apiPermission admin + * @apiVersion 1.9.0 + * + * @param enabled LDAP authentication enabled + * @param host LDAP server host + * @param portStr LDAP server port + * @param usessl LDAP use SSL (ldaps) + * @param adminDn Admin DN + * @param adminPassword Admin password + * @param baseDn Base DN + * @param filter LDAP filter + * @param defaultEmail LDAP default email + * @param defaultStorageStr LDAP default storage + * @return Response + */ + @POST + @Path("config_ldap") + public Response configLdap(@FormParam("enabled") Boolean enabled, + @FormParam("host") String host, + @FormParam("port") String portStr, + @FormParam("usessl") Boolean usessl, + @FormParam("admin_dn") String adminDn, + @FormParam("admin_password") String adminPassword, + @FormParam("base_dn") String baseDn, + @FormParam("filter") String filter, + @FormParam("default_email") String defaultEmail, + @FormParam("default_storage") String defaultStorageStr) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + ConfigDao configDao = new ConfigDao(); + + if (enabled != null && enabled) { + // LDAP enabled, validate everything + ValidationUtil.validateLength(host, "host", 1, 250); + ValidationUtil.validateInteger(portStr, "port"); + ValidationUtil.validateLength(adminDn, "admin_dn", 1, 250); + ValidationUtil.validateLength(adminPassword, "admin_password", 1, 250); + ValidationUtil.validateLength(baseDn, "base_dn", 1, 250); + ValidationUtil.validateLength(filter, "filter", 1, 250); + if (!filter.contains("USERNAME")) { + throw new ClientException("ValidationError", "'filter' must contains 'USERNAME'"); + } + ValidationUtil.validateLength(defaultEmail, "default_email", 1, 250); + ValidationUtil.validateLong(defaultStorageStr, "default_storage"); + configDao.update(ConfigType.LDAP_ENABLED, Boolean.TRUE.toString()); + configDao.update(ConfigType.LDAP_HOST, host); + configDao.update(ConfigType.LDAP_PORT, portStr); + configDao.update(ConfigType.LDAP_USESSL, usessl.toString()); + configDao.update(ConfigType.LDAP_ADMIN_DN, adminDn); + configDao.update(ConfigType.LDAP_ADMIN_PASSWORD, adminPassword); + configDao.update(ConfigType.LDAP_BASE_DN, baseDn); + configDao.update(ConfigType.LDAP_FILTER, filter); + configDao.update(ConfigType.LDAP_DEFAULT_EMAIL, defaultEmail); + configDao.update(ConfigType.LDAP_DEFAULT_STORAGE, defaultStorageStr); + } else { + // LDAP disabled + configDao.update(ConfigType.LDAP_ENABLED, Boolean.FALSE.toString()); + } + + return Response.ok().build(); + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java index 1c12c314..573cd8a3 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/AuditLogResource.java @@ -13,14 +13,14 @@ import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.util.JsonUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.GET; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; /** * Audit log REST resources. diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java index 16059c92..6e3ca8d5 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java @@ -7,9 +7,13 @@ import com.sismics.security.IPrincipal; import com.sismics.security.UserPrincipal; import com.sismics.util.filter.SecurityFilter; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + import java.security.Principal; import java.util.List; import java.util.Set; @@ -19,6 +23,8 @@ import java.util.Set; * * @author jtremeaux */ +@Consumes(MediaType.APPLICATION_FORM_URLENCODED) +@Produces(MediaType.APPLICATION_JSON) public abstract class BaseResource { /** * @apiDefine admin Admin diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java index 42b19cac..4cfa164e 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/CommentResource.java @@ -9,11 +9,11 @@ import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.ImageUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.util.List; /** diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocsMessageBodyWriter.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocsMessageBodyWriter.java new file mode 100644 index 00000000..8307e599 --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocsMessageBodyWriter.java @@ -0,0 +1,34 @@ +package com.sismics.docs.rest.resource; + +import org.glassfish.jersey.message.internal.ReaderWriter; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * When a JSON-based exception is thrown but a JSON response is not expected, + * set the media type of the response as plain text. + */ +@Provider +@Produces(MediaType.APPLICATION_OCTET_STREAM) +public class DocsMessageBodyWriter implements MessageBodyWriter { + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return true; + } + + @Override + public void writeTo(JsonObject o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { + ReaderWriter.writeToAsString(o.toString(), entityStream, MediaType.TEXT_PLAIN_TYPE); + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index c5454a31..2d897ad8 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -7,10 +7,22 @@ import com.sismics.docs.core.constant.AclType; import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.constant.PermType; -import com.sismics.docs.core.dao.*; +import com.sismics.docs.core.dao.AclDao; +import com.sismics.docs.core.dao.ContributorDao; +import com.sismics.docs.core.dao.DocumentDao; +import com.sismics.docs.core.dao.FileDao; +import com.sismics.docs.core.dao.RelationDao; +import com.sismics.docs.core.dao.RouteStepDao; +import com.sismics.docs.core.dao.TagDao; +import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.dao.criteria.DocumentCriteria; import com.sismics.docs.core.dao.criteria.TagCriteria; -import com.sismics.docs.core.dao.dto.*; +import com.sismics.docs.core.dao.dto.AclDto; +import com.sismics.docs.core.dao.dto.ContributorDto; +import com.sismics.docs.core.dao.dto.DocumentDto; +import com.sismics.docs.core.dao.dto.RelationDto; +import com.sismics.docs.core.dao.dto.RouteStepDto; +import com.sismics.docs.core.dao.dto.TagDto; import com.sismics.docs.core.event.DocumentCreatedAsyncEvent; import com.sismics.docs.core.event.DocumentDeletedAsyncEvent; import com.sismics.docs.core.event.DocumentUpdatedAsyncEvent; @@ -19,7 +31,12 @@ import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.File; import com.sismics.docs.core.model.jpa.User; -import com.sismics.docs.core.util.*; +import com.sismics.docs.core.util.ConfigUtil; +import com.sismics.docs.core.util.DocumentUtil; +import com.sismics.docs.core.util.FileUtil; +import com.sismics.docs.core.util.MetadataUtil; +import com.sismics.docs.core.util.PdfUtil; +import com.sismics.docs.core.util.TagUtil; import com.sismics.docs.core.util.jpa.PaginatedList; import com.sismics.docs.core.util.jpa.PaginatedLists; import com.sismics.docs.core.util.jpa.SortCriteria; @@ -27,12 +44,29 @@ import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; import com.sismics.rest.util.AclUtil; +import com.sismics.rest.util.RestUtil; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.EmailUtil; import com.sismics.util.JsonUtil; import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.mime.MimeType; -import org.apache.commons.lang.StringUtils; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NotFoundException; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; import org.joda.time.DateTime; @@ -41,22 +75,25 @@ import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; import org.joda.time.format.DateTimeParser; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Session; import javax.mail.internet.MimeMessage; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.text.MessageFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; /** * Document REST resources. @@ -65,6 +102,21 @@ import java.util.*; */ @Path("/document") public class DocumentResource extends BaseResource { + + protected static final DateTimeParser YEAR_PARSER = DateTimeFormat.forPattern("yyyy").getParser(); + protected static final DateTimeParser MONTH_PARSER = DateTimeFormat.forPattern("yyyy-MM").getParser(); + protected static final DateTimeParser DAY_PARSER = DateTimeFormat.forPattern("yyyy-MM-dd").getParser(); + + private static final DateTimeFormatter DAY_FORMATTER = new DateTimeFormatter(null, DAY_PARSER); + private static final DateTimeFormatter MONTH_FORMATTER = new DateTimeFormatter(null, MONTH_PARSER); + private static final DateTimeFormatter YEAR_FORMATTER = new DateTimeFormatter(null, YEAR_PARSER); + + private static final DateTimeParser[] DATE_PARSERS = new DateTimeParser[]{ + YEAR_PARSER, + MONTH_PARSER, + DAY_PARSER}; + private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder().append( null, DATE_PARSERS).toFormatter(); + /** * Returns a document. * @@ -73,6 +125,7 @@ public class DocumentResource extends BaseResource { * @apiGroup Document * @apiParam {String} id Document ID * @apiParam {String} share Share ID + * @apiParam {Booleans} files If true includes files information * @apiSuccess {String} id ID * @apiSuccess {String} title Title * @apiSuccess {String} description Description @@ -104,6 +157,7 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String="READ","WRITE"} inherited_acls.perm Permission * @apiSuccess {String} inherited_acls.source_id Source ID * @apiSuccess {String} inherited_acls.source_name Source name + * @apiSuccess {String} inherited_acls.source_color The color of the Source * @apiSuccess {String} inherited_acls.id ID * @apiSuccess {String} inherited_acls.name Target name * @apiSuccess {String="USER","GROUP","SHARE"} inherited_acls.type Target type @@ -118,6 +172,17 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} route_step.name Route step name * @apiSuccess {String="APPROVE", "VALIDATE"} route_step.type Route step type * @apiSuccess {Boolean} route_step.transitionable True if the route step is actionable by the current user + * @apiSuccess {Object[]} files List of files + * @apiSuccess {String} files.id ID + * @apiSuccess {String} files.name File name + * @apiSuccess {String} files.version Zero-based version number + * @apiSuccess {String} files.mimetype MIME type + * @apiSuccess {String} files.create_date Create date (timestamp) + * @apiSuccess {Object[]} metadata List of metadata + * @apiSuccess {String} metadata.id ID + * @apiSuccess {String} metadata.name Name + * @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} metadata.type Type + * @apiSuccess {Object} metadata.value Value * @apiError (client) NotFound Document not found * @apiPermission none * @apiVersion 1.5.0 @@ -130,7 +195,8 @@ public class DocumentResource extends BaseResource { @Path("{id: [a-z0-9\\-]+}") public Response get( @PathParam("id") String documentId, - @QueryParam("share") String shareId) { + @QueryParam("share") String shareId, + @QueryParam("files") Boolean files) { authenticate(); DocumentDao documentDao = new DocumentDao(); @@ -196,6 +262,7 @@ public class DocumentResource extends BaseResource { .add("perm", aclDto.getPerm().name()) .add("source_id", tagDto.getId()) .add("source_name", tagDto.getName()) + .add("source_color", tagDto.getColor()) .add("id", aclDto.getTargetId()) .add("name", JsonUtil.nullable(aclDto.getTargetName())) .add("type", aclDto.getTargetType())); @@ -234,7 +301,23 @@ public class DocumentResource extends BaseResource { step.add("transitionable", getTargetIdList(null).contains(routeStepDto.getTargetId())); document.add("route_step", step); } - + + // Add custom metadata + MetadataUtil.addMetadata(document, documentId); + + // Add files + if (Boolean.TRUE == files) { + FileDao fileDao = new FileDao(); + List fileList = fileDao.getByDocumentsIds(Collections.singleton(documentId)); + + JsonArrayBuilder filesArrayBuilder = Json.createArrayBuilder(); + for (File fileDb : fileList) { + filesArrayBuilder.add(RestUtil.fileToJsonObjectBuilder(fileDb)); + } + + document.add("files", filesArrayBuilder); + } + return Response.ok().entity(document.build()).build(); } @@ -321,7 +404,8 @@ public class DocumentResource extends BaseResource { * @apiParam {String} offset Start at this index * @apiParam {Number} sort_column Column index to sort on * @apiParam {Boolean} asc If true, sort in ascending order - * @apiParam {String} search Search query + * @apiParam {String} search Search query (see "Document search syntax" on the top of the page for explanations) + * @apiParam {Booleans} files If true includes files information * @apiSuccess {Number} total Total number of documents * @apiSuccess {Object[]} documents List of documents * @apiSuccess {String} documents.id ID @@ -340,6 +424,12 @@ public class DocumentResource extends BaseResource { * @apiSuccess {String} documents.tags.id ID * @apiSuccess {String} documents.tags.name Name * @apiSuccess {String} documents.tags.color Color + * @apiSuccess {Object[]} documents.files List of files + * @apiSuccess {String} documents.files.id ID + * @apiSuccess {String} documents.files.name File name + * @apiSuccess {String} documents.files.version Zero-based version number + * @apiSuccess {String} documents.files.mimetype MIME type + * @apiSuccess {String} documents.files.create_date Create date (timestamp) * @apiSuccess {String[]} suggestions List of search suggestions * @apiError (client) ForbiddenError Access denied * @apiError (server) SearchError Error searching in documents @@ -351,6 +441,7 @@ public class DocumentResource extends BaseResource { * @param sortColumn Sort column * @param asc Sorting * @param search Search query + * @param files Files list * @return Response */ @GET @@ -360,7 +451,8 @@ public class DocumentResource extends BaseResource { @QueryParam("offset") Integer offset, @QueryParam("sort_column") Integer sortColumn, @QueryParam("asc") Boolean asc, - @QueryParam("search") String search) { + @QueryParam("search") String search, + @QueryParam("files") Boolean files) { if (!authenticate()) { throw new ForbiddenClientException(); } @@ -380,6 +472,17 @@ public class DocumentResource extends BaseResource { throw new ServerException("SearchError", "Error searching in documents", e); } + // Find the files of the documents + Iterable documentsIds = CollectionUtils.collect(paginatedList.getResultList(), DocumentDto::getId); + FileDao fileDao = new FileDao(); + List filesList = null; + Map filesCountByDocument = null; + if (Boolean.TRUE == files) { + filesList = fileDao.getByDocumentsIds(documentsIds); + } else { + filesCountByDocument = fileDao.countByDocumentsIds(documentsIds); + } + for (DocumentDto documentDto : paginatedList.getResultList()) { // Get tags accessible by the current user on this document List tagDtoList = tagDao.findByCriteria(new TagCriteria() @@ -392,8 +495,18 @@ public class DocumentResource extends BaseResource { .add("name", tagDto.getName()) .add("color", tagDto.getColor())); } - - documents.add(Json.createObjectBuilder() + + Long filesCount; + Collection filesOfDocument = null; + if (Boolean.TRUE == files) { + // Find files matching the document + filesOfDocument = CollectionUtils.select(filesList, file -> file.getDocumentId().equals(documentDto.getId())); + filesCount = (long) filesOfDocument.size(); + } else { + filesCount = filesCountByDocument.getOrDefault(documentDto.getId(), 0L); + } + + JsonObjectBuilder documentObjectBuilder = Json.createObjectBuilder() .add("id", documentDto.getId()) .add("highlight", JsonUtil.nullable(documentDto.getHighlight())) .add("file_id", JsonUtil.nullable(documentDto.getFileId())) @@ -405,8 +518,16 @@ public class DocumentResource extends BaseResource { .add("shared", documentDto.getShared()) .add("active_route", documentDto.isActiveRoute()) .add("current_step_name", JsonUtil.nullable(documentDto.getCurrentStepName())) - .add("file_count", documentDto.getFileCount()) - .add("tags", tags)); + .add("file_count", filesCount) + .add("tags", tags); + if (Boolean.TRUE == files) { + JsonArrayBuilder filesArrayBuilder = Json.createArrayBuilder(); + for (File fileDb : filesOfDocument) { + filesArrayBuilder.add(RestUtil.fileToJsonObjectBuilder(fileDb)); + } + documentObjectBuilder.add("files", filesArrayBuilder); + } + documents.add(documentObjectBuilder); } JsonArrayBuilder suggestions = Json.createArrayBuilder(); @@ -420,7 +541,36 @@ public class DocumentResource extends BaseResource { return Response.ok().entity(response.build()).build(); } - + + /** + * Returns all documents. + * + * @api {post} /document/list Get documents + * @apiDescription Get documents exposed as a POST endpoint to allow longer search parameters, see the GET endpoint for the API info + * @apiName PostDocumentList + * @apiGroup Document + * @apiVersion 1.12.0 + * + * @param limit Page limit + * @param offset Page offset + * @param sortColumn Sort column + * @param asc Sorting + * @param search Search query + * @param files Files list + * @return Response + */ + @POST + @Path("list") + public Response listPost( + @FormParam("limit") Integer limit, + @FormParam("offset") Integer offset, + @FormParam("sort_column") Integer sortColumn, + @FormParam("asc") Boolean asc, + @FormParam("search") String search, + @FormParam("files") Boolean files) { + return list(limit, offset, sortColumn, asc, search, files); + } + /** * Parse a query according to the specified syntax, eg.: * tag:assurance tag:other before:2012 after:2011-09 shared:yes lang:fra thing @@ -437,39 +587,27 @@ public class DocumentResource extends BaseResource { TagDao tagDao = new TagDao(); List allTagDtoList = tagDao.findByCriteria(new TagCriteria().setTargetIdList(getTargetIdList(null)), null); UserDao userDao = new UserDao(); - DateTimeParser[] parsers = { - DateTimeFormat.forPattern("yyyy").getParser(), - DateTimeFormat.forPattern("yyyy-MM").getParser(), - DateTimeFormat.forPattern("yyyy-MM-dd").getParser() }; - DateTimeFormatter yearFormatter = new DateTimeFormatter(null, parsers[0]); - DateTimeFormatter monthFormatter = new DateTimeFormatter(null, parsers[1]); - DateTimeFormatter dayFormatter = new DateTimeFormatter(null, parsers[2]); - DateTimeFormatter formatter = new DateTimeFormatterBuilder().append( null, parsers ).toFormatter(); - String[] criteriaList = search.split(" *"); + String[] criteriaList = search.split(" +"); List query = new ArrayList<>(); List fullQuery = new ArrayList<>(); for (String criteria : criteriaList) { String[] params = criteria.split(":"); if (params.length != 2 || Strings.isNullOrEmpty(params[0]) || Strings.isNullOrEmpty(params[1])) { - // This is not a special criteria - query.add(criteria); + // This is not a special criteria, do a fulltext search on it + fullQuery.add(criteria); continue; } + String paramName = params[0]; + String paramValue = params[1]; - switch (params[0]) { + switch (paramName) { case "tag": case "!tag": // New tag criteria - List tagDtoList = TagUtil.findByName(params[1], allTagDtoList); - if (documentCriteria.getTagIdList() == null) { - documentCriteria.setTagIdList(new ArrayList<>()); - } - if (documentCriteria.getExcludedTagIdList() == null) { - documentCriteria.setExcludedTagIdList(new ArrayList<>()); - } + List tagDtoList = TagUtil.findByName(paramValue, allTagDtoList); if (tagDtoList.isEmpty()) { - // No tag found, the request must returns nothing + // No tag found, the request must return nothing documentCriteria.getTagIdList().add(Lists.newArrayList(UUID.randomUUID().toString())); } else { List tagIdList = Lists.newArrayList(); @@ -480,7 +618,7 @@ public class DocumentResource extends BaseResource { tagIdList.add(childrenTagDto.getId()); } } - if (params[0].startsWith("!")) { + if (paramName.startsWith("!")) { documentCriteria.getExcludedTagIdList().add(tagIdList); } else { documentCriteria.getTagIdList().add(tagIdList); @@ -493,9 +631,9 @@ public class DocumentResource extends BaseResource { case "ubefore": // New date span criteria try { - boolean isUpdated = params[0].startsWith("u"); - DateTime date = formatter.parseDateTime(params[1]); - if (params[0].endsWith("before")) { + boolean isUpdated = paramName.startsWith("u"); + DateTime date = DATE_FORMATTER.parseDateTime(paramValue); + if (paramName.endsWith("before")) { if (isUpdated) documentCriteria.setUpdateDateMax(date.toDate()); else documentCriteria.setCreateDateMax(date.toDate()); } else { @@ -511,11 +649,11 @@ public class DocumentResource extends BaseResource { case "uat": case "at": // New specific date criteria + boolean isUpdated = params[0].startsWith("u"); try { - boolean isUpdated = params[0].startsWith("u"); - switch (params[1].length()) { + switch (paramValue.length()) { case 10: { - DateTime date = dayFormatter.parseDateTime(params[1]); + DateTime date = DATE_FORMATTER.parseDateTime(params[1]); if (isUpdated) { documentCriteria.setUpdateDateMin(date.toDate()); documentCriteria.setUpdateDateMax(date.plusDays(1).minusSeconds(1).toDate()); @@ -526,7 +664,7 @@ public class DocumentResource extends BaseResource { break; } case 7: { - DateTime date = monthFormatter.parseDateTime(params[1]); + DateTime date = MONTH_FORMATTER.parseDateTime(params[1]); if (isUpdated) { documentCriteria.setUpdateDateMin(date.toDate()); documentCriteria.setUpdateDateMax(date.plusMonths(1).minusSeconds(1).toDate()); @@ -537,7 +675,7 @@ public class DocumentResource extends BaseResource { break; } case 4: { - DateTime date = yearFormatter.parseDateTime(params[1]); + DateTime date = YEAR_FORMATTER.parseDateTime(params[1]); if (isUpdated) { documentCriteria.setUpdateDateMin(date.toDate()); documentCriteria.setUpdateDateMax(date.plusYears(1).minusSeconds(1).toDate()); @@ -546,6 +684,10 @@ public class DocumentResource extends BaseResource { documentCriteria.setCreateDateMax(date.plusYears(1).minusSeconds(1).toDate()); } break; + } default: { + // Invalid format, returns no documents + documentCriteria.setCreateDateMin(new Date(0)); + documentCriteria.setCreateDateMax(new Date(0)); } } } catch (IllegalArgumentException e) { @@ -556,21 +698,26 @@ public class DocumentResource extends BaseResource { break; case "shared": // New shared state criteria - documentCriteria.setShared(params[1].equals("yes")); + documentCriteria.setShared(paramValue.equals("yes")); break; case "lang": // New language criteria - if (Constants.SUPPORTED_LANGUAGES.contains(params[1])) { - documentCriteria.setLanguage(params[1]); + if (Constants.SUPPORTED_LANGUAGES.contains(paramValue)) { + documentCriteria.setLanguage(paramValue); } else { + // Unsupported language, returns no documents documentCriteria.setLanguage(UUID.randomUUID().toString()); } break; + case "mime": + // New mime type criteria + documentCriteria.setMimeType(paramValue); + break; case "by": // New creator criteria - User user = userDao.getActiveByUsername(params[1]); + User user = userDao.getActiveByUsername(paramValue); if (user == null) { - // This user doesn't exists, return nothing + // This user doesn't exist, return nothing documentCriteria.setCreatorId(UUID.randomUUID().toString()); } else { // This user exists, search its documents @@ -579,14 +726,22 @@ public class DocumentResource extends BaseResource { break; case "workflow": // New shared state criteria - documentCriteria.setActiveRoute(params[1].equals("me")); + documentCriteria.setActiveRoute(paramValue.equals("me")); + break; + case "simple": + // New simple search criteria + query.add(paramValue); break; case "full": - // New full content search criteria - fullQuery.add(params[1]); + // New fulltext search criteria + fullQuery.add(paramValue); + break; + case "title": + // New title criteria + documentCriteria.getTitleList().add(paramValue); break; default: - query.add(criteria); + fullQuery.add(criteria); break; } } @@ -614,6 +769,8 @@ public class DocumentResource extends BaseResource { * @apiParam {String} [rights] Rights * @apiParam {String[]} [tags] List of tags ID * @apiParam {String[]} [relations] List of related documents ID + * @apiParam {String[]} [metadata_id] List of metadata ID + * @apiParam {String[]} [metadata_value] List of metadata values * @apiParam {String} language Language * @apiParam {Number} [create_date] Create date (timestamp) * @apiSuccess {String} id Document ID @@ -634,6 +791,8 @@ public class DocumentResource extends BaseResource { * @param rights Rights * @param tagList Tags * @param relationList Relations + * @param metadataIdList Metadata ID list + * @param metadataValueList Metadata value list * @param language Language * @param createDateStr Creation date * @return Response @@ -652,6 +811,8 @@ public class DocumentResource extends BaseResource { @FormParam("rights") String rights, @FormParam("tags") List tagList, @FormParam("relations") List relationList, + @FormParam("metadata_id") List metadataIdList, + @FormParam("metadata_value") List metadataValueList, @FormParam("language") String language, @FormParam("create_date") String createDateStr) { if (!authenticate()) { @@ -674,7 +835,7 @@ public class DocumentResource extends BaseResource { if (!Constants.SUPPORTED_LANGUAGES.contains(language)) { throw new ClientException("ValidationError", MessageFormat.format("{0} is not a supported language", language)); } - + // Create the document Document document = new Document(); document.setUserId(principal.getId()); @@ -704,10 +865,17 @@ public class DocumentResource extends BaseResource { // Update relations updateRelationList(document.getId(), relationList); + // Update custom metadata + try { + MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList); + } catch (Exception e) { + throw new ClientException("ValidationError", e.getMessage()); + } + // Raise a document created event DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent(); documentCreatedAsyncEvent.setUserId(principal.getId()); - documentCreatedAsyncEvent.setDocument(document); + documentCreatedAsyncEvent.setDocumentId(document.getId()); ThreadLocalContext.get().addAsyncEvent(documentCreatedAsyncEvent); JsonObjectBuilder response = Json.createObjectBuilder() @@ -734,6 +902,8 @@ public class DocumentResource extends BaseResource { * @apiParam {String} [rights] Rights * @apiParam {String[]} [tags] List of tags ID * @apiParam {String[]} [relations] List of related documents ID + * @apiParam {String[]} [metadata_id] List of metadata ID + * @apiParam {String[]} [metadata_value] List of metadata values * @apiParam {String} language Language * @apiParam {Number} [create_date] Create date (timestamp) * @apiSuccess {String} id Document ID @@ -763,6 +933,8 @@ public class DocumentResource extends BaseResource { @FormParam("rights") String rights, @FormParam("tags") List tagList, @FormParam("relations") List relationList, + @FormParam("metadata_id") List metadataIdList, + @FormParam("metadata_value") List metadataValueList, @FormParam("language") String language, @FormParam("create_date") String createDateStr) { if (!authenticate()) { @@ -824,7 +996,14 @@ public class DocumentResource extends BaseResource { // Update relations updateRelationList(id, relationList); - + + // Update custom metadata + try { + MetadataUtil.updateMetadata(document.getId(), metadataIdList, metadataValueList); + } catch (Exception e) { + throw new ClientException("ValidationError", e.getMessage()); + } + // Raise a document updated event DocumentUpdatedAsyncEvent documentUpdatedAsyncEvent = new DocumentUpdatedAsyncEvent(); documentUpdatedAsyncEvent.setUserId(principal.getId()); @@ -913,7 +1092,7 @@ public class DocumentResource extends BaseResource { // Raise a document created event DocumentCreatedAsyncEvent documentCreatedAsyncEvent = new DocumentCreatedAsyncEvent(); documentCreatedAsyncEvent.setUserId(principal.getId()); - documentCreatedAsyncEvent.setDocument(document); + documentCreatedAsyncEvent.setDocumentId(document.getId()); ThreadLocalContext.get().addAsyncEvent(documentCreatedAsyncEvent); // Add files to the document @@ -969,29 +1148,15 @@ public class DocumentResource extends BaseResource { // Delete the document documentDao.delete(id, principal.getId()); - long totalSize = 0L; for (File file : fileList) { - // Store the file size to update the quota - java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(file.getId()); - try { - totalSize += Files.size(storedFile); - } catch (IOException e) { - // The file doesn't exists on disk, which is weird, but not fatal - } - // Raise file deleted event FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent(); fileDeletedAsyncEvent.setUserId(principal.getId()); - fileDeletedAsyncEvent.setFile(file); + fileDeletedAsyncEvent.setFileId(file.getId()); + fileDeletedAsyncEvent.setFileSize(file.getSize()); ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent); } - // Update the user quota - UserDao userDao = new UserDao(); - User user = userDao.getById(principal.getId()); - user.setStorageCurrent(user.getStorageCurrent() - totalSize); - userDao.updateQuota(user); - // Raise a document deleted event DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent(); documentDeletedAsyncEvent.setUserId(principal.getId()); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 21d7cb3e..be5eab74 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -21,6 +21,7 @@ import com.sismics.docs.core.util.FileUtil; import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; +import com.sismics.rest.util.RestUtil; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.HttpUtil; import com.sismics.util.JsonUtil; @@ -29,19 +30,20 @@ import com.sismics.util.mime.MimeType; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.StreamingOutput; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -111,10 +113,12 @@ public class FileResource extends BaseResource { } // Keep unencrypted data temporary on disk + String name = fileBodyPart.getContentDisposition() != null ? + URLDecoder.decode(fileBodyPart.getContentDisposition().getFileName(), StandardCharsets.UTF_8) : null; java.nio.file.Path unencryptedFile; long fileSize; try { - unencryptedFile = AppContext.getInstance().getFileService().createTemporaryFile(); + unencryptedFile = AppContext.getInstance().getFileService().createTemporaryFile(name); Files.copy(fileBodyPart.getValueAs(InputStream.class), unencryptedFile, StandardCopyOption.REPLACE_EXISTING); fileSize = Files.size(unencryptedFile); } catch (IOException e) { @@ -122,8 +126,6 @@ public class FileResource extends BaseResource { } try { - String name = fileBodyPart.getContentDisposition() != null ? - URLDecoder.decode(fileBodyPart.getContentDisposition().getFileName(), "UTF-8") : null; String fileId = FileUtil.createFile(name, previousFileId, unencryptedFile, fileSize, documentDto == null ? null : documentDto.getLanguage(), principal.getId(), documentId); @@ -202,7 +204,7 @@ public class FileResource extends BaseResource { FileUpdatedAsyncEvent fileUpdatedAsyncEvent = new FileUpdatedAsyncEvent(); fileUpdatedAsyncEvent.setUserId(principal.getId()); fileUpdatedAsyncEvent.setLanguage(documentDto.getLanguage()); - fileUpdatedAsyncEvent.setFile(file); + fileUpdatedAsyncEvent.setFileId(file.getId()); fileUpdatedAsyncEvent.setUnencryptedFile(unencryptedFile); ThreadLocalContext.get().addAsyncEvent(fileUpdatedAsyncEvent); @@ -310,7 +312,7 @@ public class FileResource extends BaseResource { FileUpdatedAsyncEvent event = new FileUpdatedAsyncEvent(); event.setUserId(principal.getId()); event.setLanguage(documentDto.getLanguage()); - event.setFile(file); + event.setFileId(file.getId()); event.setUnencryptedFile(unencryptedFile); ThreadLocalContext.get().addAsyncEvent(event); } catch (Exception e) { @@ -425,36 +427,22 @@ public class FileResource extends BaseResource { } else if (!authenticated) { throw new ForbiddenClientException(); } - - FileDao fileDao = new FileDao(); - List fileList = fileDao.getByDocumentId(principal.getId(), documentId); + FileDao fileDao = new FileDao(); JsonArrayBuilder files = Json.createArrayBuilder(); - for (File fileDb : fileList) { - try { - files.add(Json.createObjectBuilder() - .add("id", fileDb.getId()) - .add("processing", FileUtil.isProcessingFile(fileDb.getId())) - .add("name", JsonUtil.nullable(fileDb.getName())) - .add("version", fileDb.getVersion()) - .add("mimetype", fileDb.getMimeType()) - .add("document_id", JsonUtil.nullable(fileDb.getDocumentId())) - .add("create_date", fileDb.getCreateDate().getTime()) - .add("size", Files.size(DirectoryUtil.getStorageDirectory().resolve(fileDb.getId())))); - } catch (IOException e) { - throw new ServerException("FileError", "Unable to get the size of " + fileDb.getId(), e); - } + for (File fileDb : fileDao.getByDocumentId(principal.getId(), documentId)) { + files.add(RestUtil.fileToJsonObjectBuilder(fileDb)); } - JsonObjectBuilder response = Json.createObjectBuilder() .add("files", files); + return Response.ok().entity(response.build()).build(); } /** * List all versions of a file. * - * @api {get} /file/id/versions Get versions of a file + * @api {get} /file/:id/versions Get versions of a file * @apiName GetFileVersions * @apiGroup File * @apiParam {String} id File ID @@ -534,21 +522,11 @@ public class FileResource extends BaseResource { FileDao fileDao = new FileDao(); fileDao.delete(file.getId(), principal.getId()); - // Update the user quota - UserDao userDao = new UserDao(); - User user = userDao.getById(principal.getId()); - java.nio.file.Path storedFile = DirectoryUtil.getStorageDirectory().resolve(id); - try { - user.setStorageCurrent(user.getStorageCurrent() - Files.size(storedFile)); - userDao.updateQuota(user); - } catch (IOException e) { - // The file doesn't exists on disk, which is weird, but not fatal - } - // Raise a new file deleted event FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent(); fileDeletedAsyncEvent.setUserId(principal.getId()); - fileDeletedAsyncEvent.setFile(file); + fileDeletedAsyncEvent.setFileId(file.getId()); + fileDeletedAsyncEvent.setFileSize(file.getSize()); ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent); if (file.getDocumentId() != null) { @@ -587,6 +565,7 @@ public class FileResource extends BaseResource { */ @GET @Path("{id: [a-z0-9\\-]+}/data") + @Produces(MediaType.APPLICATION_OCTET_STREAM) public Response data( @PathParam("id") final String fileId, @QueryParam("share") String shareId, @@ -608,7 +587,7 @@ public class FileResource extends BaseResource { if (size != null) { if (size.equals("content")) { return Response.ok(Strings.nullToEmpty(file.getContent())) - .header(HttpHeaders.CONTENT_TYPE, "text/plain") + .header(HttpHeaders.CONTENT_TYPE, "text/plain; charset=utf-8") .build(); } @@ -676,23 +655,24 @@ public class FileResource extends BaseResource { /** * Returns all files from a document, zipped. * - * @api {get} /file/zip Get zipped files + * @api {get} /file/zip Returns all files from a document, zipped. * @apiName GetFileZip * @apiGroup File * @apiParam {String} id Document ID * @apiParam {String} share Share ID * @apiSuccess {Object} file The ZIP file is the whole response - * @apiError (client) NotFound Document not found + * @apiError (client) NotFoundException Document not found * @apiError (server) InternalServerError Error creating the ZIP file * @apiPermission none * @apiVersion 1.5.0 * * @param documentId Document ID + * @param shareId Share ID * @return Response */ @GET @Path("zip") - @Produces(MediaType.APPLICATION_OCTET_STREAM) + @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN}) public Response zip( @QueryParam("id") String documentId, @QueryParam("share") String shareId) { @@ -704,12 +684,46 @@ public class FileResource extends BaseResource { if (documentDto == null) { throw new NotFoundException(); } - - // Get files and user associated with this document + + // Get files associated with this document FileDao fileDao = new FileDao(); - final UserDao userDao = new UserDao(); final List fileList = fileDao.getByDocumentId(principal.getId(), documentId); - + String zipFileName = documentDto.getTitle().replaceAll("\\W+", "_"); + return sendZippedFiles(zipFileName, fileList); + } + + /** + * Returns a list of files, zipped + * + * @api {post} /file/zip Returns a list of files, zipped + * @apiName GetFilesZip + * @apiGroup File + * @apiParam {String[]} files IDs + * @apiSuccess {Object} file The ZIP file is the whole response + * @apiError (client) NotFoundException Files not found + * @apiError (server) InternalServerError Error creating the ZIP file + * @apiPermission none + * @apiVersion 1.11.0 + * + * @param filesIdsList Files IDs + * @return Response + */ + @POST + @Path("zip") + @Produces({MediaType.APPLICATION_OCTET_STREAM, MediaType.TEXT_PLAIN}) + public Response zip( + @FormParam("files") List filesIdsList) { + authenticate(); + List fileList = findFiles(filesIdsList); + return sendZippedFiles("files", fileList); + } + + /** + * Sent the content of a list of files. + */ + private Response sendZippedFiles(String zipFileName, List fileList) { + final UserDao userDao = new UserDao(); + // Create the ZIP stream StreamingOutput stream = outputStream -> { try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { @@ -739,7 +753,7 @@ public class FileResource extends BaseResource { // Write to the output return Response.ok(stream) .header("Content-Type", "application/zip") - .header("Content-Disposition", "attachment; filename=\"" + documentDto.getTitle().replaceAll("\\W+", "_") + ".zip\"") + .header("Content-Disposition", "attachment; filename=\"" + zipFileName + ".zip\"") .build(); } @@ -756,7 +770,32 @@ public class FileResource extends BaseResource { if (file == null) { throw new NotFoundException(); } + checkFileAccessible(shareId, file); + return file; + } + + /** + * Find a list of files with access rights checking. + * + * @param filesIds Files IDs + * @return List + */ + private List findFiles(List filesIds) { + FileDao fileDao = new FileDao(); + List files = fileDao.getFiles(filesIds); + for (File file : files) { + checkFileAccessible(null, file); + } + return files; + } + + /** + * Check if a file is accessible to the current user + * @param shareId Share ID + * @param file + */ + private void checkFileAccessible(String shareId, File file) { if (file.getDocumentId() == null) { // It's an orphan file if (!file.getUserId().equals(principal.getId())) { @@ -770,6 +809,5 @@ public class FileResource extends BaseResource { throw new ForbiddenClientException(); } } - return file; } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java index da2d2a60..982d90a8 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/GroupResource.java @@ -21,11 +21,11 @@ import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; import com.sismics.util.JsonUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.text.MessageFormat; import java.util.List; import java.util.Set; @@ -313,7 +313,7 @@ public class GroupResource extends BaseResource { * @return Response */ @DELETE - @Path("{groupName: [a-zA-Z0-9_]+}/{username: [a-zA-Z0-9_]+}") + @Path("{groupName: [a-zA-Z0-9_]+}/{username: [a-zA-Z0-9_@\\.]+}") public Response removeMember(@PathParam("groupName") String groupName, @PathParam("username") String username) { if (!authenticate()) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/MetadataResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/MetadataResource.java new file mode 100644 index 00000000..73e9d50f --- /dev/null +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/MetadataResource.java @@ -0,0 +1,208 @@ +package com.sismics.docs.rest.resource; + +import com.sismics.docs.core.constant.MetadataType; +import com.sismics.docs.core.dao.MetadataDao; +import com.sismics.docs.core.dao.criteria.MetadataCriteria; +import com.sismics.docs.core.dao.dto.MetadataDto; +import com.sismics.docs.core.model.jpa.Metadata; +import com.sismics.docs.core.util.jpa.SortCriteria; +import com.sismics.docs.rest.constant.BaseFunction; +import com.sismics.rest.exception.ForbiddenClientException; +import com.sismics.rest.util.ValidationUtil; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import java.util.List; + +/** + * Metadata REST resources. + * + * @author bgamard + */ +@Path("/metadata") +public class MetadataResource extends BaseResource { + /** + * Returns the list of all configured metadata. + * + * @api {get} /metadata Get configured metadata + * @apiName GetMetadata + * @apiGroup Metadata + * @apiParam {Number} sort_column Column index to sort on + * @apiParam {Boolean} asc If true, sort in ascending order + * @apiSuccess {Object[]} metadata List of metadata + * @apiSuccess {String} metadata.id ID + * @apiSuccess {String} metadata.name Name + * @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} metadata.type Type + * @apiError (client) ForbiddenError Access denied + * @apiPermission user + * @apiVersion 1.7.0 + * + * @return Response + */ + @GET + public Response list( + @QueryParam("sort_column") Integer sortColumn, + @QueryParam("asc") Boolean asc) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + JsonArrayBuilder metadata = Json.createArrayBuilder(); + SortCriteria sortCriteria = new SortCriteria(sortColumn, asc); + + MetadataDao metadataDao = new MetadataDao(); + List metadataDtoList = metadataDao.findByCriteria(new MetadataCriteria(), sortCriteria); + for (MetadataDto metadataDto : metadataDtoList) { + metadata.add(Json.createObjectBuilder() + .add("id", metadataDto.getId()) + .add("name", metadataDto.getName()) + .add("type", metadataDto.getType().name())); + } + + JsonObjectBuilder response = Json.createObjectBuilder() + .add("metadata", metadata); + return Response.ok().entity(response.build()).build(); + } + + /** + * Add a metadata. + * + * @api {put} /metadata Add a custom metadata + * @apiName PutMetadata + * @apiGroup Metadata + * @apiParam {String{1..50}} name Name + * @apiParam {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type + * @apiSuccess {String} id ID + * @apiSuccess {String} name Name + * @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type + * @apiError (client) ForbiddenError Access denied + * @apiError (client) ValidationError Validation error + * @apiPermission admin + * @apiVersion 1.7.0 + * + * @param name Name + * @param typeStr Type + * @return Response + */ + @PUT + public Response add(@FormParam("name") String name, + @FormParam("type") String typeStr) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Validate input data + name = ValidationUtil.validateLength(name, "name", 1, 50, false); + MetadataType type = MetadataType.valueOf(ValidationUtil.validateLength(typeStr, "type", 1, 20, false)); + + // Create the metadata + MetadataDao metadataDao = new MetadataDao(); + Metadata metadata = new Metadata(); + metadata.setName(name); + metadata.setType(type); + metadataDao.create(metadata, principal.getId()); + + // Returns the metadata + JsonObjectBuilder response = Json.createObjectBuilder() + .add("id", metadata.getId()) + .add("name", metadata.getName()) + .add("type", metadata.getType().name()); + return Response.ok().entity(response.build()).build(); + } + + /** + * Update a metadata. + * + * @api {post} /metadata/:id Update a custom metadata + * @apiName PostMetadataId + * @apiGroup Metadata + * @apiParam {String} id Metadata ID + * @apiParam {String{1..50}} name Name + * @apiSuccess {String} id ID + * @apiSuccess {String} name Name + * @apiSuccess {String="STRING","INTEGER","FLOAT","DATE","BOOLEAN"} type Type + * @apiError (client) ForbiddenError Access denied + * @apiError (client) ValidationError Validation error + * @apiError (client) NotFound Metadata not found + * @apiPermission admin + * @apiVersion 1.7.0 + * + * @param id ID + * @param name Name + * @return Response + */ + @POST + @Path("{id: [a-z0-9\\-]+}") + public Response update(@PathParam("id") String id, + @FormParam("name") String name) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Validate input data + name = ValidationUtil.validateLength(name, "name", 1, 50, false); + + // Get the metadata + MetadataDao metadataDao = new MetadataDao(); + Metadata metadata = metadataDao.getActiveById(id); + if (metadata == null) { + throw new NotFoundException(); + } + + // Update the metadata + metadata.setName(name); + metadataDao.update(metadata, principal.getId()); + + // Returns the metadata + JsonObjectBuilder response = Json.createObjectBuilder() + .add("id", metadata.getId()) + .add("name", metadata.getName()) + .add("type", metadata.getType().name()); + return Response.ok().entity(response.build()).build(); + } + + /** + * Delete a metadata. + * + * @api {delete} /metadata/:id Delete a custom metadata + * @apiName DeleteMetadataId + * @apiGroup Metadata + * @apiParam {String} id Metadata ID + * @apiSuccess {String} status Status OK + * @apiError (client) ForbiddenError Access denied + * @apiError (client) NotFound Metadata not found + * @apiPermission admin + * @apiVersion 1.7.0 + * + * @param id ID + * @return Response + */ + @DELETE + @Path("{id: [a-z0-9\\-]+}") + public Response delete(@PathParam("id") String id) { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + checkBaseFunction(BaseFunction.ADMIN); + + // Get the metadata + MetadataDao metadataDao = new MetadataDao(); + Metadata metadata = metadataDao.getActiveById(id); + if (metadata == null) { + throw new NotFoundException(); + } + + // Delete the metadata + metadataDao.delete(id, principal.getId()); + + // Always return OK + JsonObjectBuilder response = Json.createObjectBuilder() + .add("status", "ok"); + return Response.ok().entity(response.build()).build(); + } +} diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java index 35a7cf69..f9de81af 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteModelResource.java @@ -20,9 +20,9 @@ import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.AclUtil; import com.sismics.rest.util.ValidationUtil; -import javax.json.*; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.*; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.io.StringReader; import java.util.List; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java index 30e71a63..cc65c660 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/RouteResource.java @@ -18,9 +18,9 @@ import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; -import javax.json.*; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.*; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.io.StringReader; import java.util.List; @@ -209,7 +209,9 @@ public class RouteResource extends BaseResource { routeStepDao.endRouteStep(routeStepDto.getId(), routeStepTransition, comment, principal.getId()); RouteStepDto newRouteStep = routeStepDao.getCurrentStep(documentId); RoutingUtil.updateAcl(documentId, newRouteStep, routeStepDto, principal.getId()); - RoutingUtil.sendRouteStepEmail(documentId, routeStepDto); + if (newRouteStep != null) { + RoutingUtil.sendRouteStepEmail(documentId, newRouteStep); + } JsonObjectBuilder response = Json.createObjectBuilder() .add("readable", aclDao.checkPermission(documentId, PermType.READ, getTargetIdList(null))); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java index c85d5586..ac347fb5 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/ShareResource.java @@ -15,10 +15,10 @@ import com.sismics.rest.util.ValidationUtil; import com.sismics.util.JsonUtil; import com.sismics.util.context.ThreadLocalContext; -import javax.json.Json; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.text.MessageFormat; import java.util.List; @@ -55,7 +55,7 @@ public class ShareResource extends BaseResource { public Response add( @FormParam("id") String documentId, @FormParam("name") String name) { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } @@ -119,7 +119,7 @@ public class ShareResource extends BaseResource { @Path("{id: [a-z0-9\\-]+}") public Response delete( @PathParam("id") String id) { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java index 731e6307..88aabcf8 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/TagResource.java @@ -14,13 +14,13 @@ import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.AclUtil; import com.sismics.rest.util.ValidationUtil; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.text.MessageFormat; import java.util.List; import java.util.Set; @@ -155,7 +155,7 @@ public class TagResource extends BaseResource { * @apiSuccess {String} id Tag ID * @apiError (client) ForbiddenError Access denied * @apiError (client) ValidationError Validation error - * @apiError (client) SpacesNotAllowed Spaces are not allowed in tag name + * @apiError (client) IllegalTagName Spaces and colons are not allowed in tag name * @apiError (client) ParentNotFound Parent not found * @apiPermission user * @apiVersion 1.5.0 @@ -177,12 +177,8 @@ public class TagResource extends BaseResource { // Validate input data name = ValidationUtil.validateLength(name, "name", 1, 36, false); ValidationUtil.validateHexColor(color, "color", true); - - // Don't allow spaces - if (name.contains(" ")) { - throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name"); - } - + ValidationUtil.validateTagName(name); + // Check the parent if (StringUtils.isEmpty(parentId)) { parentId = null; @@ -237,7 +233,7 @@ public class TagResource extends BaseResource { * @apiSuccess {String} id Tag ID * @apiError (client) ForbiddenError Access denied * @apiError (client) ValidationError Validation error - * @apiError (client) SpacesNotAllowed Spaces are not allowed in tag name + * @apiError (client) IllegalTagName Spaces and colons are not allowed in tag name * @apiError (client) ParentNotFound Parent not found * @apiError (client) CircularReference Circular reference in parent tag * @apiError (client) NotFound Tag not found @@ -263,12 +259,8 @@ public class TagResource extends BaseResource { // Validate input data name = ValidationUtil.validateLength(name, "name", 1, 36, true); ValidationUtil.validateHexColor(color, "color", true); - - // Don't allow spaces - if (name.contains(" ")) { - throw new ClientException("SpacesNotAllowed", "Spaces are not allowed in tag name"); - } - + ValidationUtil.validateTagName(name); + // Check permission AclDao aclDao = new AclDao(); if (!aclDao.checkPermission(id, PermType.WRITE, getTargetIdList(null))) { diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java index 5762a3e8..fab2ddaa 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/ThemeResource.java @@ -17,11 +17,11 @@ import com.sismics.util.css.Selector; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; -import javax.json.*; -import javax.ws.rs.*; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.StreamingOutput; +import jakarta.json.*; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.StreamingOutput; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index dd413cac..9403025b 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -30,16 +30,16 @@ import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.filter.TokenBasedSecurityFilter; import com.sismics.util.totp.GoogleAuthenticator; import com.sismics.util.totp.GoogleAuthenticatorKey; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.servlet.http.Cookie; -import javax.ws.rs.*; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.NewCookie; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.servlet.http.Cookie; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; import java.util.Date; import java.util.List; import java.util.Set; @@ -88,7 +88,7 @@ public class UserResource extends BaseResource { // Validate the input data username = ValidationUtil.validateLength(username, "username", 3, 50); - ValidationUtil.validateAlphanumeric(username, "username"); + ValidationUtil.validateUsername(username, "username"); password = ValidationUtil.validateLength(password, "password", 8, 50); email = ValidationUtil.validateLength(email, "email", 1, 100); Long storageQuota = ValidationUtil.validateLong(storageQuotaStr, "storage_quota"); @@ -195,7 +195,7 @@ public class UserResource extends BaseResource { * @return Response */ @POST - @Path("{username: [a-zA-Z0-9_]+}") + @Path("{username: [a-zA-Z0-9_@\\.]+}") public Response update( @PathParam("username") String username, @FormParam("password") String password, @@ -366,7 +366,7 @@ public class UserResource extends BaseResource { AuthenticationToken authenticationToken = new AuthenticationToken() .setUserId(user.getId()) .setLongLasted(longLasted) - .setIp(ip) + .setIp(StringUtils.abbreviate(ip, 45)) .setUserAgent(StringUtils.abbreviate(request.getHeader("user-agent"), 1000)); String token = authenticationTokenDao.create(authenticationToken); @@ -470,22 +470,8 @@ public class UserResource extends BaseResource { UserDao userDao = new UserDao(); userDao.delete(principal.getName(), principal.getId()); - // Raise deleted events for documents - for (Document document : documentList) { - DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent(); - documentDeletedAsyncEvent.setUserId(principal.getId()); - documentDeletedAsyncEvent.setDocumentId(document.getId()); - ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent); - } - - // Raise deleted events for files (don't bother sending document updated event) - for (File file : fileList) { - FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent(); - fileDeletedAsyncEvent.setUserId(principal.getId()); - fileDeletedAsyncEvent.setFile(file); - ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent); - } - + sendDeletionEvents(documentList, fileList); + // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() .add("status", "ok"); @@ -511,7 +497,7 @@ public class UserResource extends BaseResource { * @return Response */ @DELETE - @Path("{username: [a-zA-Z0-9_]+}") + @Path("{username: [a-zA-Z0-9_@\\.]+}") public Response delete(@PathParam("username") String username) { if (!authenticate()) { throw new ForbiddenClientException(); @@ -551,23 +537,9 @@ public class UserResource extends BaseResource { // Delete the user userDao.delete(user.getUsername(), principal.getId()); - - // Raise deleted events for documents - for (Document document : documentList) { - DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent(); - documentDeletedAsyncEvent.setUserId(principal.getId()); - documentDeletedAsyncEvent.setDocumentId(document.getId()); - ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent); - } - - // Raise deleted events for files (don't bother sending document updated event) - for (File file : fileList) { - FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent(); - fileDeletedAsyncEvent.setUserId(principal.getId()); - fileDeletedAsyncEvent.setFile(file); - ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent); - } - + + sendDeletionEvents(documentList, fileList); + // Always return OK JsonObjectBuilder response = Json.createObjectBuilder() .add("status", "ok"); @@ -591,7 +563,7 @@ public class UserResource extends BaseResource { * @return Response */ @POST - @Path("{username: [a-zA-Z0-9_]+}/disable_totp") + @Path("{username: [a-zA-Z0-9_@\\.]+}/disable_totp") public Response disableTotpUsername(@PathParam("username") String username) { if (!authenticate()) { throw new ForbiddenClientException(); @@ -713,7 +685,7 @@ public class UserResource extends BaseResource { * @return Response */ @GET - @Path("{username: [a-zA-Z0-9_]+}") + @Path("{username: [a-zA-Z0-9_@\\.]+}") @Produces(MediaType.APPLICATION_JSON) public Response view(@PathParam("username") String username) { if (!authenticate()) { @@ -1064,7 +1036,6 @@ public class UserResource extends BaseResource { * @apiGroup User * @apiParam {String} username Username * @apiSuccess {String} status Status OK - * @apiError (client) UserNotFound The user is not found * @apiError (client) ValidationError Validation error * @apiPermission none * @apiVersion 1.5.0 @@ -1081,11 +1052,16 @@ public class UserResource extends BaseResource { // Validate input data ValidationUtil.validateStringNotBlank("username", username); + // Prepare response + Response response = Response.ok().entity(Json.createObjectBuilder() + .add("status", "ok") + .build()).build(); + // Check for user existence UserDao userDao = new UserDao(); List userDtoList = userDao.findByCriteria(new UserCriteria().setUserName(username), null); if (userDtoList.isEmpty()) { - throw new ClientException("UserNotFound", "User not found: " + username); + return response; } UserDto user = userDtoList.get(0); @@ -1102,9 +1078,7 @@ public class UserResource extends BaseResource { AppContext.getInstance().getMailEventBus().post(passwordLostEvent); // Always return OK - JsonObjectBuilder response = Json.createObjectBuilder() - .add("status", "ok"); - return Response.ok().entity(response.build()).build(); + return response; } /** @@ -1176,4 +1150,29 @@ public class UserResource extends BaseResource { } return null; } + + /** + * Send the events about documents and files being deleted. + * @param documentList A document list + * @param fileList A file list + */ + private void sendDeletionEvents(List documentList, List fileList) { + // Raise deleted events for documents + for (Document document : documentList) { + DocumentDeletedAsyncEvent documentDeletedAsyncEvent = new DocumentDeletedAsyncEvent(); + documentDeletedAsyncEvent.setUserId(principal.getId()); + documentDeletedAsyncEvent.setDocumentId(document.getId()); + ThreadLocalContext.get().addAsyncEvent(documentDeletedAsyncEvent); + } + + // Raise deleted events for files (don't bother sending document updated event) + for (File file : fileList) { + FileDeletedAsyncEvent fileDeletedAsyncEvent = new FileDeletedAsyncEvent(); + fileDeletedAsyncEvent.setUserId(principal.getId()); + fileDeletedAsyncEvent.setFileId(file.getId()); + fileDeletedAsyncEvent.setFileSize(file.getSize()); + ThreadLocalContext.get().addAsyncEvent(fileDeletedAsyncEvent); + } + } + } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/VocabularyResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/VocabularyResource.java index 7063c7c8..c82efff7 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/VocabularyResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/VocabularyResource.java @@ -6,11 +6,11 @@ import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.util.List; /** @@ -198,7 +198,7 @@ public class VocabularyResource extends BaseResource { /** * Delete a vocabulary entry. * - * @api {delete} /vocabulary/:id Delete vocabulary entry + * @api {delete} /vocabulary/:id Delete a vocabulary entry * @apiName DeleteVocabularyId * @apiGroup Vocabulary * @apiParam {String} id Entry ID diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java index 46c96909..9b36f3da 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/WebhookResource.java @@ -10,11 +10,11 @@ import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.util.ValidationUtil; -import javax.json.Json; -import javax.json.JsonArrayBuilder; -import javax.json.JsonObjectBuilder; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; import java.util.List; /** diff --git a/docs-web/src/main/webapp/WEB-INF/web.xml b/docs-web/src/main/webapp/WEB-INF/web.xml index 5c62173a..720b328e 100644 --- a/docs-web/src/main/webapp/WEB-INF/web.xml +++ b/docs-web/src/main/webapp/WEB-INF/web.xml @@ -1,16 +1,16 @@ + xmlns="https://jakarta.ee/xml/ns/jakartaee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" + version="5.0" + metadata-complete="true"> Teedy ImageIO service provider loader/unloader - com.twelvemonkeys.servlet.image.IIOProviderContextListener + com.sismics.util.listener.IIOProviderContextListener @@ -25,7 +25,7 @@ /api/* - + requestContextFilter com.sismics.util.filter.RequestContextFilter diff --git a/docs-web/src/main/webapp/header.md b/docs-web/src/main/webapp/header.md index 5e5ac1e1..5862af86 100644 --- a/docs-web/src/main/webapp/header.md +++ b/docs-web/src/main/webapp/header.md @@ -10,7 +10,7 @@ The base URL depends on your server. If your instance of Teedy is accessible thr `https://teedy.mycompany.com`, then the base API URL is `https://teedy.mycompany.com/api`. ## Verbs and status codes -The API uses restful verbs. +The API uses RESTful verbs. | Verb | Description | |---|---| @@ -46,4 +46,43 @@ curl -i -X GET -H "Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4" http A call to this API with a given `auth_token` cookie will make it unusable for other calls. ``` curl -i -X POST -H "Cookie: auth_token=64085630-2ae6-415c-9a92-4b22c107eaa4" https://docs.mycompany.com/api/user/logout -``` \ No newline at end of file +``` + +## Document search syntax + +The `/api/document/list` endpoint use a String `search` parameter. + +This parameter is split in segments using the space character (the other whitespace characters are not considered). + +If a segment contains exactly one colon (`:`), it will used as a field criteria (see bellow). +In other cases (zero or more than one colon), the segment will be used as a search criteria for all fields including the document's files content. + +### Search fields + +If a search `VALUE` is considered invalid, the search result will be empty. + +* Content + * `full:VALUE`: `VALUE` is used as search criteria for all fields, including the document's files content + * `simple:VALUE`: `VALUE` is used as a search criteria for all fields except the document's files content +* Date + * `after:VALUE`: the document must have been created after or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` + * `at:VALUE`: the document must have been created at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` (for `yyyy` it must be the same year, for `yyyy-MM` the same month, for `yyyy-MM-dd` the same day) + * `before:VALUE`: the document must have been created before or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` + * `uafter:VALUE`: the document must have been last updated after or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` + * `at:VALUE`: the document must have been updated at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` (for `yyyy` it must be the same year, for `yyyy-MM` the same month, for `yyyy-MM-dd` the same day) + * `ubefore:VALUE`: the document must have been updated before or at the `VALUE` moment, accepted format are `yyyy`, `yyyy-MM` and `yyyy-MM-dd` +* Language + * `lang:VALUE`: the document must be of the specified language (example: `en`) +* Mime + * `mime:VALUE`: the document must be of the specified mime type (example: `image/png`) +* Shared + * `shared:VALUE`: if `VALUE` is `yes`the document must be shared, for other `VALUE`s the criteria is ignored +* Tags: several `tags` or `!tag:` can be specified and the document must match all filters + * `tag:VALUE`: the document must contain a tag or a child of a tag that starts with `VALUE`, case is ignored + * `!tag:VALUE`: the document must not contain a tag or a child of a tag that starts with `VALUE`, case is ignored +* Titles: several `title` can be specified, and the document must match any of the titles + * `title:VALUE`: the title of the document must be `VALUE` +* User + * `by:VALUE`: the document creator's username must be `VALUE` with an exact match, the user must not be deleted +* Workflow + * `workflow:VALUE`: if `VALUE` is `me` the document must have an active route, for other `VALUE`s the criteria is ignored diff --git a/docs-web/src/main/webapp/package-lock.json b/docs-web/src/main/webapp/package-lock.json index 60e38cab..23656fbd 100644 --- a/docs-web/src/main/webapp/package-lock.json +++ b/docs-web/src/main/webapp/package-lock.json @@ -23,8 +23,8 @@ "dev": true, "optional": true, "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" } }, "align-text": { @@ -33,9 +33,9 @@ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" } }, "alter": { @@ -44,7 +44,7 @@ "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", "dev": true, "requires": { - "stable": "0.1.6" + "stable": "~0.1.3" } }, "amdefine": { @@ -71,12 +71,12 @@ "integrity": "sha1-TuisYQ3t3csQBsPij6fdY0tKXOY=", "dev": true, "requires": { - "apidoc-core": "0.8.3", - "fs-extra": "3.0.1", - "lodash": "4.17.5", - "markdown-it": "8.4.1", - "nomnom": "1.8.1", - "winston": "2.3.1" + "apidoc-core": "~0.8.2", + "fs-extra": "~3.0.1", + "lodash": "~4.17.4", + "markdown-it": "^8.3.1", + "nomnom": "~1.8.1", + "winston": "~2.3.1" } }, "apidoc-core": { @@ -85,12 +85,12 @@ "integrity": "sha1-2dY1RYKd8lDSzKBJaDqH53U2S5Y=", "dev": true, "requires": { - "fs-extra": "3.0.1", - "glob": "7.1.2", - "iconv-lite": "0.4.19", - "klaw-sync": "2.1.0", - "lodash": "4.17.5", - "semver": "5.3.0" + "fs-extra": "^3.0.1", + "glob": "^7.1.1", + "iconv-lite": "^0.4.17", + "klaw-sync": "^2.1.0", + "lodash": "~4.17.4", + "semver": "~5.3.0" }, "dependencies": { "glob": { @@ -99,12 +99,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "semver": { @@ -121,7 +121,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" }, "dependencies": { "sprintf-js": { @@ -199,7 +199,7 @@ "dev": true, "optional": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "boom": { @@ -207,8 +207,9 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "dev": true, + "optional": true, "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "brace-expansion": { @@ -217,7 +218,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -227,7 +228,7 @@ "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", "dev": true, "requires": { - "pako": "0.2.9" + "pako": "~0.2.0" } }, "builtin-modules": { @@ -242,8 +243,8 @@ "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", "dev": true, "requires": { - "no-case": "2.3.2", - "upper-case": "1.1.3" + "no-case": "^2.2.0", + "upper-case": "^1.1.1" } }, "camelcase": { @@ -258,8 +259,8 @@ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { - "camelcase": "2.1.1", - "map-obj": "1.0.1" + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" } }, "caseless": { @@ -275,8 +276,8 @@ "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", "dev": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" } }, "chalk": { @@ -285,11 +286,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "change-case": { @@ -298,24 +299,24 @@ "integrity": "sha512-Mww+SLF6MZ0U6kdg11algyKd5BARbyM4TbFBepwowYSR5ClfQGCGtxNXgykpN0uF/bstWeaGDT4JWaDh8zWAHA==", "dev": true, "requires": { - "camel-case": "3.0.0", - "constant-case": "2.0.0", - "dot-case": "2.1.1", - "header-case": "1.0.1", - "is-lower-case": "1.1.3", - "is-upper-case": "1.1.2", - "lower-case": "1.1.4", - "lower-case-first": "1.0.2", - "no-case": "2.3.2", - "param-case": "2.1.1", - "pascal-case": "2.0.1", - "path-case": "2.1.1", - "sentence-case": "2.1.1", - "snake-case": "2.1.0", - "swap-case": "1.1.2", - "title-case": "2.1.1", - "upper-case": "1.1.3", - "upper-case-first": "1.1.2" + "camel-case": "^3.0.0", + "constant-case": "^2.0.0", + "dot-case": "^2.1.0", + "header-case": "^1.0.0", + "is-lower-case": "^1.1.0", + "is-upper-case": "^1.1.0", + "lower-case": "^1.1.1", + "lower-case-first": "^1.0.0", + "no-case": "^2.3.2", + "param-case": "^2.1.0", + "pascal-case": "^2.0.0", + "path-case": "^2.1.0", + "sentence-case": "^2.1.0", + "snake-case": "^2.1.0", + "swap-case": "^1.1.0", + "title-case": "^2.1.0", + "upper-case": "^1.1.1", + "upper-case-first": "^1.1.0" } }, "clean-css": { @@ -324,8 +325,8 @@ "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", "dev": true, "requires": { - "commander": "2.8.1", - "source-map": "0.4.4" + "commander": "2.8.x", + "source-map": "0.4.x" }, "dependencies": { "commander": { @@ -334,7 +335,7 @@ "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "graceful-readlink": ">= 1.0.0" } } } @@ -345,8 +346,8 @@ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", "dev": true, "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", + "center-align": "^0.1.1", + "right-align": "^0.1.1", "wordwrap": "0.0.2" } }, @@ -374,8 +375,9 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "dev": true, + "optional": true, "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -384,7 +386,7 @@ "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "graceful-readlink": ">= 1.0.0" } }, "concat-map": { @@ -399,9 +401,9 @@ "integrity": "sha512-gslSSJx03QKa59cIKqeJO9HQ/WZMotvYJCuaUULrLpjj8oG40kV2Z+gz82pVxlTkOADi4PJxQPPfhl1ELYrrXw==", "dev": true, "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.5", - "typedarray": "0.0.6" + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "constant-case": { @@ -410,8 +412,8 @@ "integrity": "sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY=", "dev": true, "requires": { - "snake-case": "2.1.0", - "upper-case": "1.1.3" + "snake-case": "^2.1.0", + "upper-case": "^1.1.1" } }, "convert-source-map": { @@ -439,7 +441,7 @@ "dev": true, "optional": true, "requires": { - "boom": "2.10.1" + "boom": "2.x.x" } }, "csslint": { @@ -454,7 +456,7 @@ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "dev": true, "requires": { - "array-find-index": "1.0.2" + "array-find-index": "^1.0.1" } }, "cycle": { @@ -470,7 +472,7 @@ "dev": true, "optional": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" }, "dependencies": { "assert-plus": { @@ -488,8 +490,8 @@ "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", "dev": true, "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" + "get-stdin": "^4.0.1", + "meow": "^3.3.0" } }, "decamelize": { @@ -508,7 +510,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "dev": true, + "optional": true }, "dot-case": { "version": "2.1.1", @@ -516,7 +519,7 @@ "integrity": "sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4=", "dev": true, "requires": { - "no-case": "2.3.2" + "no-case": "^2.2.0" } }, "ecc-jsbn": { @@ -526,7 +529,7 @@ "dev": true, "optional": true, "requires": { - "jsbn": "0.1.1" + "jsbn": "~0.1.0" } }, "entities": { @@ -542,7 +545,7 @@ "dev": true, "optional": true, "requires": { - "prr": "1.0.1" + "prr": "~1.0.1" } }, "error-ex": { @@ -551,7 +554,7 @@ "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "is-arrayish": "^0.2.1" } }, "escape-string-regexp": { @@ -589,7 +592,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true + "dev": true, + "optional": true }, "eyes": { "version": "0.1.8", @@ -603,8 +607,8 @@ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", "dev": true, "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" } }, "file-sync-cmp": { @@ -619,8 +623,8 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "findup-sync": { @@ -629,7 +633,7 @@ "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", "dev": true, "requires": { - "glob": "5.0.15" + "glob": "~5.0.0" }, "dependencies": { "glob": { @@ -638,11 +642,11 @@ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } @@ -661,9 +665,9 @@ "dev": true, "optional": true, "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.6", - "mime-types": "2.1.18" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" } }, "fs-extra": { @@ -672,9 +676,9 @@ "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "3.0.1", - "universalify": "0.1.1" + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" } }, "fs.realpath": { @@ -702,7 +706,7 @@ "dev": true, "optional": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" }, "dependencies": { "assert-plus": { @@ -720,12 +724,12 @@ "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "graceful-fs": { @@ -746,22 +750,22 @@ "integrity": "sha1-TmpeaVtwRy/VME9fqeNCNoNqc7w=", "dev": true, "requires": { - "coffeescript": "1.10.0", - "dateformat": "1.0.12", - "eventemitter2": "0.4.14", - "exit": "0.1.2", - "findup-sync": "0.3.0", - "glob": "7.0.6", - "grunt-cli": "1.2.0", - "grunt-known-options": "1.1.0", - "grunt-legacy-log": "1.0.1", - "grunt-legacy-util": "1.0.0", - "iconv-lite": "0.4.19", - "js-yaml": "3.5.5", - "minimatch": "3.0.4", - "nopt": "3.0.6", - "path-is-absolute": "1.0.1", - "rimraf": "2.2.8" + "coffeescript": "~1.10.0", + "dateformat": "~1.0.12", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.3.0", + "glob": "~7.0.0", + "grunt-cli": "~1.2.0", + "grunt-known-options": "~1.1.0", + "grunt-legacy-log": "~1.0.0", + "grunt-legacy-util": "~1.0.0", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.5.2", + "minimatch": "~3.0.2", + "nopt": "~3.0.6", + "path-is-absolute": "~1.0.0", + "rimraf": "~2.2.8" }, "dependencies": { "grunt-cli": { @@ -770,10 +774,10 @@ "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", "dev": true, "requires": { - "findup-sync": "0.3.0", - "grunt-known-options": "1.1.0", - "nopt": "3.0.6", - "resolve": "1.1.7" + "findup-sync": "~0.3.0", + "grunt-known-options": "~1.1.0", + "nopt": "~3.0.6", + "resolve": "~1.1.0" } } } @@ -784,7 +788,7 @@ "integrity": "sha1-EJYDorlf8BAZtxjHA0EmjwnYvhk=", "dev": true, "requires": { - "html-minifier": "2.1.7" + "html-minifier": "~2.1.2" } }, "grunt-apidoc": { @@ -793,7 +797,7 @@ "integrity": "sha1-mMGUWtfoq6Hx1fFVHqs9QrAQ6s0=", "dev": true, "requires": { - "apidoc": "0.17.6" + "apidoc": "*" } }, "grunt-cleanempty": { @@ -802,7 +806,7 @@ "integrity": "sha1-V4OuhKAMeD4pDq3oQdK1biImIOo=", "dev": true, "requires": { - "junk": "1.0.3" + "junk": "^1.0.2" } }, "grunt-contrib-clean": { @@ -811,8 +815,8 @@ "integrity": "sha1-Vkq/LQN4qYOhW54/MO51tzjEBjg=", "dev": true, "requires": { - "async": "1.5.2", - "rimraf": "2.6.2" + "async": "^1.5.2", + "rimraf": "^2.5.1" }, "dependencies": { "rimraf": { @@ -821,7 +825,7 @@ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "requires": { - "glob": "7.0.6" + "glob": "^7.0.5" } } } @@ -832,8 +836,8 @@ "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", "dev": true, "requires": { - "chalk": "1.1.3", - "source-map": "0.5.7" + "chalk": "^1.0.0", + "source-map": "^0.5.3" }, "dependencies": { "source-map": { @@ -850,8 +854,8 @@ "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", "dev": true, "requires": { - "chalk": "1.1.3", - "file-sync-cmp": "0.1.1" + "chalk": "^1.1.1", + "file-sync-cmp": "^0.1.0" } }, "grunt-contrib-less": { @@ -860,10 +864,10 @@ "integrity": "sha1-O73sC3XRLOqlXWKUNiXAsIYc328=", "dev": true, "requires": { - "async": "2.6.0", - "chalk": "1.1.3", - "less": "2.7.3", - "lodash": "4.17.5" + "async": "^2.0.0", + "chalk": "^1.0.0", + "less": "~2.7.1", + "lodash": "^4.8.2" }, "dependencies": { "async": { @@ -872,7 +876,7 @@ "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", "dev": true, "requires": { - "lodash": "4.17.5" + "lodash": "^4.14.0" } } } @@ -883,11 +887,11 @@ "integrity": "sha1-rmekb5FT7dTLEYE6Vetpxw19svs=", "dev": true, "requires": { - "chalk": "1.1.3", - "lodash": "4.17.5", - "maxmin": "1.1.0", - "uglify-js": "2.6.4", - "uri-path": "1.0.0" + "chalk": "^1.0.0", + "lodash": "^4.0.1", + "maxmin": "^1.1.0", + "uglify-js": "~2.6.2", + "uri-path": "^1.0.0" } }, "grunt-css": { @@ -907,7 +911,7 @@ "integrity": "sha1-SLIhUbkAVuE5qA1Mgk4PDzOsNgc=", "dev": true, "requires": { - "optimist": "0.3.7" + "optimist": "0.3.x" } } } @@ -930,11 +934,11 @@ "integrity": "sha512-rwuyqNKlI0IPz0DvxzJjcEiQEBaBNVeb1LFoZKxSmHLETFUwhwUrqOsPIxURTKSwNZHZ4ht1YLBYmVU0YZAzHQ==", "dev": true, "requires": { - "colors": "1.1.2", - "grunt-legacy-log-utils": "1.0.0", - "hooker": "0.2.3", - "lodash": "4.17.5", - "underscore.string": "3.3.4" + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.5", + "underscore.string": "~3.3.4" } }, "grunt-legacy-log-utils": { @@ -943,8 +947,8 @@ "integrity": "sha1-p7ji0Ps1taUPSvmG/BEnSevJbz0=", "dev": true, "requires": { - "chalk": "1.1.3", - "lodash": "4.3.0" + "chalk": "~1.1.1", + "lodash": "~4.3.0" }, "dependencies": { "lodash": { @@ -961,13 +965,13 @@ "integrity": "sha1-OGqnjcbtUJhsKxiVcmWxtIq7m4Y=", "dev": true, "requires": { - "async": "1.5.2", - "exit": "0.1.2", - "getobject": "0.1.0", - "hooker": "0.2.3", - "lodash": "4.3.0", - "underscore.string": "3.2.3", - "which": "1.2.14" + "async": "~1.5.2", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~4.3.0", + "underscore.string": "~3.2.3", + "which": "~1.2.1" }, "dependencies": { "lodash": { @@ -990,8 +994,8 @@ "integrity": "sha1-SZPLr1aNUdHAw74K8EoIqCKZ0Uo=", "dev": true, "requires": { - "lodash.clonedeep": "4.5.0", - "ng-annotate": "1.2.2" + "lodash.clonedeep": "^4.3.2", + "ng-annotate": "^1.2.1" } }, "grunt-text-replace": { @@ -1006,8 +1010,8 @@ "integrity": "sha1-ejZ8TUCSEDMBAhiidAZZ24yvJ5I=", "dev": true, "requires": { - "crc32": "0.2.2", - "deflate-js": "0.2.3" + "crc32": ">= 0.2.2", + "deflate-js": ">= 0.2.2" } }, "gzip-size": { @@ -1016,8 +1020,8 @@ "integrity": "sha1-Zs+LEBBHInuVus5uodoMF37Vwi8=", "dev": true, "requires": { - "browserify-zlib": "0.1.4", - "concat-stream": "1.6.1" + "browserify-zlib": "^0.1.4", + "concat-stream": "^1.4.1" } }, "har-schema": { @@ -1034,8 +1038,8 @@ "dev": true, "optional": true, "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "^4.9.1", + "har-schema": "^1.0.5" } }, "has-ansi": { @@ -1044,7 +1048,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "has-color": { @@ -1060,10 +1064,10 @@ "dev": true, "optional": true, "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" } }, "he": { @@ -1078,15 +1082,16 @@ "integrity": "sha1-lTWXMZfBRLCWE81l0xfvGZY70C0=", "dev": true, "requires": { - "no-case": "2.3.2", - "upper-case": "1.1.3" + "no-case": "^2.2.0", + "upper-case": "^1.1.3" } }, "hoek": { "version": "2.16.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true + "dev": true, + "optional": true }, "hooker": { "version": "0.2.3", @@ -1106,13 +1111,13 @@ "integrity": "sha1-kFHW/LvPIU7TB+GtdPQyu5rWVcw=", "dev": true, "requires": { - "change-case": "3.0.2", - "clean-css": "3.4.28", - "commander": "2.9.0", - "he": "1.1.1", - "ncname": "1.0.0", - "relateurl": "0.2.7", - "uglify-js": "2.6.4" + "change-case": "3.0.x", + "clean-css": "3.4.x", + "commander": "2.9.x", + "he": "1.1.x", + "ncname": "1.0.x", + "relateurl": "0.2.x", + "uglify-js": "2.6.x" } }, "http-signature": { @@ -1122,9 +1127,9 @@ "dev": true, "optional": true, "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -1146,7 +1151,7 @@ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" } }, "inflight": { @@ -1155,8 +1160,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -1183,7 +1188,7 @@ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", "dev": true, "requires": { - "builtin-modules": "1.1.1" + "builtin-modules": "^1.0.0" } }, "is-finite": { @@ -1192,7 +1197,7 @@ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-lower-case": { @@ -1201,7 +1206,7 @@ "integrity": "sha1-fhR75HaNxGbbO/shzGCzHmrWk5M=", "dev": true, "requires": { - "lower-case": "1.1.4" + "lower-case": "^1.1.0" } }, "is-typedarray": { @@ -1217,7 +1222,7 @@ "integrity": "sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8=", "dev": true, "requires": { - "upper-case": "1.1.3" + "upper-case": "^1.1.0" } }, "is-utf8": { @@ -1250,8 +1255,8 @@ "integrity": "sha1-A3fDgBfKvHMisNH7zSWkkWQfL74=", "dev": true, "requires": { - "argparse": "1.0.10", - "esprima": "2.7.3" + "argparse": "^1.0.2", + "esprima": "^2.6.0" } }, "jsbn": { @@ -1275,7 +1280,7 @@ "dev": true, "optional": true, "requires": { - "jsonify": "0.0.0" + "jsonify": "~0.0.0" } }, "json-stringify-safe": { @@ -1291,7 +1296,7 @@ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.6" } }, "jsonify": { @@ -1335,7 +1340,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } }, "klaw-sync": { @@ -1344,7 +1349,7 @@ "integrity": "sha1-PTvNhgDnv971MjHHOf8FOu1WDkQ=", "dev": true, "requires": { - "graceful-fs": "4.1.11" + "graceful-fs": "^4.1.11" } }, "lazy-cache": { @@ -1359,14 +1364,14 @@ "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", "dev": true, "requires": { - "errno": "0.1.7", - "graceful-fs": "4.1.11", - "image-size": "0.5.5", - "mime": "1.6.0", - "mkdirp": "0.5.1", - "promise": "7.3.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.2.11", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", "request": "2.81.0", - "source-map": "0.5.7" + "source-map": "^0.5.3" }, "dependencies": { "source-map": { @@ -1384,7 +1389,7 @@ "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", "dev": true, "requires": { - "uc.micro": "1.0.5" + "uc.micro": "^1.0.1" } }, "load-json-file": { @@ -1393,11 +1398,11 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "lodash": { @@ -1424,8 +1429,8 @@ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, "requires": { - "currently-unhandled": "0.4.1", - "signal-exit": "3.0.2" + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" } }, "lower-case": { @@ -1440,7 +1445,7 @@ "integrity": "sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E=", "dev": true, "requires": { - "lower-case": "1.1.4" + "lower-case": "^1.1.2" } }, "map-obj": { @@ -1455,11 +1460,11 @@ "integrity": "sha512-CzzqSSNkFRUf9vlWvhK1awpJreMRqdCrBvZ8DIoDWTOkESMIF741UPAhuAmbyWmdiFPA6WARNhnu2M6Nrhwa+A==", "dev": true, "requires": { - "argparse": "1.0.10", - "entities": "1.1.1", - "linkify-it": "2.0.3", - "mdurl": "1.0.1", - "uc.micro": "1.0.5" + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" } }, "maxmin": { @@ -1468,10 +1473,10 @@ "integrity": "sha1-cTZehKmd2Piz99X94vANHn9zvmE=", "dev": true, "requires": { - "chalk": "1.1.3", - "figures": "1.7.0", - "gzip-size": "1.0.0", - "pretty-bytes": "1.0.4" + "chalk": "^1.0.0", + "figures": "^1.0.1", + "gzip-size": "^1.0.0", + "pretty-bytes": "^1.0.0" } }, "mdurl": { @@ -1486,16 +1491,16 @@ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { - "camelcase-keys": "2.1.0", - "decamelize": "1.2.0", - "loud-rejection": "1.6.0", - "map-obj": "1.0.1", - "minimist": "1.2.0", - "normalize-package-data": "2.4.0", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "redent": "1.0.0", - "trim-newlines": "1.0.0" + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" } }, "mime": { @@ -1509,15 +1514,17 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true + "dev": true, + "optional": true }, "mime-types": { "version": "2.1.18", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "dev": true, + "optional": true, "requires": { - "mime-db": "1.33.0" + "mime-db": "~1.33.0" } }, "minimatch": { @@ -1526,7 +1533,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -1560,7 +1567,7 @@ "integrity": "sha1-W1etGLHKCShk72Kwse2BlPODtxw=", "dev": true, "requires": { - "xml-char-classes": "1.0.0" + "xml-char-classes": "^1.0.0" } }, "ng-annotate": { @@ -1569,18 +1576,18 @@ "integrity": "sha1-3D/FG6Cy+LOF2+BH9NoG9YCh/WE=", "dev": true, "requires": { - "acorn": "2.6.4", - "alter": "0.2.0", - "convert-source-map": "1.1.3", - "optimist": "0.6.1", - "ordered-ast-traverse": "1.1.1", - "simple-fmt": "0.1.0", - "simple-is": "0.2.0", - "source-map": "0.5.7", - "stable": "0.1.6", - "stringmap": "0.2.2", - "stringset": "0.2.1", - "tryor": "0.1.2" + "acorn": "~2.6.4", + "alter": "~0.2.0", + "convert-source-map": "~1.1.2", + "optimist": "~0.6.1", + "ordered-ast-traverse": "~1.1.1", + "simple-fmt": "~0.1.0", + "simple-is": "~0.2.0", + "source-map": "~0.5.3", + "stable": "~0.1.5", + "stringmap": "~0.2.2", + "stringset": "~0.2.1", + "tryor": "~0.1.2" }, "dependencies": { "minimist": { @@ -1595,8 +1602,8 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.2" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" } }, "source-map": { @@ -1613,7 +1620,7 @@ "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", "dev": true, "requires": { - "lower-case": "1.1.4" + "lower-case": "^1.1.1" } }, "nomnom": { @@ -1622,8 +1629,8 @@ "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", "dev": true, "requires": { - "chalk": "0.4.0", - "underscore": "1.6.0" + "chalk": "~0.4.0", + "underscore": "~1.6.0" }, "dependencies": { "ansi-styles": { @@ -1638,9 +1645,9 @@ "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", "dev": true, "requires": { - "ansi-styles": "1.0.0", - "has-color": "0.1.7", - "strip-ansi": "0.1.1" + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" } }, "strip-ansi": { @@ -1657,7 +1664,7 @@ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "abbrev": "1.1.1" + "abbrev": "1" } }, "normalize-package-data": { @@ -1666,10 +1673,10 @@ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", "dev": true, "requires": { - "hosted-git-info": "2.6.0", - "is-builtin-module": "1.0.0", - "semver": "5.5.0", - "validate-npm-package-license": "3.0.3" + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "number-is-nan": { @@ -1697,7 +1704,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "optimist": { @@ -1706,7 +1713,7 @@ "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", "dev": true, "requires": { - "wordwrap": "0.0.2" + "wordwrap": "~0.0.2" } }, "ordered-ast-traverse": { @@ -1715,7 +1722,7 @@ "integrity": "sha1-aEOhcLwO7otSDMjdwd3TqjD6BXw=", "dev": true, "requires": { - "ordered-esprima-props": "1.1.0" + "ordered-esprima-props": "~1.1.0" } }, "ordered-esprima-props": { @@ -1736,7 +1743,7 @@ "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", "dev": true, "requires": { - "no-case": "2.3.2" + "no-case": "^2.2.0" } }, "parse-json": { @@ -1745,7 +1752,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.1" + "error-ex": "^1.2.0" } }, "pascal-case": { @@ -1754,8 +1761,8 @@ "integrity": "sha1-LVeNNFX2YNpl7KGO+VtODekSdh4=", "dev": true, "requires": { - "camel-case": "3.0.0", - "upper-case-first": "1.1.2" + "camel-case": "^3.0.0", + "upper-case-first": "^1.1.0" } }, "path-case": { @@ -1764,7 +1771,7 @@ "integrity": "sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU=", "dev": true, "requires": { - "no-case": "2.3.2" + "no-case": "^2.2.0" } }, "path-exists": { @@ -1773,7 +1780,7 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "2.0.1" + "pinkie-promise": "^2.0.0" } }, "path-is-absolute": { @@ -1788,9 +1795,9 @@ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "performance-now": { @@ -1818,7 +1825,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "pretty-bytes": { @@ -1827,8 +1834,8 @@ "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", "dev": true, "requires": { - "get-stdin": "4.0.1", - "meow": "3.7.0" + "get-stdin": "^4.0.1", + "meow": "^3.1.0" } }, "process-nextick-args": { @@ -1844,7 +1851,7 @@ "dev": true, "optional": true, "requires": { - "asap": "2.0.6" + "asap": "~2.0.3" } }, "prr": { @@ -1874,9 +1881,9 @@ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -1885,8 +1892,8 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" } }, "readable-stream": { @@ -1895,13 +1902,13 @@ "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.0.3", + "util-deprecate": "~1.0.1" } }, "redent": { @@ -1910,8 +1917,8 @@ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { - "indent-string": "2.1.0", - "strip-indent": "1.0.1" + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" } }, "relateurl": { @@ -1932,7 +1939,7 @@ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "is-finite": "1.0.2" + "is-finite": "^1.0.0" } }, "request": { @@ -1942,28 +1949,28 @@ "dev": true, "optional": true, "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" } }, "resolve": { @@ -1978,7 +1985,7 @@ "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", "dev": true, "requires": { - "align-text": "0.1.4" + "align-text": "^0.1.1" } }, "rimraf": { @@ -2005,8 +2012,8 @@ "integrity": "sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ=", "dev": true, "requires": { - "no-case": "2.3.2", - "upper-case-first": "1.1.2" + "no-case": "^2.2.0", + "upper-case-first": "^1.1.2" } }, "signal-exit": { @@ -2033,7 +2040,7 @@ "integrity": "sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8=", "dev": true, "requires": { - "no-case": "2.3.2" + "no-case": "^2.2.0" } }, "sntp": { @@ -2043,7 +2050,7 @@ "dev": true, "optional": true, "requires": { - "hoek": "2.16.3" + "hoek": "2.x.x" } }, "source-map": { @@ -2052,7 +2059,7 @@ "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "dev": true, "requires": { - "amdefine": "1.0.1" + "amdefine": ">=0.0.4" } }, "spdx-correct": { @@ -2061,8 +2068,8 @@ "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", "dev": true, "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.0" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-exceptions": { @@ -2077,8 +2084,8 @@ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "spdx-exceptions": "2.1.0", - "spdx-license-ids": "3.0.0" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { @@ -2100,14 +2107,14 @@ "dev": true, "optional": true, "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "tweetnacl": "~0.14.0" }, "dependencies": { "assert-plus": { @@ -2137,7 +2144,7 @@ "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "dev": true, "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "~5.1.0" } }, "stringmap": { @@ -2165,7 +2172,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-bom": { @@ -2174,7 +2181,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } }, "strip-indent": { @@ -2183,7 +2190,7 @@ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { - "get-stdin": "4.0.1" + "get-stdin": "^4.0.1" } }, "supports-color": { @@ -2198,8 +2205,8 @@ "integrity": "sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM=", "dev": true, "requires": { - "lower-case": "1.1.4", - "upper-case": "1.1.3" + "lower-case": "^1.1.1", + "upper-case": "^1.1.1" } }, "title-case": { @@ -2208,8 +2215,8 @@ "integrity": "sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o=", "dev": true, "requires": { - "no-case": "2.3.2", - "upper-case": "1.1.3" + "no-case": "^2.2.0", + "upper-case": "^1.0.3" } }, "tough-cookie": { @@ -2219,7 +2226,7 @@ "dev": true, "optional": true, "requires": { - "punycode": "1.4.1" + "punycode": "^1.4.1" } }, "trim-newlines": { @@ -2241,7 +2248,7 @@ "dev": true, "optional": true, "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -2269,10 +2276,10 @@ "integrity": "sha1-ZeovswWck5RpLxX+2HwrNsFrmt8=", "dev": true, "requires": { - "async": "0.2.10", - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" + "async": "~0.2.6", + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" }, "dependencies": { "async": { @@ -2307,8 +2314,8 @@ "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", "dev": true, "requires": { - "sprintf-js": "1.1.1", - "util-deprecate": "1.0.2" + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" } }, "universalify": { @@ -2329,7 +2336,7 @@ "integrity": "sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU=", "dev": true, "requires": { - "upper-case": "1.1.3" + "upper-case": "^1.1.1" } }, "uri-path": { @@ -2357,8 +2364,8 @@ "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", "dev": true, "requires": { - "spdx-correct": "3.0.0", - "spdx-expression-parse": "3.0.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, "verror": { @@ -2368,9 +2375,9 @@ "dev": true, "optional": true, "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" }, "dependencies": { "assert-plus": { @@ -2388,7 +2395,7 @@ "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "window-size": { @@ -2403,12 +2410,12 @@ "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", "dev": true, "requires": { - "async": "1.0.0", - "colors": "1.0.3", - "cycle": "1.0.3", - "eyes": "0.1.8", - "isstream": "0.1.2", - "stack-trace": "0.0.10" + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" }, "dependencies": { "async": { @@ -2449,9 +2456,9 @@ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", "dev": true, "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", "window-size": "0.1.0" }, "dependencies": { diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 3fdcfc4e..62fa84bf 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -145,6 +145,15 @@ angular.module('docs', } } }) + .state('settings.metadata', { + url: '/metadata', + views: { + 'settings': { + templateUrl: 'partial/docs/settings.metadata.html', + controller: 'SettingsMetadata' + } + } + }) .state('settings.user', { url: '/user', views: { @@ -235,6 +244,15 @@ angular.module('docs', } } }) + .state('settings.ldap', { + url: '/ldap', + views: { + 'settings': { + templateUrl: 'partial/docs/settings.ldap.html', + controller: 'SettingsLdap' + } + } + }) .state('document', { url: '/document', abstract: true, @@ -338,7 +356,7 @@ angular.module('docs', } }) .state('login', { - url: '/login', + url: '/login?redirectState&redirectParams', views: { 'page': { templateUrl: 'partial/docs/login.html', @@ -406,17 +424,21 @@ angular.module('docs', // Configuring Angular Translate $translateProvider - .useSanitizeValueStrategy(null) + .useSanitizeValueStrategy('escapeParameters') .useStaticFilesLoader({ prefix: 'locale/', suffix: '.json?@build.date@' }) - .registerAvailableLanguageKeys(['en', 'es', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], { - 'ru_*': 'ru', + .registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW', 'sq_AL'], { 'en_*': 'en', 'es_*': 'es', + 'pt_*': 'pt', 'fr_*': 'fr', 'de_*': 'de', + 'el_*': 'el', + 'ru_*': 'ru', + 'it_*': 'it', + 'pl_*': 'pl', '*': 'en' }) .fallbackLanguage('en'); @@ -427,6 +449,9 @@ angular.module('docs', } else { // Or else determine the language based on the user's browser $translateProvider.determinePreferredLanguage(); + if (!$translateProvider.use()) { + $translateProvider.use('en'); + } } // Configuring Timago @@ -438,6 +463,9 @@ angular.module('docs', // Configuring $http to act like jQuery.ajax $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; + $httpProvider.defaults.headers.delete = { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' + }; $httpProvider.defaults.transformRequest = [function(data) { var param = function(obj) { var query = ''; @@ -510,7 +538,17 @@ angular.module('docs', { key: 'tha', label: 'ภาษาไทย' }, { key: 'kor', label: '한국어' }, { key: 'nld', label: 'Nederlands' }, - { key: 'tur', label: 'Türkçe' } + { key: 'tur', label: 'Türkçe' }, + { key: 'heb', label: 'עברית' }, + { key: 'hun', label: 'Magyar' }, + { key: 'fin', label: 'Suomi' }, + { key: 'swe', label: 'Svenska' }, + { key: 'lav', label: 'Latviešu' }, + { key: 'dan', label: 'Dansk' }, + { key: 'nor', label: 'Norsk' }, + { key: 'vie', label: 'Tiếng Việt' }, + { key: 'ces', label: 'Czech' }, + { key: 'sqi', label: 'Shqip' } ]; }) /** diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Login.js b/docs-web/src/main/webapp/src/app/docs/controller/Login.js index 9f862cda..eb7e3d92 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/Login.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/Login.js @@ -3,7 +3,7 @@ /** * Login controller. */ -angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $dialog, User, $translate, $uibModal) { +angular.module('docs').controller('Login', function(Restangular, $scope, $rootScope, $state, $stateParams, $dialog, User, $translate, $uibModal) { $scope.codeRequired = false; // Get the app configuration @@ -26,7 +26,15 @@ angular.module('docs').controller('Login', function(Restangular, $scope, $rootSc User.userInfo(true).then(function(data) { $rootScope.userInfo = data; }); - $state.go('document.default'); + + if($stateParams.redirectState !== undefined && $stateParams.redirectParams !== undefined) { + $state.go($stateParams.redirectState, JSON.parse($stateParams.redirectParams)) + .catch(function() { + $state.go('document.default'); + }); + } else { + $state.go('document.default'); + } }, function(data) { if (data.data.type === 'ValidationCodeRequired') { // A TOTP validation code is required to login diff --git a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js index 6fa92d64..3f1e5e9f 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/Navigation.js @@ -3,13 +3,18 @@ /** * Navigation controller. */ -angular.module('docs').controller('Navigation', function($scope, $state, $rootScope, User) { +angular.module('docs').controller('Navigation', function($scope, $state, $stateParams, $rootScope, User) { User.userInfo().then(function(data) { $rootScope.userInfo = data; if (data.anonymous) { - $state.go('login', {}, { - location: 'replace' - }); + if($state.current.name !== 'login') { + $state.go('login', { + redirectState: $state.current.name, + redirectParams: JSON.stringify($stateParams), + }, { + location: 'replace' + }); + } } }); diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js index 650b836f..c61d1c3c 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/Document.js @@ -173,7 +173,10 @@ angular.module('docs').controller('Document', function ($scope, $rootScope, $tim $scope.startSearch = function () { var search = ''; if (!_.isEmpty($scope.advsearch.search_simple)) { - search += $scope.advsearch.search_simple + ' '; + var simplesearch = _.map($scope.advsearch.search_simple.split(/\s+/), function (simple) { + return 'simple:' + simple + }); + search += simplesearch.join(' ') + ' '; } if (!_.isEmpty($scope.advsearch.search_fulltext)) { var fulltext = _.map($scope.advsearch.search_fulltext.split(/\s+/), function (full) { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js index 0e158675..dc77abd0 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentDefault.js @@ -13,7 +13,6 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop $scope.loadFiles = function () { Restangular.one('file/list').get().then(function (data) { $scope.files = data.files; - // TODO Keep currently uploading files }); }; $scope.loadFiles(); @@ -121,7 +120,7 @@ angular.module('docs').controller('DocumentDefault', function ($scope, $rootScop } Restangular.withConfig(function (RestangularConfigurer) { - RestangularConfigurer.setBaseUrl('https://api.sismicsdocs.com'); + RestangularConfigurer.setBaseUrl('https://api.teedy.io'); }).one('api').post('feedback', { content: content }).then(function () { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js index 3d979f1e..c7f4fd02 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentEdit.js @@ -59,9 +59,18 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ $scope.document = { tags: [], relations: [], - language: language + language: language, + metadata: [] }; + // Get custom metadata list + Restangular.one('metadata').get({ + sort_column: 1, + asc: true + }).then(function(data) { + $scope.document.metadata = data.metadata; + }); + if ($scope.navigatedTag) { $scope.document.tags.push($scope.navigatedTag); } @@ -92,7 +101,21 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $ // Extract ids from relations (only when our document is the source) document.relations = _.pluck(_.where(document.relations, { source: true }), 'id'); - + + // Extract custom metadata values + var metadata = _.reject(document.metadata, function (meta) { + return _.isUndefined(meta.value) || meta.value === '' || meta.value == null; + }); + document.metadata_id = _.pluck(metadata, 'id'); + document.metadata_value = _.pluck(metadata, 'value'); + document.metadata_value = _.map(document.metadata_value, function (val) { + if (val instanceof Date) { + return val.getTime(); + } + return val; + }); + + // Send to server if ($scope.isEdit()) { promise = Restangular.one('document', $stateParams.id).post('', document); } else { diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js index 9a5fee8b..4a731ed4 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentView.js @@ -3,7 +3,7 @@ /** * Document view controller. */ -angular.module('docs').controller('DocumentView', function ($scope, $state, $stateParams, $location, $dialog, $uibModal, Restangular, $translate) { +angular.module('docs').controller('DocumentView', function ($scope, $rootScope, $state, $stateParams, $location, $dialog, $uibModal, Restangular, $translate) { // Load document data from server Restangular.one('document', $stateParams.id).get().then(function (data) { $scope.document = data; @@ -111,10 +111,13 @@ angular.module('docs').controller('DocumentView', function ($scope, $state, $sta var title = $translate.instant('document.view.shared_document_title'); var msg = $translate.instant('document.view.shared_document_message', { link: link }); var btns = [ - {result: 'unshare', label: $translate.instant('unshare'), cssClass: 'btn-danger'}, {result: 'close', label: $translate.instant('close')} ]; + if ($rootScope.userInfo.username !== 'guest') { + btns.unshift({result: 'unshare', label: $translate.instant('unshare'), cssClass: 'btn-danger'}); + } + $dialog.messageBox(title, msg, btns, function (result) { if (result === 'unshare') { // Unshare this document and update the local shares diff --git a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js index f410c64c..9429004c 100644 --- a/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js +++ b/docs-web/src/main/webapp/src/app/docs/controller/document/DocumentViewContent.js @@ -5,6 +5,7 @@ */ angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate, $uibModal) { $scope.displayMode = _.isUndefined(localStorage.fileDisplayMode) ? 'grid' : localStorage.fileDisplayMode; + $scope.openedFile = undefined; /** * Watch for display mode change. @@ -45,7 +46,6 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root $scope.loadFiles = function () { Restangular.one('file/list').get({ id: $stateParams.id }).then(function (data) { $scope.files = data.files; - // TODO Keep currently uploading files }); }; $scope.loadFiles(); @@ -55,7 +55,8 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root */ $scope.openFile = function (file, $event) { if ($($event.target).parents('.currently-dragging').length === 0) { - $state.go('document.view.content.file', {id: $stateParams.id, fileId: file.id}) + $scope.openedFile = file; + $state.go('document.view.content.file', { id: $stateParams.id, fileId: file.id }); } }; diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsLdap.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsLdap.js new file mode 100644 index 00000000..671561eb --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsLdap.js @@ -0,0 +1,33 @@ +'use strict'; + +/** + * Settings LDAP page controller. + */ +angular.module('docs').controller('SettingsLdap', function($scope, Restangular, $translate, $timeout) { + $scope.ldap = { + enabled: false + }; + + // Get the LDAP configuration + Restangular.one('app/config_ldap').get().then(function (data) { + $scope.ldap = data; + if ($scope.ldap.default_storage) { + $scope.ldap.default_storage /= 1000000; + } + }); + + // Edit SMTP config + $scope.saveResult = undefined; + $scope.save = function () { + var ldap = angular.copy($scope.ldap); + if (ldap.default_storage) { + ldap.default_storage *= 1000000; + } + Restangular.one('app').post('config_ldap', ldap).then(function () { + $scope.saveResult = $translate.instant('settings.ldap.saved'); + $timeout(function() { + $scope.saveResult = undefined; + }, 5000); + }); + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsMetadata.js b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsMetadata.js new file mode 100644 index 00000000..6fa7d4fb --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/controller/settings/SettingsMetadata.js @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Settings metadata page controller. + */ +angular.module('docs').controller('SettingsMetadata', function($scope, Restangular) { + // Load metadata + Restangular.one('metadata').get({ + sort_column: 1, + asc: true + }).then(function(data) { + $scope.metadata = data.metadata; + }); + + // Add a metadata + $scope.addMetadata = function() { + Restangular.one('metadata').put($scope.newmetadata).then(function(data) { + $scope.metadata.push(data); + $scope.newmetadata = {}; + }); + }; + + // Delete a metadata + $scope.deleteMetadata = function(meta) { + Restangular.one('metadata', meta.id).remove().then(function() { + $scope.metadata.splice($scope.metadata.indexOf(meta), 1); + }); + }; + + // Update a metadata + $scope.updateMetadata = function(meta) { + Restangular.one('metadata', meta.id).post('', meta); + }; +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/directive/AddSpaceBetween.js b/docs-web/src/main/webapp/src/app/docs/directive/AddSpaceBetween.js new file mode 100644 index 00000000..b5c4e787 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/directive/AddSpaceBetween.js @@ -0,0 +1,12 @@ +'use strict'; + +/** + * Add space between element directive. + */ +angular.module('docs').directive('addSpaceBetween', function () { + return function (scope, element) { + if(!scope.$last) { + element.after(' '); + } + } +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/directive/InvertTextColor.js b/docs-web/src/main/webapp/src/app/docs/directive/InvertTextColor.js new file mode 100644 index 00000000..986c5049 --- /dev/null +++ b/docs-web/src/main/webapp/src/app/docs/directive/InvertTextColor.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Invert text color for more legibility directive. + */ +angular.module('docs').directive('invertTextColor', function () { + return { + restrict: 'A', + link: function(scope, element, attrs) { + attrs.$observe('invertTextColor', function(hex) { + if (!hex || hex.length !== 7) { + return; + } + + hex = hex.slice(1); + var r = parseInt(hex.slice(0, 2), 16), + g = parseInt(hex.slice(2, 4), 16), + b = parseInt(hex.slice(4, 6), 16); + element.css('color', (r * 0.299 + g * 0.587 + b * 0.114) > 186 + ? '#000000' + : '#FFFFFF'); + }); + } + } +}); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/app/docs/directive/SelectRelation.js b/docs-web/src/main/webapp/src/app/docs/directive/SelectRelation.js index bdbd3ee4..33cbe9b7 100644 --- a/docs-web/src/main/webapp/src/app/docs/directive/SelectRelation.js +++ b/docs-web/src/main/webapp/src/app/docs/directive/SelectRelation.js @@ -9,6 +9,7 @@ angular.module('docs').directive('selectRelation', function() { templateUrl: 'partial/docs/directive.selectrelation.html', replace: true, scope: { + id: '=', relations: '=', ref: '@', ngDisabled: '=' @@ -18,21 +19,12 @@ angular.module('docs').directive('selectRelation', function() { * Add a relation. */ $scope.addRelation = function($item) { - // Does the new relation is already in the model - var duplicate = _.find($scope.relations, function(relation) { - if ($item.id === relation.id) { - return relation; - } - }); - // Add the new relation - if (!duplicate) { - $scope.relations.push({ - id: $item.id, - title: $item.title, - source: true - }); - } + $scope.relations.push({ + id: $item.id, + title: $item.title, + source: true + }); $scope.input = ''; }; @@ -42,11 +34,11 @@ angular.module('docs').directive('selectRelation', function() { $scope.deleteRelation = function(deleteRelation) { $scope.relations = _.reject($scope.relations, function(relation) { return relation.id === deleteRelation.id; - }) + }); }; /** - * Returns a promise for typeahead title. + * Returns a promise for typeahead document. */ $scope.getDocumentTypeahead = function($viewValue) { var deferred = $q.defer(); @@ -57,8 +49,16 @@ angular.module('docs').directive('selectRelation', function() { asc: true, search: $viewValue }).then(function(data) { - deferred.resolve(data.documents); - }); + deferred.resolve(_.reject(data.documents, function(document) { + var duplicate = _.find($scope.relations, function(relation) { + if (document.id === relation.id) { + return relation; + } + }); + + return document.id === $scope.id || duplicate; + })); + }); return deferred.promise; }; }, diff --git a/docs-web/src/main/webapp/src/app/share/app.js b/docs-web/src/main/webapp/src/app/share/app.js index 39f47c7a..8a9c47ea 100644 --- a/docs-web/src/main/webapp/src/app/share/app.js +++ b/docs-web/src/main/webapp/src/app/share/app.js @@ -56,17 +56,21 @@ angular.module('share', // Configuring Angular Translate $translateProvider - .useSanitizeValueStrategy(null) + .useSanitizeValueStrategy('escapeParameters') .useStaticFilesLoader({ prefix: 'locale/', suffix: '.json?@build.date@' }) - .registerAvailableLanguageKeys(['en', 'es', 'fr', 'de', 'ru', 'zh_CN', 'zh_TW'], { - 'ru_*': 'ru', + .registerAvailableLanguageKeys(['en', 'es', 'pt', 'fr', 'de', 'el', 'ru', 'it', 'pl', 'zh_CN', 'zh_TW', 'sq_AL'], { 'en_*': 'en', 'es_*': 'es', + 'pt_*': 'pt', 'fr_*': 'fr', 'de_*': 'de', + 'el_*': 'el', + 'ru_*': 'ru', + 'it_*': 'it', + 'pl_*': 'pl', '*': 'en' }) .fallbackLanguage('en'); @@ -85,6 +89,9 @@ angular.module('share', // Configuring $http to act like jQuery.ajax $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8'; + $httpProvider.defaults.headers.delete = { + 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' + }; $httpProvider.defaults.transformRequest = [function(data) { var param = function(obj) { var query = ''; diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index 959419b9..e72be76c 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -93,6 +93,8 @@ + + @@ -105,6 +107,8 @@ + + @@ -180,10 +184,15 @@ English Français Deutsch + Italiano Española - русский + Português + Ελληνικά + Pусский + Polski 简体中文 繁體中文 + Shqip @@ -191,10 +200,15 @@
  • English
  • Français
  • Deutsch
  • +
  • Italiano
  • Española
  • -
  • русский
  • -
  • 简体中文
  • +
  • Português
  • +
  • Ελληνικά
  • +
  • Pусский
  • +
  • Polski
  • +
  • 简体中文
  • 繁體中文
  • +
  • Shqip
  • diff --git a/docs-web/src/main/webapp/src/lib/angular.timeago.js b/docs-web/src/main/webapp/src/lib/angular.timeago.js index 129820ea..1e64b847 100644 --- a/docs-web/src/main/webapp/src/lib/angular.timeago.js +++ b/docs-web/src/main/webapp/src/lib/angular.timeago.js @@ -34,6 +34,29 @@ angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(tim 'use strict'; +angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(timeAgoSettings) { + timeAgoSettings.strings['el'] = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'πριν', + suffixFromNow: 'από τώρα', + seconds: 'λιγότερο από ένα λεπτό', + minute: 'περίπου ένα λεπτό', + minutes: '%d λεπτά', + hour: 'περίπου μια ώρα', + hours: 'περίπου %d ώρες', + day: 'μια μέρα', + days: '%d μέρες', + month: 'περίπου ένα μήνα', + months: '%d μήνες', + year: 'περίπου ένα χρόνο', + years: '%d χρόνια', + numbers: [] + }; +}]); + +'use strict'; + angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(timeAgoSettings) { /** @@ -288,7 +311,7 @@ angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(tim 'use strict'; angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(timeAgoSettings) { - timeAgoSettings.strings['it_IT'] = { + timeAgoSettings.strings['it'] = { prefixAgo: null, prefixFromNow: null, suffixAgo: 'fa', @@ -356,6 +379,29 @@ angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(tim 'use strict'; +angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(timeAgoSettings) { + timeAgoSettings.strings['pl'] = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'temu', + suffixFromNow: 'od teraz', + seconds: 'mniej niż minuta', + minute: 'około minuty', + minutes: '%d minut', + hour: 'około godziny', + hours: 'około %d godzin', + day: 'dzień', + days: '%d dni', + month: 'około miesiąca', + months: '%d miesięcy', + year: 'około roku', + years: '%d lat', + numbers: [] + }; +}]); + +'use strict'; + angular.module('yaru22.angular-timeago').config(["timeAgoSettings", function(timeAgoSettings) { timeAgoSettings.strings['pt_BR'] = { prefixAgo: null, diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_de.js b/docs-web/src/main/webapp/src/locale/angular-locale_de.js index cc69b3af..346b998d 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_de.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_de.js @@ -104,7 +104,7 @@ $provide.value("$locale", { "mediumDate": "dd.MM.y", "mediumTime": "HH:mm:ss", "short": "dd.MM.yy HH:mm", - "shortDate": "dd.MM.yy", + "shortDate": "dd.MM.yyyy", "shortTime": "HH:mm" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_el.js b/docs-web/src/main/webapp/src/locale/angular-locale_el.js new file mode 100644 index 00000000..0cfa7757 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/angular-locale_el.js @@ -0,0 +1,125 @@ +'use strict'; +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "\u03c0.\u03bc.", + "\u03bc.\u03bc." + ], + "DAY": [ + "\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae", + "\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1", + "\u03a4\u03c1\u03af\u03c4\u03b7", + "\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7", + "\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7", + "\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae", + "\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf" + ], + "ERANAMES": [ + "\u03c0\u03c1\u03bf \u03a7\u03c1\u03b9\u03c3\u03c4\u03bf\u03cd", + "\u03bc\u03b5\u03c4\u03ac \u03a7\u03c1\u03b9\u03c3\u03c4\u03cc\u03bd" + ], + "ERAS": [ + "\u03c0.\u03a7.", + "\u03bc.\u03a7." + ], + "FIRSTDAYOFWEEK": 0, + "MONTH": [ + "\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5", + "\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5", + "\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5", + "\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5", + "\u039c\u03b1\u0390\u03bf\u03c5", + "\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5", + "\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5", + "\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5", + "\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5", + "\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5", + "\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5", + "\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5" + ], + "SHORTDAY": [ + "\u039a\u03c5\u03c1", + "\u0394\u03b5\u03c5", + "\u03a4\u03c1\u03af", + "\u03a4\u03b5\u03c4", + "\u03a0\u03ad\u03bc", + "\u03a0\u03b1\u03c1", + "\u03a3\u03ac\u03b2" + ], + "SHORTMONTH": [ + "\u0399\u03b1\u03bd", + "\u03a6\u03b5\u03b2", + "\u039c\u03b1\u03c1", + "\u0391\u03c0\u03c1", + "\u039c\u03b1\u0390", + "\u0399\u03bf\u03c5\u03bd", + "\u0399\u03bf\u03c5\u03bb", + "\u0391\u03c5\u03b3", + "\u03a3\u03b5\u03c0", + "\u039f\u03ba\u03c4", + "\u039d\u03bf\u03b5", + "\u0394\u03b5\u03ba" + ], + "STANDALONEMONTH": [ + "\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2", + "\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2", + "\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2", + "\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2", + "\u039c\u03ac\u03b9\u03bf\u03c2", + "\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2", + "\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2", + "\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2", + "\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2", + "\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2", + "\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2", + "\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2" + ], + "WEEKENDRANGE": [ + 5, + 6 + ], + "fullDate": "EEEE, d MMMM y", + "longDate": "d MMMM y", + "medium": "d MMM y h:mm:ss a", + "mediumDate": "d MMM y", + "mediumTime": "h:mm:ss a", + "short": "d/M/yy h:mm a", + "shortDate": "d/M/yy", + "shortTime": "h:mm a" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "\u20ac", + "DECIMAL_SEP": ",", + "GROUP_SEP": ".", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-", + "negSuf": "\u00a0\u00a4", + "posPre": "", + "posSuf": "\u00a0\u00a4" + } + ] + }, + "id": "el", + "localeID": "el", + "pluralCat": function(n, opt_precision) { if (n == 1) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} +}); +}]); diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_en.js b/docs-web/src/main/webapp/src/locale/angular-locale_en.js index f794bab8..ee5c4c7f 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_en.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_en.js @@ -103,8 +103,8 @@ $provide.value("$locale", { "medium": "MMM d, y h:mm:ss a", "mediumDate": "MMM d, y", "mediumTime": "h:mm:ss a", - "short": "M/d/yy h:mm a", - "shortDate": "M/d/yy", + "short": "yy/M/d h:mm a", + "shortDate": "yyyy/MM/dd", "shortTime": "h:mm a" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_es.js b/docs-web/src/main/webapp/src/locale/angular-locale_es.js index 12f9b114..9cd93a73 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_es.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_es.js @@ -86,7 +86,7 @@ $provide.value("$locale", { "mediumDate": "d MMM y", "mediumTime": "H:mm:ss", "short": "d/M/yy H:mm", - "shortDate": "d/M/yy", + "shortDate": "dd/MM/yyyy", "shortTime": "H:mm" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_fr.js b/docs-web/src/main/webapp/src/locale/angular-locale_fr.js index 55c6bb2b..764edabf 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_fr.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_fr.js @@ -86,7 +86,7 @@ $provide.value("$locale", { "mediumDate": "d MMM y", "mediumTime": "HH:mm:ss", "short": "dd/MM/y HH:mm", - "shortDate": "dd/MM/y", + "shortDate": "dd/MM/yyyy", "shortTime": "HH:mm" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_it.js b/docs-web/src/main/webapp/src/locale/angular-locale_it.js new file mode 100644 index 00000000..13887218 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/angular-locale_it.js @@ -0,0 +1,143 @@ +'use strict'; +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; +function getDecimals(n) { + n = n + ''; + var i = n.indexOf('.'); + return (i == -1) ? 0 : n.length - i - 1; +} + +function getVF(n, opt_precision) { + var v = opt_precision; + + if (undefined === v) { + v = Math.min(getDecimals(n), 3); + } + + var base = Math.pow(10, v); + var f = ((n * base) | 0) % base; + return {v: v, f: f}; +} + +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "AM", + "PM" + ], + "DAY": [ + "domenica", + "luned\u00ec", + "marted\u00ec", + "mercoled\u00ec", + "gioved\u00ec", + "venerd\u00ec", + "sabato" + ], + "ERANAMES": [ + "avanti Cristo", + "dopo Cristo" + ], + "ERAS": [ + "a.C.", + "d.C." + ], + "FIRSTDAYOFWEEK": 0, + "MONTH": [ + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre" + ], + "SHORTDAY": [ + "dom", + "lun", + "mar", + "mer", + "gio", + "ven", + "sab" + ], + "SHORTMONTH": [ + "gen", + "feb", + "mar", + "apr", + "mag", + "giu", + "lug", + "ago", + "set", + "ott", + "nov", + "dic" + ], + "STANDALONEMONTH": [ + "gennaio", + "febbraio", + "marzo", + "aprile", + "maggio", + "giugno", + "luglio", + "agosto", + "settembre", + "ottobre", + "novembre", + "dicembre" + ], + "WEEKENDRANGE": [ + 5, + 6 + ], + "fullDate": "EEEE d MMMM y", + "longDate": "d MMMM y", + "medium": "dd MMM y HH:mm:ss", + "mediumDate": "dd MMM y", + "mediumTime": "HH:mm:ss", + "short": "dd/MM/yy HH:mm", + "shortDate": "dd/MM/yy", + "shortTime": "HH:mm" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "\u20ac", + "DECIMAL_SEP": ",", + "GROUP_SEP": ".", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-", + "negSuf": "\u00a0\u00a4", + "posPre": "", + "posSuf": "\u00a0\u00a4" + } + ] + }, + "id": "it", + "localeID": "it", + "pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} +}); +}]); diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_pl.js b/docs-web/src/main/webapp/src/locale/angular-locale_pl.js new file mode 100644 index 00000000..19d635f7 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/angular-locale_pl.js @@ -0,0 +1,125 @@ +'use strict'; +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "jeden", TWO: "dwa", FEW: "trochę", MANY: "wiele", OTHER: "pozostałe"}; +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "AM", + "PM" + ], + "DAY": [ + "Niedziela", + "Poniedziałek", + "Wtorek", + "Środa", + "Czwartek", + "Piątek", + "Sobota" + ], + "ERANAMES": [ + "przed Chrystusem", + "roku Pańskiego" + ], + "ERAS": [ + "BC", + "AD" + ], + "FIRSTDAYOFWEEK": 6, + "MONTH": [ + "Styczeń", + "Luty", + "Marzec", + "Kwiecień", + "Maj", + "Czerwic", + "Lipiec", + "Sierpień", + "Wrzesień", + "Październik", + "Listopad", + "Grudzień" + ], + "SHORTDAY": [ + "N", + "Pn", + "Wt", + "Śr", + "Cz", + "Pt", + "So" + ], + "SHORTMONTH": [ + "Sty", + "Lut", + "Mar", + "Kwi", + "Maj", + "Cze", + "Lip", + "Sie", + "Wrz", + "Paź", + "Lis", + "Gru" + ], + "STANDALONEMONTH": [ + "Styczeń", + "Luty", + "Marzec", + "Kwiecień", + "Maj", + "Czerwiec", + "Lipiec", + "Sierpień", + "Wrzesień", + "Październik", + "Listopad", + "Grudzień" + ], + "WEEKENDRANGE": [ + 5, + 6 + ], + "fullDate": "EEEE d MMMM y", + "longDate": "d MMMM y", + "medium": "d MMM y HH:mm:ss", + "mediumDate": "d MMM y", + "mediumTime": "HH:mm:ss", + "short": "dd-MM-yyyy HH:mm", + "shortDate": "dd-MM-yyyy", + "shortTime": "HH:mm" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "zł", + "DECIMAL_SEP": ",", + "GROUP_SEP": ".", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-\u00a4", + "negSuf": "", + "posPre": "\u00a4", + "posSuf": "" + } + ] + }, + "id": "pl", + "localeID": "pl", + "pluralCat": function(n, opt_precision) { if (n == 1) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} +}); +}]); diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_pt.js b/docs-web/src/main/webapp/src/locale/angular-locale_pt.js new file mode 100644 index 00000000..b7c915ce --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/angular-locale_pt.js @@ -0,0 +1,125 @@ +'use strict'; +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "AM", + "PM" + ], + "DAY": [ + "domingo", + "segunda-feira", + "ter\u00e7a-feira", + "quarta-feira", + "quinta-feira", + "sexta-feira", + "s\u00e1bado" + ], + "ERANAMES": [ + "antes de Cristo", + "depois de Cristo" + ], + "ERAS": [ + "a.C.", + "d.C." + ], + "FIRSTDAYOFWEEK": 6, + "MONTH": [ + "janeiro", + "fevereiro", + "mar\u00e7o", + "abril", + "maio", + "junho", + "julho", + "agosto", + "setembro", + "outubro", + "novembro", + "dezembro" + ], + "SHORTDAY": [ + "dom", + "seg", + "ter", + "qua", + "qui", + "sex", + "s\u00e1b" + ], + "SHORTMONTH": [ + "jan", + "fev", + "mar", + "abr", + "mai", + "jun", + "jul", + "ago", + "set", + "out", + "nov", + "dez" + ], + "STANDALONEMONTH": [ + "janeiro", + "fevereiro", + "mar\u00e7o", + "abril", + "maio", + "junho", + "julho", + "agosto", + "setembro", + "outubro", + "novembro", + "dezembro" + ], + "WEEKENDRANGE": [ + 5, + 6 + ], + "fullDate": "EEEE, d 'de' MMMM 'de' y", + "longDate": "d 'de' MMMM 'de' y", + "medium": "d 'de' MMM 'de' y HH:mm:ss", + "mediumDate": "d 'de' MMM 'de' y", + "mediumTime": "HH:mm:ss", + "short": "dd/MM/y HH:mm", + "shortDate": "dd/MM/y", + "shortTime": "HH:mm" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "R$", + "DECIMAL_SEP": ",", + "GROUP_SEP": ".", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-\u00a4", + "negSuf": "", + "posPre": "\u00a4", + "posSuf": "" + } + ] + }, + "id": "pt", + "localeID": "pt", + "pluralCat": function(n, opt_precision) { var i = n | 0; if (i >= 0 && i <= 1) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} +}); +}]); diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_sq_AL.js b/docs-web/src/main/webapp/src/locale/angular-locale_sq_AL.js new file mode 100644 index 00000000..b5df89c6 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/angular-locale_sq_AL.js @@ -0,0 +1,150 @@ +'use strict'; +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; +function getDecimals(n) { + n = n + ''; + var i = n.indexOf('.'); + return (i == -1) ? 0 : n.length - i - 1; +} + +function getVF(n, opt_precision) { + var v = opt_precision; + + if (undefined === v) { + v = Math.min(getDecimals(n), 3); + } + + var base = Math.pow(10, v); + var f = ((n * base) | 0) % base; + return {v: v, f: f}; +} + +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "PD", + "MD" + ], + "DAY": [ + "E Diel", + "E Hënë", + "E Martë", + "E Mërkurë", + "E Enjte", + "E Premte", + "E Shtunë" + ], + "ERANAMES": [ + "Para Krishtit", + "Pas Krishtit" + ], + "ERAS": [ + "p.K.", + "n.K." + ], + "FIRSTDAYOFWEEK": 1, + "MONTH": [ + "Janar", + "Shkurt", + "Mars", + "Prill", + "Maj", + "Qershor", + "Korrik", + "Gusht", + "Shtator", + "Tetor", + "Nëntor", + "Dhjetor" + ], + "SHORTDAY": [ + "Die", + "Hën", + "Mar", + "Mër", + "Enj", + "Pre", + "Sht" + ], + "SHORTMONTH": [ + "Jan", + "Shk", + "Mar", + "Pri", + "Maj", + "Qer", + "Kor", + "Gus", + "Sht", + "Tet", + "Nën", + "Dhj" + ], + "STANDALONEMONTH": [ + "Janar", + "Shkurt", + "Mars", + "Prill", + "Maj", + "Qershor", + "Korrik", + "Gusht", + "Shtator", + "Tetor", + "Nëntor", + "Dhjetor" + ], + "WEEKENDRANGE": [ + 6, + 0 + ], + "fullDate": "EEEE, d MMMM y", + "longDate": "d MMMM y", + "medium": "d MMM y h:mm:ss a", + "mediumDate": "d MMM y", + "mediumTime": "h:mm:ss a", + "short": "yy-MM-dd h:mm a", + "shortDate": "yy-MM-dd", + "shortTime": "h:mm a" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "Lek", + "DECIMAL_SEP": ".", + "GROUP_SEP": ",", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-\u00a4", + "negSuf": "", + "posPre": "\u00a4", + "posSuf": "" + } + ] + }, + "id": "sq-al", + "localeID": "sq_AL", + "pluralCat": function(n, opt_precision) { + var i = n | 0; + var vf = getVF(n, opt_precision); + if (i == 1 && vf.v == 0) { + return PLURAL_CATEGORY.ONE; + } + return PLURAL_CATEGORY.OTHER; + } +}); +}]); diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_zh_CN.js b/docs-web/src/main/webapp/src/locale/angular-locale_zh_CN.js index 0471b82b..1e127dad 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_zh_CN.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_zh_CN.js @@ -86,7 +86,7 @@ $provide.value("$locale", { "mediumDate": "y\u5e74M\u6708d\u65e5", "mediumTime": "ah:mm:ss", "short": "y/M/d ah:mm", - "shortDate": "y/M/d", + "shortDate": "yyyy/MM/dd", "shortTime": "ah:mm" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/angular-locale_zh_TW.js b/docs-web/src/main/webapp/src/locale/angular-locale_zh_TW.js index 53b1b3db..6b2bbaf0 100644 --- a/docs-web/src/main/webapp/src/locale/angular-locale_zh_TW.js +++ b/docs-web/src/main/webapp/src/locale/angular-locale_zh_TW.js @@ -86,7 +86,7 @@ $provide.value("$locale", { "mediumDate": "y\u5e74M\u6708d\u65e5", "mediumTime": "ah:mm:ss", "short": "y/M/d ah:mm", - "shortDate": "y/M/d", + "shortDate": "yyyy/MM/dd", "shortTime": "ah:mm" }, "NUMBER_FORMATS": { diff --git a/docs-web/src/main/webapp/src/locale/de.json b/docs-web/src/main/webapp/src/locale/de.json index 947932b2..c0cf3810 100644 --- a/docs-web/src/main/webapp/src/locale/de.json +++ b/docs-web/src/main/webapp/src/locale/de.json @@ -32,24 +32,24 @@ "nav_documents": "Dokumente", "nav_tags": "Tags", "nav_users_groups": "Benutzer & Gruppen", - "error_info": "{{ count }} neuer Fehler{{ count > 1 ? 's' : '' }}", + "error_info": "{{ count }} {{ count > 1 ? 'neue' : 'neuer' }} Fehler", "logged_as": "Eingeloggt als {{ username }}", "nav_settings": "Einstellungen", "logout": "Logout", - "global_quota_warning": "Warnung! Der frei zur Verfügung stehende, maximale Speicherplatz ist fast erreicht bei {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet {{ total | number: 0 }}MB" + "global_quota_warning": "Warnung! Der verfügbare Speicherplatz beträgt {{ total | number: 0 }}\u00A0MB, davon sind {{ current | number: 0 }}\u00A0MB ({{ percent | number: 1 }}\u00A0%) bereits verwendet" }, "document": { - "navigation_up": "Eine Stufe höher", + "navigation_up": "Eine Ebene höher", "toggle_navigation": "Navigation ein-/ausblenden", "display_mode_list": "Dokumente in Liste anzeigen", "display_mode_grid": "Dokumente im Raster anzeigen", "search_simple": "Einfache Suche", - "search_fulltext": "Volltext Suche", + "search_fulltext": "Volltextsuche", "search_creator": "Urheber", "search_language": "Sprache", "search_before_date": "Vor diesem Datum", "search_after_date": "Nach diesem Datum", - "search_before_update_date": "Bearbeitet bevor diesem Datum", + "search_before_update_date": "Bearbeitet vor diesem Datum", "search_after_update_date": "Bearbeitet nach diesem Datum", "search_tags": "Tags", "search_shared": "Nur freigegebene Dokumente", @@ -83,9 +83,9 @@ "page_size_10": "10 pro Seite", "page_size_20": "20 pro Seite", "page_size_30": "30 pro Seite", - "upgrade_quota": "Fragen Sie Ihren Administrator, um Ihr Speicherplatz zu erweitern.", - "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) verwendet von {{ total | number: 0 }}MB", - "count": "{{ count }} Dokument{{ count > 1 ? 'e' : '' }} gefunden", + "upgrade_quota": "Fragen Sie Ihren Administrator, um Ihren Speicherplatz zu erweitern.", + "quota": "{{ current | number: 0 }}\u00A0MB ({{ percent | number: 1 }}\u00A0%) verwendet von {{ total | number: 0 }}\u00A0MB", + "count": "{{ count }} {{ count > 1 ? 'Dokumente' : 'Dokument' }} gefunden", "last_updated": "Zuletzt bearbeitet {{ date | timeAgo: dateFormat }}", "view": { "delete_comment_title": "Kommentar löschen", @@ -96,7 +96,7 @@ "shared_document_message": "Sie können dieses Dokument mit diesem Link freigeben. Beachten Sie, dass jeder, der diesen Link hat, das Dokument sehen kann.
    ", "not_found": "Dokument nicht gefunden", "forbidden": "Zugriff verweigert", - "download_files": "Datei herunterladen", + "download_files": "Dateien herunterladen", "export_pdf": "in PDF exportieren", "by_creator": "von", "comments": "Kommentar", @@ -104,20 +104,22 @@ "add_comment": "Fügen sie einen Kommentar hinzu", "error_loading_comments": "Fehler beim Laden eines Kommentars", "workflow_current": "Aktueller Workflow-Status", - "workflow_comment": "Fügen Sie einen Workflow Kommentar hinzu", + "workflow_comment": "Fügen Sie einen Workflow-Kommentar hinzu", "workflow_validated_title": "Workflow-Schritt validiert", "workflow_validated_message": "Der Workflow-Schritt wurde erfolgreich validiert.", + "display_mode_list": "Dateien in Liste anzeigen", + "display_mode_grid": "Dateien im Raster anzeigen", "content": { "content": "Inhalt", "delete_file_title": "Datei löschen", "delete_file_message": "Wollen Sie diese Datei wirklich löschen?", - "upload_pending": "Ausstehend...", - "upload_progress": "Hochladen...", + "upload_pending": "Ausstehend\u2026", + "upload_progress": "Hochladen\u2026", "upload_error": "Fehler beim Hochladen", "upload_error_quota": "Maximaler Speicherplatz erreicht", - "drop_zone": "Drag & Drop Dateien hierherziehen, um diese hochzuladen", + "drop_zone": "Legen Sie Dateien hier ab, um sie hochzuladen", "add_files": "Dateien hinzufügen", - "file_processing_indicator": "Diese Datei wird gerade bearbeitet. Die Suche wird nicht verfügbar sein, bevor der Vorgang abgeschlossen ist.", + "file_processing_indicator": "Diese Datei wird gerade verarbeitet. Die Suche ist erst verfügbar, wenn diese Verarbeitung abgeschlossen ist.", "reprocess_file": "Diese Datei erneut verarbeiten", "upload_new_version": "Neue Version hochladen", "open_versions": "Versionshistorie anzeigen" @@ -156,27 +158,27 @@ "title_placeholder": "Titel des Dokuments", "description_placeholder": "Zusammenfassung, Inhaltsverzeichnis oder Freitext", "new_files": "neue Dateien", - "orphan_files": "+ {{ count }} Datei{{ count > 1 ? 's' : '' }}", + "orphan_files": "+ {{ count }} {{ count > 1 ? 'Dateien' : 'Datei' }}", "additional_metadata": "Weitere Metadaten", "subject_placeholder": "Schlüsselwörter, abstrakte Sätze oder Klassifizierungscodes", "identifier_placeholder": "Eindeutiger Identifikator", "publisher_placeholder": "Name der Person, Organisation oder Abteilung, von der aus das Dokument veröffentlicht wurde.", "format_placeholder": "MIME-Typ oder physisches Format des Dokuments", "source_placeholder": "Ressource, aus der das Dokument stammt", - "uploading_files": "Dateien hochladen..." + "uploading_files": "Dateien hochladen\u2026" }, "default": { - "upload_pending": "Ausstehend...", - "upload_progress": "Lädt hoch...", + "upload_pending": "Ausstehend\u2026", + "upload_progress": "Lädt hoch\u2026", "upload_error": "Fehler beim Hochladen", "upload_error_quota": "Maximaler Speicherplatz erreicht", "quick_upload": "Schnelles Hochladen", - "drop_zone": "Drag & Drop Dateien hierherziehen, um diese hochzuladen", + "drop_zone": "Legen Sie Dateien hier ab, um sie hochzuladen", "add_files": "Dateien hinzufügen", "add_new_document": "Neues Dokument hinzufügen", "latest_activity": "Letzte Aktivitäten", "footer_sismics": "Programmiert mit von Sismics", - "api_documentation": "API Dokumentation", + "api_documentation": "API-Dokumentation", "feedback": "Geben Sie uns Ihr Feedback", "workflow_document_list": "Mir zugeordnete Dokumente", "select_all": "Alle auswählen", @@ -184,8 +186,8 @@ }, "pdf": { "export_title": "Export als PDF", - "export_metadata": "Export Metadaten", - "export_comments": "Export Kommentare", + "export_metadata": "Metadaten exportieren", + "export_comments": "Kommentare exportieren", "fit_to_page": "Bild an Seite anpassen", "margin": "Rand", "millimeter": "mm" @@ -198,17 +200,17 @@ }, "file": { "view": { - "previous": "Vorheriges", - "next": "Nächstes", + "previous": "Vorherige", + "next": "Nächste", "not_found": "Datei nicht gefunden" }, "edit": { "title": "Datei bearbeiten", - "name": "Dateinamen" + "name": "Dateiname" }, "versions": { "title": "Versionshistorie", - "filename": "Datiename", + "filename": "Dateiname", "mimetype": "Typ", "create_date": "Erstellungsdatum", "version": "Version" @@ -221,17 +223,17 @@ "title": "Tags", "message_1": "Tags sind Kategorien, die den Dokumenten zugeordnet sind.", "message_2": "Ein Dokument kann mit mehreren Tags versehen werden und ein Tag kann auf mehrere Dokumente angewendet werden.", - "message_3": "Unter Verwendung der Schaltfläche können Sie die Berechtigungen für ein Tag bearbeiten.", + "message_3": "Mit der -Schaltfläche können Sie die Berechtigungen für ein Tag bearbeiten.", "message_4": "Wenn ein Tag von einem anderen Benutzer oder einer anderen Gruppe gelesen werden kann, können die zugehörigen Dokumente auch von diesen Personen gelesen werden.", - "message_5": "Kennzeichnen Sie z.B. Ihre Firmendokumente mit einem Tag MyCompany und fügen Sie die Berechtigung Can read zu einer Gruppe hinzu employees" + "message_5": "Kennzeichnen Sie z.\u00A0B. Ihre Firmendokumente mit einem Tag MyCompany und fügen Sie die Berechtigung Kann lesen zu einer Gruppe Mitarbeiter hinzu" }, "edit": { "delete_tag_title": "Tag löschen", - "delete_tag_message": "Wollen Sie diesen Tag wirklich löschen?", + "delete_tag_message": "Wollen Sie dieses Tag wirklich löschen?", "name": "Name", "color": "Farbe", "parent": "Übergeordnet", - "info": "Berechtigungen für dieses Tag werden auch auf Dokumente angewendet, die mit einem Tag versehen sind {{ name }}", + "info": "Berechtigungen für dieses Tag werden auch auf Dokumente angewendet, die mit einem Tag {{ name }} versehen sind", "circular_reference_title": "Zirkuläre Referenz", "circular_reference_message": "Die Hierarchie der übergeordneten Tags bildet eine Schleife. Bitte wählen Sie ein anderes übergeordnetes Tag." } @@ -248,7 +250,7 @@ "profile": { "groups": "Gruppen", "quota_used": "Benutzter Speicherplatz", - "percent_used": "{{ percent | number: 0 }}% genutzt", + "percent_used": "{{ percent | number: 0 }}\u00A0% genutzt", "related_links": "Weiterführende Links", "document_created": "Dokumente erstellt von {{ username }}", "edit_user": "Benutzer {{ username }} bearbeiten" @@ -256,8 +258,8 @@ }, "usergroup": { "search_groups": "In Gruppen suchen", - "search_users": "In Benutzer suchen", - "you": "Eigenes Benutzerkonto!", + "search_users": "In Benutzern suchen", + "you": "Das sind Sie!", "default": { "title": "Benutzer und Gruppen", "message": "Hier können Sie Informationen über Benutzer und Gruppen einsehen." @@ -268,15 +270,31 @@ "menu_user_account": "Benutzerkonto", "menu_two_factor_auth": "Zwei-Faktor-Authentifizierung", "menu_opened_sessions": "Geöffnete Sitzungen", - "menu_file_importer": "Massen Datei Importer", - "menu_general_settings": "Generelle Einstellungen", + "menu_file_importer": "Massen-Datei-Importer", + "menu_general_settings": "Allgemeine Einstellungen", "menu_workflow": "Workflows", "menu_users": "Benutzerverwaltung", "menu_groups": "Gruppenverwaltung", "menu_vocabularies": "Vokabulareinträge", "menu_configuration": "Einstellungen", "menu_inbox": "Posteingang durchsuchen", + "menu_ldap": "LDAP Authentifizierung", + "menu_metadata": "Benutzerdefinierte Metadaten", "menu_monitoring": "Überwachung", + "ldap": { + "title": "LDAP Authentifizierung", + "enabled": "LDAP Authentifizierung aktivieren", + "host": "LDAP Hostname", + "port": "LDAP Port (standardmäßig 389)", + "usessl": "Aktiviere SSL (ldaps)", + "admin_dn": "Admin DN", + "admin_password": "Admin Passwort", + "base_dn": "Basis-Such-DN", + "filter": "Suchfilter (muss USERNAME enthalten, zum Beispiel \"(uid=USERNAME)\")", + "default_email": "Standard-E-Mail für LDAP-Benutzer", + "default_storage": "Standard Quota für LDAP-Benutzer", + "saved": "LDAP-Konfiguration erfolgreich gespeichert" + }, "user": { "title": "Benutzerverwaltung", "add_user": "Benutzer hinzufügen", @@ -286,12 +304,14 @@ "edit": { "delete_user_title": "Benutzer löschen", "delete_user_message": "Möchten Sie diesen Benutzer wirklich löschen? Alle zugehörigen Dokumente, Dateien und Tags werden gelöscht", + "user_used_title": "Benutzer in Verwendung", + "user_used_message": "Dieser Benutzer wird im Workflow \"{{ name }}\" benutzt", "edit_user_failed_title": "Dieser Benutzer existiert bereits", "edit_user_failed_message": "Dieser Benutzername wurde bereits von einem anderen Benutzer gewählt", - "edit_user_title": "Bearbeiten \"{{ username }}\"", + "edit_user_title": "Benutzer \"{{ username }}\" bearbeiten", "add_user_title": "Neuen Benutzer hinzufügen", "username": "Benutzername", - "email": "E-mail", + "email": "E-Mail", "groups": "Gruppen", "storage_quota": "Speicherkontingent", "storage_quota_placeholder": "Speicherkontingent (in MB)", @@ -299,8 +319,8 @@ "password_confirm": "Passwort (bestätigen)", "disabled": "Deaktivierter Benutzer", "password_reset_btn": "Senden Sie eine E-Mail zum Zurücksetzen des Kennworts an diesen Benutzer", - "password_lost_sent_title": "Passwort zurücksetzen Email gesendet", - "password_lost_sent_message": "Passwort zurücksetzen Email an {{ username }} gesendet.", + "password_lost_sent_title": "Passwort-zurücksetzen-E-Mail gesendet", + "password_lost_sent_message": "Passwort-zurücksetzen-E-Mail an {{ username }} gesendet.", "disable_totp_btn": "Zwei-Faktor-Authentifizierung für diesen Benutzer deaktivieren", "disable_totp_title": "Zwei-Faktor-Authentifizierung deaktivieren", "disable_totp_message": "Sind Sie sicher, dass sie die Zwei-Faktor-Authentifizierung für den Benutzer deaktivieren möchten?" @@ -314,7 +334,7 @@ "edit": { "delete_workflow_title": "Workflow löschen", "delete_workflow_message": "Möchten Sie diesen Workflow wirklich löschen? Derzeit ausgeführte Workflows werden nicht gelöscht", - "edit_workflow_title": "Bearbeiten \"{{ name }}\"", + "edit_workflow_title": "Workflow \"{{ name }}\" bearbeiten", "add_workflow_title": "Neuen Workflow hinzufügen", "name": "Name", "name_placeholder": "Name des Bearbeitungschritts oder der Beschreibung", @@ -323,8 +343,8 @@ "type_approve": "Genehmigen", "type_validate": "Bestätigen", "target": "Zugewiesen an", - "target_help": "Zulassen: Überprüfen und fortsetzen des Workflows
    Genehmigen: Übernehmen oder lehnen Sie die Überprüfung ab", - "add_step": "Workflow Schritt hinzufügen", + "target_help": "Zulassen: Überprüfen und fortsetzen des Workflows
    Genehmigen: Übernehmen oder lehnen Sie die Überprüfung ab", + "add_step": "Workflow-Schritt hinzufügen", "actions": "Was passiert danach?", "remove_action": "Aktion entfernen", "acl_info": "Nur hier definierte Benutzer und Gruppen können diesen Workflow für ein Dokument starten" @@ -334,20 +354,23 @@ "enable_totp": "Zwei-Faktor-Authentifizierung aktivieren", "enable_totp_message": "Stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben, die bereit ist, ein neues Konto hinzuzufügen.", "title": "Zwei-Faktor-Authentifizierung", - "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Absicherung Ihres {{ appName }} Benutzerkontos. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben:", + "message_1": "Die Zwei-Faktor-Authentifizierung ermöglicht Ihnen eine weitere Absicherung Ihres {{ appName }}-Benutzerkontos. Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie eine TOTP-kompatible Anwendung auf Ihrem Telefon haben:", "message_google_authenticator": "Für Android, iOS, und Blackberry: Google Authenticator", "message_duo_mobile": "Für Android und iOS: Duo Mobile", "message_authenticator": "Für Windows Phone: Authenticator", - "message_2": "Diese Anwendungen generieren automatisch einen Validierungscode, der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }} anmelden. .", + "message_2": "Diese Anwendungen generieren automatisch einen Validierungscode, der sich nach einer gewissen Zeitspanne ändert. Sie müssen diesen Validierungscode jedes Mal eingeben, wenn Sie sich bei {{ appName }} anmelden.", "secret_key": "Ihr geheimer Schlüssel lautet: {{ secret }}", - "secret_key_warning": "Konfigurieren Sie Ihre TOTP-App jetzt mit diesem geheimen Schlüssel auf Ihrem Telefon. Sie können später nicht mehr darauf zugreifen.", - "totp_enabled_message": "Die Zwei-Faktor-Authentifizierung ist in Ihrem Konto aktiviert.
    Bei jeder Anmeldung auf {{ appName }}, werden Sie in Ihrer konfigurierten Telefon-App nach einem Bestätigungscode gefragt.
    Wenn Sie Ihr Telefon verlieren, können Sie sich nicht in Ihrem Konto anmelden, aber aktive Sitzungen ermöglichen es Ihnen, einen geheimen Schlüssel neu zu generieren.", + "secret_key_warning": "Konfigurieren Sie Ihre TOTP-App jetzt mit diesem geheimen Schlüssel auf Ihrem Telefon. Sie können später nicht mehr auf diesen Schlüssel zugreifen.", + "totp_enabled_message": "Die Zwei-Faktor-Authentifizierung ist in Ihrem Konto aktiviert.
    Bei jeder Anmeldung auf {{ appName }} werden Sie in Ihrer konfigurierten Telefon-App nach einem Bestätigungscode gefragt.
    Wenn Sie Ihr Telefon verlieren, können Sie sich nicht in Ihrem Konto anmelden, aber aktive Sitzungen ermöglichen es Ihnen, einen geheimen Schlüssel neu zu generieren.", "disable_totp": { "disable_totp": "Deaktivieren der Zwei-Faktor-Authentifizierung", "message": "Ihr Konto wird nicht mehr durch die Zwei-Faktor-Authentifizierung geschützt.", - "confirm_password": "Bestätigen Sie ihr Passwort", + "confirm_password": "Bestätigen Sie Ihr Passwort", "submit": "Deaktivieren der Zwei-Faktor-Authentifizierung" - } + }, + "test_totp": "Bitte geben Sie den auf Ihrem Telefon angezeigten Validierungscode ein:", + "test_code_success": "Validierungscode OK", + "test_code_fail": "Dieser Code ist nicht gültig. Überprüfen Sie, ob Ihr Telefon ordnungsgemäß konfiguriert ist, oder deaktivieren Sie die Zwei-Faktor-Authentifizierung" }, "group": { "title": "Gruppenverwaltung", @@ -357,11 +380,13 @@ "delete_group_title": "Gruppe löschen", "delete_group_message": "Wollen Sie diese Gruppe wirklich löschen?", "edit_group_failed_title": "Gruppe existiert bereits", - "edit_group_failed_message": "Dieser Gruppenname wird bereits von einer anderen Gruppe übernommen", - "edit_group_title": "Bearbeiten \"{{ name }}\"", + "edit_group_failed_message": "Dieser Gruppenname wird bereits von einer anderen Gruppe verwendet", + "group_used_title": "Gruppe in Verwendung", + "group_used_message": "Diese Gruppe wird im Workflow \"{{ name }}\" verwendet", + "edit_group_title": "Gruppe \"{{ name }}\" bearbeiten", "add_group_title": "Neue Gruppe hinzufügen", "name": "Name", - "parent_group": "Übergruppe", + "parent_group": "Übergeordnete Gruppe", "search_group": "Gruppe suchen", "members": "Mitglieder", "new_member": "Neue Mitglieder", @@ -376,7 +401,7 @@ }, "config": { "title_guest_access": "Gastzugang", - "message_guest_access": "Der Gastzugang ist ein Modus, in dem jeder auf {{appName}} ohne Kennwort zugreifen kann.
    Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente und diejenigen zugreifen, auf die er über Berechtigungen zugreifen kann.
    ", + "message_guest_access": "Der Gastzugang ist ein Modus, in dem jeder auf {{appName}} ohne Kennwort zugreifen kann.
    Wie ein normaler Benutzer kann der Gastbenutzer nur auf seine Dokumente und diejenigen zugreifen, auf die er über Berechtigungen zugreifen kann.
    ", "enable_guest_access": "Gastzugang aktivieren", "disable_guest_access": "Gastzugang deaktivieren", "title_theme": "Aussehen anpassen", @@ -389,13 +414,13 @@ "logo": "Logo (quadratische Größe)", "background_image": "Hintergrundbild", "uploading_image": "Bild hochladen...", - "title_smtp": "SMTP Email Einstellungen für das Zürucksetzen des Passworts", - "smtp_hostname": "SMTP Server", - "smtp_port": "SMTP Port", - "smtp_from": "Absender E-Mail", - "smtp_username": "SMTP Benutzername", - "smtp_password": "SMTP Passwort", - "smtp_updated": "SMTP Konfiguration erfolgreich aktualisiert", + "title_smtp": "SMTP-E-Mail-Einstellungen für das Zurücksetzen des Passworts", + "smtp_hostname": "SMTP-Server", + "smtp_port": "SMTP-Port", + "smtp_from": "Absender-E-Mail", + "smtp_username": "SMTP-Benutzername", + "smtp_password": "SMTP-Passwort", + "smtp_updated": "SMTP-Konfiguration erfolgreich aktualisiert", "webhooks": "Webhooks", "webhooks_explain": "Webhooks werden aufgerufen, wenn das angegebene Ereignis eintritt. Die angegebene URL wird mit einer JSON-Payload gepostet, die den Ereignisnamen und die ID der betreffenden Ressource enthält.", "webhook_event": "Ereignisse", @@ -403,31 +428,41 @@ "webhook_create_date": "Erstelldatum", "webhook_add": "Webhook hinzufügen" }, + "metadata": { + "title": "Konfiguration benutzerdefinierter Metadaten", + "message": "Hier können Sie Ihren Dokumenten benutzerdefinierte Metadaten wie eine interne Kennung oder ein Ablaufdatum hinzufügen. Bitte beachten Sie, dass der Metadatentyp nach der Erstellung nicht mehr geändert werden kann.", + "name": "Metadatensatz-Name", + "type": "Metadatensatz-Typ" + }, "inbox": { "title": "Posteingang durchsuchen", "message": "Wenn Sie diese Funktion aktivieren, durchsucht das System den angegebenen Posteingang jede Minute nach ungelesenen E-Mails und importiert diese automatisch.
    Nach dem Import einer E-Mail wird diese als gelesen markiert.
    Folgen Sie den Links zu Konfigurationseinstellungen für Gmail, Outlook.com, Yahoo.", "enabled": "Durchsuchen des Posteingangs aktivieren", - "hostname": "IMAP Server", - "port": "IMAP Port (143 oder 993)", - "username": "IMAP Benutzername", - "password": "IMAP Passwort", - "tag": "Folgenden Tag zu importierten Dokumenten hinzufügen", + "hostname": "IMAP-Server", + "port": "IMAP-Port (143 oder 993)", + "starttls": "Verwende STARTTLS", + "username": "IMAP-Benutzername", + "password": "IMAP-Passwort", + "folder": "IMAP-Ordner", + "tag": "Folgendes Tag zu importierten Dokumenten hinzufügen", "test": "Konfiguration testen", - "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} E-Mail(s){{ data.count > 1 ? 's' : '' }} importiert", - "test_success": "Die Verbindung zum Posteingang war erfolgreich ({{ count }} unread message{{ count > 1 ? 's' : '' }})", + "last_sync": "Letzte Synchronisation: {{ data.date | date: 'medium' }}, {{ data.count }} {{ data.count > 1 ? 'E-Mails' : 'E-Mail' }} importiert", + "test_success": "Die Verbindung zum Posteingang war erfolgreich ({{ count }} ungelesene {{ count > 1 ? 'Nachrichten' : 'Nachricht' }})", "test_fail": "Beim Verbinden mit dem Posteingang ist ein Fehler aufgetreten, bitte überprüfen Sie die Einstellungen", - "saved": "IMAP Konfiguration erfolgreich gespeichert" + "saved": "IMAP-Konfiguration erfolgreich gespeichert", + "autoTagsEnabled": "Automatisches Hinzufügen von Tags aus der mit # markierten Betreffzeile", + "deleteImported": "Importierte Nachrichten aus Mailbox löschen" }, "monitoring": { "background_tasks": "Hintergrundaufgaben", - "queued_tasks": "Es gibt derzeit {{ count }} anstehende Tasks.", + "queued_tasks": "Es gibt derzeit {{ count }} {{ count > 1 ? 'anstehende Aufgaben' : 'anstehende Aufgabe' }}.", "queued_tasks_explain": "Dateiverarbeitung, Thumbnail-Erstellung, Index-Update, optische Zeichenerkennung sind Hintergrundaufgaben. Eine große Anzahl unbearbeiteter Aufgaben führt zu unvollständigen Suchergebnissen.", - "server_logs": "Server Logs", + "server_logs": "Server-Logs", "log_date": "Datum", "log_tag": "Tag", "log_message": "Nachricht", "indexing": "Indexierung", - "indexing_info": "Wenn Sie Unstimmigkeiten in den Suchergebnissen feststellen, können Sie versuchen, eine vollständige Neuindizierung durchzuführen. Die Suchergebnisse sind bis zum Abschluss dieser Operation unvollständig.", + "indexing_info": "Wenn Sie Unstimmigkeiten in den Suchergebnissen feststellen, können Sie versuchen, eine vollständige Neuindizierung durchzuführen. Die Suchergebnisse sind bis zum Abschluss dieser Aufgabe unvollständig.", "start_reindexing": "Vollständige Neuindizierung starten", "reindexing_started": "Neuindizierung wurde gestartet, bitte warten Sie, bis es keine Hintergrundaufgaben mehr gibt." }, @@ -452,14 +487,14 @@ "new_entry": "Neuer Eintrag" }, "fileimporter": { - "title": "Massen Datei Importer", + "title": "Massen-Datei-Importer", "advanced_users": "Für fortgeschrittene Benutzer!", "need_intro": "Wenn Sie:", "need_1": "Ganze Verzeichnisse von Dateien auf einmal importieren möchten", "need_2": "Ein Verzeichnis nach neuen Dateien durchsuchen lassen und gefunden Dateien importieren lassen möchten", "line_1": "Gehen Sie zu sismics/docs/releases und laden Sie das Datei-Importer-Tool für Ihr System herunter.", "line_2": "Folgen Sie den Anweisungen, um das Import-Tool zu nutzen.", - "line_3": "Ihre Dateien werden in Modus 'Schnelles Hochladen' importiert. Danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", + "line_3": "Ihre Dateien werden im Modus 'Schnelles Hochladen' importiert. Danach können Sie die Dateien weiterbearbeiten und Dokumenten zuordnen oder Dokumente erstellen.", "download": "Herunterladen", "instructions": "Anweisungen" } @@ -505,14 +540,14 @@ "Webhook": "Webhook" }, "selectrelation": { - "typeahead": "Tippen Sie einen Dokumentnamen ein" + "typeahead": "Geben Sie einen Dokumentnamen ein" }, "selecttag": { - "typeahead": "Tippen Sie einen Tagnamen ein" + "typeahead": "Geben Sie einen Tagnamen ein" }, "datepicker": { "current": "Heute", - "clear": "Bereinigen", + "clear": "Leeren", "close": "Erledigt" } }, @@ -540,7 +575,7 @@ "VALIDATED": "Validiert" }, "validation": { - "required": "Erfordert", + "required": "Erforderlich", "too_short": "Zu kurz", "too_long": "Zu lang", "email": "Muss eine gültige E-Mailadresse sein", @@ -559,11 +594,35 @@ "first": "Erste", "last": "Letzte" }, + "onboarding": { + "step1": { + "title": "Das erste Mal?", + "description": "Wenn Sie Teedy zum ersten Mal nutzen, klicken Sie auf die Schaltfläche \"Weiter\". Andernfalls können Sie diese Box schließen." + }, + "step2": { + "title": "Dokumente", + "description": "Teedy ist in Dokumenten organisiert und jedes Dokument enthält mehrere Dateien." + }, + "step3": { + "title": "Dateien", + "description": "Sie können Dateien hinzufügen, nachdem Sie ein Dokument erstellt haben oder vorher, indem Sie diesen Schnell-Hochlade-Bereich verwenden." + }, + "step4": { + "title": "Suche", + "description": "Dies ist die Hauptmethode, um Ihre Dokumente wiederzufinden. Es gibt auch eine erweiterte Suche mit dem Suchbutton." + }, + "step5": { + "title": "Tags", + "description": "Dokumente können in Tags organisiert werden (ähnlich wie Oberordner). Erstellen Sie sie hier." + } + }, + "yes": "Ja", + "no": "Nein", "ok": "OK", "cancel": "Abbrechen", "share": "Teilen", "unshare": "Nicht mehr teilen", - "close": "Schliessen", + "close": "Schließen", "add": "Hinzufügen", "open": "Öffnen", "see": "Ansehen", @@ -572,8 +631,9 @@ "edit": "Bearbeiten", "delete": "Löschen", "rename": "Umbenennen", - "loading": "Lädt...", + "download": "Herunterladen", + "loading": "Lädt\u2026", "send": "Absenden", "enabled": "Aktiviert", "disabled": "Deaktiviert" -} \ No newline at end of file +} diff --git a/docs-web/src/main/webapp/src/locale/el.json b/docs-web/src/main/webapp/src/locale/el.json new file mode 100644 index 00000000..d4d5c520 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/el.json @@ -0,0 +1,638 @@ +{ + "login": { + "username": "Όνομα Χρήστη", + "password": "Κωδικός", + "validation_code_required": "Απαιτείται κωδικός επαλήθευσης", + "validation_code_title": "Έχετε ενεγροποιήσει την επαλήθευση δύο σταδίων στο λογαριασμό σας. Παρακαλούμε εισάγετε ένα κωδικό επαλήθευσης από την εφαρμογή κινητού που έχετε ρυθμίσει.", + "validation_code": "Κωδικός επαλήθευσης", + "remember_me": "Να με θυμάσαι", + "submit": "Σύνδεση", + "login_as_guest": "Σύνδεση ως επισκέπτης", + "login_failed_title": "Αποτυχία σύνδεσης", + "login_failed_message": "Λανθασμένο όνομα χρήστη ή κωδικός", + "password_lost_btn": "Δεν θυμάσαι τον κωδικό;", + "password_lost_sent_title": "Έχει σταλεί το email επαναφοράς κωδικού", + "password_lost_sent_message": "Ένα email έχει σταλεί στο {{ username }} για την επαναφορά του κωδικού σου", + "password_lost_error_title": "Σφάλμα επαναφοράς κωδικού", + "password_lost_error_message": "Δεν κατέστη δυνατή η αποστολή email επαναφοράς κωδικού, παρακαλούμε επικοινωνήστε με το διαχειριστή για χειροκίνητη επαναφορά" + }, + "passwordlost": { + "title": "Ξέχασα τον κωδικό", + "message": "Παρακαλούμε συμπλήρωσε το όνομα χρήστη σου για να λάβεις ένα σύνδεσμο επαναφοράς κωδικού. Αν δεν θυμάσαι το όνομα χρήστη, παρακαλούμε επικοινώνησε με το διαχειριστή", + "submit": "Επαναφορά του κωδικού μου" + }, + "passwordreset": { + "message": "Παρακαλούμε συμπλήρωσε ένα νέο κωδικό", + "submit": "Αλλαγή του κωδικού μου", + "error_title": "Σφάλμα αλλαγής του κωδικού σου", + "error_message": "Το αίτημα ανάκτησης του κωδικού σου έχει λείξει, παρακαλούμε ζήτησε ένα νέο στη σελίδα σύνδεσης" + }, + "index": { + "toggle_navigation": "Μετακίνηση πλοήγησης", + "nav_documents": "έγγραφα", + "nav_tags": "Ετικέτες", + "nav_users_groups": "Χρήστες & Ομάδες", + "error_info": "{{ count }} Νέο σφάλμα", + "logged_as": "Σύνδεση ως {{ username }}", + "nav_settings": "Ρυθμίσεις", + "logout": "Αποσύνδεση", + "global_quota_warning": "Προειδοποίηση! Η παγκόμσια ποσόστωση έχει φτάσει στις {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) με χρήση στις {{ total | number: 0 }}MB" + }, + "document": { + "navigation_up": "Ανέβα ένα επίπεδο", + "toggle_navigation": "Μετακίνηση φακέλου πλοήγησης", + "display_mode_list": "Εμφάνιση εγγράφων σε λίστα", + "display_mode_grid": "Εμφάνιση εγγράφων σε πλέγμα", + "search_simple": "Απλή αναζήτηση", + "search_fulltext": "Αναζήτηση πλήρους κειμένου", + "search_creator": "Δημιουργός", + "search_language": "Γλώσσα", + "search_before_date": "Δημιουργήθηκε πριν από αυτή την ημερομηνία", + "search_after_date": "Δημιουργήθηκε μετά από αυτή την ημερομηνία", + "search_before_update_date": "Ενημερώθηκε πριν από αυτή την ημερομηνία", + "search_after_update_date": "Ενημερώθηκε μετά από αυτή την ημερομηνία", + "search_tags": "Ετικέτες", + "search_shared": "Μόνο κοινοποιημένα εγγραφα", + "search_workflow": "Ροή εγρασίας ανατεθειμένη σε εμένα", + "search_clear": "Καθάρισμα", + "any_language": "Οποιαδήποτε γλώσσα", + "add_document": "Προσθήκη εγγράφου", + "import_eml": "Εισαγωγή απλο ένα email (μορφή EML)", + "tags": "Ετικέτες", + "no_tags": "Δεν υπάρχουν ετικέτες", + "no_documents": "Δεν υπάρχει έγγραφο στη βάση δεδομένων", + "search": "Αναζήτηση", + "search_empty": "Δεν υπάρχουν αποτελέσματα για \"{{ search }}\"", + "shared": "Κοινοποιήθηκε", + "current_step_name": "Τρέχον βήμα", + "title": "Τίτλος", + "description": "Περιγραφή", + "contributors": "Συντελεστές", + "language": "Γλώσσα", + "creation_date": "Ημερομηνία δημιουργίας", + "subject": "Θέμα", + "identifier": "Αναγωριστικό", + "publisher": "Εκδότης", + "format": "Μορφή", + "source": "Πηγή", + "type": "Είδος", + "coverage": "Κάλυψη", + "rights": "Δικαιώματα", + "relations": "Σχέσεις", + "page_size": "Μέγεθος σελίδας", + "page_size_10": "10 ανά σελίδα", + "page_size_20": "20 ανά σελίδα", + "page_size_30": "30 ανά σελίδα", + "upgrade_quota": "Για να αυξήσεις το ποσοστό σου, ρώτησε τον διαχειριστή σου", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) χρήση σε {{ total | number: 0 }}MB", + "count": "{{ count }} Έγγραφα βρέθηκε", + "last_updated": "Τελευταία ενημέρωση {{ date | timeAgo: dateFormat }}", + "view": { + "delete_comment_title": "Διαγραφή σχολίου", + "delete_comment_message": "Θέλεις πραγματικά να διαγράψεις αυτό το σχόλιο;", + "delete_document_title": "Διαγραφή εγγράφου", + "delete_document_message": "Θες πραγματικά να διαγράψεις αυτό το έγγραφο;", + "shared_document_title": "Κοινοποιημένο έγγραφο", + "shared_document_message": "Μπορείτε να κοινοποιήσετε αυτό το έγγραφο δίνοντας αυτό το σύνδεσμο. Σημειώστε πως όλοι όσοι έχουν αυτό το σύνδεσμο μπορούν να δουν το έγγραφο.
    ", + "not_found": "Δεν βρέθηκε έγγραφο", + "forbidden": "Η πρόσβαση απαγορεύτηκε", + "download_files": "Κατέβασμα αρχείου", + "export_pdf": "Εξαγωγή σε PDF", + "by_creator": "από", + "comments": "Σχόλια", + "no_comments": "Δεν υπάρχουν σχόλια σε αυτό το έγγραφο ακόμα", + "add_comment": "Προσθήκη σχολίου", + "error_loading_comments": "Σφάλμα φόρτωσης σχολίων", + "workflow_current": "Τρέχον βήμα ροής εργασίας", + "workflow_comment": "Προσθήκη σχολίου ροής εργασίας", + "workflow_validated_title": "Το βήμα ροής εργασίας επαληθεύτηκε", + "workflow_validated_message": "Το βήμα ροής εργασίας έχει επαληθευτεί επιτυχώς.", + "content": { + "content": "Περιεχόμενο", + "delete_file_title": "Διαγραφή αρχείου", + "delete_file_message": "Θέλεις πραγματικά να διαγράψεις αυτο το αρχείο;", + "upload_pending": "Εκκρεμεί...", + "upload_progress": "Ανέβασμα...", + "upload_error": "Σφάλμα ανεβάσματος", + "upload_error_quota": "Το ποσοστό έχει φτάσει", + "drop_zone": "Σύρε & άφησε αρχεία εδώ για ανέβασμα", + "add_files": "Προσθήκη αρχείων", + "file_processing_indicator": "Γίνεται επεξεργασία αυτού του αρχείου. Η αναζήτηση δεν θα είναι διαθέσιμη πριν ολοκληρωθεί.", + "reprocess_file": "Επανάληψη επεξεργασίας αυτού του αρχείου", + "upload_new_version": "Ανλεβασε μια νέα έκδοση", + "open_versions": "Προβολή ιστορικού εκδόσεων", + "display_mode_list": "Εμφάνιση αρχείων σε λίστα", + "display_mode_grid": "Εμφάνιση αρχείων σε πλέγμα" + }, + "workflow": { + "workflow": "Ροή εργασίας", + "message": "Επαλήθευσε ή επιβεβαίωσε τα έγγραφα σου με άτομα του οργανισμού σου με τη χρήση ροών εργασίας.", + "workflow_start_label": "Ποιά ροή εργασίας να ξεκινήσει;", + "add_more_workflow": "Προσθήκη περισσότερων ροών εργασίας", + "start_workflow_submit": "Έναρξη ροής εργασίας", + "full_name": "{{ name }} ξεκίνησε στις {{ create_date | date }}", + "cancel_workflow": "Ακύρωση τρέχουσας ροής εργασίας", + "cancel_workflow_title": "Ακύρωση της ροής εργασίας", + "cancel_workflow_message": "Θέλεις πραγματικά να ακυρώσης την τρέχουσα ροή εργασίας;", + "no_workflow": "Δεν μπορείς να ξεκινήσεις οποιαδήποτε ροή εργασίας σε αυτό το έγγραφο." + }, + "permissions": { + "permissions": "ʼδειες", + "message": "ʼδειες μπορούν να εφαρμοστούν απευθείας σε αυτό το έγγραφο, ή μπορείτε να έρθετε από tags.", + "title": "ʼδειες σε αυτό το έγγραφο", + "inherited_tags": "ʼδειες που έχουν παρθεί από ετικέτες", + "acl_source": "Από", + "acl_target": "Για", + "acl_permission": "ʼδεια" + }, + "activity": { + "activity": "Δραστηριότητα", + "message": "Κάθε ενέργειες σε αυτό το έγγραφο καταγράφονται εδώ." + } + }, + "edit": { + "document_edited_with_errors": "Το έγγραφο επεξεργάστηκε επιτυχώς αλλά μερικά αρχεία δεν μπόρεσαν να ανέβουν", + "document_added_with_errors": "Το έγγραφο προστέθηκε επιτυχώς αλλά μερικά αρχεία δεν μπόρεσαν να ανέβουν", + "quota_reached": "Το ποσοστό έχει φτάσει", + "primary_metadata": "Κυρίως μεταδεδομένα", + "title_placeholder": "Ένα όνομα δόθηκε στη πηγή", + "description_placeholder": "Ένας λογαριασμός πηγής", + "new_files": "Νέα αρχεία", + "orphan_files": "+ {{ count }} αρχείο", + "additional_metadata": "Επιπρόσθετα μεταδεδομένα", + "subject_placeholder": "Το θέμα της πηγής", + "identifier_placeholder": "Μια ξεκάθαρη αναφορά στη πηγή σε συγκεκριμένο πλαίσιο", + "publisher_placeholder": "Μια οντότητα υπεύθυνη για να κάνει την πηγή διαθέσιμη", + "format_placeholder": "Η μορφή αρχείου, φυσικό ενδιάμεσο, ή διαστάσεις της πηγής", + "source_placeholder": "Μια σχετική πηγή από την οποία προέρχεται η περιγραφόμενη πηγή", + "uploading_files": "Ανέβασμα αρχείων..." + }, + "default": { + "upload_pending": "Εκκρεμεί...", + "upload_progress": "Ανεβάζει...", + "upload_error": "Σφάλμα ανεβάσματος", + "upload_error_quota": "Το ποσοστό έχει φτάσει", + "quick_upload": "Γρήγορο ανέβασμα", + "drop_zone": "Σύρε & άφησε αρχεία εδώ για ανέβασμα", + "add_files": "Προσθήκη αρχείων", + "add_new_document": "Προσθήκη νέεου εγγράφου", + "latest_activity": "Τελευταία δραστηριότητα", + "footer_sismics": "Δημιουργήθηκε με από Sismics", + "api_documentation": "API Έγγραφα", + "feedback": "Δώσε μας ανατροφοδότηση", + "workflow_document_list": "Έγγρφα που ανατέθηκαν σε εσένα", + "select_all": "Επιλογή όλων", + "select_none": "Επιλογή κανενός" + }, + "pdf": { + "export_title": "Εξαγωγή σε PDF", + "export_metadata": "Εξαγωγή σε μεταδεδομένα", + "export_comments": "Εξαγωγή σχολίων", + "fit_to_page": "Εφαρμογή εικόνας στη σελίδα", + "margin": "Περιθώριο", + "millimeter": "χιλ." + }, + "share": { + "title": "Κοινοποίηση εγγράφου", + "message": "Ονομάστε την κοινοποίηση αν θέλετε να κοινοποιήσετε πολλές φορές το ίδιο έγγραφο.", + "submit": "Κοινοποίηση" + } + }, + "file": { + "view": { + "previous": "Προηγούμενο", + "next": "Επόμενο", + "not_found": "Δεν βρέθηκε αρχείο" + }, + "edit": { + "title": "Επεξεργασία αρχείου", + "name": "Όνομα αρχείου" + }, + "versions": { + "title": "Ιστορικό εκδοχών", + "filename": "Όνομα αρχείου", + "mimetype": "Είδος", + "create_date": "Ημερομηνία δημιουργίας", + "version": "Εκδοχή" + } + }, + "tag": { + "new_tag": "Νέα ετικέτα", + "search": "Αναζήτηση", + "default": { + "title": "Ετικέτες", + "message_1": "Οι ετικέτες είναι καρτέλες σχετιζόμενες με έγγραφα.", + "message_2": "Ένα έγγραφο που μπορεί να έχει πολλές ετικέτες, και μια ετικέτα μπορεί να εφαρμοστεί σε πολλά έγγραφα.", + "message_3": "Χρησιμοποιώντας το κουμπί , μπορεί να γίνει επεξεργασία των αδειών σε μια ετικέτα.", + "message_4": "Αν μια ετικέτα μπορεί να διαβαστεί από έναν άλλο χρήστη ή ομάδα, τα σχετιζόμενα έγγραφα μπορούν επίσης να διαβαστούν από αυτά τα άτομα.", + "message_5": "Για παράδειγμα, βάλε στα έγγραφα της εταιρείας σου μια ετικέτα MyCompany και πρόσθεσε την άδεια Μπορεί να διαβαστεί σε μια ομάδα εργαζομένων" + }, + "edit": { + "delete_tag_title": "Διαγραφή ετικέτας", + "delete_tag_message": "Θέλεις πραγματικά να διαγράψεις αυτή την ετικέτα;", + "name": "Όνομα", + "color": "Χρώμα", + "parent": "Γονέας", + "info": "Οι άδειες σε αυτή την ετικέτα θα εφαρμοστούν επίσης και στα έγγραφα με ετικέτα {{ name }}", + "circular_reference_title": "Κυκλική αναφορά", + "circular_reference_message": "Η ιεραρχία των μητρικών ετικετών κάνει κύκλο, παρακαλούμε επέλεξε διαφορετικό γονέα." + } + }, + "group": { + "profile": { + "members": "Μέλη", + "no_members": "Χωρίς μέλος", + "related_links": "Σχετικοί συνδέσμοι", + "edit_group": "Επεξεργασία {{ name }} ομάδας" + } + }, + "user": { + "profile": { + "groups": "Ομάδες", + "quota_used": "Χρησημοποιημένο ποσοστό", + "percent_used": "{{ percent | number: 0 }}% Χρησιμοποιημένο", + "related_links": "Σχετικοί συνδέσμοι", + "document_created": "Έγγραφα που δημιουργήθηκαν από {{ username }}", + "edit_user": "Επεξεργασία {{ username }} χρήστη" + } + }, + "usergroup": { + "search_groups": "Αναζήτηση σε ομάδες", + "search_users": "Αναζήτηση σε χρήστες", + "you": "Είσαι εσύ!", + "default": { + "title": "Χρήστες & Ομάδες", + "message": "Εδώ μπορείς να δεις πληροφορίες για χρήστες και ομάδες." + } + }, + "settings": { + "menu_personal_settings": "Προσωπικές ρυθμίσεις", + "menu_user_account": "Λογαριασμός χρήστη", + "menu_two_factor_auth": "Επαλήθευση δύο σταδίων", + "menu_opened_sessions": "Ανοιχτές περιόδοι", + "menu_file_importer": "Εισαγωγέας αρχείων όγκου", + "menu_general_settings": "Γενικές ρυθμίσεις", + "menu_workflow": "Ροή εργασίας", + "menu_users": "Χρήστες", + "menu_groups": "Ομάδες", + "menu_vocabularies": "Λεξιλόγια", + "menu_configuration": "Διαμόρφωση", + "menu_inbox": "Σάρωση εισερχομένων", + "menu_ldap": "Επαλήθευση LDAP", + "menu_metadata": "Μεταδεδομένα προσαρμοσμένων χαρακτηριστικών", + "menu_monitoring": "Έλεγχος", + "ldap": { + "title": "Επαλήθευση LDAP", + "enabled": "Ενεργοποίηση επαλήθευσης LDAP", + "host": "LDAP όνομα εξυπηρετητή", + "port": "LDAP θύρα (389 από προεπιλογή)", + "admin_dn": "Διαχειριστής DN", + "admin_password": "Κωδικός Διαχειριστή", + "base_dn": "Βάση αναζήτησης DN", + "filter": "Φίλτρο αναζήτησης (πρέπει να περιέχει ΟΝΟΜΑ ΧΡΗΣΤΗ, πχ. \"(uid=USERNAME)\")", + "default_email": "Προεπιλεγμένο email για χρήστη LDAP", + "default_storage": "Προεπιλεγμένη αποθήκευση για χρήστη LDAP", + "saved": "Η διαμόρφωση LDAP αποθηκεύτηκε με επιτυχία" + }, + "user": { + "title": "Διοίκηση χρηστών", + "add_user": "Προσθήκη χρήστη", + "username": "Όνομα χρήστη", + "create_date": "Ημερομηνία δημιουργίας", + "totp_enabled": "Η επαλήθευση δύο σταδίων ενεργοποιήθηκε για αυτό το λογαριασμό", + "edit": { + "delete_user_title": "Διαγραφή χρήστη", + "delete_user_message": "Θέλεις πραγματικά να διαγράψεις αυτό το χρήστη; Όλα τα σχετικά έγγραφα, αρχεία και ετικέτες θα διαγραφούν", + "user_used_title": "Χρήστης σε χρήση", + "user_used_message": "Αυτός ο χρήστης χρησιμοποιείται στη ροή εργασίας \"{{ name }}\"", + "edit_user_failed_title": "Ο χρήστης υπάρχει ήδη", + "edit_user_failed_message": "Αυτό το όνομα χρήστη έχει παρθεί ήδη από έναν άλλο χρήστη", + "edit_user_title": "Επαξεργασία \"{{ username }}\"", + "add_user_title": "Προσθήκη χρήστη", + "username": "Όνομα χρήστη", + "email": "E-mail", + "groups": "Ομάδες", + "storage_quota": "Ποσοστό αποθήκευσης", + "storage_quota_placeholder": "Ποσοστό αποθήκευσης (σε MB)", + "password": "Κωδικός", + "password_confirm": "Κωδικός (επιβεβαίωση)", + "disabled": "Απενεργοποίηση χρήστη", + "password_reset_btn": "Αποστολή ενός email για επαναφορά κωδικού αυτού το χρήστη", + "password_lost_sent_title": "Το email επαναφοράς κωδικού έχει σταλεί", + "password_lost_sent_message": "Ένα email επαναφοράς κωδικού έχει σταλεί στον/στην {{ username }}", + "disable_totp_btn": "Απενεργοποίηση επαλήθευσης δύο-σταδίων για αυτό το χρήστη", + "disable_totp_title": "Απενεργοποίηση επαλήθευσης δύο σταδίων", + "disable_totp_message": "Είσαι σίγουρος πως θέλεις να απενεργοποιήσεις την επαλήθευση δύο σταδίων για αυτό το χρήστη;" + } + }, + "workflow": { + "title": "Διαμόρφωση ροής εργασίας", + "add_workflow": "Προσθήκηροής εργασίας", + "name": "Όνομα", + "create_date": "Ημερομηνία δημιουργίας", + "edit": { + "delete_workflow_title": "Διαγραφή ροής εργασίας", + "delete_workflow_message": "Θέλεις πραγματικά να διαγράψεις αυτή τη ροή εργασίας; Οι τρέχουσες ροές εργασίας δεν θα διαγραφούν", + "edit_workflow_title": "Επεξεργασία \"{{ name }}\"", + "add_workflow_title": "Προσθήκη ροής εργασίας", + "name": "Όνομα", + "name_placeholder": "Όνομα βήματος ή περιγραφή", + "drag_help": "Σύρε και άφησε για επανάληψη παραγγελίας βήματος", + "type": "Είδος βήματος", + "type_approve": "Έγκριση", + "type_validate": "Επιβεβαίωση", + "target": "Αναθέτηκε σε", + "target_help": "Έγκριση: Αποδοχή ή απόρριψη αξιολόγησης
    Επιβεβαίωση: Αξιολόγηση και συνέχιση της ροής εργασίας", + "add_step": "Προσθήκη βήματος ροής εργασίας", + "actions": "Τι γίνεται μετά;", + "remove_action": "Αφαίρεση ενέργειας", + "acl_info": "Μόνο χρήστες και ομάδες που καθορίζοντε εδώ θα είναι σε θέση να ξεκινήσουν αυτή τη ροή εργασίας σε ένα έγγραφο" + } + }, + "security": { + "enable_totp": "Ενεργοποίηση επαλήθευσης δύο σταδίων", + "enable_totp_message": "Βεβαιώσου πως έχει μια εφαρμογή συμβατή με TOTP στο κινητό σου έτοιμη για προσθήκη ενός νέου λογαριασμού", + "title": "Επαλήθευση δύο σταδίων", + "message_1": "Η επαλήθευση δύο σταδίων σου επιτρέπει να προσθέσεις ένα στρώμα ασφάλειας στο {{ appName }} λογαριασμό σου.
    Πριν την ενεργοποίηση αυτής της λειτουργίας, βαβαιώσου πως έχεις μια εφαρμογή συμβατή με TOTP στο κινητό σου:", + "message_google_authenticator": "Για Android, iOS, και Blackberry: Google Authenticator", + "message_duo_mobile": "Για Android και iOS: Duo Mobile", + "message_authenticator": "Για Τηλέφωνο Windows: Authenticator", + "message_2": "Αυτές οι εφαρμογές δημιουργούν αυτόματα ένα κωδικό επιβεβαίωσης ο οποίος αλλάζει μετά από κάποια χρονική περιόδο.
    Θα πρέπει να εισάγεις αυτόν τον κωδικό επιβεβαίωσης κάθε φορά που κάνεις σύνδεστη στο {{ appName }}.", + "secret_key": "Το μυστικό κλειδί σου είναι: {{ secret }}", + "secret_key_warning": "Διαμόρφωσε την TOTP εφαρμογή στο κινητό σου με αυτό το μυστικό κλειδί τώρα, δεν θα μπορείς να έχεις πρόσβαση αργότερα.", + "totp_enabled_message": "Η επαλήθευση δύο σταδίων έχει ενεργοποιηθεί στο λογαριασμό σου.
    Κάθε φορά που κάνεις σύνδεση στο {{ appName }}, θα σου ζητείται ο κωδικός επαλήθευσης από τη διαμορφωμένη εφαρμογή στο κινητό σου.
    Αν χάσεις το τηλέφωνο σου, δεν θα μπορείς να κάνεις σύνδεση στο λογαριασμό σου αλλά οι ενεργές περιόδοι θα σου επιτρέπουν να regenerate a secrey key.", + "disable_totp": { + "disable_totp": "Απενεργοποίηση επαλήθευσης δύο σταδίων", + "message": "Ο λογαριασμός σου δεν θα προστατευεται από την επαλήθευση δύο σταδίων πλέον.", + "confirm_password": "Επιβεβαίωση του κωδικού σου", + "submit": "Απενεργοποίηση επαλήθευσης δύο σταδίων" + }, + "test_totp": "Παρακαλούμε συμπλήρωσε τον κωδικό επαλήθευσης που εμφανίζεται στο τηλέφωνο σου :", + "test_code_success": "Κωδικός επαλήθευσης OK", + "test_code_fail": "Αυτός ο κωδικός δεν είναι έγκυρος, παρακαλούμε έλεγξε ξανά πως το τηλέφωνο έχει διαμορφωθεί σωστά ή απενεργοποίησε την επαλήθευση δύο σταδίων" + }, + "group": { + "title": "Διαχείριση ομάδων", + "add_group": "Προσθήκη ομάδας", + "name": "Όνομα", + "edit": { + "delete_group_title": "Διαγραφή ομάδας", + "delete_group_message": "Θέλεις πραγματικά να διαγράψεις αυτή την ομάδα;", + "edit_group_failed_title": "Η ομάδα υπάρχει ήδη", + "edit_group_failed_message": "Αυτό το όνομα ομάδας έχει ήδη παρθεί από μια άλλη ομάδα", + "group_used_title": "Ομάδα σε χρήση", + "group_used_message": "Αυτή η ομάδα χρησιμοποιείται στη ροή εργασίας \"{{ name }}\"", + "edit_group_title": "Επεξεργασία \"{{ name }}\"", + "add_group_title": "Προσθήκη ομάδας", + "name": "Όνομα", + "parent_group": "Ομάδα γονέας", + "search_group": "Αναζήτηση ομάδας", + "members": "Μέλη", + "new_member": "Νέο μέλος", + "search_user": "Αναζήτηση χρήστη" + } + }, + "account": { + "title": "Λογαριασμός χρήστη", + "password": "Κωδικός", + "password_confirm": "Κωδικός (επιβεβαίωση)", + "updated": "Ο λογαριασμός ενημερώθηκε επιτυχώς" + }, + "config": { + "title_guest_access": "Πρόσβαση επισκέπτη", + "message_guest_access": "Η πρόσβαση επισκέπτη με την οποία μπορεί ο καθένα να έχει πρόσβαση στο {{ appName }} χωρίς κωδικό.
    Όπως ένας κανονικός χρήστης, ο χρήστης επισκέπτης μπορεί μόνο ν αέχει πρόσβαση στα έγγραφα του και σε αυτά που είναι προσβάσιμα μέσω αδειών.
    ", + "enable_guest_access": "Ενεργοποίηση πρόσβασης επισκέπτη", + "disable_guest_access": "Απενεργοποίηση πρόσβασης επισκέπτη", + "title_theme": "Προσαρμογή θέματος", + "title_general": "Γενική διαμόρφωση", + "default_language": "Προεπιλεγμένη γλώσσα για νέα έγγραφα", + "application_name": "Όνομα εφαρμογής", + "main_color": "Κεντρικό χρώμα", + "custom_css": "Προσαρμοσμένο CSS", + "custom_css_placeholder": "Προσαρμοσμένο CSS για προσθήκη μετά το κεντρικό φύλλο είδους", + "logo": "Λογότυπο (τετράγωνο μέγεθος)", + "background_image": "Εικόνα φόντου", + "uploading_image": "Ανέβασμα εικόνας...", + "title_smtp": "Διαμόρφωση Email", + "smtp_hostname": "Όνομα εξυπηρετητή SMTP", + "smtp_port": "SMTP θύρα", + "smtp_from": "e-mail αποστολέα", + "smtp_username": "SMTP όνομα χρήστη", + "smtp_password": "SMTP κωδικός", + "smtp_updated": "SMTP διαμόρφωση ενημερώθηκε επιτυχώς", + "webhooks": "Διαδικτυακοί συνδέσμοι", + "webhooks_explain": "Οι διαδικτυακοί συνδέσμοι θα κληθούν όταν γίνει ένα συγκεκριμένο γεγονός. Το URL θα γίνει ανάρτηση με ένα ωφέλημο φορτίο JSON που περιέχει το όνομα του γεγονότος και τηνταυτότητα της πηγής.", + "webhook_event": "Γεγονός", + "webhook_url": "URL", + "webhook_create_date": "Ημερομηνία δημιουργίας", + "webhook_add": "Προσθήκη διαδικτυακού συνδέσμου" + }, + "metadata": { + "title": "Προσαρμοσμένη διαμόρφωση μεταδεδομένων", + "message": "Εδώ μπορείς να προσθέσεις προσαρμοσμένα μεταδεδομένα στα έγγραφα σου όπως εσωτερική αναγνώριση ή ημερομηνία λήξης. Παρακαλούμε σημείωσε πως το είδος των μεταδεδομένων δεν μπορεί νααλλάξει μετά τη δημιουργία.", + "name": "Όνομα μεταδεδομένων", + "type": "Είδος μεταδεδομένων" + }, + "inbox": { + "title": "Σάρωση εισερχομένων", + "message": "Ενεργοποιώντας αυτή τη λειτουργία, το σύστημα θα σαρώσει τα συγκεκριμένα εισερχόμενα κάθε λεπτό για αδιάβαστα emails και αυτόματα θα τα εισάγει.
    Μετά την εισαγωγή ενός email, θα σημειώνεται ως διαβασμένο.
    Οι ρυθμίσεις διαμόρφωσης για Gmail, Outlook.com, Yahoo.", + "enabled": "Ενεργοποίηση σάρωσης εισερχομένων", + "hostname": "IMAP όνομα εξυπηρετητή", + "port": "IMAP θύρα (143 ή 993)", + "username": "IMAP όνομα χρήστη", + "password": "IMAP κωδικός", + "folder": "IMAP φάκελο", + "tag": "Ετικέτα που προστέθηκε σε ειχερχόμενα έγγραφα", + "test": "Δοκιμή μαραμέτρων", + "last_sync": "Τελευταίος συγχρονισμός: {{ data.date | date: 'medium' }}, {{ data.count }} μήνυμα εισήχθη", + "test_success": "Η σύνδεση στα εισερχόμενα είναι επιτυχής ({{ count }} αδιάβαστο μήνυμα", + "test_fail": "Προέκυψε ένα σφάλμα κατά τη σύνδεση στα εισερχόμενα, παρακαλούμε έλεγξε τις παραμέτρους", + "saved": "Η διαμόρφωση IMAP αποθηκεύτηκε επιτυχώς", + "autoTagsEnabled": "Αυτόματη προσθήκη επτικετών από τη γραμμή θέματος με το σήμα #", + "deleteImported": "Διαγραφή μηνυματος από το κουτί αλληλογραφίας μετά την εισαγωγή" + }, + "monitoring": { + "background_tasks": "Εργασίες φόντου", + "queued_tasks": "Υπάρχουν για την ώρα {{ count }} εργασίες σε σειρά.", + "queued_tasks_explain": "Επεξεργασία αρχείου, δημιουγία thumbnail, ενημέρωση ευρετηρίου, αναγνώρηση οπτικών χαρακτήρων είναι εργασίες φόντου. Ένα μεγάλο ποσό των μη επεξεργασμένων εργασιών θα έχει ως αποτέλεσμα τα μη ολοκληρωμένα αποτελέσματα αναζήτησης.", + "server_logs": "Αρχεία καταγραφής διακομιστή", + "log_date": "Ημερομηνία", + "log_tag": "Ετικέτα", + "log_message": "Μήνυμα", + "indexing": "Καταλογογράφηση", + "indexing_info": "Αν προσέξεις αποκλίσεις στα αποτελέσματα αναζήτησης, μπορείς να δοκιμάσεις να κάνεις μια πλήρη καταλογογράφηση ξανά. Τα αποτελέσματα αναζήτησης θα είναι μη ολοκληρωμένα μέχρι να γίνει αυτή η διεργασία.", + "start_reindexing": "Έναρξη πλήρους καταλογογράφησης", + "reindexing_started": "Η επανάληψη καταλογογράφησης ξεκίνησε, παρακαλούμε περίμενε μέχρι να μην υπάρχουν άλλες εργασίες φόντου." + }, + "session": { + "title": "Ανοιχτές περίοδοι", + "created_date": "Ημερομηνία δημιουργίας", + "last_connection_date": "Ημερομηνία τελευταίας σύνδεσης", + "user_agent": "Από", + "current": "Τρέχουσα", + "current_session": "Αυτή είναι η τρέχουσα περίοδος", + "clear_message": "Όλες οι άλλες συσκευές που είναι συνδεδεμένες σε αυτό το λογαριασμό θα αποσυνδεθούν", + "clear": "Καθαρισμός όλων των άλλων περιόδων" + }, + "vocabulary": { + "title": "Καταχωρήσεις λεξιλογίου", + "choose_vocabulary": "Επέλεξε ένα λεξιλόγιο για επεξεργασία", + "type": "Είδος", + "coverage": "Κάλυψη", + "rights": "Δικαιώματα", + "value": "Αξία", + "order": "Παραγγελία", + "new_entry": "Νέα καταχώρηση" + }, + "fileimporter": { + "title": "Εισαγωγέας όγκου αρχείων", + "advanced_users": "Για προχωρημένους χρήστες!", + "need_intro": "Αν θέλεις να:", + "need_1": "Εισάγεις ένα ευρετήριο αρχείων με τη μια", + "need_2": "Σαρώσεις ένα ευρετήριο για νέα αρχεία και να τα εισάγεις", + "line_1": "Πήγαινε στο sismics/docs/releases και κατέβασε το εγαλείο εισαγωγής αρχείων για το σύστημα σου.", + "line_2": "Ακολούθησε τις οδηγίες εδώ για να χρησιμοποιήσεις αυτό το εργαλείο.", + "line_3": "Τα αρχεία σου θα εισηχθούν σε έγγραφα σύμφωνα με τη διαμόρφωση εισαγωγής αρχείου.", + "download": "Κατέβασε", + "instructions": "Οδηγίες" + } + }, + "feedback": { + "title": "Δώσε μας ανατροφοδότηση", + "message": "Οποιαδήποτε πρόταση ή ερώτηση σχετικά με το Teedy? Σε ακούμε!", + "sent_title": "Η ανατροφοδότηση έχει σταλεί", + "sent_message": "Ευχαριστούμε για την ανατροφοδότηση! Θα μας βοηθήσει να κάνουμε το Teedy ακόμα καλύτερο." + }, + "import": { + "title": "Εισάγει", + "error_quota": "Το όριο έχει φτάσει, επικοινώνησε με το διαχειριστή σου για να αυξήσεις το ποσοστό σου", + "error_general": "Προέκυψε ένα σφάλμα κατά την προσπάθεια εισαγωγής του αρχείου σου, παρακαλούμε βεβαιώσου πως είναι ένα έγκυρο αρχείο EML" + }, + "app_share": { + "main": "Ζήτησε ένα σύνδεσμο κοινοποιημένου εγγράφου για να λάβεις πρόσβαση", + "403": { + "title": "Μη εγκεκριμένο", + "message": "Το έγγραφο που προσπαθείς να δεις δεν είναι κοινό πλέον" + } + }, + "directive": { + "acledit": { + "acl_target": "Για", + "acl_permission": "ʼδεια", + "add_permission": "Προσθήκη άδειας", + "search_user_group": "Αναζήτηση χρήστη ή ομάδας" + }, + "auditlog": { + "log_created": "δημιουργήθηκε", + "log_updated": "ενημερώθηκε", + "log_deleted": "διαγράφηκε", + "Acl": "ACL", + "Comment": "Σχόλιο", + "Document": "Έγγραφο", + "File": "Αρχείο", + "Group": "Ομάδα", + "Route": "Ροή εργασίας", + "RouteModel": "Μοντέλο ροής εργασίας", + "Tag": "Ετικέτα", + "User": "Χρήστης", + "Webhook": "Διαδικτυακός σύνδεσμος" + }, + "selectrelation": { + "typeahead": "Συμπλήρωσε έναν τίτλο εγγράφου" + }, + "selecttag": { + "typeahead": "Συμπλήρωσε μια ετικέτα" + }, + "datepicker": { + "current": "Σήμερα", + "clear": "Καθαρισμός", + "close": "Έγινε" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Μπορεί να διαβάσει", + "READWRITE": "Μπορεί να γράψει", + "WRITE": "Μπορεί να γράψει", + "USER": "Χρήστης", + "GROUP": "Ομάδα", + "SHARE": "Κοινό" + }, + "workflow_type": { + "VALIDATE": "Επαλήθευση", + "APPROVE": "Αποδοχή" + }, + "workflow_transition": { + "APPROVED": "Εγκρίθηκε", + "REJECTED": "Απορρίφθηκε", + "VALIDATED": "Επαληθεύτηκε" + }, + "validation": { + "required": "Απαιτείται", + "too_short": "Πολύ μικρό", + "too_long": "Πολύ μεγάλο", + "email": "Πρέπει να είναι ένα έγκυρο e-mail", + "password_confirm": "Ο κωδικός και η επιβεβαίωση κωδικού πρέπει να ταιριάζουν", + "number": "Αριθμός που απαιτείται", + "no_space": "Κενά και άνω κάτω τελείες δεν επιτρέπονται", + "alphanumeric": "Μόνο γράμματα και αριθμοί επιτρέπονται" + }, + "action_type": { + "ADD_TAG": "Προσθήκη ετικέτας", + "REMOVE_TAG": "Αφαίρεση ετικέτας", + "PROCESS_FILES": "Επεξεργασία αρχείων" + }, + "pagination": { + "previous": "Προηγούμενο", + "next": "Επόμενο", + "first": "Πρώτο", + "last": "Τελευταίο" + }, + "onboarding": { + "step1": { + "title": "Όνομα;", + "description": "Αν είναι η πρώτη σου φορά στο Teedy, κάνε κλικ στο κουμπί Επόμενο, αλλιώς μπορείς να κάνεις κλείσιμο." + }, + "step2": { + "title": "Έγγραφα", + "description": "Το Teedy είναι οργανωμένο σε έγγραφα και κάθε έγγραφο περιέχει πολλαπλά αρχεία." + }, + "step3": { + "title": "Αρχεία", + "description": "Μπορείς να προσθέσεις αρχεία μετά τη δημιουργία ενός εγγράφου ή πριν τη χρήση αυτής της περιοχής γρήγορου ανεβάσματος." + }, + "step4": { + "title": "Αναζήτηση", + "description": "Αυτός είναι ο κετρνικός τρόπος να βρεις τα έγγραφα σου. Υπάρχει επίσης μια προχωρημένη αναζήτηση με το κουμπί μεγένθυσης." + }, + "step5": { + "title": "Ετικέτες", + "description": "Τα εγγραφα μπορούν να οργανωθούν σε ετικέτες (που είναι σαν σούπες φάκελοι). Δημιούργησε του εδώ." + } + }, + "yes": "Ναι", + "no": "Όχι", + "ok": "OK", + "cancel": "Ακύρωση", + "share": "Κοινοποίηση", + "unshare": "Κατάργηση κοινοποίησης", + "close": "Κλείσιμο", + "add": "Προσθήκη", + "open": "ʼνοιγμα", + "see": "Δες", + "save": "Αποθήκευση", + "export": "Εξαγωγή", + "edit": "Επεξεργασία", + "delete": "Διαγραφή", + "rename": "Μετονομασία", + "download": "Κατέβασμα", + "loading": "Φόρτωση...", + "send": "Αποστολή", + "enabled": "Ενεργοποιημένο", + "disabled": "Απενενργοποιημένο" +} diff --git a/docs-web/src/main/webapp/src/locale/en.json b/docs-web/src/main/webapp/src/locale/en.json index af7c70fe..a6f98f0e 100644 --- a/docs-web/src/main/webapp/src/locale/en.json +++ b/docs-web/src/main/webapp/src/locale/en.json @@ -3,7 +3,7 @@ "username": "Username", "password": "Password", "validation_code_required": "A validation code is required", - "validation_code_title": "You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app your configured.", + "validation_code_title": "You have activated the two-factor authentication on your account. Please enter a validation code generated by the phone app you have configured.", "validation_code": "Validation code", "remember_me": "Remember me", "submit": "Sign in", @@ -25,7 +25,7 @@ "message": "Please enter a new password", "submit": "Change my password", "error_title": "Error changing your password", - "error_message": "Your password recovery request is expired, please ask a new one on the login page" + "error_message": "Your password recovery request is expired, please ask for a new one on the login page" }, "index": { "toggle_navigation": "Toggle navigation", @@ -278,7 +278,23 @@ "menu_vocabularies": "Vocabularies", "menu_configuration": "Configuration", "menu_inbox": "Inbox scanning", + "menu_ldap": "LDAP authentication", + "menu_metadata": "Custom metadata", "menu_monitoring": "Monitoring", + "ldap": { + "title": "LDAP authentication", + "enabled": "Enable LDAP authentication", + "host": "LDAP hostname", + "port": "LDAP port (389 by default)", + "usessl": "Enable SSL (ldaps)", + "admin_dn": "Admin DN", + "admin_password": "Admin password", + "base_dn": "Base search DN", + "filter": "Search filter (must contains USERNAME, eg. \"(uid=USERNAME)\")", + "default_email": "Default email for LDAP user", + "default_storage": "Default storage for LDAP user", + "saved": "LDAP configuration saved successfully" + }, "user": { "title": "Users management", "add_user": "Add a user", @@ -345,7 +361,7 @@ "message_2": "Those applications automatically generate a validation code that changes after a certain period of time.
    You will be required to enter this validation code each time you login on {{ appName }}.", "secret_key": "Your secret key is: {{ secret }}", "secret_key_warning": "Configure your TOTP app on your phone with this secret key now, you will not be able to access it later.", - "totp_enabled_message": "Two-factor authentication is enabled on your account.
    Each time you login on {{ appName }}, you will be asked a validation code from your configured phone app.
    If you lose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key.", + "totp_enabled_message": "Two-factor authentication is enabled on your account.
    Each time you login on {{ appName }}, you will be asked for a validation code from your configured phone app.
    If you lose your phone, you will not be able to login into your account but active sessions will allow you to regenerate a secrey key.", "disable_totp": { "disable_totp": "Disable two-factor authentication", "message": "Your account will not be protected by the two-factor authentication anymore.", @@ -412,20 +428,30 @@ "webhook_create_date": "Create date", "webhook_add": "Add a webhook" }, + "metadata": { + "title": "Custom metadata configuration", + "message": "Here you can add custom metadata to your documents like an internal identifier or an expiration date. Please note that the metadata type cannot be changed after creation.", + "name": "Metadata name", + "type": "Metadata type" + }, "inbox": { "title": "Inbox scanning", "message": "By enabling this feature, the system will scan the specified inbox every minute for unread emails and automatically import them.
    After importing an email, it will be marked as read.
    Configuration settings for Gmail, Outlook.com, Yahoo.", "enabled": "Enable inbox scanning", "hostname": "IMAP hostname", "port": "IMAP port (143 or 993)", + "starttls": "Enable STARTTLS", "username": "IMAP username", "password": "IMAP password", + "folder": "IMAP folder", "tag": "Tag added to imported documents", "test": "Test the parameters", "last_sync": "Last synchronization: {{ data.date | date: 'medium' }}, {{ data.count }} message{{ data.count > 1 ? 's' : '' }} imported", "test_success": "The connection to the inbox is successful ({{ count }} unread message{{ count > 1 ? 's' : '' }})", "test_fail": "An error occurred while connecting to the inbox, please check the parameters", - "saved": "IMAP configuration saved successfully" + "saved": "IMAP configuration saved successfully", + "autoTagsEnabled": "Automatically add tags from subject line marked with #", + "deleteImported": "Delete message from mailbox after import" }, "monitoring": { "background_tasks": "Background tasks", @@ -485,7 +511,7 @@ "error_general": "An error occurred while trying to import your file, please make sure it is a valid EML file" }, "app_share": { - "main": "Ask a shared document link to access it", + "main": "Ask for a shared document link to access it", "403": { "title": "Not authorized", "message": "The document you are trying to view is not shared anymore" @@ -555,7 +581,8 @@ "email": "Must be a valid e-mail", "password_confirm": "Password and password confirmation must match", "number": "Number required", - "no_space": "Spaces are not allowed" + "no_space": "Spaces and colons are not allowed", + "alphanumeric": "Only letters and numbers are allowed" }, "action_type": { "ADD_TAG": "Add a tag", @@ -590,6 +617,8 @@ "description": "Documents can be organized in tags (which are like super-folders). Create them here." } }, + "yes": "Yes", + "no": "No", "ok": "OK", "cancel": "Cancel", "share": "Share", @@ -608,4 +637,4 @@ "send": "Send", "enabled": "Enabled", "disabled": "Disabled" -} \ No newline at end of file +} diff --git a/docs-web/src/main/webapp/src/locale/es.json b/docs-web/src/main/webapp/src/locale/es.json index b2b90acc..ee8c9163 100644 --- a/docs-web/src/main/webapp/src/locale/es.json +++ b/docs-web/src/main/webapp/src/locale/es.json @@ -416,6 +416,7 @@ "port": "Puerto IMAP (143 o 993)", "username": "Usuario IMAP", "password": "Contraseña IMAP", + "folder": "Carpeta IMAP", "tag": "Etiqueta añadida a documentos importado", "test": "Comprobar parámetros", "last_sync": "Última sincronización: {{ data.date | date: 'medium' }}, {{ data.count }} mensaje{{ data.count > 1 ? 's' : '' }} importado{{ data.count > 1 ? 's' : '' }}", diff --git a/docs-web/src/main/webapp/src/locale/fr.json b/docs-web/src/main/webapp/src/locale/fr.json index 932f287f..5feec3eb 100644 --- a/docs-web/src/main/webapp/src/locale/fr.json +++ b/docs-web/src/main/webapp/src/locale/fr.json @@ -278,7 +278,23 @@ "menu_vocabularies": "Vocabulaires", "menu_configuration": "Configuration", "menu_inbox": "Scanning de boîte de réception", + "menu_ldap": "Authentification LDAP", + "menu_metadata": "Métadonnées spécifiques", "menu_monitoring": "Monitoring", + "ldap": { + "title": "Authentification LDAP", + "enabled": "Activer l'authentication LDAP", + "host": "Hôte LDAP", + "port": "Port LDAP (389 par défaut)", + "usessl": "Activer SSL (ldaps)", + "admin_dn": "DN administrateur", + "admin_password": "Mot de passe administrateur", + "base_dn": "DN de recherche", + "filter": "Filtre de recherche (doit contenir USERNAME, cf. \"(uid=USERNAME)\")", + "default_email": "Email par défaut pour les utilisateurs LDAP", + "default_storage": "Stockage par défaut pour les utilisateurs LDAP", + "saved": "Configuration IMAP sauvegardée avec succès" + }, "user": { "title": "Gestion des utilisateurs", "add_user": "Ajouter un utilisateur", @@ -288,6 +304,8 @@ "edit": { "delete_user_title": "Supprimer un utilisateur", "delete_user_message": "Etes-vous sûr de vouloir supprimer cet utilisateur ? Tous les documents, fichiers et tags associés seront supprimés", + "user_used_title": "Utilisateur utilisé", + "user_used_message": "Cet utilisateur est utilisée dans le workflow \"{{ name }}\"", "edit_user_failed_title": "Cet utilisateur existe déjà", "edit_user_failed_message": "Ce nom d'utilisateur est déjà pris par un autre utilisateur", "edit_user_title": "Modifier \"{{ username }}\"", @@ -363,6 +381,8 @@ "delete_group_message": "Etes-vous sûr de vouloir supprimer ce groupe ?", "edit_group_failed_title": "Ce groupe existe déjà", "edit_group_failed_message": "Ce nom de groupe est déjà pris par un autre groupe", + "group_used_title": "Groupe utilisé", + "group_used_message": "Ce groupé est utilisé dans le workflow \"{{ name }}\"", "edit_group_title": "Modifier \"{{ name }}\"", "add_group_title": "Ajouter un groupe", "name": "Nom", @@ -408,20 +428,30 @@ "webhook_create_date": "Date de création", "webhook_add": "Ajouter un webhook" }, + "metadata": { + "title": "Configuration des métadonnées spécifiques", + "message": "Vous pouvez ajouter ici des métadonnées spécifiques à vos documents, comme un identifiant interne ou une date d'expiration. Veuillez notez que le type d'une métadonnée ne peut être modifié après sa création.", + "name": "Nom de métadonnée", + "type": "Type de métadonnée" + }, "inbox": { "title": "Scanning de boîte de réception", "message": "En activant cette fonctionnalité, le système scanne périodiquement la boîte de réception spécifiée pour rechercher de nouveaux messages et les importer automatiquement.
    Après l'importation d'un email, celui-ci sera marqué comme lu.
    Paramétrage pour Gmail, Outlook.com, Yahoo.", "enabled": "Activer le scan de boîte de réception", "hostname": "Nom d'hôte IMAP", "port": "Port IMAP (143 ou 993)", + "starttls": "Activer STARTTLS", "username": "Nom d'utilisateur IMAP", "password": "Mot de passe IMAP", + "folder": "Dossier IMAP", "tag": "Tag ajouté aux documents importés", "test": "Tester les paramètres", "last_sync": "Dernière synchronisation : {{ data.date | date: 'medium' }}, {{ data.count }} message{{ data.count> 1 ? 's' : '' }} importé{{ data.count> 1 ? 's' : '' }}", "test_success": "La connexion à la boîte de réception est réussie ({{ count }} message{{ count> 1 ? 's' : '' }}) non lus", "test_fail": "Une erreur est survenue lors de la connexion à la boîte de réception, veuillez vérifier les paramètres", - "saved": "Configuration IMAP sauvegardée avec succès" + "saved": "Configuration IMAP sauvegardée avec succès", + "autoTagsEnabled": "Ajouter automatiquement les tags des titres marqués par #", + "deleteImported": "Supprimer les messages de la boîte de réception après leur importation" }, "monitoring": { "background_tasks": "Tâches d'arrière-plan", @@ -551,7 +581,8 @@ "email": "Doit être une adresse e-mail valide", "password_confirm": "Le mot de passe et sa confirmation doivent être identiques", "number": "Nombre requis", - "no_space": "Les espaces ne sont pas autorisés" + "no_space": "Les espaces ne sont pas autorisés", + "alphanumeric": "Seuls les lettres et les chiffres sont autorisés" }, "action_type": { "ADD_TAG": "Ajouter un tag", @@ -586,6 +617,8 @@ "description": "Les documents peuvent être organisés en tags (qui sont comme des super-dossiers). Créez-les ici." } }, + "yes": "Oui", + "no": "Non", "ok": "OK", "cancel": "Annuler", "share": "Partager", @@ -604,4 +637,4 @@ "send": "Envoyer", "enabled": "Activé", "disabled": "Désactivé" -} \ No newline at end of file +} diff --git a/docs-web/src/main/webapp/src/locale/it.json b/docs-web/src/main/webapp/src/locale/it.json new file mode 100644 index 00000000..68681f6a --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/it.json @@ -0,0 +1,638 @@ +{ + "login": { + "username": "Nome utente", + "password": "Password", + "validation_code_required": "Codice di validazione richiesto", + "validation_code_title": "Hai attivato l'autenticazione a due fattori per il tuo account. Per favore inserisci un codice di validazione generato dall'app scelta.", + "validation_code": "Codice di validazione", + "remember_me": "Ricordami", + "submit": "Registrati", + "login_as_guest": "Accedi come ospite", + "login_failed_title": "Accesso fallito", + "login_failed_message": "Nome utente o password invalidi", + "password_lost_btn": "Password dimenticata?", + "password_lost_sent_title": "Email per reimpostare della password inviata", + "password_lost_sent_message": "Email inviata all'indirizzo {{ username }} per reimpostare la password", + "password_lost_error_title": "Errore nel reset della password", + "password_lost_error_message": "Impossibile inviare mail per reimpostare la password, per favore contatta l'amministratore per un reset manuale." + }, + "passwordlost": { + "title": "Password dimenticata", + "message": "Per favore inserisci il tuo nome utente per ricevere un link per reimpostare la password. Se non ti ricordi il nome utente, per favore contatta l'amministratore", + "submit": "Reimposta la password" + }, + "passwordreset": { + "message": "Per favore inserisci una nuova password", + "submit": "Cambia password", + "error_title": "Errore durante il cambio password", + "error_message": "La tua richiesta di cambio password è scaduta, per favore effettua una nuova richiesta nella pagina di accesso." + }, + "index": { + "toggle_navigation": "Attiva/Disattiva navigazione", + "nav_documents": "Documenti", + "nav_tags": "Tags", + "nav_users_groups": "Utenti & Gruppi", + "error_info": "{{ count }} nuov{{ count > 1 ? 'i' : 'o' }} error{{ count > 1 ? 'i' : 'e' }}", + "logged_as": "Effettuato l'accesso come {{ username }}", + "nav_settings": "Impostazioni", + "logout": "Disconnetti", + "global_quota_warning": "Attenzione! È stata quasi raggiunta la quota totale di {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) usati su {{ total | number: 0 }}MB" + }, + "document": { + "navigation_up": "Vai su di un livello", + "toggle_navigation": "Attiva/Disattiva navigazione cartella", + "display_mode_list": "Visualizza documenti in lista", + "display_mode_grid": "Visualizza documenti in griglia", + "search_simple": "Ricerca semplice", + "search_fulltext": "Ricerca intero testo", + "search_creator": "Creatore", + "search_language": "Lingua", + "search_before_date": "Creato prima di questa data", + "search_after_date": "Creato dopo questa data", + "search_before_update_date": "Aggiornato prima di questa data", + "search_after_update_date": "Aggiornato dopo questa data", + "search_tags": "Tag", + "search_shared": "Solo documenti condivisi", + "search_workflow": "Flusso di lavoro assegnato a me", + "search_clear": "Cancella", + "any_language": "Qualunque lingua", + "add_document": "Aggiungi un documento", + "import_eml": "Importa da email (formato EML)", + "tags": "Tag", + "no_tags": "Nessun tag", + "no_documents": "Nessun documento nel database", + "search": "Cerca", + "search_empty": "Nessuna corrispondenza per \"{{ search }}\"", + "shared": "Condiviso", + "current_step_name": "Passo corrente", + "title": "Titolo", + "description": "Descrizione", + "contributors": "Collaboratori", + "language": "Lingua", + "creation_date": "Data di creazione", + "subject": "Oggetto", + "identifier": "Identificativo", + "publisher": "Editore", + "format": "Formato", + "source": "Fonte", + "type": "Tipo", + "coverage": "Copertura", + "rights": "Diritti", + "relations": "Relazioni", + "page_size": "Risultati per pagina", + "page_size_10": "10 per pagina", + "page_size_20": "20 per pagina", + "page_size_30": "30 per pagina", + "upgrade_quota": "Per aggiornare la tua quota, contatta l'amministratore", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) usato su {{ total | number: 0 }}MB", + "count": "{{ count }} document{{ count > 1 ? 'i' : 'o' }} trovat{{ count > 1 ? 'i' : 'o' }}", + "last_updated": "Ultimo aggiornamento {{ date | timeAgo: dateFormat }}", + "view": { + "delete_comment_title": "Cancella commento", + "delete_comment_message": "Vuoi veramente cancellare questo commento?", + "delete_document_title": "Cancella documento", + "delete_document_message": "Vuoi veramente cancellare questo documento?", + "shared_document_title": "Documento condiviso", + "shared_document_message": "Puoi condividere il documento tramite questo link. Tieni a mente che chiunque in possesso di questo link potrà accedere al documento.
    ", + "not_found": "Documento non trovato", + "forbidden": "Accesso negato", + "download_files": "Scarica file", + "export_pdf": "Esporta in PDF", + "by_creator": "da", + "comments": "Commenti", + "no_comments": "Non ci sono ancora commenti per questo documento", + "add_comment": "Aggiungi un commento", + "error_loading_comments": "Errore nel caricamento dei commenti", + "workflow_current": "Passo corrente nel flusso di lavoro", + "workflow_comment": "Aggiungi un commento al flusso di lavoro", + "workflow_validated_title": "Passo nel flusso di lavoro validato", + "workflow_validated_message": "Il passo nel flusso di lavoro è stato correttamente validato.", + "content": { + "content": "Contenuto", + "delete_file_title": "Cancella file", + "delete_file_message": "Vuoi veramente chiudere questo file?", + "upload_pending": "In attesa...", + "upload_progress": "In caricamento...", + "upload_error": "Errore nel caricamento", + "upload_error_quota": "Quota limite raggiunta", + "drop_zone": "Trascina i file qua per il caricamento", + "add_files": "Aggiungi file", + "file_processing_indicator": "Questo file è stato processato. La ricerca non sarà disponibile finché l'operazione non sarà conclusa.", + "reprocess_file": "Processa nuovamente questo file", + "upload_new_version": "Effettua il caricamento di una nuova versione", + "open_versions": "Mostra la cronologia delle versioni", + "display_mode_list": "Mostra i file in lista", + "display_mode_grid": "Mostra i file in griglia" + }, + "workflow": { + "workflow": "Flusso di lavoro", + "message": "Verifica o valida i tuoi documenti con persone all'interno della tua organizzazione utilizzando i flussi di lavoro.", + "workflow_start_label": "Quale flusso di lavoro vuoi avviare?", + "add_more_workflow": "Aggiungi flussi di lavoro", + "start_workflow_submit": "Inizia flusso di lavoro", + "full_name": "{{ name }} iniziato il {{ create_date | date }}", + "cancel_workflow": "Annulla il flusso di lavoro corrente", + "cancel_workflow_title": "Annulla il flusso di lavoro", + "cancel_workflow_message": "Vuoi veramente annullare il flusso di lavoro corrente?", + "no_workflow": "Non è possibile avviare alcun flusso di lavoro su questo documento." + }, + "permissions": { + "permissions": "Permessi", + "message": "I permessi possono essere applicati direttamente a questo documento, o possono essere derivate dai tag.", + "title": "Permessi su questo documento", + "inherited_tags": "Permessi ereditati dai tag", + "acl_source": "Da", + "acl_target": "Per", + "acl_permission": "Permessi" + }, + "activity": { + "activity": "Attività", + "message": "Tutte le azioni sul documento sono loggate qua." + } + }, + "edit": { + "document_edited_with_errors": "Documento modificato con successo ,ma per alcuni file il caricamento non è andato a buon fine", + "document_added_with_errors": "Documento aggiunto con successo ,ma per alcuni file il caricamento non è andato a buon fine", + "quota_reached": "Raggiunta quota limite", + "primary_metadata": "Metadati primari", + "title_placeholder": "Un nome assegnato alla risorsa", + "description_placeholder": "Un conto della risorsa", + "new_files": "Nuovi file", + "orphan_files": "+ file", + "additional_metadata": "Metadati addizionali", + "subject_placeholder": "L'argomento della risorsa", + "identifier_placeholder": "Un riferimento non ambiguo alla risorsa nel dato contesto", + "publisher_placeholder": "Un'entità responsabile a rendere disponibile la risorsa", + "format_placeholder": "Il formato del file, il mezzo fisico o le dimensioni della risorsa", + "source_placeholder": "Una risorsa relativa dalla quale la risorsa descritta è derivata", + "uploading_files": "Caricamento file..." + }, + "default": { + "upload_pending": "In sospeso...", + "upload_progress": "Caricamento...", + "upload_error": "Errore durante il caricamento", + "upload_error_quota": "Quota limite raggiunta", + "quick_upload": "Caricamento veloce", + "drop_zone": "Trascina i file qui per il caricamento", + "add_files": "Aggiungi file", + "add_new_document": "Aggiungi nuovo documento", + "latest_activity": "Ultima attività", + "footer_sismics": "Realizzato con by Sismics", + "api_documentation": "Documentazione API", + "feedback": "Dacci un feedback", + "workflow_document_list": "Documenti assegnati a te", + "select_all": "Seleziona tutti", + "select_none": "Seleziona nessuno" + }, + "pdf": { + "export_title": "Esporta in PDF", + "export_metadata": "Esporta metadati", + "export_comments": "Esporta commenti", + "fit_to_page": "Adatta immagine alla pagina", + "margin": "Margine", + "millimeter": "mm" + }, + "share": { + "title": "Condividi documento", + "message": "Indica la condivisione se vuoi condividere più volte lo stesso documento.", + "submit": "Condividi" + } + }, + "file": { + "view": { + "previous": "Precedente", + "next": "Successivo", + "not_found": "File non trovato" + }, + "edit": { + "title": "Modifica file", + "name": "Nome file" + }, + "versions": { + "title": "Cronologia delle versioni", + "filename": "Nome file", + "mimetype": "Tipo", + "create_date": "Data di creazione", + "version": "Versione" + } + }, + "tag": { + "new_tag": "Nuovo tag", + "search": "Cerca", + "default": { + "title": "Tag", + "message_1": "I Tag sono etichette associate ai documenti.", + "message_2": "Un documento può essere associato a più tag e un tag può essere applicato a più documenti.", + "message_3": "Usando il pulsante , puoi modificare i permessi di un tag.", + "message_4": "Se un tag può essere letto da un altro utente o gruppo, anche i documenti associati possono essere letti dalle stesse persone.", + "message_5": "Per esempio, tagga i documenti della tua azienda con il tag MiaAzienda ed aggiungi i permessi di lettura Può leggere al gruppo impiegati" + }, + "edit": { + "delete_tag_title": "Cancella tag", + "delete_tag_message": "Vuoi veramente cancellare questo tag?", + "name": "Nome", + "color": "Colore", + "parent": "Padre", + "info": "I permessi su questo tag saranno anche applicati ai documenti taggati con {{ name }}", + "circular_reference_title": "Referenza circolare", + "circular_reference_message": "La gerarchia dei tag padri genera un ciclo, per favore scegli un altro padre." + } + }, + "group": { + "profile": { + "members": "Membri", + "no_members": "Nessun membro", + "related_links": "Link collegati", + "edit_group": "Modifica il gruppo {{ name }}" + } + }, + "user": { + "profile": { + "groups": "Gruppi", + "quota_used": "Quota usata", + "percent_used": "{{ percent | number: 0 }}% Usata", + "related_links": "Link collegati", + "document_created": "Documenti creati da {{ username }}", + "edit_user": "Modifica utente {{ username }}" + } + }, + "usergroup": { + "search_groups": "Cerca tra i gruppi", + "search_users": "Cerca tra gli utenti", + "you": "Sei tu!", + "default": { + "title": "Utenti e Gruppi", + "message": "Qua puoi trovare informazioni su utenti e gruppi." + } + }, + "settings": { + "menu_personal_settings": "Impostazioni personali", + "menu_user_account": "Profilo utente", + "menu_two_factor_auth": "Autenticazione a due fattori", + "menu_opened_sessions": "Sessioni aperte", + "menu_file_importer": "Importazione massiva dei file", + "menu_general_settings": "Impostazioni generali", + "menu_workflow": "Flusso di lavoro", + "menu_users": "Utenti", + "menu_groups": "Gruppi", + "menu_vocabularies": "Vocabolari", + "menu_configuration": "Configurazioni", + "menu_inbox": "Scansione posta in arrivo", + "menu_ldap": "Autenticazione LDAP", + "menu_metadata": "Metadati personalizzati", + "menu_monitoring": "Monitoraggio", + "ldap": { + "title": "Autenticazione LDAP", + "enabled": "Abilita autenticazione LDAP", + "host": "hostname LDAP", + "port": "porta LDAP (389 di default)", + "admin_dn": "DN amministratore", + "admin_password": "Password amministratore", + "base_dn": "Ricerca base DN", + "filter": "Filtro di ricerca (deve contenere USERNAME, es. \"(uid=USERNAME)\")", + "default_email": "Email di default per utenti LDAP", + "default_storage": "Archiviazione di default per utenti LDAP", + "saved": "Configurazione LDAP salvata con successo" + }, + "user": { + "title": "Gestione utenti", + "add_user": "Aggiungi utente", + "username": "Nome utente", + "create_date": "Data di creazione", + "totp_enabled": "Autenticazione a due fattori abilitata per questo profilo", + "edit": { + "delete_user_title": "Cancella utente", + "delete_user_message": "Vuoi veramente cancellare questo utente? Tutti i documenti, file e tag associati saranno eliminati", + "user_used_title": "Utente in uso", + "user_used_message": "Questo utente è usato nel flusso di lavoro \"{{ name }}\"", + "edit_user_failed_title": "Utente già esistente", + "edit_user_failed_message": "Questo nome utente è già stato utilizzato da un altro utente", + "edit_user_title": "Modifica \"{{ username }}\"", + "add_user_title": "Aggiungi un utente", + "username": "Nome utente", + "email": "E-mail", + "groups": "Grouppi", + "storage_quota": "Quota di archiviazione", + "storage_quota_placeholder": "Quota di archiviazione (in MB)", + "password": "Password", + "password_confirm": "Password (conferma)", + "disabled": "Disabilita utente", + "password_reset_btn": "Invia un'email per reimpostare la password a questo utente", + "password_lost_sent_title": "Email per reimpostare la password inviata", + "password_lost_sent_message": "Una email per reimpostare la password è stata inviata a {{ username }}", + "disable_totp_btn": "Disabilita l'autenticazione a due fattori per questo utente", + "disable_totp_title": "Disabilita l'autenticazione a due fattori", + "disable_totp_message": "Sei sicuro di voler disabilitare l'autenticazione a due fattori per questo utente?" + } + }, + "workflow": { + "title": "Configurazione del flusso di lavoro", + "add_workflow": "Aggiungi un flusso di lavoro", + "name": "Nome", + "create_date": "Data di creazione", + "edit": { + "delete_workflow_title": "Elimina flusso di lavoro", + "delete_workflow_message": "Vuoi veramente eliminare questo flusso di lavoro? I flussi di lavoro al momento in uso non saranno eliminati", + "edit_workflow_title": "Modifica \"{{ name }}\"", + "add_workflow_title": "Aggiungi un flusso di lavoro", + "name": "Nome", + "name_placeholder": "Nome o descrizione del passo", + "drag_help": "Trascina per riordinare il passo", + "type": "Tipo di stadio di avanzamento", + "type_approve": "Approva", + "type_validate": "Valida", + "target": "Assegnato a", + "target_help": "Approva: Accetta o respingi la revisione
    Valida: Rivedi e continua il flusso di lavoro", + "add_step": "Aggiungi uno passo nel flusso di lavoro", + "actions": "Cosa succede dopo?", + "remove_action": "Rimuovi azione", + "acl_info": "Solo gli utenti e i gruppi definiti qua saranno abilitati ad iniziare questo flusso di lavoro su un documento" + } + }, + "security": { + "enable_totp": "Abilita autenticazione a due fattori", + "enable_totp_message": "Assicurati di avere un'applicazione compatibile con TOTP sul tuo smartphone in grado di aggiungere un nuovo profilo", + "title": "Autenticazione a due fattori", + "message_1": "L'autenticazione a due fattori ti permette di aggiungere un livello di sicurezza al tuo profilo {{ appName }} .
    Prima di attivare questa funzionalità, assicurati di avere un'applicazione compatibile con TOTP sul tuo smartphone:", + "message_google_authenticator": "Per Android, iOS, e Blackberry: Google Authenticator", + "message_duo_mobile": "Per Android ed iOS: Duo Mobile", + "message_authenticator": "Per Windows Phone: Authenticator", + "message_2": "Queste applicazioni generano automaticamente un codice di validazione che cambia dopo un certo periodo di tempo.
    Ti sarà richiesto di inserire questo codice ogni volta che effettuerai l'accesso ad {{ appName }}.", + "secret_key": "La tua chiave segreta è: {{ secret }}", + "secret_key_warning": "Configura la tua applicazione TOTP sul tuo smartphone con questa chiave segreta ora, non sarai in grado di farlo in futuro.", + "totp_enabled_message": "L'autenticazione a due fattori è abilitata sul tuo profilo.
    Ogni volta che effettui l'accesso ad {{ appName }}, ti sarà richiesto un codice di validazione dall'app configurata sul tuo smartphone.
    Se perdi il tuo smartphone, non sarai in grado di effettuare l'accesso, ma le tue sessioni attive ti permetteranno di rigenerare una chiave segreta.", + "disable_totp": { + "disable_totp": "Disabilita autenticazione a due fattori", + "message": "Il tuo profilo non sarà protetto più dall'autenticazione a due fattori.", + "confirm_password": "Conferma password", + "submit": "Disabilita autenticazione a due fattori" + }, + "test_totp": "Per favore inserisci il codice di validazione mostrato sul tuo smartphone:", + "test_code_success": "Codice di validazione OK", + "test_code_fail": "Questo codice non è valido, per favore controlla nuovamente che il tuo smartphone è correttamente configurato oppure disabilita l'autenticazione a due fattori" + }, + "group": { + "title": "Gestione gruppi", + "add_group": "Aggiungi un gruppo", + "name": "Nome", + "edit": { + "delete_group_title": "Elimina gruppo", + "delete_group_message": "Vuoi veramente eliminare questo gruppo?", + "edit_group_failed_title": "Il gruppo è già presente", + "edit_group_failed_message": "Il nome di questo gruppo è già usato da un altro gruppo", + "group_used_title": "Gruppo in uso", + "group_used_message": "Questo gruppo è usato nel flusso di lavoro \"{{ name }}\"", + "edit_group_title": "Modifica \"{{ name }}\"", + "add_group_title": "Aggiungi un gruppo", + "name": "Nome", + "parent_group": "Gruppo padre", + "search_group": "Cerca un gruppo", + "members": "Membri", + "new_member": "Nuovo membro", + "search_user": "Cerca un utente" + } + }, + "account": { + "title": "Profilo utente", + "password": "Password", + "password_confirm": "Password (conferma)", + "updated": "Profilo aggiornato con successo" + }, + "config": { + "title_guest_access": "Accesso come ospite", + "message_guest_access": "L'accesso come ospite è una modalità nella quale chiunque può accedere {{ appName }} senza password.
    Come un normale utente, l'utente ospite può accedere solo ai suoi documenti e a quelli accessibili tramite permessi.
    ", + "enable_guest_access": "Abilita accesso ospite", + "disable_guest_access": "Disabilita accesso ospite", + "title_theme": "Personalizzazione tema", + "title_general": "Configurazione generale", + "default_language": "Lingua predefinita per i nuovi documenti", + "application_name": "Nome dell'applicazione", + "main_color": "Colore principale", + "custom_css": "CSS personalizzato", + "custom_css_placeholder": "CSS personalizzato da aggiungere dopo il foglio dei temi principale", + "logo": "Logo (di forma quadrata)", + "background_image": "Immaggine di sfondo", + "uploading_image": "Caricamento immagine...", + "title_smtp": "Configurazione email", + "smtp_hostname": "Hostname SMTP", + "smtp_port": "porta SMTP", + "smtp_from": "Email mittente", + "smtp_username": "Nome utente SMTP", + "smtp_password": "Password SMTP", + "smtp_updated": "Configurazione SMTP aggiornata con successo", + "webhooks": "Webhooks", + "webhooks_explain": "I webhooks verranno chiamati quando si verifica un evento specifico. L'URL fornito verrà invocato tramite POST con un payload JSON contenente il nome dell'evento e l'ID della risorsa d'interesse.", + "webhook_event": "Evento", + "webhook_url": "URL", + "webhook_create_date": "Data di creazione", + "webhook_add": "Aggiungi un webhook" + }, + "metadata": { + "title": "Configurazione dei metadati personalizzati", + "message": "Qua puoi aggiungere metadati personalizzati ai tuoi documenti come un identificativo interno o una data di scadenza. Ricorda che i tipi dei metadati non possono essere cambiati dopo la creazione.", + "name": "Nome del metadato", + "type": "Tipo del metadato" + }, + "inbox": { + "title": "Scansione posta in ingresso", + "message": "Abilitando questa funzionalità, il sistema scansionerà la casella specifica ogni minuto per trovare email non lette ed importarle automaticamente.
    Dopo aver importato una email, verrà segnata come letta.
    Impostazioni di configurazione per Gmail, Outlook.com, Yahoo.", + "enabled": "Abilita scansione posta in ingresso", + "hostname": "Hostname IMAP", + "port": "Porta IMAP (143 o 993)", + "username": "Nome utente IMAP", + "password": "Password IMAP", + "folder": "Cartella IMAP", + "tag": "Tag aggiunti a documenti importati", + "test": "Testa i parametri", + "last_sync": "Ultima sincronizzazione: {{ data.date | date: 'medium' }}, {{ data.count }} messaggi{{ data.count > 1 ? '' : 'o' }} importat{{ data.count > 1 ? 'i' : 'o' }}", + "test_success": "Connessione alla casella di posta effettuata con successo ({{ count }} messaggi{{ data.count > 1 ? '' : 'o' }} non lett{{ data.count > 1 ? 'i' : 'o' }})", + "test_fail": "Si è verificato un errore durante la connessione alla casella di posta, per favore controlla i parametri", + "saved": "Configurazione IMAP salvata con successo", + "autoTagsEnabled": "Aggiungi automaticamente tag da oggetti segnati con #", + "deleteImported": "Cancella i messaggi dalla casella postale dopo l'importazione" + }, + "monitoring": { + "background_tasks": "Task di background", + "queued_tasks": "Al momento ci sono {{ count }} task accodati.", + "queued_tasks_explain": "Processamento file, creazione minuature, aggiornamento indici, riconoscimento ottico dei caratteri sono tutti task di background. Un grande numero di task non processati risulterà in una ricerca dai risultati incompleti.", + "server_logs": "Log del server", + "log_date": "Data", + "log_tag": "Tag", + "log_message": "Messaggio", + "indexing": "Indicizzazione", + "indexing_info": "Se noti discrepanze nei risultati di ricerca, puoi provare ad eseguire una reindicizzazione completa. I risultati di ricerca saranno incompleti finché quest'operazione non sarà conclusa.", + "start_reindexing": "Inizia reindicizzazione completa", + "reindexing_started": "Reindicizzazione avviata, si prega di attendere finché tutti i task di background non saranno completati." + }, + "session": { + "title": "Sessioni aperte", + "created_date": "Data di creazione", + "last_connection_date": "Data dell'ultima connessione", + "user_agent": "Da", + "current": "Corrente", + "current_session": "Questa è la sessione corrente", + "clear_message": "Tutti gli altri dispositivi connessi a questo profilo verranno disconnessi", + "clear": "Elimina tutte le altre sessioni" + }, + "vocabulary": { + "title": "Voci del vocabolario", + "choose_vocabulary": "Scegli un vocabolario da modificare", + "type": "Tipo", + "coverage": "Copertura", + "rights": "Diritti", + "value": "Valore", + "order": "Ordine", + "new_entry": "Nuova voce" + }, + "fileimporter": { + "title": "Importazione massiva dei file", + "advanced_users": "Per utenti avanzati!", + "need_intro": "Se hai bisogno di:", + "need_1": "Importare una cartella di file in un colpo solo", + "need_2": "Scansionare una cartella alla ricerca di nuovi file ed importarli", + "line_1": "Visita sismics/docs/releases e scarica lo strumento di importazione file adatto al tuo sistema.", + "line_2": "Segui le istruzioni per usare questo strumento.", + "line_3": "I tuoi file verranno importati tra i documenti secondo le configurazioni dello strumento di importazione.", + "download": "Scarica", + "instructions": "Istruzioni" + } + }, + "feedback": { + "title": "Lascia un feedback", + "message": "Hai qualche suggerimento o domanda su Teedy? Ti ascoltiamo!", + "sent_title": "Feedback inviato", + "sent_message": "Grazie per il feedback! Ci aiuterà a migliorare Teedy sempre di più." + }, + "import": { + "title": "Importazione", + "error_quota": "Quota limite raggiunta, contatta l'amministratore per incrementare la quota", + "error_general": "Si è verificato un errore cercando di importare il file, per favore assicurati che si tratti di un file EML valido" + }, + "app_share": { + "main": "Richiedi un link al documento condiviso per accedergli", + "403": { + "title": "Non autorizzato", + "message": "Il documento che stai cercando di vedere non è più condiviso" + } + }, + "directive": { + "acledit": { + "acl_target": "Per", + "acl_permission": "Permesso", + "add_permission": "Aggiungi un permesso", + "search_user_group": "Cerca un utente o un gruppo" + }, + "auditlog": { + "log_created": "creato", + "log_updated": "aggiornato", + "log_deleted": "cancellato", + "Acl": "ACL", + "Comment": "Commento", + "Document": "Documento", + "File": "File", + "Group": "Gruppo", + "Route": "Flusso di lavoro", + "RouteModel": "Modello del flusso di lavoro", + "Tag": "Tag", + "User": "Utente", + "Webhook": "Webhook" + }, + "selectrelation": { + "typeahead": "Inserisci il titolo di un documento" + }, + "selecttag": { + "typeahead": "Inserisci un tag" + }, + "datepicker": { + "current": "Oggi", + "clear": "Cancella", + "close": "Chiudi" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Può leggere", + "READWRITE": "Può scrivere", + "WRITE": "Può scrivere", + "USER": "Utente", + "GROUP": "Gruppo", + "SHARE": "Condiviso" + }, + "workflow_type": { + "VALIDATE": "Validazione", + "APPROVE": "Approvazione" + }, + "workflow_transition": { + "APPROVED": "Approvato", + "REJECTED": "Respinto", + "VALIDATED": "Validato" + }, + "validation": { + "required": "Obbligatorio", + "too_short": "Troppo corto", + "too_long": "Troppo lungo", + "email": "Dev'essere un'e-mail valida", + "password_confirm": "Le due password devono coincidere", + "number": "Numero richiesto", + "no_space": "Gli spazi e i due punti non sono ammessi", + "alphanumeric": "Sono ammesse solo lettere e numeri" + }, + "action_type": { + "ADD_TAG": "Aggiungi un tag", + "REMOVE_TAG": "Rimuovi un tag", + "PROCESS_FILES": "Processa file" + }, + "pagination": { + "previous": "Precedente", + "next": "Successiva", + "first": "Prima", + "last": "Ultima" + }, + "onboarding": { + "step1": { + "title": "Prima volta?", + "description": "Se è la tua prima volta su Teedy, premi il pulsante Successiva, altrimenti sentiti libero di chiudermi." + }, + "step2": { + "title": "Documenti", + "description": "Teedy è organizzato in documenti e ogni documento contiene più file." + }, + "step3": { + "title": "File", + "description": "Puoi aggiungere dei file dopo aver creato un documento oppure prima utilizzando questa zona di caricamento veloce." + }, + "step4": { + "title": "Ricerca", + "description": "Questo è il modo principale di cercare i tuoi documenti. È anche presente una ricerca avanzata tramite il pulsante con la lente d'ingrandimento." + }, + "step5": { + "title": "Tag", + "description": "I documenti possono essere organizzati in tag (che sono come delle cartelle). Puoi crearli qua." + } + }, + "yes": "Sì", + "no": "No", + "ok": "OK", + "cancel": "Annulla", + "share": "Condividi", + "unshare": "Elimina condivisione", + "close": "Chiudi", + "add": "Aggiungi", + "open": "Apri", + "see": "Vedi", + "save": "Salva", + "export": "Esporta", + "edit": "Modifica", + "delete": "Cancella", + "rename": "Rinomina", + "download": "Scarica", + "loading": "Caricamento...", + "send": "Invia", + "enabled": "Abilitato", + "disabled": "Disabilitato" +} diff --git a/docs-web/src/main/webapp/src/locale/pl.json b/docs-web/src/main/webapp/src/locale/pl.json new file mode 100644 index 00000000..affb7cb6 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/pl.json @@ -0,0 +1,638 @@ +{ + "login": { + "username": "Użytkownik", + "password": "Hasło", + "validation_code_required": "Kod weryfikacyjny jest wymagany", + "validation_code_title": "Masz aktywowane uwierzytelnienie dwuskładnikowe swojego konta. Podaj kod weryfikacyjny wygenerowany w aplikacji autoryzującej.", + "validation_code": "Kod weryfikacyjny", + "remember_me": "Zapamiętaj mnie", + "submit": "Zarejestruj się", + "login_as_guest": "Zaloguj jako gość", + "login_failed_title": "Logowanie nieudane", + "login_failed_message": "Nieprawidłowy użytkownik lub hasło", + "password_lost_btn": "Zgubułeś hasło?", + "password_lost_sent_title": "E-mail z instrukcją zmiany hasła został wysłany", + "password_lost_sent_message": "Na adres e-mail użytkownika {{ username }} została wysłane instrukcja zmiany hasła", + "password_lost_error_title": "Błąd zmiany hasła", + "password_lost_error_message": "Nie można wysłać instrukcji zmiany hasła, proszę skontaktować się z administratorem, aby zmienić hasło ręcznie" + }, + "passwordlost": { + "title": "Zapomniane hasło", + "message": "Proszę podać nazwę użytkownika aby otrzymać link zmiany hasła. Jesli nie pamiętasz nazwy swojego konta, skontaktuj się ze swoim administratorem", + "submit": "Zresetuj moje hasło" + }, + "passwordreset": { + "message": "Proszę podać nowe hasło", + "submit": "Ustaw hasło", + "error_title": "Nie udało się zmienić twojego hasła", + "error_message": "Twoje żądanie odzyskania hasła wygasło, proszę ponownie wygenerować żądanie na stronie logowania" + }, + "index": { + "toggle_navigation": "Przełącz nawigację", + "nav_documents": "Dokumenty", + "nav_tags": "Etykiety", + "nav_users_groups": "Użytkownicy i Grupy", + "error_info": "{{ count }} nowe błędy", + "logged_as": "Zalogowany jako {{ username }}", + "nav_settings": "Ustawienia", + "logout": "Wyloguj", + "global_quota_warning": "Ostrzeżenie! Zajęta przestrzeń dyskowa osiągnęła {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) z dostępnych {{ total | number: 0 }}MB" + }, + "document": { + "navigation_up": "Skocz do wyższego poziomu", + "toggle_navigation": "Przełącz nawigację folderu", + "display_mode_list": "Wyświetl dokumenty jako listę", + "display_mode_grid": "Wyświetl dokumenty jako siatkę", + "search_simple": "Proste wyszukiwanie", + "search_fulltext": "Wyszukiwanie pełnotekstowe", + "search_creator": "Twórca", + "search_language": "Język", + "search_before_date": "Utworzone przed datą", + "search_after_date": "Utworzone po dacie", + "search_before_update_date": "Zmienione przed datą", + "search_after_update_date": "Zmienione po dacie", + "search_tags": "Etykiety", + "search_shared": "Tylko udostępnione dokumenty", + "search_workflow": "Przepływ przypisany do mnie", + "search_clear": "Czyść", + "any_language": "Dowolny język", + "add_document": "Dodaj dokument", + "import_eml": "Importuj z wiadomości e-mail (format EML)", + "tags": "Etykiety", + "no_tags": "Brak etykiet", + "no_documents": "Brak dokumentów w bazie", + "search": "Szukaj", + "search_empty": "Nie znaleziono wyników dla \"{{ search }}\"", + "shared": "Udostępniony", + "current_step_name": "Bieżący krok", + "title": "Tytuł", + "description": "Opis", + "contributors": "Współtwórcy", + "language": "Język", + "creation_date": "Data utworzenia", + "subject": "Temat", + "identifier": "Identyfikator", + "publisher": "Publikujący", + "format": "Format", + "source": "Źródło", + "type": "Rodzaj", + "coverage": "Okładka", + "rights": "Prawa", + "relations": "Powiązania", + "page_size": "Rozmiar strony", + "page_size_10": "10 na stronę", + "page_size_20": "20 na stronę", + "page_size_30": "30 na stronę", + "upgrade_quota": "Aby zwiększyć twój limit, zapytaj swojego administratora", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) z dostępnych {{ total | number: 0 }}MB", + "count": "{{ count }} dokument{{ count > 1 ? 'ów' : '' }} znaleziony", + "last_updated": "Ostatnio zmieniony {{ date | timeAgo: dateFormat }}", + "view": { + "delete_comment_title": "Usuń komentarz", + "delete_comment_message": "Naprawdę chcesz usunąć ten komentarz?", + "delete_document_title": "Usuń dokument", + "delete_document_message": "Naprawdę chcesz usunąć ten dokument?", + "shared_document_title": "Udostępniony dokument", + "shared_document_message": "Możesz udostępnić ten dokument, podając ten link. Pamiętaj, że każdy, kto ma ten link, może zobaczyć dokument.
    ", + "not_found": "Nie znaleziono dokumentu", + "forbidden": "Odmowa dostępu", + "download_files": "Pobierz pliki", + "export_pdf": "Eksportuj do PDF", + "by_creator": "przez", + "comments": "Komentarze", + "no_comments": "Brak komentarzy do tego dokumentu", + "add_comment": "Dodaj komentarz", + "error_loading_comments": "Błąd ładowania komentarzy", + "workflow_current": "Bieżący etap przepływu", + "workflow_comment": "Dodaj komentarz do przepływu", + "workflow_validated_title": "Etap przepływu sprawdzony", + "workflow_validated_message": "Etap przepływu został zweryfikowany poprawnie.", + "content": { + "content": "Treść", + "delete_file_title": "Usuń plik", + "delete_file_message": "Naprawdę chcesz usunąć ten plik?", + "upload_pending": "Oczekuje...", + "upload_progress": "Przesyłam...", + "upload_error": "Błąd przesyłania", + "upload_error_quota": "Przekroczony limit", + "drop_zone": "Przeciągnij i upuść tutaj, aby przesłać", + "add_files": "Dodaj pliki", + "file_processing_indicator": "Ten plik jest właśnie przetwarzany. Wyszukiwanie nie jest dostępne zanim się nie zakończy.", + "reprocess_file": "Przetwórz plik ponownie", + "upload_new_version": "Prześlij nową wersję", + "open_versions": "Pokaż historię wersji", + "display_mode_list": "Wyświetl pliki jako listę", + "display_mode_grid": "Wyświetl pliki jako siatkę" + }, + "workflow": { + "workflow": "Przepływ", + "message": "Zweryfikuj lub zaakceptuj dokument przez osoby z organizacji używając przepływów.", + "workflow_start_label": "Który przepływ uruchomić?", + "add_more_workflow": "Dodaj więcej przepływów", + "start_workflow_submit": "Uruchom przepływ", + "full_name": "{{ name }} uruchomiony {{ create_date | date }}", + "cancel_workflow": "Anuluj bieżący przepływ", + "cancel_workflow_title": "Anuluj przepływ", + "cancel_workflow_message": "Naprawdę chcesz anulować bieżący przepływ?", + "no_workflow": "Nie możesz uruchomoć żadnego przepływu dla tego dokumentu." + }, + "permissions": { + "permissions": "Uprawnienia", + "message": "Uprawnienia mogą być zastosowane do tego dokumentu lub z etykietą.", + "title": "Uprawnienia do tego dokumentu", + "inherited_tags": "Uprawnienia dziedziczone z etykiet", + "acl_source": "Od", + "acl_target": "Do", + "acl_permission": "Uprawnienie" + }, + "activity": { + "activity": "Aktywność", + "message": "Każda akcja na tym dokumencie jest logowana tutaj." + } + }, + "edit": { + "document_edited_with_errors": "Dokument prawidłowo edytowany ale niektóre pliki nie zostały przesłane", + "document_added_with_errors": "Dokument prawidłowo dodany, ale niektóre pliki nie mogą zostać przesłane", + "quota_reached": "Przekroczony dostępny limit", + "primary_metadata": "Podstawowe metadane", + "title_placeholder": "Nazwa tego zasobu", + "description_placeholder": "Konto zasobu", + "new_files": "Nowe pliki", + "orphan_files": "+ {{ count }} plik{{ count > 1 ? 'i' : '' }}", + "additional_metadata": "Dodatkowe metadane", + "subject_placeholder": "Temat zasobu", + "identifier_placeholder": "Niejednoznaczne odniesienie do zasobu w danym kontekście", + "publisher_placeholder": "Podmiot odpowiedzialny za udostępnienie zasobu", + "format_placeholder": "Format pliku, nośnik fizyczny lub wymiary zasobu", + "source_placeholder": "Powiązany zasób, z którego pochodzi opisany zasób", + "uploading_files": "Przesyłanie plików..." + }, + "default": { + "upload_pending": "Oczekiwanie...", + "upload_progress": "Przesyłam...", + "upload_error": "Błąd przesyłania", + "upload_error_quota": "Przekroczony limit pojemności", + "quick_upload": "Szybkie przesyłanie", + "drop_zone": "Przeciągnij i upuść tutaj, aby przesłać", + "add_files": "Dodaj pliki", + "add_new_document": "Dodaj do nowego dokumentu", + "latest_activity": "Ostatnie aktywności", + "footer_sismics": "Wykonane z przez Sismics", + "api_documentation": "Dokumentacja API", + "feedback": "Przeslij uwagi", + "workflow_document_list": "Dokumenty przypisane do ciebie", + "select_all": "Zaznacz wszystko", + "select_none": "Odznacz wszystko" + }, + "pdf": { + "export_title": "Eksportuj do PDF", + "export_metadata": "Eksport metadanych", + "export_comments": "Eksport komentarzy", + "fit_to_page": "Dostosuj obraz do strony", + "margin": "Margines", + "millimeter": "mm" + }, + "share": { + "title": "Udostępnij dokument", + "message": "Nazwa udostępnienie, jeśli chcesz udostępnić wiele razy ten sam dokument.", + "submit": "Udostępnij" + } + }, + "file": { + "view": { + "previous": "Poprzedni", + "next": "Nastepny", + "not_found": "Plik nie znaleziony" + }, + "edit": { + "title": "Edytuj plik", + "name": "Nazwa pliku" + }, + "versions": { + "title": "Historia wersji", + "filename": "Nazwa pliku", + "mimetype": "Rodzaj", + "create_date": "Data utworzenia", + "version": "Wersja" + } + }, + "tag": { + "new_tag": "Nowa etykieta", + "search": "Znajdź", + "default": { + "title": "Etykiety", + "message_1": "Etykiety przypisane do dokumentów.", + "message_2": "Dokument może posiadać wiele etykiet oraz etykieta może być przypisana do wielu dokumentów.", + "message_3": "Użyj przycisku, aby edytować uprawnienia do etykiety.", + "message_4": "Jeśli etykieta może być odczytywana przez innego użytkownika lub grupę, powiązanie dokumenty mogą być również odczytywane przez tych użytkowników.", + "message_5": "Dla przykładu, oznacz dokumenty firmowe etykietów MojaOrganizacja i dodaj uprawnienia Odczyt dla grupy pracownicy" + }, + "edit": { + "delete_tag_title": "Usuń etykietę", + "delete_tag_message": "Naprawdę chcesz usunąć tą etykietę?", + "name": "Nazwa", + "color": "Kolor", + "parent": "nadrzędny", + "info": "Uprawnienia do tej etykiety zostaną zastosowane do dokumentów oznaczonych {{ name }}", + "circular_reference_title": "Zapętlone odwołanie", + "circular_reference_message": "Hierarchia nadrzędnej etykiety tworzy pętlę, proszę wybrać innego rodzica." + } + }, + "group": { + "profile": { + "members": "Członkowie", + "no_members": "Brak członków", + "related_links": "Powiązane linki", + "edit_group": "Edytuj grupę {{ name }}" + } + }, + "user": { + "profile": { + "groups": "Grupy", + "quota_used": "Wykorzystany limit", + "percent_used": "{{ percent | number: 0 }}% w użyciu", + "related_links": "Powiązane linki", + "document_created": "Dokumenty utworzone przez {{ username }}", + "edit_user": "Edytuj użytkownika {{ username }}" + } + }, + "usergroup": { + "search_groups": "Szukaj w grupach", + "search_users": "Szukaj w użytkownikach", + "you": "To ty!", + "default": { + "title": "Użytkownicy i grupy", + "message": "Tutaj dowiesz się o użytkownikach i grupach." + } + }, + "settings": { + "menu_personal_settings": "Ustawienia prywatności", + "menu_user_account": "Konto użytkownika", + "menu_two_factor_auth": "Uwierzytelnienie dwuskładnikowe (M2F)", + "menu_opened_sessions": "Otwarte sesje", + "menu_file_importer": "Wsadowy import plików", + "menu_general_settings": "Ustawienia główne", + "menu_workflow": "Przepływ", + "menu_users": "Użytkownicy", + "menu_groups": "Grupy", + "menu_vocabularies": "Słowniki", + "menu_configuration": "Konfiguracja", + "menu_inbox": "Skanowanie skrzynki wejściowej", + "menu_ldap": "Uwierzytelnienie LDAP", + "menu_metadata": "Niestandardowe metadane", + "menu_monitoring": "Monitorowanie", + "ldap": { + "title": "Uwierzytelnienie LDAP", + "enabled": "Włącz uwierzytelnienie LDAP", + "host": "Adres (host) LDAP", + "port": "Port LDAP (domyślnie 389)", + "admin_dn": "Admin DN", + "admin_password": "Admin password", + "base_dn": "Podstawowy DN wyszukiwania", + "filter": "Filtr wyszukiwania (musi zawierać określenie USERNAME, np. \"(uid=USERNAME)\")", + "default_email": "Domyślny e-mail dla użytkowników LDAP", + "default_storage": "Domyślny magazyn dla użytkowników LDAP", + "saved": "LDAP configuration saved successfully" + }, + "user": { + "title": "Zarządzanie użytkownikami", + "add_user": "Dodaj użytkownika", + "username": "Nazwa użytkownika", + "create_date": "Data utowrzenia", + "totp_enabled": "Uwierzytelnienie dwuskładnikowe jest włączone dla tego konta", + "edit": { + "delete_user_title": "Usuń użytkownika", + "delete_user_message": "Naprawdę chcesz usunąć tego użytkownika? Wszystkie powiązane dokumenty, pliki oraz etykiety zostaną usunięte", + "user_used_title": "Użytkownicy w użyciu", + "user_used_message": "Ten użytkownik jest użyty w przepływie \"{{ name }}\"", + "edit_user_failed_title": "Użytkownik już istnieje", + "edit_user_failed_message": "Nazwa użytkownika jest już używana", + "edit_user_title": "Edytuj \"{{ username }}\"", + "add_user_title": "Dodaj użytkownika", + "username": "nazwa użytkownika", + "email": "E-mail", + "groups": "Grupy", + "storage_quota": "Limit rozmiaru magazynu", + "storage_quota_placeholder": "Limit rozmiaru (w MB)", + "password": "Hasło", + "password_confirm": "hasło (powtórzenie)", + "disabled": "Użytkownik wyłączony", + "password_reset_btn": "Wyśli wiadomość ze zmianą hasła do użytkownika", + "password_lost_sent_title": "Wiadomość ze zmianą hasła została wysłana", + "password_lost_sent_message": "Wiadomość z instrukcją zmiany hasła została wysłana do {{ username }}", + "disable_totp_btn": "Wyłącz uwierzytelnienie dwuskładnikowe dla tego użytkownika", + "disable_totp_title": "Wyłącz uwierzytelnienie dwuskładnikowe", + "disable_totp_message": "Jesteś pewien, że chcesz wyłączyć uwierzytelnienie wduskładnikowe dla tego użytkownika?" + } + }, + "workflow": { + "title": "Konfiguracja przepływu", + "add_workflow": "Dodaj przepływ", + "name": "Nazwa", + "create_date": "Data utworzenia", + "edit": { + "delete_workflow_title": "Usuń przepływ", + "delete_workflow_message": "Naprawdę chcesz usunąć ten przepływ? Uruchomione przepływy nie mogą zostać usunięte", + "edit_workflow_title": "Edytuj \"{{ name }}\"", + "add_workflow_title": "Dodaj przepływ", + "name": "Nazwa", + "name_placeholder": "Nazwa kroku lub opis", + "drag_help": "Przeciągnij i upuść aby, zmienić kolejność", + "type": "Typ kroku", + "type_approve": "Akceptacja", + "type_validate": "Sprawdzenie", + "target": "Przypisany do", + "target_help": "Akceptacja: Zaakceptuj lub odrzuć przegląd
    Sprawdzenie: Przejrzyj i kontynuuj przepływ", + "add_step": "Dodaj krok przepływu", + "actions": "Co dzieje cię później?", + "remove_action": "Usuń akcję", + "acl_info": "Tylko użytkownicy i grupy zdefiniowane tutaj mogą wystartować przepływ na dokumencie." + } + }, + "security": { + "enable_totp": "Aktywuj uwierzytelnienie dwuskładnikowe", + "enable_totp_message": "Upewnij się, że masz w telefonie aplikację kompatybilną z TOTP, gotową do dodania nowego konta", + "title": "Uwierzytelnienie dwuskładnikowe", + "message_1": "Uwierzytelnienie dwuskładnikowe pozwala na dodanie warstwy zabezpieczeń do konta {{ appName }}.
    Przed aktywacją tej funkcji upewnij się, że masz w telefonie aplikację zgodną z TOTP:", + "message_google_authenticator": "Na Androida, iOS i Blackberry: Google Authenticator", + "message_duo_mobile": "Na Androida i iOS: Duo Mobile", + "message_authenticator": "Na Windows Phone: Authenticator", + "message_2": "Te aplikacje automatycznie generują kod weryfikacyjny, który zmienia się po pewnym czasie.
    Będziesz musiał wprowadzić ten kod weryfikacyjny przy każdym logowaniu {{ appName }}.", + "secret_key": "Twój tajny klucz to: {{ secret }}", + "secret_key_warning": "Skonfiguruj teraz swoją aplikację TOTP na telefonie za pomocą tego tajnego klucza, później nie będziesz mieć do niego dostępu.", + "totp_enabled_message": "Na Twoim koncie jest włączone uwierzytelnianie dwuskładnikowe.
    Za każdym razem, gdy logujesz się w {{appName}}, pojawi się prośba o kod weryfikacyjny ze skonfigurowanej aplikacji telefonu.
    Jeśli zgubisz telefon, nie będziesz mógł zalogować się na swoje konto, ale aktywne sesje pozwolą Ci zregenerować tajny klucz.", + "disable_totp": { + "disable_totp": "Wyłącz uwierzytelnienie wduskładnikowe", + "message": "Twoje konto nie będzie już chronione przez uwierzytelnianie dwuskładnikowe.", + "confirm_password": "Powtórz swoje hasło", + "submit": "Wyłącz uwierzytelnienie dwuskładnikowe" + }, + "test_totp": "Wprowadź kod weryfikacyjny wyświetlony w telefonie:", + "test_code_success": "Kod weryfikacyjny jest poprawny", + "test_code_fail": "Ten kod jest nieprawidłowy. Sprawdź dokładnie, czy Twój telefon jest poprawnie skonfigurowany lub wyłącz uwierzytelnianie dwuskładnikowe" + }, + "group": { + "title": "Zarządzanie grupami", + "add_group": "Dodaj grupę", + "name": "Nazwa", + "edit": { + "delete_group_title": "Usuń grupę", + "delete_group_message": "Czy na pewno chcesz usunąć tę grupę?", + "edit_group_failed_title": "Grupa już istnieje", + "edit_group_failed_message": "Ta nazwa grupy jest już zajęta przez inną grupę", + "group_used_title": "Grupa w użyciu", + "group_used_message": "Ta grupa jest używana w przepływie \"{{ name }}\"", + "edit_group_title": "Edytuj \"{{ name }}\"", + "add_group_title": "Dodaj grupę", + "name": "Nazwa", + "parent_group": "nadrzędna grupa", + "search_group": "Znajdź grupę", + "members": "Członkowie grupy", + "new_member": "Nowy członek", + "search_user": "Znajdź użytkownika" + } + }, + "account": { + "title": "Konto użytkownika", + "password": "Hasło", + "password_confirm": "Hasło (powtórzenie)", + "updated": "Konto zostało zaktualizowane" + }, + "config": { + "title_guest_access": "Dostęp gościa", + "message_guest_access": "Dostęp gościa jest trybem, kiedy każdy może uzyskać dostęp do {{ appName }} bez hasła.
    Użyj normalnego użytkownika jeśli chesz, aby gość uzyskał dostęp do dokumentów zgodnie z przypisanymi uprawnieniami.
    ", + "enable_guest_access": "Włącz dostęp gościa", + "disable_guest_access": "Wyłącz dostęp gościa", + "title_theme": "Zmiana szablonu", + "title_general": "Ustawienia podstawowe", + "default_language": "Domyślny język dla nowych dokumentów", + "application_name": "Nazwa aplikacji", + "main_color": "Podstawowy kolor", + "custom_css": "Niestandardowe CSS", + "custom_css_placeholder": "Niestandardowe CSS zostaną dodane do głównych ustawień styli", + "logo": "Logo (rozmiar kwadratowy)", + "background_image": "Obraz tła", + "uploading_image": "Przesyłanie obrazu...", + "title_smtp": "Ustawienia e-mail", + "smtp_hostname": "Host SMTP", + "smtp_port": "Port SMTP", + "smtp_from": "Nadawca e-mail (nazwa)", + "smtp_username": "Uzytkownik SMTP", + "smtp_password": "Hasło SMTP", + "smtp_updated": "Ustawienia SMTP zostały zaktualizowano", + "webhooks": "Webhook-i", + "webhooks_explain": "Webhook zostanie wywołany, gdy wystąpi określone zdarzenie. Podany adres URL zostanie wywołany ze strukturą JSON zawierającym nazwę zdarzenia i identyfikator danego zasobu.", + "webhook_event": "Zdarzenia", + "webhook_url": "URL", + "webhook_create_date": "Data utworzenia", + "webhook_add": "Dodaj webhook" + }, + "metadata": { + "title": "Niestandardowa konfiguracja metadanych", + "message": "Tutaj możesz dodać niestandardowe metadane do swoich dokumentów, takie jak wewnętrzny identyfikator lub data ważności. Pamiętaj, że po utworzeniu nie można zmienić typu metadanych.", + "name": "Nazwa metadanych", + "type": "Typ metadanych" + }, + "inbox": { + "title": "Skanowanie poczty przychodzącej", + "message": "Po włączeniu tej funkcji system będzie skanować określoną skrzynkę odbiorczą co minutę w poszukiwaniu nieprzeczytanych wiadomości e-mail i automatycznie je importować.
    Po zaimportowaniu wiadomości e-mail zostanie oznaczona jako przeczytana.
    Konfiguracja ustawienia dla Gmail, Outlook.com, Yahoo.", + "enabled": "Włącz skanowanie poczty przychodzącej", + "hostname": "Host IMAP", + "port": "Port IMAP (143 or 993)", + "username": "Użytkownik IMAP", + "password": "Hasło IMAP", + "folder": "Folderze IMAP", + "tag": "Etykieta dodawana do za zaimportowanych dokumentów", + "test": "Przetestuj połączenie", + "last_sync": "Ostatnia synchronizacja: {{ data.date | date: 'medium' }}, {{ data.count }} zaimportowano {{ data.count > 1 ? 's' : '' }} dokumentów", + "test_success": "W poczcie przychodzącej jest ({{ count }} nieprzeczytanych wiadomoś{{ count > 1 ? 'ci' : 'ć' }})", + "test_fail": "Wystąpił błąd podczas łączenia się ze skrzynką odbiorczą, sprawdź ustawienia", + "saved": "Konfiguracja IMAP została zapisana pomyślnie", + "autoTagsEnabled": "Automatycznie dodawaj etykiety z wiersza tematu oznaczonego #", + "deleteImported": "Usuń wiadomość ze skrzynki pocztowej po zaimportowaniu" + }, + "monitoring": { + "background_tasks": "Zadania w tle", + "queued_tasks": "Obecnie w kolejce znajduje się {{count}} zadań.", + "queued_tasks_explain": "Przetwarzanie plików, tworzenie miniatur, aktualizacja indeksu, optyczne rozpoznawanie znaków to zadania w tle. Duża liczba nieprzetworzonych zadań spowoduje niekompletne wyniki wyszukiwania.", + "server_logs": "Dziennik serwera", + "log_date": "Data", + "log_tag": "Etykieta", + "log_message": "Wiadomość", + "indexing": "Indeksowanie", + "indexing_info": "Jeśli zauważysz rozbieżności w wynikach wyszukiwania, możesz spróbować wykonać pełne reindeksowanie. Wyniki wyszukiwania będą niekompletne, dopóki ta operacja nie zostanie wykonana.", + "start_reindexing": "Rozpocznij pełne reindeksowanie", + "reindexing_started": "Rozpoczęto ponowne indeksowanie. Poczekaj, aż nie będzie już żadnych zadań w tle." + }, + "session": { + "title": "otwarte sesje", + "created_date": "Utworzono dnia", + "last_connection_date": "Data ostatniego połączenia", + "user_agent": "From", + "current": "Bieżący", + "current_session": "To jest bieżąca sesja", + "clear_message": "Wszystkie inne urządzenia podłączone do tego konta zostaną odłączone", + "clear": "Wyczyść pozostałe sesje" + }, + "vocabulary": { + "title": "Hasła słownikowe", + "choose_vocabulary": "Wybierz słownik do edycji", + "type": "Rodzaj", + "coverage": "Zakres", + "rights": "Prawa", + "value": "Wartość", + "order": "Kolejność", + "new_entry": "Nowa pozycja" + }, + "fileimporter": { + "title": "Wsadowy importer plików", + "advanced_users": "Dla zaawansowanych użytkowników!", + "need_intro": "Jeśli potrzebujesz:", + "need_1": "Importuj katalog plików naraz", + "need_2": "Przeskanuj katalog w poszukiwaniu nowych plików i zaimportuj je", + "line_1": "Idź do sismics/docs/releases i pobierz narzedzie importu dla twojego systemu.", + "line_2": "Sprawdź instrukcję jak używać tego narzędzia.", + "line_3": "Twoje pliki zostaną zaimportowane w dokumentach zgodnie z konfiguracją importera plików.", + "download": "Pobierz", + "instructions": "Instrukcje" + } + }, + "feedback": { + "title": "Przekaż nam swoją opinię", + "message": "Jakieś sugestie lub pytania dotyczące Teedy? Słuchamy cię!", + "sent_title": "Wysłano opinię`", + "sent_message": "Dziękujemy za twoją opinię! Pomoże nam to uczynić Teedy jeszcze lepszym." + }, + "import": { + "title": "Importowanie", + "error_quota": "Osiągnięto limit, skontaktuj się z administratorem, aby zwiększyć limit", + "error_general": "Wystąpił błąd podczas próby zaimportowania pliku, upewnij się, że jest to prawidłowy plik EML" + }, + "app_share": { + "main": "Poproś o łącze do udostępnionego dokumentu, aby uzyskać do niego dostęp", + "403": { + "title": "Brak dostępu", + "message": "Dokument, który próbujesz wyświetlić, nie jest już udostępniany" + } + }, + "directive": { + "acledit": { + "acl_target": "For", + "acl_permission": "Uprawnienia", + "add_permission": "Dodaj uprawnienie", + "search_user_group": "Wyszukaj użytkownika lub grupę" + }, + "auditlog": { + "log_created": "utworzony", + "log_updated": "zaktualizowany", + "log_deleted": "usunięty", + "Acl": "ACL", + "Comment": "Komentarz", + "Document": "Dokument", + "File": "Plik", + "Group": "Grupa", + "Route": "Przepływ", + "RouteModel": "Model przepływu", + "Tag": "Etykieta", + "User": "Użytkownik", + "Webhook": "Webhook" + }, + "selectrelation": { + "typeahead": "Wpisz tytuł dokumentu" + }, + "selecttag": { + "typeahead": "Wybierz etykietę" + }, + "datepicker": { + "current": "Dzisiaj", + "clear": "Czyść", + "close": "Zakończ" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Do odczytu", + "READWRITE": "Do zapisu i odczytu", + "WRITE": "Do zapisu", + "USER": "Użytkownik", + "GROUP": "Grupa", + "SHARE": "Udostępnienienie" + }, + "workflow_type": { + "VALIDATE": "Sprawdzenie", + "APPROVE": "Zatwierdzenie" + }, + "workflow_transition": { + "APPROVED": "Zatwierdzony", + "REJECTED": "odrzucony", + "VALIDATED": "Sprawdzony" + }, + "validation": { + "required": "Wymagany", + "too_short": "Za krótki", + "too_long": "Za długi", + "email": "Musi być prawidłowy adres e-mail", + "password_confirm": "Hasło i potwierdzenie hasła muszą być zgodne", + "number": "Wymagana liczba", + "no_space": "Spacje i dwukropki są niedozwolone", + "alphanumeric": "Dozwolone są tylko litery i cyfry" + }, + "action_type": { + "ADD_TAG": "Dodaj etykietę", + "REMOVE_TAG": "Usuń etykietę", + "PROCESS_FILES": "Przetwarzane pliki" + }, + "pagination": { + "previous": "Poprzedni", + "next": "Następny", + "first": "Pierwszy", + "last": "Ostatni" + }, + "onboarding": { + "step1": { + "title": "Pierwszy raz?", + "description": "Jeśli to Twój pierwszy raz na Teedy, kliknij przycisk Dalej, w przeciwnym razie możesz mnie zamknąć." + }, + "step2": { + "title": "Dokumenty", + "description": "Teedy jest zorganizowany w dokumentach, a każdy dokument zawiera wiele plików." + }, + "step3": { + "title": "Pliki", + "description": "Możesz dodawać pliki po utworzeniu dokumentu lub przed skorzystaniem z tego obszaru szybkiego przesyłania." + }, + "step4": { + "title": "Wyszukiwanie", + "description": "To jest główny sposób na odzyskanie dokumentów. Istnieje również zaawansowane wyszukiwanie za pomocą przycisku lupy." + }, + "step5": { + "title": "Etykiety", + "description": "Dokumenty można organizować w znaczniki (przypominające superfoldery). Utwórz je tutaj." + } + }, + "yes": "Tak", + "no": "Nie", + "ok": "OK", + "cancel": "Anuluj", + "share": "Udostępnij", + "unshare": "Udwołaj udostępnianie", + "close": "Zamknij", + "add": "Dodaj", + "open": "Otwórz", + "see": "Zobacz", + "save": "Zapisz", + "export": "Eksportuj", + "edit": "Edytuj", + "delete": "Usuń", + "rename": "Zmień nazwę", + "download": "Pobierz", + "loading": "Ładowanie...", + "send": "Wyślij", + "enabled": "Włączony", + "disabled": "Wyłączony" +} diff --git a/docs-web/src/main/webapp/src/locale/pt.json b/docs-web/src/main/webapp/src/locale/pt.json new file mode 100644 index 00000000..e4dc91c8 --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/pt.json @@ -0,0 +1,639 @@ +{ + "login": { + "username": "Nome de utilizador", + "password": "Palavra-passe", + "validation_code_required": "É necessário um código de validação", + "validation_code_title": "Ativou a autenticação de dois fatores na sua conta. Por favor insira um código de validação gerado pela aplicação de telemóvel que configurou.", + "validation_code": "Código de validação", + "remember_me": "Lembrar-me", + "submit": "Iniciar sessão", + "login_as_guest": "Iniciar sessão como convidado", + "login_failed_title": "Falha no início de sessão", + "login_failed_message": "Nome de utilizador ou palavra-passe inválidos", + "password_lost_btn": "Perdeu a palavra-passe?", + "password_lost_sent_title": "Email de recuperação de palavra-passe enviado", + "password_lost_sent_message": "Foi enviado um email para {{ username }} para redefinir a sua palavra-passe", + "password_lost_error_title": "Erro na recuperação de palavra-passe", + "password_lost_error_message": "Não foi possível enviar um email de recuperação de palavra-passe. Por favor contacte o seu administrador para uma redefinição manual" + }, + "passwordlost": { + "title": "Perdeu a palavra-passe", + "message": "Por favor insira o seu nome de utilizador para receber um link de redefinição de palavra-passe. Se não se lembrar do seu nome de utilizador, por favor contacte o seu administrador", + "submit": "Redefinir a minha palavra-passe" + }, + "passwordreset": { + "message": "Por favor insira uma nova palavra-passe", + "submit": "Alterar a minha palavra-passe", + "error_title": "Erro ao alterar a sua palavra-passe", + "error_message": "O seu pedido de recuperação de palavra-passe expirou. Por favor peça um novo na página de início de sessão" + }, + "index": { + "toggle_navigation": "Alternar navegação", + "nav_documents": "Documentos", + "nav_tags": "Tags", + "nav_users_groups": "Utilizadores e Grupos", + "error_info": "{{ count }} novo erro{{ count > 1 ? 's' : '' }}", + "logged_as": "Sessão iniciada como {{ username }}", + "nav_settings": "Definições", + "logout": "Terminar sessão", + "global_quota_warning": "Aviso! Quota global quase atingida em {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) usados em {{ total | number: 0 }}MB" + }, + "document": { + "navigation_up": "Ir para cima", + "toggle_navigation": "Alternar navegação da pasta", + "display_mode_list": "Mostrar documentos em lista", + "display_mode_grid": "Mostrar documentos em grelha", + "search_simple": "Pesquisa simples", + "search_fulltext": "Pesquisa de texto completo", + "search_creator": "Criador", + "search_language": "Idioma", + "search_before_date": "Criado antes desta data", + "search_after_date": "Criado depois desta data", + "search_before_update_date": "Atualizado antes desta data", + "search_after_update_date": "Atualizado depois desta data", + "search_tags": "Etiquetas", + "search_shared": "Apenas documentos partilhados", + "search_workflow": "Fluxo de trabalho atribuído a mim", + "search_clear": "Limpar", + "any_language": "Qualquer idioma", + "add_document": "Adicionar um documento", + "import_eml": "Importar de um e-mail (formato EML)", + "tags": "Etiquetas", + "no_tags": "Sem etiquetas", + "no_documents": "Não há documentos na base de dados", + "search": "Pesquisar", + "search_empty": "Sem correspondências para \"{{ search }}\"", + "shared": "Partilhado", + "current_step_name": "Passo atual", + "title": "Título", + "description": "Descrição", + "contributors": "Contribuidores", + "language": "Idioma", + "creation_date": "Data de criação", + "subject": "Assunto", + "identifier": "Identificador", + "publisher": "Editor", + "format": "Formato", + "source": "Fonte", + "type": "Tipo", + "coverage": "Cobertura", + "rights": "Direitos", + "relations": "Relações", + "page_size": "Tamanho da página", + "page_size_10": "10 por página", + "page_size_20": "20 por página", + "page_size_30": "30 por página", + "upgrade_quota": "Para atualizar a sua quota, peça ao seu administrador", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) utilizados em {{ total | number: 0 }}MB", + "count": "{{ count }} documento{{ count > 1 ? 's' : '' }} encontrado{{ count > 1 ? 's' : '' }}", + "last_updated": "Última atualização {{ date | timeAgo: dateFormat }}", + "view": { + "delete_comment_title": "Apagar comentário", + "delete_comment_message": "Tem a certeza que pretende apagar este comentário?", + "delete_document_title": "Apagar documento", + "delete_document_message": "Tem a certeza que pretende apagar este documento?", + "shared_document_title": "Documento partilhado", + "shared_document_message": "Pode partilhar este documento dando este link. Note que todas as pessoas que tiverem este link podem ver o documento.
    ", + "not_found": "Documento não encontrado", + "forbidden": "Acesso proibido", + "download_files": "Transferir ficheiros", + "export_pdf": "Exportar para PDF", + "by_creator": "por", + "comments": "Comentários", + "no_comments": "Ainda não existem comentários neste documento", + "add_comment": "Adicionar um comentário", + "error_loading_comments": "Erro ao carregar os comentários", + "workflow_current": "Etapa atual do fluxo de trabalho", + "workflow_comment": "Adicionar um comentário ao fluxo de trabalho", + "workflow_validated_title": "Etapa do fluxo de trabalho validada", + "workflow_validated_message": "A etapa do fluxo de trabalho foi validada com sucesso.", + "content": { + "content": "Conteúdo", + "delete_file_title": "Apagar ficheiro", + "delete_file_message": "Tem a certeza que pretende apagar este ficheiro?", + "upload_pending": "Pendente...", + "upload_progress": "A enviar...", + "upload_error": "Erro ao enviar", + "upload_error_quota": "Limite atingido", + "drop_zone": "Arraste e solte ficheiros aqui para enviar", + "add_files": "Adicionar ficheiros", + "file_processing_indicator": "Este ficheiro está a ser processado. A pesquisa não estará disponível até que esteja completo.", + "reprocess_file": "Voltar a processar este ficheiro", + "upload_new_version": "Enviar uma nova versão", + "open_versions": "Mostrar histórico de versões", + "display_mode_list": "Mostrar ficheiros em lista", + "display_mode_grid": "Mostrar ficheiros em grelha" + }, + "workflow": { + "workflow": "Fluxo de trabalho", + "message": "Verifique ou valide os seus documentos com pessoas da sua organização utilizando fluxos de trabalho.", + "workflow_start_label": "Qual fluxo de trabalho deseja iniciar?", + "add_more_workflow": "Adicionar mais fluxos de trabalho", + "start_workflow_submit": "Iniciar fluxo de trabalho", + "full_name": "{{ name }} iniciado em {{ create_date | date }}", + "cancel_workflow": "Cancelar o fluxo de trabalho atual", + "cancel_workflow_title": "Cancelar o fluxo de trabalho", + "cancel_workflow_message": "Tem a certeza que pretende cancelar o fluxo de trabalho atual?", + "no_workflow": "Não é possível iniciar qualquer fluxo de trabalho neste documento." + }, + "permissions": { + "permissions": "Permissões", + "message": "As permissões podem ser aplicadas diretamente a este documento ou podem vir de tags.", + "title": "Permissões deste documento", + "inherited_tags": "Permissões herdadas das tags", + "acl_source": "De", + "acl_target": "Para", + "acl_permission": "Permissão" + }, + "activity": { + "activity": "Actividade", + "message": "Todas as ações neste documento são registadas aqui." + } + }, + "edit": { + "document_edited_with_errors": "Documento editado com sucesso, mas alguns ficheiros não puderam ser carregados", + "document_added_with_errors": "Documento adicionado com sucesso, mas alguns ficheiros não puderam ser carregados", + "quota_reached": "Limite de quota atingido", + "primary_metadata": "Metadados primários", + "title_placeholder": "Um nome dado ao recurso", + "description_placeholder": "Uma descrição do recurso", + "new_files": "Novos ficheiros", + "orphan_files": "+ {{ count }} ficheiro{{ count > 1 ? 's' : '' }}", + "additional_metadata": "Metadados adicionais", + "subject_placeholder": "O tema do recurso", + "identifier_placeholder": "Uma referência inequívoca ao recurso num determinado contexto", + "publisher_placeholder": "Uma entidade responsável por disponibilizar o recurso", + "format_placeholder": "O formato do ficheiro, meio físico ou dimensões do recurso", + "source_placeholder": "Um recurso relacionado do qual o recurso descrito é derivado", + "uploading_files": "A carregar ficheiros..." + }, + "default": { + "upload_pending": "Pendente...", + "upload_progress": "A carregar...", + "upload_error": "Erro de carregamento", + "upload_error_quota": "Limite de quota atingido", + "quick_upload": "Carregamento rápido", + "drop_zone": "Arraste e solte ficheiros aqui para carregar", + "add_files": "Adicionar ficheiros", + "add_new_document": "Adicionar a novo documento", + "latest_activity": "Atividade mais recente", + "footer_sismics": "Desenvolvido com por Sismics", + "api_documentation": "Documentação da API", + "feedback": "Dê-nos um feedback", + "workflow_document_list": "Documentos atribuídos a si", + "select_all": "Selecionar tudo", + "select_none": "Não selecionar nenhum" + }, + "pdf": { + "export_title": "Exportar para PDF", + "export_metadata": "Exportar metadados", + "export_comments": "Exportar commentários", + "fit_to_page": "Ajustar imagem à página", + "margin": "Margem", + "millimeter": "mm" + }, + "share": { + "title": "Partilhar documento", + "message": "Dê um nome à partilha se quiser partilhar o mesmo documento várias vezes.", + "submit": "Partilha" + } + }, + "file": { + "view": { + "previous": "Anterior", + "next": "Seguinte", + "not_found": "Ficheiro não encontrado" + }, + "edit": { + "title": "Editar ficheiro", + "name": "Nome do ficheiro" + }, + "versions": { + "title": "Histórico de versões", + "filename": "Nome do ficheiro", + "mimetype": "Tipo", + "create_date": "Data de criação", + "version": "Versão" + } +}, +"tag": { + "new_tag": "Nova etiqueta", + "search": "Pesquisar", + "default": { + "title": "Etiquetas", + "message_1": "Etiquetas são rótulos associados a documentos.", + "message_2": "Um documento pode ser etiquetado com várias etiquetas e uma etiqueta pode ser aplicada a vários documentos.", + "message_3": "Usando o botão , pode editar as permissões de uma etiqueta.", + "message_4": "Se uma etiqueta puder ser lida por outro utilizador ou grupo, os documentos associados também poderão ser lidos por essas pessoas.", + "message_5": "Por exemplo, etiquete os documentos da sua empresa com a etiqueta MinhaEmpresa e adicione a permissão Pode ler a um grupo funcionários." + }, + "edit": { + "delete_tag_title": "Eliminar etiqueta", + "delete_tag_message": "Tem a certeza de que deseja eliminar esta etiqueta?", + "name": "Nome", + "color": "Cor", + "parent": "Pai", + "info": "As permissões nesta etiqueta serão também aplicadas aos documentos etiquetados com {{ name }}", + "circular_reference_title": "Referência circular", + "circular_reference_message": "A hierarquia das etiquetas pai faz um loop, por favor escolha outra etiqueta pai." + } +}, +"group": { + "profile": { + "members": "Membros", + "no_members": "Sem membros", + "related_links": "Links relacionados", + "edit_group": "Editar grupo {{ name }}" + } +}, +"user": { + "profile": { + "groups": "Grupos", + "quota_used": "Quota utilizada", + "percent_used": "{{ percent | number: 0 }}% utilizada", + "related_links": "Links relacionados", + "document_created": "Documentos criados por {{ username }}", + "edit_user": "Editar utilizador {{ username }}" + } +}, + "usergroup": { + "search_groups": "Pesquisar em grupos", + "search_users": "Pesquisar em utilizadores", + "you": "És tu!", + "default": { + "title": "Utilizadores e Grupos", + "message": "Aqui podes visualizar informações sobre utilizadores e grupos." + } + }, + "settings": { + "menu_personal_settings": "Definições pessoais", + "menu_user_account": "Conta de utilizador", + "menu_two_factor_auth": "Autenticação de dois fatores", + "menu_opened_sessions": "Sessões abertas", + "menu_file_importer": "Importador de ficheiros em massa", + "menu_general_settings": "Definições gerais", + "menu_workflow": "Fluxo de trabalho", + "menu_users": "Utilizadores", + "menu_groups": "Grupos", + "menu_vocabularies": "Vocabulários", + "menu_configuration": "Configuração", + "menu_inbox": "Verificação de caixa de entrada", + "menu_ldap": "Autenticação LDAP", + "menu_metadata": "Metadados personalizados", + "menu_monitoring": "Monitorização", + "ldap": { + "title": "Autenticação LDAP", + "enabled": "Ativar autenticação LDAP", + "host": "Nome do anfitrião LDAP", + "port": "Porta LDAP (por defeito 389)", + "admin_dn": "DN do administrador", + "admin_password": "Palavra-passe do administrador", + "base_dn": "DN de pesquisa base", + "filter": "Filtro de pesquisa (deve conter USERNAME, por exemplo, \"(uid=USERNAME)\")", + "default_email": "E-mail padrão para utilizador LDAP", + "default_storage": "Armazenamento padrão para utilizador LDAP", + "saved": "Configuração LDAP guardada com sucesso" + }, + "user": { + "title": "Gestão de Utilizadores", + "add_user": "Adicionar utilizador", + "username": "Nome de utilizador", + "create_date": "Data de criação", + "totp_enabled": "Autenticação de dois fatores ativada para esta conta", + "edit": { + "delete_user_title": "Apagar utilizador", + "delete_user_message": "Tem a certeza de que deseja apagar este utilizador? Todos os documentos, ficheiros e etiquetas associados serão apagados", + "user_used_title": "Utilizador em uso", + "user_used_message": "Este utilizador é utilizado no fluxo de trabalho \"{{ name }}\"", + "edit_user_failed_title": "Utilizador já existe", + "edit_user_failed_message": "Este nome de utilizador já está em uso por outro utilizador", + "edit_user_title": "Editar \"{{ username }}\"", + "add_user_title": "Adicionar utilizador", + "username": "Nome de utilizador", + "email": "E-mail", + "groups": "Grupos", + "storage_quota": "Cota de armazenamento", + "storage_quota_placeholder": "Cota de armazenamento (em MB)", + "password": "Palavra-passe", + "password_confirm": "Palavra-passe (confirmar)", + "disabled": "Utilizador desativado", + "password_reset_btn": "Enviar um email para reset de password para este utilizador", + "password_lost_sent_title": "Email de reset de password enviado", + "password_lost_sent_message": "Foi enviado um email de reset de password para {{ username }}", + "disable_totp_btn": "Desativar autenticação de dois fatores para este utilizador", + "disable_totp_title": "Desativar autenticação de dois fatores", + "disable_totp_message": "Tem a certeza de que deseja desativar a autenticação de dois fatores para este utilizador?" +}, +"workflow": { + } + }, + "workflow": { + "title": "Configuração de fluxo de trabalho", + "add_workflow": "Adicionar um fluxo de trabalho", + "name": "Nome", + "edit": { + "delete_workflow_title": "Eliminar fluxo de trabalho", + "delete_workflow_message": "Tem a certeza de que deseja eliminar este fluxo de trabalho? Os fluxos de trabalho em execução não serão eliminados", + "edit_workflow_title": "Editar \"{{ name }}\"", + "add_workflow_title": "Adicionar um fluxo de trabalho", + "name": "Nome", + "name_placeholder": "Nome ou descrição da etapa", + "drag_help": "Arrastar e soltar para reordenar a etapa", + "type": "Tipo de etapa", + "type_approve": "Aprovar", + "type_validate": "Validar", + "target": "Atribuído a", + "target_help": "Aprovar: Aceitar ou rejeitar a revisão
    Validar: Rever e continuar o fluxo de trabalho", + "add_step": "Adicionar uma etapa ao fluxo de trabalho", + "actions": "O que acontece a seguir?", + "remove_action": "Remover ação", + "acl_info": "Apenas os utilizadores e grupos definidos aqui poderão iniciar este fluxo de trabalho num documento" + } + }, + "security": { + "enable_totp": "Ativar autenticação de dois fatores", + "enable_totp_message": "Certifique-se de que tem uma aplicação compatível com TOTP no seu telemóvel pronta para adicionar uma nova conta", + "title": "Autenticação de dois fatores", + "message_1": "A autenticação de dois fatores permite adicionar uma camada de segurança à sua conta {{ appName }}.
    Antes de ativar esta funcionalidade, certifique-se de que tem uma aplicação compatível com TOTP no seu telemóvel:", + "message_google_authenticator": "Para Android, iOS e Blackberry: Google Authenticator", + "message_duo_mobile": "Para Android e iOS: Duo Mobile", + "message_authenticator": "Para Windows Phone: Authenticator", + "message_2": "Essas aplicações geram automaticamente um código de validação que muda após um certo período de tempo.
    Você terá que inserir este código de validação cada vez que fizer login em {{ appName }}.", + "secret_key": "Sua chave secreta é: {{ secret }}", + "secret_key_warning": "Configure seu aplicativo TOTP no seu telefone com esta chave secreta agora, pois você não poderá acessá-la posteriormente.", + "totp_enabled_message": "A autenticação de dois fatores está habilitada em sua conta.
    Cada vez que fizer login em {{ appName }}, você será solicitado a inserir um código de validação do seu aplicativo de telefone configurado.
    Se você perder seu telefone, não poderá fazer login em sua conta, mas as sessões ativas permitirão que você regenere uma chave secreta.", + "disable_totp": { + "disable_totp": "Desativar autenticação de dois fatores", + "message": "Sua conta não será mais protegida pela autenticação de dois fatores.", + "confirm_password": "Confirme sua senha", + "submit": "Desactivar autenticação de dois factores" + }, + "test_totp": "Por favor insira o código de validação exibido no seu telefone:", + "test_code_success": "Código de validação OK", + "test_code_fail": "Este código não é válido, por favor verifique se o seu telefone está configurado corretamente ou desative a autenticação de dois fatores" + }, + "group": { + "title": "Gestão de grupos", + "add_group": "Adicionar um grupo", + "name": "Nome", + "edit": { + "delete_group_title": "Eliminar grupo", + "delete_group_message": "Tem a certeza de que deseja eliminar este grupo?", + "edit_group_failed_title": "Grupo já existe", + "edit_group_failed_message": "Este nome de grupo já está em uso por outro grupo", + "group_used_title": "Grupo em uso", + "group_used_message": "Este grupo é utilizado no fluxo de trabalho \"{{ name }}\"", + "edit_group_title": "Editar \"{{ name }}\"", + "add_group_title": "Adicionar um grupo", + "name": "Nome", + "parent_group": "Grupo principal", + "search_group": "Pesquisar um grupo", + "members": "Membros", + "new_member": "Novo membro", + "search_user": "Pesquisar um utilizador" + } + }, + "account": { + "title": "Conta de utilizador", + "password": "Palavra-passe", + "password_confirm": "Palavra-passe (confirmação)", + "updated": "Conta atualizada com sucesso" + }, + "config": { + "title_guest_access": "Acesso de convidado", + "message_guest_access": "O acesso de convidado é um modo em que qualquer pessoa pode aceder ao {{ appName }} sem palavra-passe.
    Como um utilizador normal, o utilizador convidado só pode aceder aos seus documentos e aos que têm permissão de acesso.
    ", + "enable_guest_access": "Ativar acesso de convidado", + "disable_guest_access": "Desativar acesso de convidado", + "title_theme": "Personalização do tema", + "title_general": "Configuração geral", + "default_language": "Idioma predefinido para novos documentos", + "application_name": "Nome da aplicação", + "main_color": "Cor principal", + "custom_css": "CSS personalizado", + "custom_css_placeholder": "CSS personalizado a adicionar após a folha de estilos principal", + "logo": "Logótipo (tamanho quadrado)", + "background_image": "Imagem de fundo", + "uploading_image": "A enviar a imagem...", + "title_smtp": "Configuração de email", + "smtp_hostname": "Nome do servidor SMTP", + "smtp_port": "Porta SMTP", + "smtp_from": "Email do remetente", + "smtp_username": "Nome de utilizador SMTP", + "smtp_password": "Palavra-passe SMTP", + "smtp_updated": "Configuração SMTP atualizada com sucesso", + "webhooks": "Webhooks", + "webhooks_explain": "Os webhooks serão chamados quando o evento especificado ocorrer. A URL fornecida será enviada por POST com uma carga útil JSON contendo o nome do evento e o ID do recurso em causa.", + "webhook_event": "Evento", + "webhook_url": "URL", + "webhook_create_date": "Data de criação", + "webhook_add": "Adicionar um webhook" + }, + "metadata": { + "title": "Configuração de metadados personalizados", + "message": "Aqui pode adicionar metadados personalizados aos seus documentos, como um identificador interno ou uma data de validade. Por favor, note que o tipo de metadados não pode ser alterado após a criação.", + "name": "Nome dos metadados", + "type": "Tipo de metadados" + }, + "inbox": { + "title": "Análise de caixa de entrada", + "message": "Ao ativar esta funcionalidade, o sistema irá analisar a caixa de entrada especificada a cada minuto em busca de emails não lidos e importá-los automaticamente.
    Após a importação de um email, ele será marcado como lido.
    As configurações de configuração para Gmail, Outlook.com, Yahoo.", + "enabled": "Ativar análise de caixa de entrada", + "hostname": "Nome do servidor IMAP", + "port": "Porta IMAP (143 ou 993)", + "username": "Nome de utilizador IMAP", + "password": "Palavra-passe IMAP", + "folder": "Pasta IMAP", + "tag": "Tag adicionada aos documentos importados", + "test": "Testar os parâmetros", + "last_sync": "Última sincronização: {{ data.date | date: 'medium' }}, {{ data.count }} mensagem{{ data.count > 1 ? 's' : '' }} importada{{ data.count > 1 ? 's' : '' }}", + "test_success": "A ligação à caixa de entrada foi estabelecida com sucesso ({{ count }} mensagem{{ count > 1 ? 's' : '' }} não lida{{ count > 1 ? 's' : '' }})", + "test_fail": "Ocorreu um erro ao ligar à caixa de entrada, por favor verifique os parâmetros", + "saved": "Configuração IMAP guardada com sucesso", + "autoTagsEnabled": "Adicionar automaticamente tags da linha de assunto marcadas com #", + "deleteImported": "Apagar a mensagem da caixa de correio após a importação" + }, + "monitoring": { + "background_tasks": "Tarefas em segundo plano", + "queued_tasks": "Existem atualmente {{ count }} tarefas em fila.", + "queued_tasks_explain": "O processamento de ficheiros, a criação de miniaturas, a atualização de índices e o reconhecimento ótico de caracteres são tarefas em segundo plano. Uma grande quantidade de tarefas por processar resultará em resultados de pesquisa incompletos.", + "server_logs": "Registos do servidor", + "log_date": "Data", + "log_tag": "Tag", + "log_message": "Mensagem", + "indexing": "Indexação", + "indexing_info": "Se notar discrepâncias nos resultados de pesquisa, pode tentar fazer uma reindexação completa. Os resultados de pesquisa serão incompletos até que esta operação esteja concluída.", + "start_reindexing": "Iniciar reindexação completa", + "reindexing_started": "A reindexação foi iniciada, por favor aguarde até que não haja mais tarefas em segundo plano." + }, + "session": { + "title": "Sessões abertas", + "created_date": "Data de criação", + "last_connection_date": "Data da última conexão", + "user_agent": "De", + "current": "Atual", + "current_session": "Esta é a sessão atual", + "clear_message": "Todos os outros dispositivos conectados a esta conta serão desconectados", + "clear": "Limpar todas as outras sessões" + }, + "vocabulary": { + "title": "Entradas de vocabulário", + "choose_vocabulary": "Escolha um vocabulário para editar", + "type": "Tipo", + "coverage": "Cobertura", + "rights": "Direitos", + "value": "Valor", + "order": "Ordem", + "new_entry": "Nova entrada" + }, + "fileimporter": { + "title": "Importador em massa de ficheiros", + "advanced_users": "Para utilizadores avançados!", + "need_intro": "Se precisar de:", + "need_1": "Importar um diretório de ficheiros de uma só vez", + "need_2": "Analisar um diretório à procura de novos ficheiros e importá-los", + "line_1": "Vá para sismics/docs/releases e descarregue a ferramenta de importação de ficheiros para o seu sistema.", + "line_2": "Siga as instruções aqui para usar esta ferramenta.", + "line_3": "Os seus ficheiros serão importados em documentos de acordo com a configuração do importador de ficheiros.", + "download": "Descarregar", + "instructions": "Instruções" + } + }, + "feedback": { + "title": "Dê-nos o seu feedback", + "message": "Alguma sugestão ou pergunta sobre o Teedy? Nós ouvimos você!", + "sent_title": "Feedback enviado", + "sent_message": "Obrigado pelo seu feedback! Isso nos ajudará a tornar o Teedy ainda melhor." + }, + "import": { + "title": "A importar", + "error_quota": "Limite de quota atingido, contacte o seu administrador para aumentar a sua quota", + "error_general": "Ocorreu um erro ao tentar importar o seu ficheiro, por favor certifique-se de que é um ficheiro EML válido" + }, + "app_share": { + "main": "Peça um link de documento partilhado para aceder", + "403": { + "title": "Não autorizado", + "message": "O documento que está a tentar visualizar já não está partilhado" + } + }, + "directive": { + "acledit": { + "acl_target": "Para", + "acl_permission": "Permissão", + "add_permission": "Adicionar permissão", + "search_user_group": "Procurar um utilizador ou grupo" + }, + "auditlog": { + "log_created": "criado", + "log_updated": "atualizado", + "log_deleted": "eliminado", + "Acl": "ACL", + "Comment": "Comentário", + "Document": "Documento", + "File": "Ficheiro", + "Group": "Grupo", + "Route": "Fluxo de trabalho", + "RouteModel": "Modelo de fluxo de trabalho", + "Tag": "Tag", + "User": "Utilizador", + "Webhook": "Webhook" + }, + "selectrelation": { + "typeahead": "Escreva o título do documento" + }, + "selecttag": { + "typeahead": "Escreva uma tag" + }, + "datepicker": { + "current": "Hoje", + "clear": "Limpar", + "close": "Concluído" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Pode ler", + "READWRITE": "Pode escrever", + "WRITE": "Pode escrever", + "USER": "Utilizador", + "GROUP": "Grupo", + "SHARE": "Partilhado" + }, + "workflow_type": { + "VALIDATE": "Validação", + "APPROVE": "Aprovação" + }, + "workflow_transition": { + "APPROVED": "Aprovado", + "REJECTED": "Rejeitado", + "VALIDATED": "Validado" + }, + "validation": { + "required": "Obrigatório", + "too_short": "Muito curto", + "too_long": "Muito longo", + "email": "Tem de ser um e-mail válido", + "password_confirm": "A palavra-passe e a confirmação da palavra-passe têm de corresponder", + "number": "É necessário um número", + "no_space": "Espaços e dois pontos não são permitidos", + "alphanumeric": "Apenas letras e números são permitidos" + }, + "action_type": { + "ADD_TAG": "Adicionar uma tag", + "REMOVE_TAG": "Remover uma tag", + "PROCESS_FILES": "Processar ficheiros" + }, + "pagination": { + "previous": "Anterior", + "next": "Seguinte", + "first": "Primeiro", + "last": "Último" + }, + "onboarding": { + "step1": { + "title": "Primeira vez?", + "description": "Se é a sua primeira vez no Teedy, clique no botão Seguinte, caso contrário, pode fechar esta janela." + }, + "step2": { + "title": "Documentos", + "description": "O Teedy está organizado em documentos e cada documento contém vários ficheiros." + }, + "step3": { + "title": "Ficheiros", + "description": "Pode adicionar ficheiros depois de criar um documento ou antes utilizando esta área de upload rápido." + }, + "step4": { + "title": "Pesquisa", + "description": "Esta é a forma principal de encontrar os seus documentos. Também existe uma pesquisa avançada com o botão de lupa." + }, + "step5": { + "title": "Tags", + "description": "Os documentos podem ser organizados em tags (que são como superpastas). Crie-as aqui." + } + }, + "yes": "Sim", + "no": "Não", + "ok": "OK", + "cancel": "Cancelar", + "share": "Partilhar", + "unshare": "Despartilhar", + "close": "Fechar", + "add": "Adicionar", + "open": "Abrir", + "see": "Ver", + "save": "Guardar", + "export": "Exportar", + "edit": "Editar", + "delete": "Eliminar", + "rename": "Renomear", + "download": "Transferir", + "loading": "A carregar...", + "send": "Enviar", + "enabled": "Ativado", + "disabled": "Desativado" +} diff --git a/docs-web/src/main/webapp/src/locale/ru.json b/docs-web/src/main/webapp/src/locale/ru.json index 69201ff0..1b88c57f 100644 --- a/docs-web/src/main/webapp/src/locale/ru.json +++ b/docs-web/src/main/webapp/src/locale/ru.json @@ -369,6 +369,7 @@ "port": "Порт IMAP (143 или 993)", "username": "Имя пользователя IMAP", "password": "Пароль IMAP", + "folder": "Папке IMAP", "tag": "Тег добавлен в импортированные документы", "test": "Проверить параметры", "last_sync": "Последняя синхронизация: {{data.date | date}}, {{data.count}} импортировано", diff --git a/docs-web/src/main/webapp/src/locale/sq_AL.json b/docs-web/src/main/webapp/src/locale/sq_AL.json new file mode 100644 index 00000000..849243fe --- /dev/null +++ b/docs-web/src/main/webapp/src/locale/sq_AL.json @@ -0,0 +1,640 @@ +{ + "login": { + "username": "Emri i përdoruesit", + "password": "Fjalëkalimi", + "validation_code_required": "Kërkohet një kod verifikimi", + "validation_code_title": "Ju keni aktivizuar vërtetimin me dy faktorë në llogarinë tuaj. ", + "validation_code": "Kodi i verifikimit", + "remember_me": "Më kujto mua", + "submit": "Hyni", + "login_as_guest": "Identifikohu si i ftuar", + "login_failed_title": "Identifikimi dështoi", + "login_failed_message": "Emri i përdoruesit ose fjalëkalimi është i pavlefshëm", + "password_lost_btn": "Fjalëkalimi i humbur?", + "password_lost_sent_title": "Email për rivendosjen e fjalëkalimit u dërgua", + "password_lost_sent_message": "Një email është dërguar në {{ username }} për të rivendosur fjalëkalimin tuaj", + "password_lost_error_title": "Gabim i rivendosjes së fjalëkalimit", + "password_lost_error_message": "Nuk mund të dërgohet një email për rivendosjen e fjalëkalimit, ju lutemi kontaktoni administratorin tuaj për një rivendosje manuale" + }, + "passwordlost": { + "title": "Fjalëkalimi ka humbur", + "message": "Ju lutemi shkruani emrin tuaj të përdoruesit për të marrë një lidhje të rivendosjes së fjalëkalimit. ", + "submit": "Rivendos fjalëkalimin tim" + }, + "passwordreset": { + "message": "Ju lutemi shkruani një fjalëkalim të ri", + "submit": "Ndrysho fjalëkalimin tim", + "error_title": "Gabim gjatë ndryshimit të fjalëkalimit tuaj", + "error_message": "Kërkesa juaj për rikuperimin e fjalëkalimit ka skaduar, ju lutemi kërkoni një të re në faqen e hyrjes" + }, + "index": { + "toggle_navigation": "Ndrysho navigimin", + "nav_documents": "Dokumentet", + "nav_tags": "Etiketa", + "nav_users_groups": "Përdoruesit", + "error_info": "{{ count }} gabim i ri{{ count > 1 ? 's' : '' }}", + "logged_as": "I identifikuar si {{ username }}", + "nav_settings": "Cilësimet", + "logout": "Shkyç", + "global_quota_warning": "Paralajmërim! Kuota globale pothuajse arriti në {{ current | number: 0 }}MB ({{ percent | number: 1 }}%) përdoret në {{ total | number: 0 }}MB" + }, + "document": { + "navigation_up": "Ngjitu një nivel", + "toggle_navigation": "Ndrysho navigimin e dosjeve", + "display_mode_list": "Shfaq dokumentet në listë", + "display_mode_grid": "Shfaq dokumentet në rrjet", + "search_simple": "Kërkim i thjeshtë", + "search_fulltext": "Kërkimi i tekstit të plotë", + "search_creator": "Krijuesi", + "search_language": "Gjuhe", + "search_before_date": "Krijuar para kësaj date", + "search_after_date": "Krijuar pas kësaj date", + "search_before_update_date": "Përditësuar përpara kësaj date", + "search_after_update_date": "Përditësuar pas kësaj date", + "search_tags": "Etiketa", + "search_shared": "Vetëm dokumente të përbashkëta", + "search_workflow": "Rrjedha e punës më është caktuar", + "search_clear": "Qartë", + "any_language": "Çdo gjuhë", + "add_document": "Shto një dokument", + "import_eml": "Importo nga një email (format EML)", + "tags": "Etiketa", + "no_tags": "Nuk ka etiketa", + "no_documents": "Asnjë dokument në bazën e të dhënave", + "search": "Kërko", + "search_empty": "Nuk ka ndeshje për \"{{ search }}\"", + "shared": "Të përbashkëta", + "current_step_name": "Hapi aktual", + "title": "Titulli", + "description": "Përshkrim", + "contributors": "Kontribuesit", + "language": "Gjuhe", + "creation_date": "Data e krijimit", + "subject": "Subjekti", + "identifier": "Identifikues", + "publisher": "Botues", + "format": "Formati", + "source": "Burimi", + "type": "Lloji", + "coverage": "Mbulimi", + "rights": "Të drejtat", + "relations": "Marrëdhëniet", + "page_size": "Madhësia e faqes", + "page_size_10": "10 për faqe", + "page_size_20": "20 për faqe", + "page_size_30": "30 për faqe", + "upgrade_quota": "Për të përmirësuar kuotën tuaj, pyesni administratorin tuaj", + "quota": "{{ current | number: 0 }}MB ({{ percent | number: 1 }}%) përdoret në {{ total | number: 0 }}MB", + "count": "{{ count }} dokument{{ count > 1 ? 's' : '' }} gjetur", + "last_updated": "Përditësimi i fundit {{ date | timeAgo: dateFormat }}", + "view": { + "delete_comment_title": "Fshi komentin", + "delete_comment_message": "Dëshiron vërtet ta fshish këtë koment?", + "delete_document_title": "Fshi dokumentin", + "delete_document_message": "Dëshiron vërtet ta fshish këtë dokument?", + "shared_document_title": "Dokument i përbashkët", + "shared_document_message": "Ju mund ta ndani këtë dokument duke dhënë këtë lidhje.
    ", + "not_found": "Dokumenti nuk u gjet", + "forbidden": "Qasja është e ndaluar", + "download_files": "Shkarko skedarët", + "export_pdf": "Eksporto në PDF", + "by_creator": "nga", + "comments": "Komentet", + "no_comments": "Ende nuk ka komente për këtë dokument", + "add_comment": "Shto një koment", + "error_loading_comments": "Gabim gjatë ngarkimit të komenteve", + "workflow_current": "Hapi aktual i rrjedhës së punës", + "workflow_comment": "Shto një koment të rrjedhës së punës", + "workflow_validated_title": "Hapi i rrjedhës së punës u vërtetua", + "workflow_validated_message": "Hapi i rrjedhës së punës është vërtetuar me sukses.", + "content": { + "content": "përmbajtja", + "delete_file_title": "Fshi skedarin", + "delete_file_message": "Dëshiron vërtet ta fshish këtë skedar?", + "upload_pending": "Në pritje...", + "upload_progress": "Po ngarkohet...", + "upload_error": "Gabim ngarkimi", + "upload_error_quota": "Kuota u arrit", + "drop_zone": "Zvarrit", + "add_files": "Shtoni skedarë", + "file_processing_indicator": "Ky skedar është duke u përpunuar. ", + "reprocess_file": "Ripërpunoni këtë skedar", + "upload_new_version": "Ngarko një version të ri", + "open_versions": "Shfaq historikun e versionit", + "display_mode_list": "Shfaq skedarët në listë", + "display_mode_grid": "Shfaq skedarët në rrjet" + }, + "workflow": { + "workflow": "Rrjedha e punës", + "message": "Verifikoni ose vërtetoni dokumentet tuaja me njerëzit e organizatës suaj duke përdorur rrjedhat e punës.", + "workflow_start_label": "Cilin rrjedhë pune të filloni?", + "add_more_workflow": "Shto më shumë flukse pune", + "start_workflow_submit": "Filloni rrjedhën e punës", + "full_name": "{{ name }} filloi më {{ create_date | date }}", + "cancel_workflow": "Anuloni rrjedhën aktuale të punës", + "cancel_workflow_title": "Anuloni rrjedhën e punës", + "cancel_workflow_message": "Dëshiron vërtet të anulosh rrjedhën aktuale të punës?", + "no_workflow": "Nuk mund të filloni asnjë rrjedhë pune në këtë dokument." + }, + "permissions": { + "permissions": "Lejet", + "message": "Lejet mund të aplikohen drejtpërdrejt në këtë dokument, ose mund të vijnë nga etiketa.", + "title": "Lejet për këtë dokument", + "inherited_tags": "Lejet e trashëguara nga etiketat", + "acl_source": "Nga", + "acl_target": "Për", + "acl_permission": "Leja" + }, + "activity": { + "activity": "Aktiviteti", + "message": "Çdo veprim në këtë dokument regjistrohet këtu." + } + }, + "edit": { + "document_edited_with_errors": "Dokumenti u redaktua me sukses, por disa skedarë nuk mund të ngarkohen", + "document_added_with_errors": "Dokumenti u shtua me sukses, por disa skedarë nuk mund të ngarkohen", + "quota_reached": "Kuota u arrit", + "primary_metadata": "Meta të dhënat primare", + "title_placeholder": "Një emër i dhënë burimit", + "description_placeholder": "Një llogari e burimit", + "new_files": "Skedarë të rinj", + "orphan_files": "{{ count }} dosje{{ count > 1 ? 's' : '' }}", + "additional_metadata": "Meta të dhëna shtesë", + "subject_placeholder": "Tema e burimit", + "identifier_placeholder": "Një referencë e paqartë për burimin brenda një konteksti të caktuar", + "publisher_placeholder": "Një subjekt përgjegjës për vënien në dispozicion të burimit", + "format_placeholder": "Formati i skedarit, mediumi fizik ose dimensionet e burimit", + "source_placeholder": "Një burim i lidhur nga i cili rrjedh burimi i përshkruar", + "uploading_files": "Skedarët po ngarkohen..." + }, + "default": { + "upload_pending": "Në pritje...", + "upload_progress": "Po ngarkohet...", + "upload_error": "Gabim ngarkimi", + "upload_error_quota": "Kuota u arrit", + "quick_upload": "Ngarkimi i shpejtë", + "drop_zone": "Zvarrit", + "add_files": "Shtoni skedarë", + "add_new_document": "Shto në dokument të ri", + "latest_activity": "Aktiviteti i fundit", + "footer_sismics": "E punuar me nga Sizmike", + "api_documentation": "Dokumentacioni API", + "feedback": "Na jepni një koment", + "workflow_document_list": "Dokumentet e caktuara për ju", + "select_all": "Selektoj të gjitha", + "select_none": "Zgjidh asnjë" + }, + "pdf": { + "export_title": "Eksporto në PDF", + "export_metadata": "Eksporto të dhëna meta", + "export_comments": "Eksporto komente", + "fit_to_page": "Përshtat imazhin në faqe", + "margin": "Marzhi", + "millimeter": "mm" + }, + "share": { + "title": "Ndani dokumentin", + "message": "Emërtoni ndarjen nëse dëshironi të ndani disa herë të njëjtin dokument.", + "submit": "Shpërndaje" + } + }, + "file": { + "view": { + "previous": "E mëparshme", + "next": "Tjetra", + "not_found": "Skedari nuk u gjet" + }, + "edit": { + "title": "Redakto skedarin", + "name": "Emri i skedarit" + }, + "versions": { + "title": "Historia e versionit", + "filename": "Emri i skedarit", + "mimetype": "Lloji", + "create_date": "Data e krijimit", + "version": "Version" + } + }, + "tag": { + "new_tag": "Etiketë e re", + "search": "Kërko", + "default": { + "title": "Etiketa", + "message_1": "Etiketa janë etiketa të lidhura me dokumentet.", + "message_2": "Një dokument mund të etiketohet me etiketa të shumta dhe një etiketë mund të aplikohet në dokumente të shumta.", + "message_3": "Duke perdorur butonin, ju mund të modifikoni lejet në një etiketë.", + "message_4": "Nëse një etiketë mund të lexohet nga një përdorues ose grup tjetër, dokumentet shoqëruese mund të lexohen gjithashtu nga ata njerëz.", + "message_5": "Për shembull, etiketoni dokumentet e kompanisë suaj me një etiketë Kompania ime dhe shtoni lejen Mund të lexojë në një grup punonjësit" + }, + "edit": { + "delete_tag_title": "Fshi etiketën", + "delete_tag_message": "Dëshiron vërtet ta fshish këtë etiketë?", + "name": "Emri", + "color": "Ngjyrë", + "parent": "Prindi", + "info": "Lejet për këtë etiketë do të zbatohen gjithashtu për dokumentet e etiketuara {{ name }}", + "circular_reference_title": "Referencë rrethore", + "circular_reference_message": "Hierarkia e etiketave prind krijon një lak, ju lutemi zgjidhni një prind tjetër." + } + }, + "group": { + "profile": { + "members": "Anëtarët", + "no_members": "Asnjë anëtar", + "related_links": "Lidhje të ngjashme", + "edit_group": "Redakto {{ name }} grup" + } + }, + "user": { + "profile": { + "groups": "Grupet", + "quota_used": "Kuota e përdorur", + "percent_used": "{{ percent | number: 0 }}% e përdorur", + "related_links": "Lidhje të ngjashme", + "document_created": "Dokumentet e krijuara nga {{ username }}", + "edit_user": "Redakto {{ username }} përdorues" + } + }, + "usergroup": { + "search_groups": "Kërkoni në grupe", + "search_users": "Kërkoni në përdoruesit", + "you": "je ti!", + "default": { + "title": "Përdoruesit", + "message": "Këtu mund të shikoni informacione rreth përdoruesve dhe grupeve." + } + }, + "settings": { + "menu_personal_settings": "Cilësimet personale", + "menu_user_account": "Llogaria e përdoruesit", + "menu_two_factor_auth": "Autentifikimi me dy faktorë", + "menu_opened_sessions": "Seancat e hapura", + "menu_file_importer": "Importuesi i skedarëve në masë", + "menu_general_settings": "Cilësimet e përgjithshme", + "menu_workflow": "Rrjedha e punës", + "menu_users": "Përdoruesit", + "menu_groups": "Grupet", + "menu_vocabularies": "Fjalorët", + "menu_configuration": "Konfigurimi", + "menu_inbox": "Skanimi i kutisë hyrëse", + "menu_ldap": "Autentifikimi LDAP", + "menu_metadata": "Meta të dhëna të personalizuara", + "menu_monitoring": "Monitorimi", + "ldap": { + "title": "Autentifikimi LDAP", + "enabled": "Aktivizo vërtetimin LDAP", + "host": "Emri i hostit LDAP", + "port": "Porta LDAP (389 si parazgjedhje)", + "usessl": "Aktivizo SSL (ldaps)", + "admin_dn": "Admin DN", + "admin_password": "Fjalëkalimi i administratorit", + "base_dn": "Kërkimi bazë DN", + "filter": "Filtri i kërkimit (duhet të përmbajë USERNAME, p.sh. \"(uid=USERNAME)\")", + "default_email": "Email-i i parazgjedhur për përdoruesin LDAP", + "default_storage": "Hapësira ruajtëse e paracaktuar për përdoruesin LDAP", + "saved": "Konfigurimi LDAP u ruajt me sukses" + }, + "user": { + "title": "Menaxhimi i përdoruesve", + "add_user": "Shto një përdorues", + "username": "Emri i përdoruesit", + "create_date": "Krijo datë", + "totp_enabled": "Për këtë llogari është aktivizuar vërtetimi me dy faktorë", + "edit": { + "delete_user_title": "Fshi përdoruesin", + "delete_user_message": "Dëshiron vërtet ta fshish këtë përdorues? ", + "user_used_title": "Përdoruesi në përdorim", + "user_used_message": "Ky përdorues përdoret në rrjedhën e punës \"{{ name }}\"", + "edit_user_failed_title": "Përdoruesi ekziston tashmë", + "edit_user_failed_message": "Ky emër përdoruesi është marrë tashmë nga një përdorues tjetër", + "edit_user_title": "Redakto \"{{ username }}\"", + "add_user_title": "Shto një përdorues", + "username": "Emri i përdoruesit", + "email": "E-mail", + "groups": "Grupet", + "storage_quota": "Kuota e ruajtjes", + "storage_quota_placeholder": "Kuota e hapësirës ruajtëse (në MB)", + "password": "Fjalëkalimi", + "password_confirm": "Fjalëkalimi (konfirmo)", + "disabled": "Përdorues me aftësi të kufizuara", + "password_reset_btn": "Dërgoni një email për rivendosjen e fjalëkalimit te ky përdorues", + "password_lost_sent_title": "Email për rivendosjen e fjalëkalimit u dërgua", + "password_lost_sent_message": "Është dërguar një email për rivendosjen e fjalëkalimit {{ username }}", + "disable_totp_btn": "Çaktivizo vërtetimin me dy faktorë për këtë përdorues", + "disable_totp_title": "Çaktivizo vërtetimin me dy faktorë", + "disable_totp_message": "Jeni i sigurt që dëshironi të çaktivizoni vërtetimin me dy faktorë për këtë përdorues?" + } + }, + "workflow": { + "title": "Konfigurimi i rrjedhës së punës", + "add_workflow": "Shto një rrjedhë pune", + "name": "Emri", + "create_date": "Krijo datë", + "edit": { + "delete_workflow_title": "Fshi fluksin e punës", + "delete_workflow_message": "Dëshiron vërtet ta fshish këtë rrjedhë pune? ", + "edit_workflow_title": "Redakto \"{{ name }}\"", + "add_workflow_title": "Shto një rrjedhë pune", + "name": "Emri", + "name_placeholder": "Emri ose përshkrimi i hapit", + "drag_help": "Zvarrit dhe lësho për të rirenditur hapin", + "type": "Lloji i hapit", + "type_approve": "Mirato", + "type_validate": "Vërtetoni", + "target": "Caktuar për", + "target_help": "Mirato: Pranoni ose refuzoni rishikimin
    Vërteto: Rishikoni dhe vazhdoni rrjedhën e punës", + "add_step": "Shto një hap të rrjedhës së punës", + "actions": "Çfarë ndodh më pas?", + "remove_action": "Hiq veprimin", + "acl_info": "Vetëm përdoruesit dhe grupet e përcaktuara këtu do të mund të fillojnë këtë rrjedhë pune në një dokument" + } + }, + "security": { + "enable_totp": "Aktivizo vërtetimin me dy faktorë", + "enable_totp_message": "Sigurohuni që të keni një aplikacion të përputhshëm me TOTP në telefonin tuaj gati për të shtuar një llogari të re", + "title": "Autentifikimi me dy faktorë", + "message_1": "Autentifikimi me dy faktorë ju lejon të shtoni një shtresë sigurie në tuaj {{ appName }} llogari.
    Përpara se të aktivizoni këtë veçori, sigurohuni që të keni një aplikacion të pajtueshëm me TOTP në telefonin tuaj:", + "message_google_authenticator": "Për Android, iOS dhe Blackberry: Google Authenticator", + "message_duo_mobile": "Për Android dhe iOS: Duo Mobile", + "message_authenticator": "Për Windows Phone: Vërtetuesi", + "message_2": "Këto aplikacione gjenerojnë automatikisht një kod verifikimi që ndryshon pas një periudhe të caktuar kohe.
    Do t'ju kërkohet të vendosni këtë kod verifikimi sa herë që identifikoheni {{ appName }}.", + "secret_key": "Çelësi juaj sekret është: {{ secret }}", + "secret_key_warning": "Konfiguro aplikacionin tënd TOTP në telefonin tënd me këtë çelës sekret tani, nuk do të mund ta qasesh më vonë.", + "totp_enabled_message": "Autentifikimi me dy faktorë është aktivizuar në llogarinë tuaj.
    Sa herë që identifikoheni {{ appName }}, do t'ju kërkohet një kod verifikimi nga aplikacioni i telefonit tuaj të konfiguruar.
    Nëse e humbni telefonin, nuk do të jeni në gjendje të identifikoheni në llogarinë tuaj, por seancat aktive do t'ju lejojnë të rigjeneroni një çelës sekret.", + "disable_totp": { + "disable_totp": "Çaktivizo vërtetimin me dy faktorë", + "message": "Llogaria juaj nuk do të mbrohet më nga vërtetimi me dy faktorë.", + "confirm_password": "Konfirmoni fjalëkalimin tuaj", + "submit": "Çaktivizo vërtetimin me dy faktorë" + }, + "test_totp": "Ju lutemi shkruani kodin e vërtetimit të shfaqur në telefonin tuaj:", + "test_code_success": "Kodi i verifikimit në rregull", + "test_code_fail": "Ky kod nuk është i vlefshëm, ju lutemi kontrolloni dy herë nëse telefoni juaj është i konfiguruar siç duhet ose çaktivizoni vërtetimin me dy faktorë" + }, + "group": { + "title": "Menaxhimi i grupeve", + "add_group": "Shto një grup", + "name": "Emri", + "edit": { + "delete_group_title": "Fshi grupin", + "delete_group_message": "Dëshiron vërtet ta fshish këtë grup?", + "edit_group_failed_title": "Grupi tashmë ekziston", + "edit_group_failed_message": "Ky emër grupi është marrë tashmë nga një grup tjetër", + "group_used_title": "Grupi në përdorim", + "group_used_message": "Ky grup përdoret në rrjedhën e punës \"{{ name }}\"", + "edit_group_title": "Redakto \"{{ name }}\"", + "add_group_title": "Shto një grup", + "name": "Emri", + "parent_group": "Grupi i prindërve", + "search_group": "Kërkoni një grup", + "members": "Anëtarët", + "new_member": "Anëtar i ri", + "search_user": "Kërkoni një përdorues" + } + }, + "account": { + "title": "Llogaria e përdoruesit", + "password": "Fjalëkalimi", + "password_confirm": "Fjalëkalimi (konfirmo)", + "updated": "Llogaria u përditësua me sukses" + }, + "config": { + "title_guest_access": "Qasja e mysafirëve", + "message_guest_access": "Qasja e mysafirëve është një mënyrë ku çdokush mund të hyjë {{ appName }} pa fjalëkalim.
    Ashtu si një përdorues normal, përdoruesi mysafir mund të qaset vetëm në dokumentet e tij dhe ato të aksesueshme përmes lejeve.
    ", + "enable_guest_access": "Aktivizo qasjen e vizitorëve", + "disable_guest_access": "Çaktivizo qasjen e vizitorëve", + "title_theme": "Personalizimi i temës", + "title_general": "Konfigurimi i përgjithshëm", + "default_language": "Gjuha e parazgjedhur për dokumentet e reja", + "application_name": "Emri i aplikacionit", + "main_color": "Ngjyra kryesore", + "custom_css": "CSS e personalizuar", + "custom_css_placeholder": "CSS e personalizuar për t'u shtuar pas fletës kryesore të stilit", + "logo": "Logo (madhësia katrore)", + "background_image": "Imazhi i sfondit", + "uploading_image": "Po ngarkon imazhin...", + "title_smtp": "Konfigurimi i emailit", + "smtp_hostname": "Emri i hostit SMTP", + "smtp_port": "Porta SMTP", + "smtp_from": "E-mail i dërguesit", + "smtp_username": "Emri i përdoruesit SMTP", + "smtp_password": "Fjalëkalimi SMTP", + "smtp_updated": "Konfigurimi SMTP u përditësua me sukses", + "webhooks": "Uebhooks", + "webhooks_explain": "Webhooks do të thirren kur të ndodhë ngjarja e specifikuar. ", + "webhook_event": "Ngjarja", + "webhook_url": "URL", + "webhook_create_date": "Krijo datë", + "webhook_add": "Shto një uebhook" + }, + "metadata": { + "title": "Konfigurimi i personalizuar i meta të dhënave", + "message": "Këtu mund të shtoni meta të dhëna të personalizuara në dokumentet tuaja si një identifikues i brendshëm ose një datë skadimi. ", + "name": "Emri i meta të dhënave", + "type": "Lloji i meta të dhënave" + }, + "inbox": { + "title": "Skanimi i kutisë hyrëse", + "message": "Duke aktivizuar këtë veçori, sistemi do të skanojë kutinë hyrëse të specifikuar çdo minutë i palexuar emailet dhe i importoni automatikisht.
    Pas importimit të një emaili, ai do të shënohet si i lexuar.
    Cilësimet e konfigurimit për Gmail, Outlook.com, Yahoo.", + "enabled": "Aktivizo skanimin e kutisë hyrëse", + "hostname": "Emri i hostit IMAP", + "port": "Porta IMAP (143 ose 993)", + "starttls": "Aktivizo STARTTLS", + "username": "Emri i përdoruesit IMAP", + "password": "Fjalëkalimi IMAP", + "folder": "Dosja IMAP", + "tag": "Etiketa u shtua në dokumentet e importuara", + "test": "Testoni parametrat", + "last_sync": "Sinkronizimi i fundit: {{ data.date | date: 'medium' }}, {{ data.count }} mesazh{{ data.count > 1 ? 's' : '' }} të importuara", + "test_success": "Lidhja me kutinë hyrëse është e suksesshme ({{ count }} i palexuar mesazh{{ count > 1 ? 's' : '' }})", + "test_fail": "Ndodhi një gabim gjatë lidhjes me kutinë hyrëse, ju lutemi kontrolloni parametrat", + "saved": "Konfigurimi IMAP u ruajt me sukses", + "autoTagsEnabled": "Shtoni automatikisht etiketat nga rreshti i subjektit të shënuar me", + "deleteImported": "Fshi mesazhin nga kutia postare pas importimit" + }, + "monitoring": { + "background_tasks": "Detyrat e sfondit", + "queued_tasks": "Aktualisht ka {{ count }} detyrat në radhë.", + "queued_tasks_explain": "Përpunimi i skedarëve, krijimi i miniaturave, përditësimi i indeksit, njohja optike e karaktereve janë detyra në sfond. ", + "server_logs": "Regjistrat e serverit", + "log_date": "Data", + "log_tag": "Etiketë", + "log_message": "Mesazh", + "indexing": "Indeksimi", + "indexing_info": "Nëse vëreni mospërputhje në rezultatet e kërkimit, mund të provoni të bëni një riindeksim të plotë. ", + "start_reindexing": "Filloni riindeksimin e plotë", + "reindexing_started": "Ri-indeksimi filloi, ju lutemi prisni derisa të mos ketë më detyra në sfond." + }, + "session": { + "title": "Seancat e hapura", + "created_date": "Data e krijimit", + "last_connection_date": "Data e fundit e lidhjes", + "user_agent": "Nga", + "current": "Aktuale", + "current_session": "Ky është sesioni aktual", + "clear_message": "Të gjitha pajisjet e tjera të lidhura me këtë llogari do të shkëputen", + "clear": "Pastro të gjitha seancat e tjera" + }, + "vocabulary": { + "title": "Shënimet e fjalorit", + "choose_vocabulary": "Zgjidhni një fjalor për të redaktuar", + "type": "Lloji", + "coverage": "Mbulimi", + "rights": "Të drejtat", + "value": "Vlera", + "order": "Rendit", + "new_entry": "Hyrje e re" + }, + "fileimporter": { + "title": "Importuesi i skedarëve në masë", + "advanced_users": "Për përdoruesit e avancuar!", + "need_intro": "Nëse ju duhet:", + "need_1": "Importoni një direktori skedarësh menjëherë", + "need_2": "Skanoni një drejtori për skedarë të rinj dhe importojini ato", + "line_1": "Shkoni në sismics/docs/releases dhe shkarkoni mjetin e importuesit të skedarëve për sistemin tuaj.", + "line_2": "Ndiq udhëzime këtu për të përdorur këtë mjet.", + "line_3": "Skedarët tuaj do të importohen në dokumente sipas konfigurimit të importuesit të skedarëve.", + "download": "Shkarko", + "instructions": "Udhëzimet" + } + }, + "feedback": { + "title": "Na jepni një koment", + "message": "Ndonjë sugjerim apo pyetje në lidhje me Teedy? ", + "sent_title": "Komentet u dërguan", + "sent_message": "Faleminderit për komentin tuaj! " + }, + "import": { + "title": "Importimi", + "error_quota": "U arrit kufiri i kuotës, kontaktoni administratorin tuaj për të rritur kuotën tuaj", + "error_general": "Ndodhi një gabim gjatë përpjekjes për të importuar skedarin tuaj, ju lutemi sigurohuni që ai është një skedar i vlefshëm EML" + }, + "app_share": { + "403": { + "title": "I pa autorizuar", + "message": "Dokumenti që po përpiqeni të shikoni nuk ndahet më" + }, + "main": "Kërkoni një lidhje të përbashkët të dokumentit për të hyrë në të" + }, + "directive": { + "acledit": { + "acl_target": "Për", + "acl_permission": "Leja", + "add_permission": "Shto një leje", + "search_user_group": "Kërkoni një përdorues ose grup" + }, + "auditlog": { + "log_created": "krijuar", + "log_updated": "përditësuar", + "log_deleted": "fshihet", + "Acl": "ACL", + "Comment": "Komentoni", + "Document": "Dokumenti", + "File": "Skedari", + "Group": "Grupi", + "Route": "Rrjedha e punës", + "RouteModel": "Modeli i rrjedhës së punës", + "Tag": "Etiketë", + "User": "Përdoruesi", + "Webhook": "Uebhook" + }, + "selectrelation": { + "typeahead": "Shkruani një titull dokumenti" + }, + "selecttag": { + "typeahead": "Shkruani një etiketë" + }, + "datepicker": { + "current": "Sot", + "clear": "Qartë", + "close": "U krye" + } + }, + "filter": { + "filesize": { + "mb": "MB", + "kb": "kB" + } + }, + "acl": { + "READ": "Mund të lexojë", + "READWRITE": "Mund të shkruajë", + "WRITE": "Mund të shkruajë", + "USER": "Përdoruesi", + "GROUP": "Grupi", + "SHARE": "Të përbashkëta" + }, + "workflow_type": { + "VALIDATE": "Vleresimi", + "APPROVE": "Miratimi" + }, + "workflow_transition": { + "APPROVED": "Miratuar", + "REJECTED": "Refuzuar", + "VALIDATED": "E vërtetuar" + }, + "validation": { + "required": "E detyrueshme", + "too_short": "Shumë e shkurtër", + "too_long": "Shume gjate", + "email": "Duhet të jetë një e-mail i vlefshëm", + "password_confirm": "Fjalëkalimi dhe konfirmimi i fjalëkalimit duhet të përputhen", + "number": "Numri i kërkuar", + "no_space": "Hapësirat dhe dy pikat nuk lejohen", + "alphanumeric": "Lejohen vetëm shkronja dhe numra" + }, + "action_type": { + "ADD_TAG": "Shto një etiketë", + "REMOVE_TAG": "Hiq një etiketë", + "PROCESS_FILES": "Përpunoni skedarët" + }, + "pagination": { + "previous": "E mëparshme", + "next": "Tjetra", + "first": "Së pari", + "last": "E fundit" + }, + "onboarding": { + "step1": { + "title": "Hera e parë?", + "description": "Nëse është hera juaj e parë në Teedy, klikoni butonin Next, përndryshe mos ngurroni të më mbyllni." + }, + "step2": { + "title": "Dokumentet", + "description": "Teedy është i organizuar në dokumente dhe çdo dokument përmban skedarë të shumtë." + }, + "step3": { + "title": "Skedarët", + "description": "Mund të shtoni skedarë pas krijimit të një dokumenti ose përpara se të përdorni këtë zonë të ngarkimit të shpejtë." + }, + "step4": { + "title": "Kërko", + "description": "Kjo është mënyra kryesore për të gjetur përsëri dokumentet tuaja. " + }, + "step5": { + "title": "Etiketa", + "description": "Dokumentet mund të organizohen në etiketa (të cilat janë si super-dosje). " + } + }, + "yes": "po", + "no": "Nr", + "ok": "Në rregull", + "cancel": "Anulo", + "share": "Shpërndaje", + "unshare": "Shpërndaje", + "close": "Mbylle", + "add": "Shtoni", + "open": "Hapur", + "see": "Shiko", + "save": "Ruaj", + "export": "Eksporto", + "edit": "Redakto", + "delete": "Fshije", + "rename": "Riemërto", + "download": "Shkarko", + "loading": "Po ngarkohet...", + "send": "Dërgo", + "enabled": "Aktivizuar", + "disabled": "I paaftë" +} \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/locale/zh_CN.json b/docs-web/src/main/webapp/src/locale/zh_CN.json index 8b738a83..823db244 100644 --- a/docs-web/src/main/webapp/src/locale/zh_CN.json +++ b/docs-web/src/main/webapp/src/locale/zh_CN.json @@ -369,6 +369,7 @@ "port": "IMAP端口(143或993)", "username": "IMAP用户名", "password": "IMAP密码", + "folder": "IMAP 件夹中", "tag": "标签添加到导入的文档", "test": "测试参数", "last_sync": "上次同步:{{ data.date | date }},{{ data.count }}消息导入", diff --git a/docs-web/src/main/webapp/src/locale/zh_TW.json b/docs-web/src/main/webapp/src/locale/zh_TW.json index e413c3de..3fd3acbc 100644 --- a/docs-web/src/main/webapp/src/locale/zh_TW.json +++ b/docs-web/src/main/webapp/src/locale/zh_TW.json @@ -369,6 +369,7 @@ "port": "IMAP端口(143或993)", "username": "IMAP用戶名", "password": "IMAP密碼", + "folder": "IMAP 資料夾", "tag": "標籤添加到導入的文檔", "test": "測試參數", "last_sync": "上次同步:{{ data.date | date }},{{data.count}}消息導入", diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selectionrelation.typeahead.html b/docs-web/src/main/webapp/src/partial/docs/directive.selectionrelation.typeahead.html new file mode 100644 index 00000000..51ab4b31 --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selectionrelation.typeahead.html @@ -0,0 +1,4 @@ + +
    +
    {{ match.label.create_date | date: $root.dateFormat }}
    +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html index a5651dc3..c0a00d05 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selectrelation.html @@ -7,6 +7,7 @@ \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html index 2c333a44..d74716f0 100644 --- a/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html +++ b/docs-web/src/main/webapp/src/partial/docs/directive.selecttag.html @@ -1,7 +1,7 @@
    • - {{ tag.name }} + {{ tag.name }}
    • diff --git a/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html b/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html index 993c9724..c36eb53c 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.add.tag.html @@ -1,14 +1,18 @@ -
      +
    -
    +
    - +
    + + + + +
    @@ -58,7 +63,7 @@
    -
    +
    + + +
    + +
    + + +
    + + + + +
    + + + + + + +
    +
    @@ -146,7 +191,7 @@
    - +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.html b/docs-web/src/main/webapp/src/partial/docs/document.html index 3bcf5a38..71c80b9a 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.html @@ -211,6 +211,8 @@
    +
    + @@ -258,7 +260,7 @@
    - + {{ tag.name }}
    @@ -287,9 +289,9 @@ -
    +
    -
    +
    {{ document.title }} ({{ document.file_count }}) diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html index 7ccee7a8..623a24c7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.content.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.content.html @@ -35,6 +35,13 @@ + +
    +
    {{ meta.name }}
    +
    {{ meta.value }}
    +
    {{ meta.value | date: dateFormat }}
    +
    {{ meta.value ? 'yes' : 'no' | translate }}
    +
    @@ -154,7 +161,7 @@
    - + diff --git a/docs-web/src/main/webapp/src/partial/docs/file.view.html b/docs-web/src/main/webapp/src/partial/docs/file.view.html index eef54062..ef796b37 100644 --- a/docs-web/src/main/webapp/src/partial/docs/file.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/file.view.html @@ -41,8 +41,8 @@ img-error="error = true" ng-show="!error && canDisplayPreview()" /> - - + {{ 'settings.menu_workflow' | translate }}{{ 'settings.menu_users' | translate }}{{ 'settings.menu_groups' | translate }} - {{ 'settings.menu_inbox' | translate }}{{ 'settings.menu_vocabularies' | translate }}{{ 'settings.menu_configuration' | translate }} + {{ 'settings.menu_metadata' | translate }} + {{ 'settings.menu_inbox' | translate }} + {{ 'settings.menu_ldap' | translate }}{{ 'settings.menu_monitoring' | translate }} diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html b/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html index 4cb4c109..35b81be2 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.inbox.html @@ -17,6 +17,20 @@ +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    @@ -31,6 +45,13 @@
    +
    + +
    + +
    +
    +
    @@ -46,7 +67,14 @@
    - + +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    + +
    {{ 'filter.filesize.mb' | translate }}
    +
    +
    + +
    + {{ 'validation.number' | translate }} +
    +
    + +
    +
    + +
    +
    + + +
    + {{ saveResult }} +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.metadata.html b/docs-web/src/main/webapp/src/partial/docs/settings.metadata.html new file mode 100644 index 00000000..6c6b4bbc --- /dev/null +++ b/docs-web/src/main/webapp/src/partial/docs/settings.metadata.html @@ -0,0 +1,70 @@ +

    + +

    +

    + +
    +
    +
    + +
    + +
    +
    + {{ 'validation.required' | translate }} + {{ 'validation.too_long' | translate }} +
    +
    + +
    + +
    + +
    +
    + {{ 'validation.required' | translate }} +
    +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index d4d3905e..d019a27f 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -50,7 +50,7 @@

    - @@ -63,7 +63,7 @@

    • - {{ tag.name }} + {{ tag.name }}
    @@ -145,7 +145,7 @@
    - +
    {{ comment.creator }} diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html index 016b866d..a39998b5 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.permissions.html @@ -22,7 +22,7 @@
    -   +   {{ acl[0].source_name }}
    + + + + + + + + + + + + + + +
    {{ 'settings.metadata.name' | translate }}{{ 'settings.metadata.type' | translate }}
    + + + {{ meta.type }} + + +
    + + \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html index 879f7887..2cf9f6e9 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.user.edit.html @@ -9,6 +9,7 @@
    @@ -16,6 +17,7 @@ {{ 'validation.required' | translate }} {{ 'validation.too_short' | translate }} {{ 'validation.too_long' | translate }} + {{ 'validation.alphanumeric' | translate }} @@ -48,8 +50,8 @@
    - +
    {{ 'filter.filesize.mb' | translate }}
    @@ -127,4 +129,4 @@ - \ No newline at end of file + diff --git a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html index cc2aa997..bc0194ad 100644 --- a/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html +++ b/docs-web/src/main/webapp/src/partial/docs/settings.vocabulary.html @@ -11,41 +11,45 @@ + - - - - - - - - - - - - - - - - - - - - - - - -
    {{ 'settings.vocabulary.value' | translate }}{{ 'settings.vocabulary.order' | translate }}
    - - - - - -
     
    - - - - - -
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {{ 'settings.vocabulary.value' | translate }}{{ 'settings.vocabulary.order' | translate }}
    + + + + + +
     
    + + + + + +
    +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.edit.html b/docs-web/src/main/webapp/src/partial/docs/tag.edit.html index 513dd8d7..626b2056 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.edit.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.edit.html @@ -13,16 +13,19 @@
    + ng-maxlength="36" required ng-model="tag.name" ui-validate="{ space: '!$value || $value.indexOf(\' \') == -1 && $value.indexOf(\':\') == -1' }">
    -
    +
    -   +
    +   + + +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/tag.html b/docs-web/src/main/webapp/src/partial/docs/tag.html index a2f3e2dc..bb1fd7c7 100644 --- a/docs-web/src/main/webapp/src/partial/docs/tag.html +++ b/docs-web/src/main/webapp/src/partial/docs/tag.html @@ -13,13 +13,17 @@
    -
    -

    + +

      + + +
    +
    - {{ 'add' | translate }} -

    + ng-maxlength="36" required ng-model="tag.name" ui-validate="{ space: '!$value || $value.indexOf(\' \') == -1 && $value.indexOf(\':\') == -1' }"> +
    + {{ 'add' | translate }} {{ 'validation.no_space' | translate }}
    diff --git a/docs-web/src/main/webapp/src/partial/share/share.html b/docs-web/src/main/webapp/src/partial/share/share.html index 5e87cf92..2173cab9 100644 --- a/docs-web/src/main/webapp/src/partial/share/share.html +++ b/docs-web/src/main/webapp/src/partial/share/share.html @@ -94,7 +94,7 @@
    - +
    {{ comment.creator }} diff --git a/docs-web/src/main/webapp/src/share.html b/docs-web/src/main/webapp/src/share.html index ac317e1e..5bf57f2e 100644 --- a/docs-web/src/main/webapp/src/share.html +++ b/docs-web/src/main/webapp/src/share.html @@ -6,7 +6,7 @@ - + @@ -70,8 +70,12 @@ English Français Deutsch + Italiano Española - русский + Português + Ελληνικά + Pусский + Polski 简体中文 繁體中文 @@ -81,9 +85,13 @@
  • English
  • Français
  • Deutsch
  • +
  • Italiano
  • Española
  • -
  • русский
  • -
  • 简体中文
  • +
  • Português
  • +
  • Ελληνικά
  • +
  • Pусский
  • +
  • Polski
  • +
  • 简体中文
  • 繁體中文
  • @@ -94,4 +102,4 @@
    - \ No newline at end of file + diff --git a/docs-web/src/main/webapp/src/style/main.less b/docs-web/src/main/webapp/src/style/main.less index befba00b..46c696cf 100644 --- a/docs-web/src/main/webapp/src/style/main.less +++ b/docs-web/src/main/webapp/src/style/main.less @@ -120,6 +120,10 @@ ul.tag-tree { cursor: pointer; td { + .tags { + line-height: 190%; + } + .label { margin-left: 5px; } diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 8e983362..37e03ad0 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=23 \ No newline at end of file +db.version=30 diff --git a/docs-web/src/prod/resources/log4j.properties b/docs-web/src/prod/resources/log4j.properties index 130264d3..f58c58d8 100644 --- a/docs-web/src/prod/resources/log4j.properties +++ b/docs-web/src/prod/resources/log4j.properties @@ -6,4 +6,7 @@ log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender log4j.appender.MEMORY.size=1000 log4j.logger.com.sismics=INFO -log4j.logger.org.apache.pdfbox=ERROR \ No newline at end of file +log4j.logger.org.apache.pdfbox=ERROR +log4j.logger.org.glassfish.jersey.servlet.WebComponent=ERROR +log4j.logger.org.apache.directory=ERROR +log4j.logger.org.odftoolkit=ERROR \ No newline at end of file diff --git a/docs-web/src/stress/resources/config.properties b/docs-web/src/stress/resources/config.properties deleted file mode 100644 index 8e983362..00000000 --- a/docs-web/src/stress/resources/config.properties +++ /dev/null @@ -1,3 +0,0 @@ -api.current_version=${project.version} -api.min_version=1.0 -db.version=23 \ No newline at end of file diff --git a/docs-web/src/stress/resources/hibernate.properties b/docs-web/src/stress/resources/hibernate.properties deleted file mode 100644 index b6b34941..00000000 --- a/docs-web/src/stress/resources/hibernate.properties +++ /dev/null @@ -1 +0,0 @@ -\ugggg \ No newline at end of file diff --git a/docs-web/src/stress/resources/log4j.properties b/docs-web/src/stress/resources/log4j.properties deleted file mode 100644 index 0b05e8e9..00000000 --- a/docs-web/src/stress/resources/log4j.properties +++ /dev/null @@ -1,8 +0,0 @@ -log4j.rootCategory=WARN, CONSOLE, MEMORY -log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender -log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout -log4j.appender.CONSOLE.layout.ConversionPattern=%d{DATE} %p %l %m %n -log4j.appender.MEMORY=com.sismics.util.log4j.MemoryAppender -log4j.appender.MEMORY.size=1000 - -log4j.logger.com.sismics=DEBUG diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java index 8365780d..1bcd96ef 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAclResource.java @@ -4,12 +4,12 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.util.Date; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index b4c72fa6..a60b87ea 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -8,12 +8,12 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; /** @@ -243,6 +243,7 @@ public class TestAppResource extends BaseJerseyTest { Assert.assertEquals(993, json.getJsonNumber("port").intValue()); Assert.assertEquals("", json.getString("username")); Assert.assertEquals("", json.getString("password")); + Assert.assertEquals("INBOX", json.getString("folder")); Assert.assertEquals("", json.getString("tag")); JsonObject lastSync = json.getJsonObject("last_sync"); Assert.assertTrue(lastSync.isNull("date")); @@ -254,10 +255,13 @@ public class TestAppResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .post(Entity.form(new Form() .param("enabled", "true") + .param("autoTagsEnabled", "false") + .param("deleteImported", "false") .param("hostname", "localhost") .param("port", "9755") .param("username", "test@sismics.com") .param("password", "12345678") + .param("folder", "INBOX") .param("tag", tagInboxId) ), JsonObject.class); @@ -270,6 +274,7 @@ public class TestAppResource extends BaseJerseyTest { Assert.assertEquals(9755, json.getInt("port")); Assert.assertEquals("test@sismics.com", json.getString("username")); Assert.assertEquals("12345678", json.getString("password")); + Assert.assertEquals("INBOX", json.getString("folder")); Assert.assertEquals(tagInboxId, json.getString("tag")); ServerSetup serverSetupSmtp = new ServerSetup(9754, null, ServerSetup.PROTOCOL_SMTP); @@ -329,4 +334,95 @@ public class TestAppResource extends BaseJerseyTest { greenMail.stop(); } + + /** + * Test the LDAP authentication. + */ + @Test + public void testLdapAuthentication() throws Exception { +// // Start LDAP server +// final DirectoryServiceFactory factory = new DefaultDirectoryServiceFactory(); +// factory.init("Test"); +// +// final DirectoryService directoryService = factory.getDirectoryService(); +// directoryService.getChangeLog().setEnabled(false); +// directoryService.setShutdownHookEnabled(true); +// +// final Partition partition = new AvlPartition(directoryService.getSchemaManager()); +// partition.setId("Test"); +// partition.setSuffixDn(new Dn(directoryService.getSchemaManager(), "o=TEST")); +// partition.initialize(); +// directoryService.addPartition(partition); +// +// final LdapServer ldapServer = new LdapServer(); +// ldapServer.setTransports(new TcpTransport("localhost", 11389)); +// ldapServer.setDirectoryService(directoryService); +// +// directoryService.startup(); +// ldapServer.start(); +// +// // Load test data in LDAP +// new LdifFileLoader(directoryService.getAdminSession(), new File(Resources.getResource("test.ldif").getFile()), null).execute(); +// +// // Login admin +// String adminToken = clientUtil.login("admin", "admin", false); +// +// // Get the LDAP configuration +// JsonObject json = target().path("/app/config_ldap").request() +// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) +// .get(JsonObject.class); +// Assert.assertFalse(json.getBoolean("enabled")); +// +// // Change LDAP configuration +// target().path("/app/config_ldap").request() +// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) +// .post(Entity.form(new Form() +// .param("enabled", "true") +// .param("host", "localhost") +// .param("port", "11389") +// .param("admin_dn", "uid=admin,ou=system") +// .param("admin_password", "secret") +// .param("base_dn", "o=TEST") +// .param("filter", "(&(objectclass=inetOrgPerson)(uid=USERNAME))") +// .param("default_email", "devnull@teedy.io") +// .param("default_storage", "100000000") +// ), JsonObject.class); +// +// // Get the LDAP configuration +// json = target().path("/app/config_ldap").request() +// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) +// .get(JsonObject.class); +// Assert.assertTrue(json.getBoolean("enabled")); +// Assert.assertEquals("localhost", json.getString("host")); +// Assert.assertEquals(11389, json.getJsonNumber("port").intValue()); +// Assert.assertEquals("uid=admin,ou=system", json.getString("admin_dn")); +// Assert.assertEquals("secret", json.getString("admin_password")); +// Assert.assertEquals("o=TEST", json.getString("base_dn")); +// Assert.assertEquals("(&(objectclass=inetOrgPerson)(uid=USERNAME))", json.getString("filter")); +// Assert.assertEquals("devnull@teedy.io", json.getString("default_email")); +// Assert.assertEquals(100000000L, json.getJsonNumber("default_storage").longValue()); +// +// // Login with a LDAP user +// String ldapTopen = clientUtil.login("ldap1", "secret", false); +// +// // Check user informations +// json = target().path("/user").request() +// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) +// .get(JsonObject.class); +// Assert.assertEquals("ldap1@teedy.io", json.getString("email")); +// +// // List all documents +// json = target().path("/document/list") +// .queryParam("sort_column", 3) +// .queryParam("asc", true) +// .request() +// .cookie(TokenBasedSecurityFilter.COOKIE_NAME, ldapTopen) +// .get(JsonObject.class); +// JsonArray documents = json.getJsonArray("documents"); +// Assert.assertEquals(0, documents.size()); +// +// // Stop LDAP server +// ldapServer.stop(); +// directoryService.shutdown(); + } } \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java index d8efd589..7dfecc3e 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAuditLogResource.java @@ -4,10 +4,10 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; import java.util.Date; /** @@ -99,7 +99,7 @@ public class TestAuditLogResource extends BaseJerseyTest { long update1Date = json.getJsonNumber("update_date").longValue(); // Add a file to the document - clientUtil.addFileToDocument("file/wikipedia.pdf", "wikipedia.pdf", auditlog1Token, document1Id); + clientUtil.addFileToDocument(FILE_WIKIPEDIA_PDF, auditlog1Token, document1Id); // Get document 1 json = target().path("/document/" + document1Id).request() @@ -136,4 +136,4 @@ public class TestAuditLogResource extends BaseJerseyTest { } return count; } -} \ No newline at end of file +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java index d3a7c1f4..e5b27440 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestCommentResource.java @@ -2,11 +2,11 @@ package com.sismics.docs.rest; import java.util.Date; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import org.junit.Assert; import org.junit.Test; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java index d500bc83..5061ca5c 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestDocumentResource.java @@ -4,8 +4,6 @@ import com.google.common.io.ByteStreams; import com.google.common.io.Resources; import com.sismics.docs.core.util.DirectoryUtil; import com.sismics.util.filter.TokenBasedSecurityFilter; -import com.sismics.util.mime.MimeType; -import com.sismics.util.mime.MimeTypeUtil; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; @@ -13,13 +11,13 @@ import org.joda.time.format.DateTimeFormat; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.io.InputStream; import java.util.Date; @@ -83,8 +81,17 @@ public class TestDocumentResource extends BaseJerseyTest { .param("create_date", Long.toString(create1Date))), JsonObject.class); String document1Id = json.getString("id"); Assert.assertNotNull(document1Id); - - // Create a document with document1 + + // Add a file to this document + String file1Id = clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, + document1Token, document1Id); + + // Share this document + target().path("/share").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) + .put(Entity.form(new Form().param("id", document1Id)), JsonObject.class); + + // Create another document with document1 json = target().path("/document").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) .put(Entity.form(new Form() @@ -94,16 +101,7 @@ public class TestDocumentResource extends BaseJerseyTest { .param("relations", document1Id)), JsonObject.class); String document2Id = json.getString("id"); Assert.assertNotNull(document2Id); - - // Add a file - String file1Id = clientUtil.addFileToDocument("file/Einstein-Roosevelt-letter.png", - "Einstein-Roosevelt-letter.png", document1Token, document1Id); - // Share this document - target().path("/share").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) - .put(Entity.form(new Form().param("id", document1Id)), JsonObject.class); - // List all documents json = target().path("/document/list") .queryParam("sort_column", 3) @@ -143,16 +141,25 @@ public class TestDocumentResource extends BaseJerseyTest { json = target().path("/document").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document3Token) .put(Entity.form(new Form() - .param("title", "My super title document 3") + .param("title", "My_super_title_document_3") .param("description", "My super description for document 3") .param("language", "eng") .param("create_date", Long.toString(create3Date))), JsonObject.class); String document3Id = json.getString("id"); Assert.assertNotNull(document3Id); - // Add a file - clientUtil.addFileToDocument("file/Einstein-Roosevelt-letter.png", - "Einstein-Roosevelt-letter.png", document3Token, document3Id); + // Add a file to this document + clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, + document3Token, document3Id); + + // Create another document with document3 + json = target().path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document3Token) + .put(Entity.form(new Form() + .param("title", "My_super_title_document_4") + .param("language", "eng")), JsonObject.class); + String document4Id = json.getString("id"); + Assert.assertNotNull(document4Id); // List all documents from document3 json = target().path("/document/list") @@ -162,7 +169,7 @@ public class TestDocumentResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document3Token) .get(JsonObject.class); documents = json.getJsonArray("documents"); - Assert.assertEquals(1, documents.size()); + Assert.assertEquals(2, documents.size()); // Check highlights json = target().path("/document/list") @@ -214,7 +221,11 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(0, searchDocuments("tag:super !tag:hr", document1Token)); Assert.assertEquals(1, searchDocuments("shared:yes", document1Token)); Assert.assertEquals(2, searchDocuments("lang:eng", document1Token)); - Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08 tag:super shared:yes lang:eng title description full:uranium", document1Token)); + Assert.assertEquals(1, searchDocuments("mime:image/png", document1Token)); + Assert.assertEquals(0, searchDocuments("mime:empty/void", document1Token)); + Assert.assertEquals(1, searchDocuments("after:2010 before:2040-08 tag:super shared:yes lang:eng simple:title simple:description full:uranium", document1Token)); + Assert.assertEquals(1, searchDocuments("title:My_super_title_document_3", document3Token)); + Assert.assertEquals(2, searchDocuments("title:My_super_title_document_3 title:My_super_title_document_4", document3Token)); // Search documents (nothing) Assert.assertEquals(0, searchDocuments("random", document1Token)); @@ -226,6 +237,7 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(0, searchDocuments("before:2040-05-38", document1Token)); Assert.assertEquals(0, searchDocuments("tag:Nop", document1Token)); Assert.assertEquals(0, searchDocuments("lang:fra", document1Token)); + Assert.assertEquals(0, searchDocuments("title:Unknown title", document3Token)); // Get document 1 json = target().path("/document/" + document1Id).request() @@ -260,7 +272,8 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(document2Id, relations.getJsonObject(0).getString("id")); Assert.assertFalse(relations.getJsonObject(0).getBoolean("source")); Assert.assertEquals("My super title document 2", relations.getJsonObject(0).getString("title")); - + Assert.assertFalse(json.containsKey("files")); + // Get document 2 json = target().path("/document/" + document2Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -271,7 +284,8 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(document1Id, relations.getJsonObject(0).getString("id")); Assert.assertTrue(relations.getJsonObject(0).getBoolean("source")); Assert.assertEquals("My super title document 1", relations.getJsonObject(0).getString("title")); - + Assert.assertFalse(json.containsKey("files")); + // Create a tag json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -326,7 +340,26 @@ public class TestDocumentResource extends BaseJerseyTest { .get(JsonObject.class); documents = json.getJsonArray("documents"); Assert.assertEquals(1, documents.size()); - + Assert.assertEquals(document1Id, documents.getJsonObject(0).getString("id")); + Assert.assertFalse(documents.getJsonObject(0).containsKey("files")); + + // Search documents by query with files + json = target().path("/document/list") + .queryParam("files", true) + .queryParam("search", "new") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) + .get(JsonObject.class); + documents = json.getJsonArray("documents"); + Assert.assertEquals(1, documents.size()); + Assert.assertEquals(1, documents.size()); + Assert.assertEquals(document1Id, documents.getJsonObject(0).getString("id")); + JsonArray files = documents.getJsonObject(0).getJsonArray("files"); + Assert.assertEquals(1, files.size()); + Assert.assertEquals(file1Id, files.getJsonObject(0).getString("id")); + Assert.assertEquals("Einstein-Roosevelt-letter.png", files.getJsonObject(0).getString("name")); + Assert.assertEquals("image/png", files.getJsonObject(0).getString("mimetype")); + // Get document 1 json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -349,7 +382,20 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals("document1", contributors.getJsonObject(0).getString("username")); relations = json.getJsonArray("relations"); Assert.assertEquals(0, relations.size()); - + Assert.assertFalse(json.containsKey("files")); + + // Get document 1 with its files + json = target().path("/document/" + document1Id) + .queryParam("files", true) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) + .get(JsonObject.class); + files = json.getJsonArray("files"); + Assert.assertEquals(1, files.size()); + Assert.assertEquals(file1Id, files.getJsonObject(0).getString("id")); + Assert.assertEquals("Einstein-Roosevelt-letter.png", files.getJsonObject(0).getString("name")); + Assert.assertEquals("image/png", files.getJsonObject(0).getString("mimetype")); + // Get document 2 json = target().path("/document/" + document1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) @@ -362,7 +408,13 @@ public class TestDocumentResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) .delete(JsonObject.class); Assert.assertEquals("ok", json.getString("status")); - + + // Deletes a non-existing document + response = target().path("/document/69b79238-84bb-4263-a32f-9cbdf8c92188").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, document1Token) + .delete(); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + // Check that the associated files are deleted from FS java.io.File storedFile = DirectoryUtil.getStorageDirectory().resolve(file1Id).toFile(); java.io.File webFile = DirectoryUtil.getStorageDirectory().resolve(file1Id + "_web").toFile(); @@ -406,22 +458,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentOdtToken = clientUtil.login("document_odt"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentOdtToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentOdtToken); // Add a PDF file - String file1Id = clientUtil.addFileToDocument("file/document.odt", "document.odt", documentOdtToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_DOCUMENT_ODT, documentOdtToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:ipsum") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentOdtToken) @@ -437,7 +480,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Export a document in PDF format response = target().path("/document/" + document1Id + "/pdf") @@ -466,22 +508,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentDocxToken = clientUtil.login("document_docx"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentDocxToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentDocxToken); // Add a PDF file - String file1Id = clientUtil.addFileToDocument("file/document.docx", "document.docx", documentDocxToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_DOCUMENT_DOCX, documentDocxToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:dolor") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentDocxToken) @@ -497,7 +530,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Export a document in PDF format response = target().path("/document/" + document1Id + "/pdf") @@ -526,22 +558,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentPdfToken = clientUtil.login("document_pdf"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPdfToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentPdfToken); // Add a PDF file - String file1Id = clientUtil.addFileToDocument("file/wikipedia.pdf", "wikipedia.pdf", documentPdfToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_WIKIPEDIA_PDF, documentPdfToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:vrandecic") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPdfToken) @@ -557,7 +580,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Export a document in PDF format response = target().path("/document/" + document1Id + "/pdf") @@ -586,22 +608,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentPlainToken = clientUtil.login("document_plain"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPlainToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentPlainToken); // Add a plain text file - String file1Id = clientUtil.addFileToDocument("file/document.txt", "document.txt", documentPlainToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_DOCUMENT_TXT, documentPlainToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:love") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPlainToken) @@ -617,7 +630,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Get the content data response = target().path("/file/" + file1Id + "/data") @@ -656,22 +668,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentVideoToken = clientUtil.login("document_video"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentVideoToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentVideoToken); // Add a video file - String file1Id = clientUtil.addFileToDocument("file/video.webm", "video.webm", documentVideoToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_VIDEO_WEBM, documentVideoToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:vp9") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentVideoToken) @@ -687,7 +690,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Export a document in PDF format response = target().path("/document/" + document1Id + "/pdf") @@ -716,22 +718,13 @@ public class TestDocumentResource extends BaseJerseyTest { String documentPptxToken = clientUtil.login("document_pptx"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPptxToken) - .put(Entity.form(new Form() - .param("title", "My super title document 1") - .param("description", "My super description for document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(documentPptxToken); // Add a PPTX file - String file1Id = clientUtil.addFileToDocument("file/apache.pptx", "apache.pptx", documentPptxToken, document1Id); + String file1Id = clientUtil.addFileToDocument(FILE_APACHE_PPTX, documentPptxToken, document1Id); // Search documents by query in full content - json = target().path("/document/list") + JsonObject json = target().path("/document/list") .queryParam("search", "full:scaling") .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, documentPptxToken) @@ -747,7 +740,6 @@ public class TestDocumentResource extends BaseJerseyTest { InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); 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, null)); // Export a document in PDF format response = target().path("/document/" + document1Id + "/pdf") @@ -819,4 +811,190 @@ public class TestDocumentResource extends BaseJerseyTest { Assert.assertEquals(279276L, files.getJsonObject(1).getJsonNumber("size").longValue()); Assert.assertEquals("application/pdf", files.getJsonObject(1).getString("mimetype")); } -} \ No newline at end of file + + /** + * Test custom metadata. + */ + @Test + public void testCustomMetadata() { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + + // Login metadata1 + clientUtil.createUser("metadata1"); + String metadata1Token = clientUtil.login("metadata1"); + + // Create some metadata with admin + JsonObject json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "0str") + .param("type", "STRING")), JsonObject.class); + String metadataStrId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "1int") + .param("type", "INTEGER")), JsonObject.class); + String metadataIntId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "2float") + .param("type", "FLOAT")), JsonObject.class); + String metadataFloatId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "3date") + .param("type", "DATE")), JsonObject.class); + String metadataDateId = json.getString("id"); + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "4bool") + .param("type", "BOOLEAN")), JsonObject.class); + String metadataBoolId = json.getString("id"); + + // Create a document with metadata1 + json = target().path("/document").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .put(Entity.form(new Form() + .param("title", "Metadata 1") + .param("language", "eng") + .param("metadata_id", metadataStrId) + .param("metadata_id", metadataIntId) + .param("metadata_id", metadataFloatId) + .param("metadata_value", "my string") + .param("metadata_value", "50") + .param("metadata_value", "12.4")), JsonObject.class); + String document1Id = json.getString("id"); + + // Check the values + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .get(JsonObject.class); + JsonArray metadata = json.getJsonArray("metadata"); + Assert.assertEquals(5, metadata.size()); + JsonObject meta = metadata.getJsonObject(0); + Assert.assertEquals(metadataStrId, meta.getString("id")); + Assert.assertEquals("0str", meta.getString("name")); + Assert.assertEquals("STRING", meta.getString("type")); + Assert.assertEquals("my string", meta.getString("value")); + meta = metadata.getJsonObject(1); + Assert.assertEquals(metadataIntId, meta.getString("id")); + Assert.assertEquals("1int", meta.getString("name")); + Assert.assertEquals("INTEGER", meta.getString("type")); + Assert.assertEquals(50, meta.getInt("value")); + meta = metadata.getJsonObject(2); + Assert.assertEquals(metadataFloatId, meta.getString("id")); + Assert.assertEquals("2float", meta.getString("name")); + Assert.assertEquals("FLOAT", meta.getString("type")); + Assert.assertEquals(12.4, meta.getJsonNumber("value").doubleValue(), 0); + meta = metadata.getJsonObject(3); + Assert.assertEquals(metadataDateId, meta.getString("id")); + Assert.assertEquals("3date", meta.getString("name")); + Assert.assertEquals("DATE", meta.getString("type")); + Assert.assertFalse(meta.containsKey("value")); + meta = metadata.getJsonObject(4); + Assert.assertEquals(metadataBoolId, meta.getString("id")); + Assert.assertEquals("4bool", meta.getString("name")); + Assert.assertEquals("BOOLEAN", meta.getString("type")); + Assert.assertFalse(meta.containsKey("value")); + + // Update the document with metadata1 (add more metadata) + long dateValue = new Date().getTime(); + target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .post(Entity.form(new Form() + .param("title", "Metadata 1") + .param("language", "eng") + .param("metadata_id", metadataStrId) + .param("metadata_id", metadataIntId) + .param("metadata_id", metadataFloatId) + .param("metadata_id", metadataDateId) + .param("metadata_id", metadataBoolId) + .param("metadata_value", "my string 2") + .param("metadata_value", "52") + .param("metadata_value", "14.4") + .param("metadata_value", Long.toString(dateValue)) + .param("metadata_value", "true")), JsonObject.class); + + // Check the values + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .get(JsonObject.class); + metadata = json.getJsonArray("metadata"); + Assert.assertEquals(5, metadata.size()); + meta = metadata.getJsonObject(0); + Assert.assertEquals(metadataStrId, meta.getString("id")); + Assert.assertEquals("0str", meta.getString("name")); + Assert.assertEquals("STRING", meta.getString("type")); + Assert.assertEquals("my string 2", meta.getString("value")); + meta = metadata.getJsonObject(1); + Assert.assertEquals(metadataIntId, meta.getString("id")); + Assert.assertEquals("1int", meta.getString("name")); + Assert.assertEquals("INTEGER", meta.getString("type")); + Assert.assertEquals(52, meta.getInt("value")); + meta = metadata.getJsonObject(2); + Assert.assertEquals(metadataFloatId, meta.getString("id")); + Assert.assertEquals("2float", meta.getString("name")); + Assert.assertEquals("FLOAT", meta.getString("type")); + Assert.assertEquals(14.4, meta.getJsonNumber("value").doubleValue(), 0); + meta = metadata.getJsonObject(3); + Assert.assertEquals(metadataDateId, meta.getString("id")); + Assert.assertEquals("3date", meta.getString("name")); + Assert.assertEquals("DATE", meta.getString("type")); + Assert.assertEquals(dateValue, meta.getJsonNumber("value").longValue()); + meta = metadata.getJsonObject(4); + Assert.assertEquals(metadataBoolId, meta.getString("id")); + Assert.assertEquals("4bool", meta.getString("name")); + Assert.assertEquals("BOOLEAN", meta.getString("type")); + Assert.assertTrue(meta.getBoolean("value")); + + // Update the document with metadata1 (remove some metadata) + target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .post(Entity.form(new Form() + .param("title", "Metadata 1") + .param("language", "eng") + .param("metadata_id", metadataFloatId) + .param("metadata_id", metadataDateId) + .param("metadata_id", metadataBoolId) + .param("metadata_value", "14.4") + .param("metadata_value", Long.toString(dateValue)) + .param("metadata_value", "true")), JsonObject.class); + + // Check the values + json = target().path("/document/" + document1Id).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, metadata1Token) + .get(JsonObject.class); + metadata = json.getJsonArray("metadata"); + Assert.assertEquals(5, metadata.size()); + meta = metadata.getJsonObject(0); + Assert.assertEquals(metadataStrId, meta.getString("id")); + Assert.assertEquals("0str", meta.getString("name")); + Assert.assertEquals("STRING", meta.getString("type")); + Assert.assertFalse(meta.containsKey("value")); + meta = metadata.getJsonObject(1); + Assert.assertEquals(metadataIntId, meta.getString("id")); + Assert.assertEquals("1int", meta.getString("name")); + Assert.assertEquals("INTEGER", meta.getString("type")); + Assert.assertFalse(meta.containsKey("value")); + meta = metadata.getJsonObject(2); + Assert.assertEquals(metadataFloatId, meta.getString("id")); + Assert.assertEquals("2float", meta.getString("name")); + Assert.assertEquals("FLOAT", meta.getString("type")); + Assert.assertEquals(14.4, meta.getJsonNumber("value").doubleValue(), 0); + meta = metadata.getJsonObject(3); + Assert.assertEquals(metadataDateId, meta.getString("id")); + Assert.assertEquals("3date", meta.getString("name")); + Assert.assertEquals("DATE", meta.getString("type")); + Assert.assertEquals(dateValue, meta.getJsonNumber("value").longValue()); + meta = metadata.getJsonObject(4); + Assert.assertEquals(metadataBoolId, meta.getString("id")); + Assert.assertEquals("4bool", meta.getString("name")); + Assert.assertEquals("BOOLEAN", meta.getString("type")); + Assert.assertTrue(meta.getBoolean("value")); + } +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index b86c9939..43783d8a 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -12,17 +12,18 @@ import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Date; +import java.util.zip.ZipInputStream; /** * Exhaustive test of the file resource. @@ -37,53 +38,18 @@ public class TestFileResource extends BaseJerseyTest { */ @Test public void testFileResource() throws Exception { - // Login file1 - clientUtil.createUser("file1"); - String file1Token = clientUtil.login("file1"); + // Login file_resources + clientUtil.createUser("file_resources"); + String file1Token = clientUtil.login("file_resources"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) - .put(Entity.form(new Form() - .param("title", "File test document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(file1Token); // Add a file - String file1Id; - try (InputStream is = Resources.getResource("file/PIA00452.jpg").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "PIA00452.jpg"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - Assert.assertEquals(163510L, json.getJsonNumber("size").longValue()); - } - } + String file1Id = clientUtil.addFileToDocument(FILE_PIA_00452_JPG, file1Token, document1Id); // Add a file - String file2Id; - try (InputStream is = Resources.getResource("file/PIA00452.jpg").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "PIA00452.jpg"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file2Id = json.getString("id"); - Assert.assertNotNull(file2Id); - } - } + String file2Id = clientUtil.addFileToDocument(FILE_PIA_00452_JPG, file1Token, document1Id); // Get the file data Response response = target().path("/file/" + file1Id + "/data").request() @@ -91,7 +57,6 @@ public class TestFileResource extends BaseJerseyTest { .get(); InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); Assert.assertTrue(fileBytes.length > 0); // Get the thumbnail data @@ -103,7 +68,6 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); is = (InputStream) response.getEntity(); fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); Assert.assertTrue(fileBytes.length > 0); // Get the content data @@ -123,7 +87,6 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); is = (InputStream) response.getEntity(); fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); Assert.assertTrue(fileBytes.length > 0); // Check that the files are not readable directly from FS @@ -131,7 +94,7 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals(MimeType.DEFAULT, MimeTypeUtil.guessMimeType(storedFile, null)); // Get all files from a document - json = target().path("/file/list") + JsonObject json = target().path("/file/list") .queryParam("id", document1Id) .request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) @@ -142,7 +105,7 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals("PIA00452.jpg", files.getJsonObject(0).getString("name")); Assert.assertEquals("image/jpeg", files.getJsonObject(0).getString("mimetype")); Assert.assertEquals(0, files.getJsonObject(0).getInt("version")); - Assert.assertEquals(163510L, files.getJsonObject(0).getJsonNumber("size").longValue()); + Assert.assertEquals(FILE_PIA_00452_JPG_SIZE, files.getJsonObject(0).getJsonNumber("size").longValue()); Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id")); Assert.assertEquals("PIA00452.jpg", files.getJsonObject(1).getString("name")); Assert.assertEquals(0, files.getJsonObject(1).getInt("version")); @@ -191,10 +154,7 @@ public class TestFileResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) .get(); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); - is = (InputStream) response.getEntity(); - fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.APPLICATION_ZIP, MimeTypeUtil.guessMimeType(fileBytes, null)); - + // Deletes a file json = target().path("/file/" + file1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) @@ -293,6 +253,66 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals("document.txt", files.getJsonObject(0).getString("name")); Assert.assertEquals(1, files.getJsonObject(0).getInt("version")); } + + @Test + public void testFileResourceZip() throws Exception { + // Login file_resources + clientUtil.createUser("file_resources_zip"); + String file1Token = clientUtil.login("file_resources_zip"); + + // Create a document + String document1Id = clientUtil.createDocument(file1Token); + + // Add a file + String file1Id = clientUtil.addFileToDocument(FILE_PIA_00452_JPG, file1Token, document1Id); + + // Get a ZIP from all files of the document + Response response = target().path("/file/zip") + .queryParam("id", document1Id) + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) + .get(); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + InputStream is = (InputStream) response.getEntity(); + ZipInputStream zipInputStream = new ZipInputStream(is); + Assert.assertEquals(zipInputStream.getNextEntry().getName(), "0-PIA00452.jpg"); + Assert.assertNull(zipInputStream.getNextEntry()); + + // Fail if we don't have access to the document + response = target().path("/file/zip") + .queryParam("id", document1Id) + .request() + .get(); + Assert.assertEquals(Status.NOT_FOUND, Status.fromStatusCode(response.getStatus())); + + // Create a document + String document2Id = clientUtil.createDocument(file1Token); + + // Add a file + String file2Id = clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, file1Token, document2Id); + + // Get a ZIP from both files + response = target().path("/file/zip") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) + .post(Entity.form(new Form() + .param("files", file1Id) + .param("files", file2Id))); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + is = (InputStream) response.getEntity(); + zipInputStream = new ZipInputStream(is); + Assert.assertNotNull(zipInputStream.getNextEntry().getName()); + Assert.assertNotNull(zipInputStream.getNextEntry().getName()); + Assert.assertNull(zipInputStream.getNextEntry()); + + // Fail if we don't have access to the files + response = target().path("/file/zip") + .request() + .post(Entity.form(new Form() + .param("files", file1Id) + .param("files", file2Id))); + Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); + } /** * Test using a ZIP file. @@ -300,38 +320,16 @@ public class TestFileResource extends BaseJerseyTest { * @throws Exception e */ @Test - public void testZipFile() throws Exception { - // Login file1 - clientUtil.createUser("file2"); - String file2Token = clientUtil.login("file2"); + public void testZipFileUpload() throws Exception { + // Login file_zip + clientUtil.createUser("file_zip"); + String fileZipToken = clientUtil.login("file_zip"); // Create a document - long create1Date = new Date().getTime(); - JsonObject json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file2Token) - .put(Entity.form(new Form() - .param("title", "File test document 1") - .param("language", "eng") - .param("create_date", Long.toString(create1Date))), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + String document1Id = clientUtil.createDocument(fileZipToken); // Add a file - String file1Id; - try (InputStream is = Resources.getResource("file/wikipedia.zip").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "wikipedia.zip"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file2Token) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - Assert.assertEquals(525069L, json.getJsonNumber("size").longValue()); - } - } + clientUtil.addFileToDocument(FILE_WIKIPEDIA_ZIP, fileZipToken, document1Id); } /** @@ -341,29 +339,16 @@ public class TestFileResource extends BaseJerseyTest { */ @Test public void testOrphanFile() throws Exception { - // Login file3 - clientUtil.createUser("file3"); - String file3Token = clientUtil.login("file3"); + // Login file_orphan + clientUtil.createUser("file_orphan"); + String fileOrphanToken = clientUtil.login("file_orphan"); // Add a file - String file1Id; - try (InputStream is = Resources.getResource("file/PIA00452.jpg").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "PIA00452.jpg"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - JsonObject json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } + String file1Id = clientUtil.addFileToDocument(FILE_PIA_00452_JPG, fileOrphanToken, null); // Get all orphan files JsonObject json = target().path("/file/list").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .get(JsonObject.class); JsonArray files = json.getJsonArray("files"); Assert.assertEquals(1, files.size()); @@ -372,66 +357,45 @@ public class TestFileResource extends BaseJerseyTest { Response response = target().path("/file/" + file1Id + "/data") .queryParam("size", "thumb") .request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .get(); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); InputStream is = (InputStream) response.getEntity(); byte[] fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); Assert.assertTrue(fileBytes.length > 0); // Get the file data response = target().path("/file/" + file1Id + "/data").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .get(); is = (InputStream) response.getEntity(); fileBytes = ByteStreams.toByteArray(is); - Assert.assertEquals(MimeType.IMAGE_JPEG, MimeTypeUtil.guessMimeType(fileBytes, null)); - Assert.assertEquals(163510, fileBytes.length); + Assert.assertEquals(FILE_PIA_00452_JPG_SIZE, fileBytes.length); - // Create a document - json = target().path("/document").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) - .put(Entity.form(new Form() - .param("title", "File test document 1") - .param("language", "eng")), JsonObject.class); - String document1Id = json.getString("id"); - Assert.assertNotNull(document1Id); + // Create another document + String document2Id = clientUtil.createDocument(fileOrphanToken); // Attach a file to a document target().path("/file/" + file1Id + "/attach").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .post(Entity.form(new Form() - .param("id", document1Id)), JsonObject.class); + .param("id", document2Id)), JsonObject.class); // Get all files from a document json = target().path("/file/list") - .queryParam("id", document1Id) + .queryParam("id", document2Id) .request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .get(JsonObject.class); files = json.getJsonArray("files"); Assert.assertEquals(1, files.size()); // Add a file - String file2Id; - try (InputStream is0 = Resources.getResource("file/PIA00452.jpg").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is0, "PIA00452.jpg"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file2Id = json.getString("id"); - Assert.assertNotNull(file2Id); - } - } + String file2Id = clientUtil.addFileToDocument(FILE_PIA_00452_JPG, fileOrphanToken, null); // Deletes a file json = target().path("/file/" + file2Id).request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileOrphanToken) .delete(JsonObject.class); Assert.assertEquals("ok", json.getString("status")); } @@ -448,90 +412,38 @@ public class TestFileResource extends BaseJerseyTest { String fileQuotaToken = clientUtil.login("file_quota"); // Add a file (292641 bytes large) - String file1Id; - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - JsonObject json = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - file1Id = json.getString("id"); - Assert.assertNotNull(file1Id); - } - } - + String file1Id = clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null); + // Check current quota - JsonObject json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(292641L, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE, getUserQuota(fileQuotaToken)); // Add a file (292641 bytes large) - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - } - } + clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null); // Check current quota - json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken)); // Add a file (292641 bytes large) - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - } - } + clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null); // Check current quota - json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(877923L, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 3, getUserQuota(fileQuotaToken)); // Add a file (292641 bytes large) - try (InputStream is = Resources.getResource("file/Einstein-Roosevelt-letter.png").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "Einstein-Roosevelt-letter.png"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - Response response = target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .put(Entity.entity(multiPart.bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE)); - Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - } + try { + clientUtil.addFileToDocument(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG, fileQuotaToken, null); + Assert.fail(); + } catch (jakarta.ws.rs.BadRequestException ignored) { } // Deletes a file - json = target().path("/file/" + file1Id).request() + JsonObject json = target().path("/file/" + file1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) .delete(JsonObject.class); Assert.assertEquals("ok", json.getString("status")); // Check current quota - json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken)); // Create a document long create1Date = new Date().getTime(); @@ -545,23 +457,10 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertNotNull(document1Id); // Add a file to this document (163510 bytes large) - try (InputStream is = Resources.getResource("file/PIA00452.jpg").openStream()) { - StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart("file", is, "PIA00452.jpg"); - try (FormDataMultiPart multiPart = new FormDataMultiPart()) { - target() - .register(MultiPartFeature.class) - .path("/file").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .put(Entity.entity(multiPart.field("id", document1Id).bodyPart(streamDataBodyPart), - MediaType.MULTIPART_FORM_DATA_TYPE), JsonObject.class); - } - } + clientUtil.addFileToDocument(FILE_PIA_00452_JPG, fileQuotaToken, document1Id); // Check current quota - json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(748792, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2 + FILE_PIA_00452_JPG_SIZE, getUserQuota(fileQuotaToken)); // Deletes the document json = target().path("/document/" + document1Id).request() @@ -570,9 +469,12 @@ public class TestFileResource extends BaseJerseyTest { Assert.assertEquals("ok", json.getString("status")); // Check current quota - json = target().path("/user").request() - .cookie(TokenBasedSecurityFilter.COOKIE_NAME, fileQuotaToken) - .get(JsonObject.class); - Assert.assertEquals(585282L, json.getJsonNumber("storage_current").longValue()); + Assert.assertEquals(FILE_EINSTEIN_ROOSEVELT_LETTER_PNG_SIZE * 2, getUserQuota(fileQuotaToken)); } -} \ No newline at end of file + + private long getUserQuota(String userToken) { + return target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, userToken) + .get(JsonObject.class).getJsonNumber("storage_current").longValue(); + } +} diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java index 785ea611..d68a5b3d 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestGroupResource.java @@ -4,11 +4,11 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java new file mode 100644 index 00000000..656a144c --- /dev/null +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestMetadataResource.java @@ -0,0 +1,82 @@ +package com.sismics.docs.rest; + +import com.sismics.util.filter.TokenBasedSecurityFilter; +import org.junit.Assert; +import org.junit.Test; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; + + +/** + * Test the metadata resource. + * + * @author bgamard + */ +public class TestMetadataResource extends BaseJerseyTest { + /** + * Test the metadata resource. + */ + @Test + public void testMetadataResource() { + // Login admin + String adminToken = clientUtil.login("admin", "admin", false); + + // Get all metadata with admin + JsonObject json = target().path("/metadata") + .queryParam("sort_column", "2") + .queryParam("asc", "false") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + JsonArray metadata = json.getJsonArray("metadata"); + Assert.assertEquals(0, metadata.size()); + + // Create a metadata with admin + json = target().path("/metadata").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .put(Entity.form(new Form() + .param("name", "ISBN 13") + .param("type", "STRING")), JsonObject.class); + String metadataIsbnId = json.getString("id"); + Assert.assertNotNull(metadataIsbnId); + Assert.assertEquals("ISBN 13", json.getString("name")); + Assert.assertEquals("STRING", json.getString("type")); + + // Get all metadata with admin + json = target().path("/metadata") + .queryParam("sort_column", "2") + .queryParam("asc", "false") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + metadata = json.getJsonArray("metadata"); + Assert.assertEquals(1, metadata.size()); + + // Update a metadata with admin + json = target().path("/metadata/" + metadataIsbnId).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .post(Entity.form(new Form() + .param("name", "ISBN 10")), JsonObject.class); + Assert.assertEquals(metadataIsbnId, json.getString("id")); + Assert.assertEquals("ISBN 10", json.getString("name")); + Assert.assertEquals("STRING", json.getString("type")); + + // Delete a metadata with admin + target().path("/metadata/" + metadataIsbnId).request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .delete(JsonObject.class); + + // Get all metadata with admin + json = target().path("/metadata") + .queryParam("sort_column", "2") + .queryParam("asc", "false") + .request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) + .get(JsonObject.class); + metadata = json.getJsonArray("metadata"); + Assert.assertEquals(0, metadata.size()); + } +} \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java index c6fd94eb..2adf0ca1 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteModelResource.java @@ -4,10 +4,10 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; /** diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java index 082a4e69..fc09d63b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestRouteResource.java @@ -4,11 +4,11 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; /** @@ -226,7 +226,7 @@ public class TestRouteResource extends BaseJerseyTest { .param("transition", "APPROVED")), JsonObject.class); Assert.assertFalse(json.containsKey("route_step")); Assert.assertTrue(json.getBoolean("readable")); // Admin can read everything - Assert.assertTrue(popEmail().contains("workflow step")); + Assert.assertNull(popEmail()); // Last step does not send any email // Get the route on document 1 json = target().path("/route") diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java index 602415a3..d5984d45 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java @@ -1,15 +1,15 @@ package com.sismics.docs.rest; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import com.sismics.util.filter.HeaderBasedSecurityFilter; import org.junit.Assert; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.junit.Test; import com.sismics.util.filter.TokenBasedSecurityFilter; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java index 5dc86364..0d465423 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestShareResource.java @@ -2,13 +2,13 @@ package com.sismics.docs.rest; import java.io.InputStream; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java index b8f4b5fa..0b0e5dcd 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestTagResource.java @@ -4,12 +4,12 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; /** * Test the tag resource. @@ -25,7 +25,23 @@ public class TestTagResource extends BaseJerseyTest { // Login tag1 clientUtil.createUser("tag1"); String tag1Token = clientUtil.login("tag1"); - + + // Create a tag with a wrong name + Response response = target().path("/tag").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .put(Entity.form(new Form() + .param("name", "Tag:3") + .param("color", "#ff0000"))); + Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + + // Create a tag with a wrong name + response = target().path("/tag").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) + .put(Entity.form(new Form() + .param("name", "Tag 3") + .param("color", "#ff0000"))); + Assert.assertEquals(Status.BAD_REQUEST, Status.fromStatusCode(response.getStatus())); + // Create a tag JsonObject json = target().path("/tag").request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) @@ -46,7 +62,7 @@ public class TestTagResource extends BaseJerseyTest { Assert.assertNotNull(tag4Id); // Create a circular reference - Response response = target().path("/tag/" + tag3Id).request() + response = target().path("/tag/" + tag3Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, tag1Token) .post(Entity.form(new Form() .param("name", "Tag3") diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java index 5f314476..6d085bce 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestThemeResource.java @@ -8,11 +8,11 @@ import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.InputStream; /** diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java index 2aeab9fa..e967f46b 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestUserResource.java @@ -5,12 +5,12 @@ import com.sismics.util.totp.GoogleAuthenticator; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; import java.util.Date; import java.util.Locale; import java.util.regex.Matcher; @@ -439,13 +439,11 @@ public class TestUserResource extends BaseJerseyTest { // Create absent_minded who lost his password clientUtil.createUser("absent_minded"); - // User no_such_user try to recovery its password: invalid user - Response response = target().path("/user/password_lost").request() + // User no_such_user try to recovery its password: silently do nothing to avoid leaking users + JsonObject json = target().path("/user/password_lost").request() .post(Entity.form(new Form() - .param("username", "no_such_user"))); - Assert.assertEquals(Response.Status.BAD_REQUEST, Response.Status.fromStatusCode(response.getStatus())); - JsonObject json = response.readEntity(JsonObject.class); - Assert.assertEquals("UserNotFound", json.getString("type")); + .param("username", "no_such_user")), JsonObject.class); + Assert.assertEquals("ok", json.getString("status")); // User absent_minded try to recovery its password: OK json = target().path("/user/password_lost").request() @@ -461,7 +459,7 @@ public class TestUserResource extends BaseJerseyTest { String key = keyMatcher.group(1).replaceAll("=", ""); // User absent_minded resets its password: invalid key - response = target().path("/user/password_reset").request() + Response response = target().path("/user/password_reset").request() .post(Entity.form(new Form() .param("key", "no_such_key") .param("password", "87654321"))); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java index 9fd83517..d45f8966 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestVocabularyResource.java @@ -1,15 +1,14 @@ package com.sismics.docs.rest; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; - +import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import com.sismics.util.filter.TokenBasedSecurityFilter; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; /** * Exhaustive test of the vocabulary resource. @@ -100,7 +99,7 @@ public class TestVocabularyResource extends BaseJerseyTest { Assert.assertEquals(1, entry.getJsonNumber("order").intValue()); // Delete a vocabulary entry with admin - json = target().path("/vocabulary/" + vocabulary1Id).request() + target().path("/vocabulary/" + vocabulary1Id).request() .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .delete(JsonObject.class); diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java index 08a34a34..6a10281c 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestWebhookResource.java @@ -5,10 +5,10 @@ import com.sismics.util.filter.TokenBasedSecurityFilter; import org.junit.Assert; import org.junit.Test; -import javax.json.JsonArray; -import javax.json.JsonObject; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Form; +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Form; import java.util.Date; diff --git a/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java b/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java index b3082b00..c81d9a47 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/resource/ThirdPartyWebhookResource.java @@ -1,9 +1,11 @@ package com.sismics.docs.rest.resource; -import javax.json.JsonObject; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Response; +import jakarta.json.JsonObject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; /** * Webhook REST resources. @@ -23,6 +25,7 @@ public class ThirdPartyWebhookResource extends BaseResource { * @return Response */ @POST + @Consumes(MediaType.APPLICATION_JSON) public Response webhook(JsonObject request) { lastPayload = request; return Response.ok().build(); diff --git a/docs-web/src/test/resources/hibernate.properties b/docs-web/src/test/resources/hibernate.properties index 7e4efc32..77fc66e4 100644 --- a/docs-web/src/test/resources/hibernate.properties +++ b/docs-web/src/test/resources/hibernate.properties @@ -8,7 +8,6 @@ hibernate.show_sql=false hibernate.format_sql=false hibernate.max_fetch_depth=5 hibernate.cache.use_second_level_cache=false -hibernate.c3p0.min_size=1 -hibernate.c3p0.max_size=10 -hibernate.c3p0.timeout=0 -hibernate.c3p0.max_statements=0 \ No newline at end of file +hibernate.connection.initial_pool_size=1 +hibernate.connection.pool_size=10 +hibernate.connection.pool_validation_interval=5 \ No newline at end of file diff --git a/docs-web/src/test/resources/log4j.properties b/docs-web/src/test/resources/log4j.properties index 85def7fe..c0e4e3d6 100644 --- a/docs-web/src/test/resources/log4j.properties +++ b/docs-web/src/test/resources/log4j.properties @@ -10,3 +10,6 @@ log4j.logger.com.sismics.util.jpa=ERROR log4j.logger.org.hibernate=ERROR log4j.logger.org.apache.pdfbox=INFO log4j.logger.com.mchange=ERROR +log4j.logger.org.apache.directory=ERROR +log4j.logger.org.glassfish.grizzly=ERROR +log4j.logger.org.odftoolkit=ERROR \ No newline at end of file diff --git a/docs-web/src/test/resources/test.ldif b/docs-web/src/test/resources/test.ldif new file mode 100644 index 00000000..480faf77 --- /dev/null +++ b/docs-web/src/test/resources/test.ldif @@ -0,0 +1,19 @@ +version: 1 + +dn: o=TEST +objectclass: domain +objectclass: top +objectclass: extensibleObject +dc: TEST +o: TEST + +dn: uid=ldap1,o=TEST +objectClass: top +objectClass: inetOrgPerson +objectClass: person +objectClass: organizationalPerson +cn: ldap1 +sn: LDAP 1 +uid: ldap1 +userPassword: secret +mail: ldap1@teedy.io \ No newline at end of file diff --git a/docs.xml b/docs.xml index 01e310e5..e7ed8524 100644 --- a/docs.xml +++ b/docs.xml @@ -1,3 +1,5 @@ + + / /webapps/docs.war diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/pom.xml b/pom.xml index 020d7c4c..1994ba03 100644 --- a/pom.xml +++ b/pom.xml @@ -6,60 +6,60 @@ com.sismics.docs docs-parent pom - 1.6-SNAPSHOT + 1.12-SNAPSHOT Docs Parent - 1.8 - 1.8 + 11 + 11 UTF-8 - 1.18 - 2.6 - 2.6 + 1.22 + 3.12.0 + 2.11.0 1.5 - 2.3.28 - 1.4 - 26.0-jre - 1.2.16 - 1.6.4 - 1.6.6 - 4.12 - 1.4.197 - 2.27 - 1.1.3 - 0.3m - 7.5.0 + 2.3.32 + 31.1-jre + 1.2.17 + 1.7.30 + 1.7.30 + 1.7.30 + 4.13.2 + 1.4.199 + 2.1.1 + 0.10.2 + 8.7.0 4.2 - 2.0.12 - 1.61 - 2.10 - 5.3.7.Final - 4.0.1 - 2.0.1 - 4.2.1 - 3.3.2 - 1.6.5 - 1.3.0 - 42.2.5 + 2.0.27 + 1.70 + 2.12.2 + 5.6.15.Final + 2.0.4 + 5.13.0 + 3.9.4 + 2.0 + 1.4.0 + 42.6.0 1.2 - 1.5.8 - 1.6.2 - 1.11.3 - 3.11.0 + 1.6.14 + 1.15.4 + 4.10.0 + 2.1.3 - 9.4.17.v20190418 - 9.4.17.v20190418 - 9.4.17.v20190418 + 3.0.10 + 5.0.0 + 11.0.14 + 11.0.14 + 11.0.14 - 1.8 - 3.1.0 - 3.2.2 - 2.22.1 - 9.4.17.v20190418 + 3.1.0 + 3.3.0 + 3.3.2 + 3.0.0 + 11.0.14 @@ -170,11 +170,11 @@ - javax.servlet - javax.servlet-api - ${javax.servlet.javax.servlet-api.version} + jakarta.servlet + jakarta.servlet-api + ${jakarta.servlet.jakarta.servlet-api.version} - + org.apache.commons commons-compress @@ -182,9 +182,9 @@ - commons-lang - commons-lang - ${commons-lang.commons-lang.version} + org.apache.commons + commons-lang3 + ${org.apache.commons.commons-lang3.version} @@ -240,7 +240,13 @@ jcl-over-slf4j ${org.slf4j.jcl-over-slf4j.version} - + + + org.slf4j + jul-to-slf4j + ${org.slf4j.jul-to-slf4j.version} + + junit junit @@ -248,9 +254,9 @@ - org.mindrot - jbcrypt - ${org.mindrot.jbcrypt} + at.favre.lib + bcrypt + ${at.favre.lib.bcrypt.version} @@ -309,9 +315,9 @@ - org.glassfish - javax.json - ${org.glassfish.javax.json.version} + jakarta.json + jakarta.json-api + ${jakarta.json.jakarta.json-api.version} @@ -322,28 +328,10 @@ org.hibernate - hibernate-core + hibernate-core-jakarta ${org.hibernate.hibernate.version} - - org.hibernate - hibernate-entitymanager - ${org.hibernate.hibernate.version} - - - - org.hibernate - hibernate-c3p0 - ${org.hibernate.hibernate.version} - - - - commons-dbcp - commons-dbcp - ${commons-dbcp.version} - - org.freemarker freemarker @@ -440,25 +428,18 @@ ${com.icegreen.greenmail.version} - - com.sun.mail - javax.mail - ${com.sun.mail.javax.mail.version} - - org.postgresql postgresql ${org.postgresql.postgresql.version} - - com.twelvemonkeys.servlet - servlet - ${com.twelvemonkeys.imageio.version} + org.apache.directory.api + api-all + ${org.apache.directory.api.api-all.version} - +