diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..e080392b5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,123 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Environment bootstrap + +Every shell that builds, deploys, or runs CDB **must first source `setup.sh`** from the repo root. It sets `CDB_ROOT_DIR`, `CDB_INSTALL_DIR`, `CDB_DATA_DIR`, `CDB_VAR_DIR`, `CDB_SUPPORT_DIR`, `CDB_GLASSFISH_DIR`, `CDB_PYTHON_DIR`, `PYTHONPATH`, and prepends `bin/`, Ant, Java, Payara, NetBeans-bundled Maven, the support Python, and MariaDB to `PATH`. Most `make` targets and `sbin/` scripts assume those variables exist; running them without sourcing first will fail in non-obvious ways. + +The layout is intentionally **out-of-tree**: `CDB_ROOT_DIR` is the source checkout, but the dependency tree lives at `$CDB_INSTALL_DIR/support-/` (Payara, Java, Ant, NetBeans, MariaDB, Python) created by `make support`. The deployed app, data, logs, and config live under `$CDB_INSTALL_DIR/{etc,var,data,backup}`. Do not commit or read anything from `support-*/` — it is host-specific. + +## Common commands + +All `make` targets must be run from the repo root after `source setup.sh`. + +```sh +# First-time / dev environment setup +make support # downloads & builds Payara, Java, Python, etc. into support-/ +make support-mysql # optional bundled MariaDB +make support-netbeans # optional NetBeans IDE bundle +make dev-config # interactive: writes LDAP/email config & per-host build properties + +# Database (production "cdb" vs dev "cdb_dev" — every db/deploy target has a -dev twin) +make clean-db # drop & recreate cdb with seed data from db/sql/clean +make test-db # recreate cdb with fixtures from db/sql/test +make db # recreate cdb without populating (schema only) +make backup # dumps cdb to $CDB_INSTALL_DIR/backup/cdb// +make clean-db-dev # same, against cdb_dev + +# Build & deploy the Java web portal (WAR -> Payara) +make configure-web-portal # one-time: writes glassfish-resources.xml, etc. +make deploy-web-portal # builds dist/CdbWebPortal.war via Ant and deploys to Payara +make undeploy-web-portal +make deploy-web-portal-dev # deploys to dev domain, uses cdb_dev + +# Plugins +make deploy-cdb-plugin # interactive picker; see tools/developer_tools/cdb_plugins/plugins/ + +# Full test suite (interactive — prompts for DB root password) +make test # runs ./sbin/cdb_test.sh: backs up cdb, swaps in test DB, + # runs Maven unit tests + pytest API tests + Selenium GUI tests, + # then restores the backup +``` + +### Test targets in isolation + +```sh +# Java unit tests (Arquillian + embedded Glassfish) +cd tools/developer_tools/code_testing/CdbWebPortalTest && mvn test +mvn test -Dtest=ClassName#methodName # single test + +# Python API tests — require the portal to be running on localhost:8080 +cd tools/developer_tools/python-client && ./generatePyClient.sh http://localhost:8080/cdb +cd tools/developer_tools/python-client/test && pytest api_test.py +pytest api_test.py::TestClass::test_method # single test + +# Selenium GUI tests +cd tools/developer_tools/portal_testing/PythonSeleniumTest && pytest gui_test.py +``` + +### Server / log management + +```sh +./etc/init.d/cdb-glassfish restart +./etc/init.d/cdb-mysqld start # only if using bundled MariaDB +tail -f $CDB_SUPPORT_DIR/payara/$CDB_HOST_ARCH/glassfish/domains/production/logs/server.log +``` + +## High-level architecture + +ComponentDB has **three deployable pieces** plus a generated client. They communicate through the database and through the portal's REST API. + +### 1. Java web portal — `src/java/CdbWebPortal/` + +A JSF (PrimeFaces) + JAX-RS application packaged as a WAR and deployed to **Payara 5**. Built with **Ant via the NetBeans project** (`build.xml` → `nbproject/build-impl.xml`); the `make` target invokes `cdb-ant` from the support dir. Maven `pom-wip-jdk11.xml` exists but is not the build of record. + +Source root: `src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/` with these top-level packages: + +- `portal/` — the JSF webapp. + - `model/db/entities/` — JPA entities (EclipseLink). Item hierarchy is the core domain model: `Item` is the abstract parent; `ItemDomain*` subclasses (`ItemDomainCatalog`, `ItemDomainInventory`, `ItemDomainLocation`, `ItemDomainMachineDesign`, `ItemDomainCableCatalog`, `ItemDomainCableDesign`, `ItemDomainCableInventory`, `ItemDomainApp`, `ItemDomainMAARC`, `ItemDomainAppDeployment`) implement domain-specific behavior. Catalog/Inventory base classes are shared. + - `model/db/beans/` — `@Stateless` EJB facades wrapping JPA queries. + - `controllers/` — `@SessionScoped`/`@ViewScoped` JSF managed beans. Naming mirrors the entity: e.g. `ItemDomainCatalogController` ↔ `ItemDomainCatalog`. `CdbDomainEntityController` is the common base. `controllers/extensions/` adds cross-cutting helpers, `controllers/settings/` persists user UI prefs. + - `import_export/` — wizard-driven spreadsheet (XLSX) import/export framework; `import_/objects/specs/` defines per-domain column specs and `handlers/` define value coercion. + - `view/` — JSF beans and value objects backing views. + - `plugins/` — `CdbPluginManager` discovers plugin JARs deployed under `support/`; see `tools/developer_tools/cdb_plugins/` for the plugin install tooling. +- `rest/` — JAX-RS endpoints exposed at `/cdb/api`. One `*Route` class per resource (`ItemRoute`, `MachineDesignItemRoute`, `CableDesignItemRoute`, `UsersRoute`, `AuthenticationRoute`, etc.) under `rest/routes/`. `rest/entities/` are the request/response DTOs; `rest/authentication/` issues JWTs; `rest/provider/` registers JAX-RS providers. The OpenAPI surface is declared in `src/java/openapi.yaml` and drives Python client generation — keep it in sync when adding/changing routes. +- `common/` — shared exceptions, constants, value objects used by both `portal/` and `rest/`. +- `api/`, `connectors/` — internal API surfaces and external system connectors. + +**JSF views, templates, and static assets** live in `web/` (`web/views/`, `web/templates/`, `web/resources/`, `web/WEB-INF/`). `web/WEB-INF/glassfish-web.xml` and `src/java/cdb.portal.properties` are generated from `.template` files by `make dev-config` / `make configure-web-portal` — never edit the generated files; edit the templates. + +### 2. Python CDB web service — `src/python/cdb/` + +A CherryPy service that runs on port 10232 (`./sbin/cdbWebService.sh`), handling email notifications, async tasks, and a legacy SOAP/REST surface. Marked **deprecated** but still part of the distribution for legacy integrations. Layout follows the `common/` + `cdb_web_service/` split with `api/`, `cli/`, `impl/`, `service/`, `tasks/`, `plugins/`, `java_api/` (Python wrappers around the Java REST API). New features should go in the Java REST layer instead. + +### 3. Python client + CLI — `tools/developer_tools/python-client/` + +- `cdbApi/` is **generated** from `openapi.yaml` by `./generatePyClient.sh ` (uses openapi-generator). Do not hand-edit it — regenerate after changing the Java REST routes. `setup-api.py` packages it as `ComponentDB_API` for PyPI. +- `CdbApiFactory.py` is the hand-written entry point that hides authentication & client wiring. +- `cdbCli/` is the user-facing CLI (`cdbSearch`, `cdbInfo`, `cdb-cli`); see `docs/CLI.md` for usage and option reference. `cli-template/` holds skeletons for new commands. + +### Database — `db/sql/` + +MariaDB 10.5. Authoritative schema is the set of `create_cdb_tables.sql`, `create_stored_procedures.sql`, `create_triggers.sql`, `create_views.sql` in `db/sql/`, applied by `sbin/cdb_create_db.sh`. Initial reference data lives in `db/sql/clean/populate_*.sql` (used by `make clean-db`). Test fixtures in `db/sql/test/`. + +**Releases bump the version in `etc/version` and add `db/sql/updates/updateTo.sql`** containing the idempotent (INSERT IGNORE / ALTER ... IF NOT EXISTS) migrations needed to bring an existing prod DB to the new schema. Header comments in each update file document how to run it. The existing version chain (e.g. `updateTo3.15.5.sql` → `updateTo3.17.0.sql`) is the template; do not retroactively edit older update scripts. + +**Adding a setting:** `setting_type` ids are organized into per-type blocks with numeric gaps between blocks (e.g. `15xxx` Search, `16xxx` Source, `22xxx` MAARC) so each type has room for new entries. A new setting takes the next free id at the end of its type block — never reuse an id or insert one mid-block (the gap before the next block is the room reserved for this). Add the identical row to all seed copies (`db/sql/{clean,test,static}/populate_setting_type.sql`) and to the current release's `db/sql/updates/updateTo.sql`. + +## Repo-specific conventions + +- **Generated files that look like sources** are common: `cdb.portal.properties`, `web/WEB-INF/glassfish-web.xml`, `web/WEB-INF/web.xml`, `setup/glassfish-resources.xml`, `nbproject/private/private.properties`, and everything under `tools/developer_tools/python-client/cdbApi/`. They are produced from `*.template` files (or via openapi-generator) by `dev-config` / `configure-web-portal` / `dist` targets and are in `.gitignore`. Edit the template, regenerate, then commit only the template. +- **`cdb` vs `cdb_dev`**: every db, deploy, undeploy, and backup target has a `-dev` variant that targets a separate database name and Payara domain, so a developer can run both side-by-side. When adding new admin scripts, follow the same pattern (first arg is DB name, defaulting to `cdb`). +- **Single canonical version string**: update `etc/version`, `src/java/CdbWebPortal/src/java/openapi.yaml` (`info.version`), `tools/developer_tools/python-client/setup-api.py`, and `tools/developer_tools/python-client/setup-cli.py` together. Past release commits (`git log --grep "Prepare relase"`) show the full set of files that need bumping. +- **Plugins** under `tools/developer_tools/cdb_plugins/plugins//` extend both the Java portal (deployed via `make deploy-cdb-plugin` → `update_plugin_generated_files.py`) and the Python web service. `pluginTemplates/` is the scaffold. +- **REST changes touch three places**: the Java route, `openapi.yaml`, and (after `generatePyClient.sh`) the regenerated `cdbApi/` package. The Python API tests in `tools/developer_tools/python-client/test/api_test.py` are the integration check. + +## Reference docs + +- `README.md` — full deployment and upgrade walk-through (commands here are abbreviated). +- `docs/CLI.md` — `cdbSearch` / `cdbInfo` / `cdb-cli` reference. +- `docs/release-notes/.md` — per-release notes; mirror this format for new releases. +- `docs/db/CdbSchema-v3.0-3.pdf` — ER diagram (somewhat dated but useful for the Item hierarchy). +- External developer guide: (referenced from README). diff --git a/db/sql/clean/populate_setting_type.sql b/db/sql/clean/populate_setting_type.sql index dec7b41cf..8e5c78d50 100644 --- a/db/sql/clean/populate_setting_type.sql +++ b/db/sql/clean/populate_setting_type.sql @@ -285,6 +285,9 @@ INSERT INTO `setting_type` VALUES (15011,'Search.Display.Users','Display search result for users.','false'), (15012,'Search.Display.UserGroups','Display search result for user groups.','false'), (15013,'Search.DisplayItemDomainMachineDesign','Display search result for machine design items','true'), +(15018,'Search.Display.ItemDomainMachineDesignIOC','Display search result for IOC items.','true'), +(15019,'Search.Display.ItemDomainApp','Display search result for application items.','true'), +(15020,'Search.Display.PropertyValue','Display search result for property values.','true'), (16000,'Source.List.Display.ContactInfo','Display source contact info.','true'), (16001,'Source.List.Display.Description','Display source description.','true'), (16002,'Source.List.Display.Url','Display source URL.','true'), diff --git a/db/sql/create_stored_procedures.sql b/db/sql/create_stored_procedures.sql index 44a8b8f11..95638c54c 100644 --- a/db/sql/create_stored_procedures.sql +++ b/db/sql/create_stored_procedures.sql @@ -95,6 +95,34 @@ BEGIN LIMIT limit_row; END // +DROP PROCEDURE IF EXISTS search_ioc_items;// +CREATE PROCEDURE `search_ioc_items` (IN limit_row int, IN search_string VARCHAR(255)) +BEGIN + DECLARE ioc_entity_type_id INT; + SET ioc_entity_type_id = 11; -- EntityTypeName.IOC_ID + + SET search_string = CONCAT('%', search_string, '%'); + SELECT DISTINCT item.* from item + INNER JOIN v_item_self_element ise ON item.id = ise.item_id + INNER JOIN item_element ie ON ise.self_element_id = ie.id + INNER JOIN entity_info ei ON ise.entity_info_id = ei.id + INNER JOIN user_info owneru ON ei.owner_user_id = owneru.id + INNER JOIN user_info creatoru ON ei.created_by_user_id = creatoru.id + INNER JOIN user_info updateu ON ei.last_modified_by_user_id = updateu.id + INNER JOIN item_entity_type iet ON iet.item_id = item.id AND iet.entity_type_id = ioc_entity_type_id + WHERE ( + item.name LIKE search_string + OR item.qr_id LIKE search_string + OR item.item_identifier1 LIKE search_string + OR item.item_identifier2 LIKE search_string + OR ie.description LIKE search_string + OR owneru.username LIKE search_string + OR creatoru.username LIKE search_string + OR updateu.username LIKE search_string + ) + LIMIT limit_row; +END // + DROP PROCEDURE IF EXISTS search_item_elements;// CREATE PROCEDURE `search_item_elements` (IN limit_row int, IN search_string VARCHAR(255)) BEGIN diff --git a/db/sql/static/populate_setting_type.sql b/db/sql/static/populate_setting_type.sql index 9f69f12e6..9b0164290 100644 --- a/db/sql/static/populate_setting_type.sql +++ b/db/sql/static/populate_setting_type.sql @@ -284,7 +284,10 @@ description.','true'), (15014,'Search.Display.ItemDomainCableCatalog','Display search result for cable catalog items.','true'), (15015,'Search.Display.ItemDomainCableInventory','Display search result for cable inventory items.','true'), (15016,'Search.Display.ItemDomainCableDesign','Display search result for cable design items.','true'), -(15017,'Search.Display.ItemDomainMAARC', 'Display search result for MAARC items', 'false'), +(15017,'Search.Display.ItemDomainMAARC', 'Display search result for MAARC items', 'false'), +(15018,'Search.Display.ItemDomainMachineDesignIOC','Display search result for IOC items.','true'), +(15019,'Search.Display.ItemDomainApp','Display search result for application items.','true'), +(15020,'Search.Display.PropertyValue','Display search result for property values.','true'), (16000,'Source.List.Display.ContactInfo','Display source contact info.','true'), (16001,'Source.List.Display.Description','Display source description.','true'), (16002,'Source.List.Display.Url','Display source URL.','true'), diff --git a/db/sql/test/populate_setting_type.sql b/db/sql/test/populate_setting_type.sql index a52297204..4ffa9d7c1 100644 --- a/db/sql/test/populate_setting_type.sql +++ b/db/sql/test/populate_setting_type.sql @@ -284,6 +284,9 @@ INSERT INTO `setting_type` VALUES (15015,'Search.Display.ItemDomainCableInventory','Display search result for cable inventory items.','true'), (15016,'Search.Display.ItemDomainCableDesign','Display search result for cable design items.','true'), (15017,'Search.Display.ItemDomainMAARC','Display search result for MAARC items','false'), +(15018,'Search.Display.ItemDomainMachineDesignIOC','Display search result for IOC items.','true'), +(15019,'Search.Display.ItemDomainApp','Display search result for application items.','true'), +(15020,'Search.Display.PropertyValue','Display search result for property values.','true'), (16000,'Source.List.Display.ContactInfo','Display source contact info.','true'), (16001,'Source.List.Display.Description','Display source description.','true'), (16002,'Source.List.Display.Url','Display source URL.','true'), diff --git a/db/sql/updates/updateTo3.18.0.sql b/db/sql/updates/updateTo3.18.0.sql new file mode 100644 index 000000000..8bb7496d9 --- /dev/null +++ b/db/sql/updates/updateTo3.18.0.sql @@ -0,0 +1,46 @@ +-- +-- Copyright (c) UChicago Argonne, LLC. All rights reserved. +-- See LICENSE file. +-- + +-- Execute by running `mysql CDB_DB_NAME -h 127.0.0.1 -u cdb -p < updateTo3.16.2.sql` + +INSERT IGNORE INTO `setting_type` VALUES +(15018,'Search.Display.ItemDomainMachineDesignIOC','Display search result for IOC items.','true'), +(15019,'Search.Display.ItemDomainApp','Display search result for application items.','true'), +(15020,'Search.Display.PropertyValue','Display search result for property values.','true'); + +-- Add text property value (value/text/tag) matching to the item search procedures +-- and a dedicated IOC item search procedure (used to search markdown IOC instructions). + +delimiter // + +DROP PROCEDURE IF EXISTS search_ioc_items;// +CREATE PROCEDURE `search_ioc_items` (IN limit_row int, IN search_string VARCHAR(255)) +BEGIN + DECLARE ioc_entity_type_id INT; + SET ioc_entity_type_id = 11; -- EntityTypeName.IOC_ID + + SET search_string = CONCAT('%', search_string, '%'); + SELECT DISTINCT item.* from item + INNER JOIN v_item_self_element ise ON item.id = ise.item_id + INNER JOIN item_element ie ON ise.self_element_id = ie.id + INNER JOIN entity_info ei ON ise.entity_info_id = ei.id + INNER JOIN user_info owneru ON ei.owner_user_id = owneru.id + INNER JOIN user_info creatoru ON ei.created_by_user_id = creatoru.id + INNER JOIN user_info updateu ON ei.last_modified_by_user_id = updateu.id + INNER JOIN item_entity_type iet ON iet.item_id = item.id AND iet.entity_type_id = ioc_entity_type_id + WHERE ( + item.name LIKE search_string + OR item.qr_id LIKE search_string + OR item.item_identifier1 LIKE search_string + OR item.item_identifier2 LIKE search_string + OR ie.description LIKE search_string + OR owneru.username LIKE search_string + OR creatoru.username LIKE search_string + OR updateu.username LIKE search_string + ) + LIMIT limit_row; +END // + +delimiter ; diff --git a/src/java/CdbWebPortal/lib/log4j-api-2.17.0.jar b/src/java/CdbWebPortal/lib/log4j-api-2.17.0.jar deleted file mode 100644 index e39dab0d3..000000000 Binary files a/src/java/CdbWebPortal/lib/log4j-api-2.17.0.jar and /dev/null differ diff --git a/src/java/CdbWebPortal/lib/log4j-api-2.26.0.jar b/src/java/CdbWebPortal/lib/log4j-api-2.26.0.jar new file mode 100644 index 000000000..840048c73 Binary files /dev/null and b/src/java/CdbWebPortal/lib/log4j-api-2.26.0.jar differ diff --git a/src/java/CdbWebPortal/lib/log4j-core-2.17.0.jar b/src/java/CdbWebPortal/lib/log4j-core-2.17.0.jar deleted file mode 100644 index b853057e9..000000000 Binary files a/src/java/CdbWebPortal/lib/log4j-core-2.17.0.jar and /dev/null differ diff --git a/src/java/CdbWebPortal/lib/log4j-core-2.26.0.jar b/src/java/CdbWebPortal/lib/log4j-core-2.26.0.jar new file mode 100644 index 000000000..d86326093 Binary files /dev/null and b/src/java/CdbWebPortal/lib/log4j-core-2.26.0.jar differ diff --git a/src/java/CdbWebPortal/nbproject/build-impl.xml b/src/java/CdbWebPortal/nbproject/build-impl.xml index 7399a8274..618fd6268 100644 --- a/src/java/CdbWebPortal/nbproject/build-impl.xml +++ b/src/java/CdbWebPortal/nbproject/build-impl.xml @@ -1026,8 +1026,8 @@ exists or setup the property manually. For example like this: - - + + @@ -1089,8 +1089,8 @@ exists or setup the property manually. For example like this: - - + + diff --git a/src/java/CdbWebPortal/nbproject/genfiles.properties b/src/java/CdbWebPortal/nbproject/genfiles.properties index a8a840fa3..8f2b63737 100644 --- a/src/java/CdbWebPortal/nbproject/genfiles.properties +++ b/src/java/CdbWebPortal/nbproject/genfiles.properties @@ -1,10 +1,10 @@ -build.xml.data.CRC32=29a3f23e +build.xml.data.CRC32=5e66231e build.xml.script.CRC32=67492cbd build.xml.stylesheet.CRC32=1707db4f@1.94.0.1 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. -nbproject/build-impl.xml.data.CRC32=29a3f23e -nbproject/build-impl.xml.script.CRC32=8f8d24c4 +nbproject/build-impl.xml.data.CRC32=5e66231e +nbproject/build-impl.xml.script.CRC32=fa9fe960 nbproject/build-impl.xml.stylesheet.CRC32=334708a0@1.94.0.1 nbproject/rest-build.xml.data.CRC32=83e13ee9 nbproject/rest-build.xml.script.CRC32=0d1fb1b4 diff --git a/src/java/CdbWebPortal/nbproject/project.properties b/src/java/CdbWebPortal/nbproject/project.properties index fec38e202..f1510c9c0 100644 --- a/src/java/CdbWebPortal/nbproject/project.properties +++ b/src/java/CdbWebPortal/nbproject/project.properties @@ -76,8 +76,8 @@ file.reference.javax.servlet-api-4.0.1.jar=lib/javax.servlet-api-4.0.1.jar file.reference.javax.xml.soap-api-1.4.0.jar=lib/javax.xml.soap-api-1.4.0.jar file.reference.jersey-media-json-jackson-2.28.jar=lib/jersey-media-json-jackson-2.28.jar file.reference.libphonenumber-8.12.3.jar=lib/libphonenumber-8.12.3.jar -file.reference.log4j-api-2.17.0.jar=lib/log4j-api-2.17.0.jar -file.reference.log4j-core-2.17.0.jar=lib/log4j-core-2.17.0.jar +file.reference.log4j-api-2.26.0.jar=lib/log4j-api-2.26.0.jar +file.reference.log4j-core-2.26.0.jar=lib/log4j-core-2.26.0.jar file.reference.metadata-extractor-2.17.0.jar=lib/metadata-extractor-2.17.0.jar file.reference.omnifaces-3.10.1.jar-1=lib/omnifaces-3.10.1.jar file.reference.pdfbox-2.0.24.jar=lib/pdfbox-2.0.24.jar @@ -155,8 +155,8 @@ javac.classpath=\ ${file.reference.xmpcore-6.1.10.jar}:\ ${file.reference.metadata-extractor-2.17.0.jar}:\ ${file.reference.javax.xml.soap-api-1.4.0.jar}:\ - ${file.reference.log4j-api-2.17.0.jar}:\ - ${file.reference.log4j-core-2.17.0.jar}:\ + ${file.reference.log4j-api-2.26.0.jar}:\ + ${file.reference.log4j-core-2.26.0.jar}:\ ${file.reference.dm-api-3.3.1.jar}:\ ${file.reference.dm-base-3.3.1.jar}:\ ${file.reference.flexmark-0.64.8.jar}:\ diff --git a/src/java/CdbWebPortal/nbproject/project.xml b/src/java/CdbWebPortal/nbproject/project.xml index 3c71c19e2..807c1a486 100644 --- a/src/java/CdbWebPortal/nbproject/project.xml +++ b/src/java/CdbWebPortal/nbproject/project.xml @@ -183,11 +183,11 @@ WEB-INF/lib - ${file.reference.log4j-api-2.17.0.jar} + ${file.reference.log4j-api-2.26.0.jar} WEB-INF/lib - ${file.reference.log4j-core-2.17.0.jar} + ${file.reference.log4j-core-2.26.0.jar} WEB-INF/lib diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemController.java index 413827e04..cfb685a00 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemController.java @@ -1906,6 +1906,18 @@ public ItemDomainEntity selectByViewRequestParams() throws CdbException { urlParams += "mode=" + mode + "&"; } + // Forward a property permalink target through the domain redirect so the + // domain view can open the corresponding property dialog on load. + String propertyValueIdParam = SessionUtility.getRequestParameterValue("propertyValueId"); + if (propertyValueIdParam != null) { + try { + Integer propertyValueId = Integer.parseInt(propertyValueIdParam); + urlParams += "propertyValueId=" + propertyValueId + "&"; + } catch (NumberFormatException ex) { + // Ignore a malformed permalink parameter. + } + } + try { if (paramValue != null) { idParam = Integer.parseInt(paramValue); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/PropertyValueController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/PropertyValueController.java index 30046529d..92f6e89a1 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/PropertyValueController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/PropertyValueController.java @@ -14,6 +14,7 @@ import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue.PropertyValueMetadata; import gov.anl.aps.cdb.portal.model.db.beans.PropertyValueFacade; +import gov.anl.aps.cdb.portal.model.db.entities.Item; import gov.anl.aps.cdb.portal.model.db.entities.PropertyMetadata; import gov.anl.aps.cdb.portal.model.db.entities.PropertyType; import gov.anl.aps.cdb.portal.model.db.entities.PropertyValueBase; @@ -65,6 +66,107 @@ public static PropertyValueController getInstance() { return (PropertyValueController) SessionUtility.findBean("propertyValueController"); } + /** + * Short property permalink entry point. Backs the page + * {@code /views/propertyValue/redirect?id=}: resolves the + * owning item for the supplied property value id and redirects to that + * item's view page with the property value's dialog opened on load. + * + * @return faces navigation outcome to the owning item view, or to the home + * page when the property value or its parent item cannot be resolved + */ + public String redirectToItemForPropertyValueViewParam() { + String idParam = SessionUtility.getRequestParameterValue("id"); + if (idParam == null) { + SessionUtility.addWarningMessage("Warning", "No property value id supplied."); + return "/index.xhtml?faces-redirect=true"; + } + Integer propertyValueId; + try { + propertyValueId = Integer.parseInt(idParam); + } catch (NumberFormatException ex) { + SessionUtility.addWarningMessage("Warning", "Invalid property value id: " + idParam); + return "/index.xhtml?faces-redirect=true"; + } + Item parentItem = propertyValueFacade.getParentItemForPropertyValue(propertyValueId); + if (parentItem == null) { + SessionUtility.addWarningMessage("Warning", + "No item found for property value id " + propertyValueId + "."); + return "/index.xhtml?faces-redirect=true"; + } + return "/views/item/view.xhtml?faces-redirect=true&id=" + parentItem.getId() + + "&propertyValueId=" + propertyValueId; + } + + private boolean openMarkdownDialogOnLoad = false; + private boolean openDetailsDialogOnLoad = false; + + /** + * Handle a property permalink request. When the view is loaded with a + * {@code propertyValueId} request parameter, automatically open the + * relevant dialog for that property value: the markdown dialog for markdown + * properties, or a read-only details dialog otherwise. The parent item is + * already loaded by the item controller's view action which runs before + * this one. + */ + @Override + public void processPreRender() { + super.processPreRender(); + + openMarkdownDialogOnLoad = false; + openDetailsDialogOnLoad = false; + + String pvIdParam = SessionUtility.getRequestParameterValue("propertyValueId"); + if (pvIdParam == null) { + return; + } + + Integer pvId; + try { + pvId = Integer.parseInt(pvIdParam); + } catch (NumberFormatException ex) { + // Ignore a malformed permalink parameter; the page still loads. + return; + } + + PropertyValue propertyValue = findById(pvId); + if (propertyValue == null) { + return; + } + + if (displayMarkdownValue(propertyValue)) { + setCurrentAndUpdateGeneratedHTML(propertyValue); + openMarkdownDialogOnLoad = true; + } else { + setCurrent(propertyValue); + openDetailsDialogOnLoad = true; + } + } + + /** + * Whether the markdown dialog should be shown on initial (non-postback) + * page load in response to a property permalink. Gated to non-postback + * renders so subsequent ajax updates (e.g. an {@code update="@form"} edit) + * do not reopen the dialog. + */ + public boolean isOpenMarkdownDialogOnLoad() { + if (FacesContext.getCurrentInstance().isPostback()) { + return false; + } + return openMarkdownDialogOnLoad; + } + + /** + * Whether the property details dialog should be shown on initial + * (non-postback) page load in response to a property permalink. + */ + public boolean isOpenDetailsDialogOnLoad() { + if (FacesContext.getCurrentInstance().isPostback()) { + return false; + } + return openDetailsDialogOnLoad; + } + public boolean isItemElementAssignedToProperty(PropertyValue propertyValue) { if (propertyValue.getItemElementList() != null) { return propertyValue.getItemElementList().size() > 0; @@ -267,8 +369,19 @@ public void setCurrentAndEditNewMarkdown(PropertyValue propertyValue) { setCurrent(propertyValue); } + public String getRenderedMarkdownHtml(PropertyValue propertyValue) { + if (propertyValue == null) { + return null; + } + if (propertyValue.getGeneratedHTMLText() == null) { + propertyValue.setGeneratedHTMLText( + MarkdownParser.parseMarkdownAsHTML(propertyValue.getText())); + } + return propertyValue.getGeneratedHTMLText(); + } + public void setCurrentAndUpdateGeneratedHTML(PropertyValue propertyValue) { - // Fetch latest text before generating html. + // Fetch latest text before generating html. PropertyValue latestProperty = findById(propertyValue.getId()); propertyValue.setText(latestProperty.getText()); @@ -281,6 +394,12 @@ public void setCurrentAndUpdateGeneratedHTML(PropertyValue propertyValue) { setCurrent(propertyValue); } + public void setCurrentAndEditMarkdown(PropertyValue propertyValue) { + // Load latest text / html and open directly in edit mode. + setCurrentAndUpdateGeneratedHTML(propertyValue); + propertyValue.setEditMode(true); + } + public static String getAPIDownloadPath(PropertyValue propertyValue) { Integer id = propertyValue.getId(); return StorageUtility.getApiDownloadByPropertyValueIdPath(id); diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/SearchController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/SearchController.java index 6b837be23..466e18cf7 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/SearchController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/SearchController.java @@ -91,9 +91,15 @@ public void search() { else if (controller instanceof ItemDomainInventoryController) { if (!searchSettings.getDisplayInventoryItems()) continue; } + else if (controller instanceof ItemDomainMachineDesignIOCController) { + if (!searchSettings.getDisplayIOCItems()) continue; + } else if (controller instanceof ItemDomainMachineDesignController) { if (!searchSettings.getDisplayMachineDesignItems()) continue; } + else if (controller instanceof ItemDomainAppController) { + if (!searchSettings.getDisplayAppItems()) continue; + } else if (controller instanceof ItemDomainCableCatalogController) { if (!searchSettings.getDisplayCableCatalogItems()) continue; } @@ -115,6 +121,9 @@ else if (controller instanceof ItemCategoryController) { else if (controller instanceof ItemDomainLocationController) { if (!searchSettings.getDisplayLocationItems()) continue; } + else if (controller instanceof PropertyValueController) { + if (!searchSettings.getDisplayPropertyValues()) continue; + } else if (controller instanceof PropertyTypeController) { if (!searchSettings.getDisplayPropertyTypes()) continue; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/settings/SearchSettings.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/settings/SearchSettings.java index e744de8ce..4f6230573 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/settings/SearchSettings.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/settings/SearchSettings.java @@ -27,11 +27,14 @@ public class SearchSettings extends S private static final String DisplayLocationsSettingTypeKey = "Search.Display.ItemDomainLocation"; private static final String DisplayCatalogItemsSettingTypeKey = "Search.Display.ItemDomainCatalog"; private static final String DisplayInventoryItemsSettingTypeKey = "Search.Display.ItemDomainInventory"; - private static final String DisplayMachineDesignItemsSettingTypeKey = "Search.DisplayItemDomainMachineDesign"; + private static final String DisplayMachineDesignItemsSettingTypeKey = "Search.DisplayItemDomainMachineDesign"; + private static final String DisplayIOCItemsSettingTypeKey = "Search.Display.ItemDomainMachineDesignIOC"; + private static final String DisplayAppItemsSettingTypeKey = "Search.Display.ItemDomainApp"; private static final String DisplayCableCatalogItemsSettingTypeKey = "Search.Display.ItemDomainCableCatalog"; private static final String DisplayCableInventoryItemsSettingTypeKey = "Search.Display.ItemDomainCableInventory"; private static final String DisplayCableDesignItemsSettingTypeKey = "Search.Display.ItemDomainCableDesign"; private static final String DisplayMAARCItemsSettingTypeKey= "Search.Display.ItemDomainMAARC"; + private static final String DisplayPropertyValuesSettingTypeKey = "Search.Display.PropertyValue"; private static final String DisplayPropertyTypesSettingTypeKey = "Search.Display.PropertyTypes"; private static final String DisplayPropertyTypeCategoriesSettingTypeKey = "Search.Display.PropertyTypeCategories"; private static final String DisplaySourcesSettingTypeKey = "Search.Display.Sources"; @@ -41,6 +44,8 @@ public class SearchSettings extends S private Boolean caseInsensitive = true; protected Boolean displayMachineDesignItems = null; + protected Boolean displayIOCItems = null; + protected Boolean displayAppItems = null; protected Boolean displayCatalogItems = null; protected Boolean displayInventoryItems = null; protected Boolean displayCableCatalogItems = null; @@ -51,6 +56,7 @@ public class SearchSettings extends S protected Boolean displayItemTypes = null; protected Boolean displayItemCategories = null; protected Boolean displayItemElements = null; + protected Boolean displayPropertyValues = null; protected Boolean displayPropertyTypes = null; protected Boolean displayPropertyTypeCategories = null; protected Boolean displaySources = null; @@ -70,7 +76,9 @@ public void updateSettingsFromSettingTypeDefaults(Map setti caseInsensitive = Boolean.parseBoolean(settingTypeMap.get(CaseInsensitiveSettingTypeKey).getDefaultValue()); displayCatalogItems = Boolean.parseBoolean(settingTypeMap.get(DisplayCatalogItemsSettingTypeKey).getDefaultValue()); displayInventoryItems = Boolean.parseBoolean(settingTypeMap.get(DisplayInventoryItemsSettingTypeKey).getDefaultValue()); - displayMachineDesignItems = Boolean.parseBoolean(settingTypeMap.get(DisplayMachineDesignItemsSettingTypeKey).getDefaultValue()); + displayMachineDesignItems = Boolean.parseBoolean(settingTypeMap.get(DisplayMachineDesignItemsSettingTypeKey).getDefaultValue()); + displayIOCItems = Boolean.parseBoolean(settingTypeMap.get(DisplayIOCItemsSettingTypeKey).getDefaultValue()); + displayAppItems = Boolean.parseBoolean(settingTypeMap.get(DisplayAppItemsSettingTypeKey).getDefaultValue()); displayCableCatalogItems = Boolean.parseBoolean(settingTypeMap.get(DisplayCableCatalogItemsSettingTypeKey).getDefaultValue()); displayCableInventoryItems = Boolean.parseBoolean(settingTypeMap.get(DisplayCableInventoryItemsSettingTypeKey).getDefaultValue()); displayCableDesignItems = Boolean.parseBoolean(settingTypeMap.get(DisplayCableDesignItemsSettingTypeKey).getDefaultValue()); @@ -79,6 +87,7 @@ public void updateSettingsFromSettingTypeDefaults(Map setti displayItemCategories = Boolean.parseBoolean(settingTypeMap.get(DisplayItemCategoriesSettingTypeKey).getDefaultValue()); displayItemElements = Boolean.parseBoolean(settingTypeMap.get(DisplayItemElementsSettingTypeKey).getDefaultValue()); displayLocationItems = Boolean.parseBoolean(settingTypeMap.get(DisplayLocationsSettingTypeKey).getDefaultValue()); + displayPropertyValues = Boolean.parseBoolean(settingTypeMap.get(DisplayPropertyValuesSettingTypeKey).getDefaultValue()); displayPropertyTypes = Boolean.parseBoolean(settingTypeMap.get(DisplayPropertyTypesSettingTypeKey).getDefaultValue()); displayPropertyTypeCategories = Boolean.parseBoolean(settingTypeMap.get(DisplayPropertyTypeCategoriesSettingTypeKey).getDefaultValue()); displaySources = Boolean.parseBoolean(settingTypeMap.get(DisplaySourcesSettingTypeKey).getDefaultValue()); @@ -95,8 +104,10 @@ public void updateSettingsFromSessionSettingEntity(SettingEntity settingEntity) caseInsensitive = settingEntity.getSettingValueAsBoolean(CaseInsensitiveSettingTypeKey, caseInsensitive); displayCatalogItems = settingEntity.getSettingValueAsBoolean(DisplayCatalogItemsSettingTypeKey, displayCatalogItems); displayInventoryItems = settingEntity.getSettingValueAsBoolean(DisplayInventoryItemsSettingTypeKey, displayInventoryItems); - displayMachineDesignItems = settingEntity.getSettingValueAsBoolean(DisplayMachineDesignItemsSettingTypeKey, displayMachineDesignItems); - displayCableCatalogItems = settingEntity.getSettingValueAsBoolean(DisplayCableCatalogItemsSettingTypeKey, displayCableCatalogItems); + displayMachineDesignItems = settingEntity.getSettingValueAsBoolean(DisplayMachineDesignItemsSettingTypeKey, displayMachineDesignItems); + displayIOCItems = settingEntity.getSettingValueAsBoolean(DisplayIOCItemsSettingTypeKey, displayIOCItems); + displayAppItems = settingEntity.getSettingValueAsBoolean(DisplayAppItemsSettingTypeKey, displayAppItems); + displayCableCatalogItems = settingEntity.getSettingValueAsBoolean(DisplayCableCatalogItemsSettingTypeKey, displayCableCatalogItems); displayCableInventoryItems = settingEntity.getSettingValueAsBoolean(DisplayCableInventoryItemsSettingTypeKey, displayCableInventoryItems); displayCableDesignItems = settingEntity.getSettingValueAsBoolean(DisplayCableDesignItemsSettingTypeKey, displayCableDesignItems); displayMAARCItems = settingEntity.getSettingValueAsBoolean(DisplayMAARCItemsSettingTypeKey, displayMAARCItems); @@ -104,6 +115,7 @@ public void updateSettingsFromSessionSettingEntity(SettingEntity settingEntity) displayItemCategories = settingEntity.getSettingValueAsBoolean(DisplayItemCategoriesSettingTypeKey, displayItemCategories); displayItemElements = settingEntity.getSettingValueAsBoolean(DisplayItemElementsSettingTypeKey, displayItemElements); displayLocationItems = settingEntity.getSettingValueAsBoolean(DisplayLocationsSettingTypeKey, displayLocationItems); + displayPropertyValues = settingEntity.getSettingValueAsBoolean(DisplayPropertyValuesSettingTypeKey, displayPropertyValues); displayPropertyTypes = settingEntity.getSettingValueAsBoolean(DisplayPropertyTypesSettingTypeKey, displayPropertyTypes); displayPropertyTypeCategories = settingEntity.getSettingValueAsBoolean(DisplayPropertyTypeCategoriesSettingTypeKey, displayPropertyTypeCategories); displaySources = settingEntity.getSettingValueAsBoolean(DisplaySourcesSettingTypeKey, displaySources); @@ -122,7 +134,9 @@ public void saveSettingsForSessionSettingEntity(SettingEntity settingEntity) { settingEntity.setSettingValue(DisplayInventoryItemsSettingTypeKey, displayCatalogItems); settingEntity.setSettingValue(DisplayCatalogItemsSettingTypeKey, displayInventoryItems); settingEntity.setSettingValue(DisplayMachineDesignItemsSettingTypeKey, displayMachineDesignItems); - settingEntity.setSettingValue(DisplayCableCatalogItemsSettingTypeKey, displayCableCatalogItems); + settingEntity.setSettingValue(DisplayIOCItemsSettingTypeKey, displayIOCItems); + settingEntity.setSettingValue(DisplayAppItemsSettingTypeKey, displayAppItems); + settingEntity.setSettingValue(DisplayCableCatalogItemsSettingTypeKey, displayCableCatalogItems); settingEntity.setSettingValue(DisplayCableInventoryItemsSettingTypeKey, displayCableInventoryItems); settingEntity.setSettingValue(DisplayCableDesignItemsSettingTypeKey, displayCableDesignItems); settingEntity.setSettingValue(DisplayMAARCItemsSettingTypeKey, displayMAARCItems); @@ -130,6 +144,7 @@ public void saveSettingsForSessionSettingEntity(SettingEntity settingEntity) { settingEntity.setSettingValue(DisplayItemCategoriesSettingTypeKey, displayItemCategories); settingEntity.setSettingValue(DisplayItemElementsSettingTypeKey, displayItemElements); settingEntity.setSettingValue(DisplayLocationsSettingTypeKey, displayLocationItems); + settingEntity.setSettingValue(DisplayPropertyValuesSettingTypeKey, displayPropertyValues); settingEntity.setSettingValue(DisplayPropertyTypesSettingTypeKey, displayPropertyTypes); settingEntity.setSettingValue(DisplayPropertyTypeCategoriesSettingTypeKey, displayPropertyTypeCategories); settingEntity.setSettingValue(DisplaySourcesSettingTypeKey, displaySources); @@ -161,6 +176,30 @@ public void setDisplayMachineDesignItems(Boolean displayMachineDesignItems) { this.displayMachineDesignItems = displayMachineDesignItems; } + public Boolean getDisplayIOCItems() { + return displayIOCItems; + } + + public void setDisplayIOCItems(Boolean displayIOCItems) { + this.displayIOCItems = displayIOCItems; + } + + public Boolean getDisplayAppItems() { + return displayAppItems; + } + + public void setDisplayAppItems(Boolean displayAppItems) { + this.displayAppItems = displayAppItems; + } + + public Boolean getDisplayPropertyValues() { + return displayPropertyValues; + } + + public void setDisplayPropertyValues(Boolean displayPropertyValues) { + this.displayPropertyValues = displayPropertyValues; + } + public Boolean getDisplayCatalogItems() { return displayCatalogItems; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java index fead9cc4a..cf7dfff81 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/CdbEntityControllerUtility.java @@ -385,21 +385,8 @@ public LinkedList performEntitySearch(String searchString, boolean return searchResultList; } - // Start new search - Pattern searchPattern; - String patternString; - if (searchString.contains("?") || searchString.contains("*")) { - patternString = searchString.replace("*", ".*"); - patternString = patternString.replace("?", "."); - } else { - patternString = Pattern.quote(searchString); - } - - if (caseInsensitive) { - searchPattern = Pattern.compile(patternString, Pattern.CASE_INSENSITIVE); - } else { - searchPattern = Pattern.compile(patternString); - } + // Start new search + Pattern searchPattern = buildSearchPattern(searchString, caseInsensitive); List allObjectList = searchEntities(searchString); for (EntityType entity : allObjectList) { try { @@ -413,9 +400,35 @@ public LinkedList performEntitySearch(String searchString, boolean } - return searchResultList; + return searchResultList; } - + + /** + * Builds the regex pattern used to identify which entity attributes match a + * search string. The default treats the search string as a single contiguous + * pattern (with * and ? wildcards). Subclasses may override to change the + * matching semantics (e.g. word-order independent matching). + * + * @param searchString search string + * @param caseInsensitive use case insensitive matching + * @return compiled search pattern + */ + protected Pattern buildSearchPattern(String searchString, boolean caseInsensitive) { + String patternString; + if (searchString.contains("?") || searchString.contains("*")) { + patternString = searchString.replace("*", ".*"); + patternString = patternString.replace("?", "."); + } else { + patternString = Pattern.quote(searchString); + } + + if (caseInsensitive) { + return Pattern.compile(patternString, Pattern.CASE_INSENSITIVE); + } else { + return Pattern.compile(patternString); + } + } + public PropertyValue preparePropertyTypeValueAdd(EntityType cdbDomainEntity, PropertyType propertyType) { return preparePropertyTypeValueAdd(cdbDomainEntity, propertyType, propertyType.getDefaultValue(), null); } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignIOCControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignIOCControllerUtility.java index e172641eb..7d1588a6c 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignIOCControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/ItemDomainMachineDesignIOCControllerUtility.java @@ -34,6 +34,11 @@ public List getItemList() { return itemFacade.getIOCItems(); } + @Override + public List searchEntities(String searchString) { + return itemFacade.searchIOCItems(searchString); + } + @Override public String getDisplayEntityTypeName() { return "IOC Item"; diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/PropertyValueControllerUtility.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/PropertyValueControllerUtility.java index a48e2eb05..2f3cd4fab 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/PropertyValueControllerUtility.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/utilities/PropertyValueControllerUtility.java @@ -7,6 +7,8 @@ import gov.anl.aps.cdb.portal.model.db.beans.PropertyValueFacade; import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import java.util.List; +import java.util.regex.Pattern; /** * @@ -26,7 +28,43 @@ public String getEntityTypeName() { @Override public PropertyValue createEntityInstance(UserInfo sessionUser) { - return new PropertyValue(); + return new PropertyValue(); } - + + @Override + public List searchEntities(String searchString) { + return getEntityDbFacade().searchPropertyValues(searchString); + } + + /** + * Builds a word-order independent pattern: each whitespace-delimited word of + * the search string is matched independently (alternation), so a field is + * recorded as a match when it contains any of the search words. This mirrors + * the facade's per-word query so DB-returned rows are never dropped. + * + * Each word is wrapped in its own capturing group so consumers can identify + * which word a match belongs to (used to build the shortest match snippet). + */ + @Override + protected Pattern buildSearchPattern(String searchString, boolean caseInsensitive) { + String[] tokens = searchString.trim().split("\\s+"); + StringBuilder patternString = new StringBuilder(); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) { + patternString.append("|"); + } + String token = tokens[i]; + String tokenRegex; + if (token.contains("*") || token.contains("?")) { + tokenRegex = token.replace("*", ".*").replace("?", "."); + } else { + tokenRegex = Pattern.quote(token); + } + patternString.append("(").append(tokenRegex).append(")"); + } + + int flags = caseInsensitive ? Pattern.CASE_INSENSITIVE : 0; + return Pattern.compile(patternString.toString(), flags); + } + } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/ItemDomainMachineDesignFacade.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/ItemDomainMachineDesignFacade.java index 096b8f63f..a139d489f 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/ItemDomainMachineDesignFacade.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/ItemDomainMachineDesignFacade.java @@ -58,7 +58,15 @@ public List getIOCItems() { return findByDomainAndEntityType( ItemDomainName.machineDesign.getValue(), EntityTypeName.ioc.getValue() - ); + ); + } + + public List searchIOCItems(String searchString) { + searchString = convertWildcards(searchString); + return (List) em.createNamedStoredProcedureQuery("item.searchIOCItems") + .setParameter("limit_row", SEARCH_RESULT_LIMIT) + .setParameter("search_string", searchString) + .getResultList(); } public List getMachineDesignTemplates() { diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/PropertyValueFacade.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/PropertyValueFacade.java index 43dd91f32..c260a158b 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/PropertyValueFacade.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/beans/PropertyValueFacade.java @@ -4,6 +4,7 @@ */ package gov.anl.aps.cdb.portal.model.db.beans; +import gov.anl.aps.cdb.portal.model.db.entities.Item; import gov.anl.aps.cdb.portal.model.db.entities.PropertyValue; import gov.anl.aps.cdb.portal.utilities.SessionUtility; import java.util.List; @@ -11,6 +12,7 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.PersistenceContext; +import javax.persistence.Query; import javax.persistence.StoredProcedureQuery; /** @@ -23,6 +25,8 @@ public class PropertyValueFacade extends CdbEntityFacade { @PersistenceContext(unitName = "CdbWebPortalPU") private EntityManager em; + private static final Integer SEARCH_RESULT_LIMIT = 1000; + @Override protected EntityManager getEntityManager() { return em; @@ -62,5 +66,59 @@ public List fetchRelationshipParentPropertyValues(Integer itemId, } return null; } - + + /** + * Searches item-attached property values, matching each whitespace-delimited + * word of the search string independently (word-order independent). A property + * value matches when every word appears in at least one of its value, tag, or + * text fields. + * + * @param searchString whitespace-delimited search words + * @return matching property values, limited to SEARCH_RESULT_LIMIT + */ + public List searchPropertyValues(String searchString) { + String[] tokens = searchString.trim().split("\\s+"); + + StringBuilder jpql = new StringBuilder( + "SELECT DISTINCT pv FROM PropertyValue pv JOIN pv.itemElementList ie WHERE "); + for (int i = 0; i < tokens.length; i++) { + if (i > 0) { + jpql.append(" AND "); + } + jpql.append("(pv.value LIKE :t").append(i) + .append(" OR pv.tag LIKE :t").append(i) + .append(" OR pv.text LIKE :t").append(i).append(")"); + } + + Query query = em.createQuery(jpql.toString()); + for (int i = 0; i < tokens.length; i++) { + query.setParameter("t" + i, "%" + convertWildcards(tokens[i]) + "%"); + } + query.setMaxResults(SEARCH_RESULT_LIMIT); + return (List) query.getResultList(); + } + + /** + * Resolves the owning item of a property value by walking + * property value -> item element -> parent item. Performed at redirect time + * (rather than per search result row) to keep the search results page light. + * + * @param propertyValueId id of the property value + * @return the first parent item found, or null if none + */ + public Item getParentItemForPropertyValue(Integer propertyValueId) { + try { + List resultList = (List) em.createQuery( + "SELECT ie.parentItem FROM PropertyValue pv JOIN pv.itemElementList ie WHERE pv.id = :id") + .setParameter("id", propertyValueId) + .setMaxResults(1) + .getResultList(); + if (!resultList.isEmpty()) { + return resultList.get(0); + } + } catch (NoResultException ex) { + } + return null; + } + } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java index 6f90b204c..1903ab377 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/Item.java @@ -235,6 +235,23 @@ ) } ), + @NamedStoredProcedureQuery( + name = "item.searchIOCItems", + procedureName = "search_ioc_items", + resultClasses = ItemDomainMachineDesign.class, + parameters = { + @StoredProcedureParameter( + name = "limit_row", + mode = ParameterMode.IN, + type = Integer.class + ), + @StoredProcedureParameter( + name = "search_string", + mode = ParameterMode.IN, + type = String.class + ) + } + ), @NamedStoredProcedureQuery( name = "item.searchItemsNoEntityType", procedureName = "search_items_no_entity_type", @@ -1781,6 +1798,7 @@ public SearchResult createSearchResultInfo(Pattern searchPattern) { searchResult.doesValueContainPattern("owned by", getEntityInfo().getOwnerUser().getUsername(), searchPattern); } searchResult.doesValueContainPattern("description", getDescription(), searchPattern); + return searchResult; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventory.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventory.java index 5d3db12cb..470d4e8dd 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventory.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/ItemDomainInventory.java @@ -10,7 +10,6 @@ import gov.anl.aps.cdb.portal.controllers.ItemDomainInventoryController; import gov.anl.aps.cdb.portal.controllers.utilities.ItemControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainInventoryControllerUtility; -import gov.anl.aps.cdb.portal.model.jsf.beans.SparePartsBean; import java.util.List; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/PropertyValue.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/PropertyValue.java index 110013b20..465927603 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/PropertyValue.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/model/db/entities/PropertyValue.java @@ -7,12 +7,19 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import gov.anl.aps.cdb.common.utilities.ObjectUtility; +import gov.anl.aps.cdb.portal.utilities.SearchResult; import java.io.Serializable; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.persistence.Basic; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -86,6 +93,14 @@ public class PropertyValue extends PropertyValueBase implements Serializable { private static final long serialVersionUID = 1L; + + // Number of context characters kept on each side of a matched word in search + // result match descriptions. + private static final int SEARCH_SNIPPET_CONTEXT = 40; + + // Matched words separated by more than this many words are shown as separate + // snippet blocks rather than one continuous run. + private static final int SEARCH_SNIPPET_MAX_GAP_WORDS = 3; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @@ -219,6 +234,152 @@ public void setText(String text) { this.text = text; } + @Override + public SearchResult createSearchResultInfo(Pattern searchPattern) { + String label = "Property value"; + if (propertyType != null && propertyType.getName() != null) { + label = propertyType.getName(); + } + if (value != null && !value.isEmpty()) { + label += ": " + value; + } else if (tag != null && !tag.isEmpty()) { + label += " [" + tag + "]"; + } + + SearchResult searchResult = new SearchResult(this, id, label); + searchResult.doesValueContainPattern("value", value, searchPattern); + searchResult.doesValueContainPattern("tag", tag, searchPattern); + addMatchSnippet(searchResult, "text", text, searchPattern); + return searchResult; + } + + /** + * Records a match for the given field, but instead of storing the entire field + * value (the text field can be very large) it stores a short snippet showing + * the matched word(s) with surrounding context, e.g. "...word matchWord word...". + * + * For multi-word searches the snippet is based on the shortest region of the + * field that contains every matched word (a minimum window), so the result + * shows the tightest place where the words occur together. If words within + * that window are more than SEARCH_SNIPPET_MAX_GAP_WORDS apart the snippet is + * split into separate blocks joined by ellipses rather than one long run. The + * pattern wraps each search word in its own capturing group, which is how an + * individual match is attributed to a word. + */ + private void addMatchSnippet(SearchResult searchResult, String key, String fieldValue, Pattern searchPattern) { + if (fieldValue == null || fieldValue.isEmpty()) { + return; + } + + Matcher matcher = searchPattern.matcher(fieldValue); + int wordCount = matcher.groupCount(); + + // Collect each match as {wordIndex, start, end} in text order. + List matches = new ArrayList<>(); + Set matchedWords = new HashSet<>(); + while (matcher.find()) { + if (matcher.end() == matcher.start()) { + // Zero-width match, stop to avoid an infinite loop. + break; + } + for (int word = 1; word <= wordCount; word++) { + if (matcher.group(word) != null) { + matches.add(new int[]{word, matcher.start(word), matcher.end(word)}); + matchedWords.add(word); + break; + } + } + } + if (matches.isEmpty()) { + return; + } + + // Find the shortest span containing one occurrence of each matched word. + int need = matchedWords.size(); + Map windowCounts = new HashMap<>(); + int satisfied = 0; + int left = 0; + int bestLeft = 0; + int bestRight = matches.size() - 1; + int bestLength = Integer.MAX_VALUE; + for (int right = 0; right < matches.size(); right++) { + int word = matches.get(right)[0]; + if (windowCounts.merge(word, 1, Integer::sum) == 1) { + satisfied++; + } + while (satisfied == need) { + int spanLength = matches.get(right)[2] - matches.get(left)[1]; + if (spanLength < bestLength) { + bestLength = spanLength; + bestLeft = left; + bestRight = right; + } + int leftWord = matches.get(left)[0]; + if (windowCounts.merge(leftWord, -1, Integer::sum) == 0) { + satisfied--; + } + left++; + } + } + + // Within the shortest span, group matches into blocks, starting a new block + // whenever more than SEARCH_SNIPPET_MAX_GAP_WORDS words separate consecutive + // matches, so a wide span is not shown as one long continuous run. + List blocks = new ArrayList<>(); + for (int i = bestLeft; i <= bestRight; i++) { + int[] match = matches.get(i); + if (!blocks.isEmpty()) { + int[] block = blocks.get(blocks.size() - 1); + if (countWords(fieldValue.substring(block[1], match[1])) <= SEARCH_SNIPPET_MAX_GAP_WORDS) { + block[1] = match[2]; + continue; + } + } + blocks.add(new int[]{match[1], match[2]}); + } + + // Render each block with surrounding context, clamped to the midpoint + // between neighboring blocks so their context does not overlap, joined by + // ellipses. + int fieldLength = fieldValue.length(); + StringBuilder snippet = new StringBuilder(); + for (int i = 0; i < blocks.size(); i++) { + int[] block = blocks.get(i); + int leftLimit = (i == 0) ? 0 : (blocks.get(i - 1)[1] + block[0]) / 2; + int rightLimit = (i == blocks.size() - 1) ? fieldLength : (block[1] + blocks.get(i + 1)[0]) / 2; + int blockStart = Math.max(leftLimit, block[0] - SEARCH_SNIPPET_CONTEXT); + int blockEnd = Math.min(rightLimit, block[1] + SEARCH_SNIPPET_CONTEXT); + + if (i == 0) { + if (blockStart > 0) { + snippet.append("..."); + } + } else { + snippet.append(" ... "); + } + snippet.append(fieldValue.substring(blockStart, blockEnd).replaceAll("\\s+", " ").trim()); + if (i == blocks.size() - 1 && blockEnd < fieldLength) { + snippet.append("..."); + } + } + + searchResult.addAttributeMatch(key, snippet.toString()); + } + + /** + * Counts the whitespace-delimited words in a string (0 for null/blank). + */ + private static int countWords(String value) { + if (value == null) { + return 0; + } + value = value.trim(); + if (value.isEmpty()) { + return 0; + } + return value.split("\\s+").length; + } + public String getUnits() { return units; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesOptions.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesOptions.java index bd8e0423d..1ff1b1dfa 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesOptions.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesOptions.java @@ -18,6 +18,8 @@ public class SearchEntitiesOptions { private final boolean includeCableInventory; private final boolean includeCableDesign; private final boolean includeMachineDesign; + private final boolean includeIOC; + private final boolean includeApp; private final boolean includeItemLocation; private final boolean includeMAARC; private final boolean includeItemElement; @@ -41,6 +43,8 @@ public SearchEntitiesOptions() { this.includeMAARC = false; this.includeItemLocation = false; this.includeMachineDesign = false; + this.includeIOC = false; + this.includeApp = false; this.includeCableDesign = false; this.includeCableInventory = false; this.includeCableCatalog = false; @@ -76,6 +80,14 @@ public boolean isIncludeMachineDesign() { return includeMachineDesign; } + public boolean isIncludeIOC() { + return includeIOC; + } + + public boolean isIncludeApp() { + return includeApp; + } + public boolean isIncludeItemLocation() { return includeItemLocation; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesResults.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesResults.java index 00e30902e..74027ea5f 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesResults.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/entities/SearchEntitiesResults.java @@ -16,6 +16,8 @@ public class SearchEntitiesResults { LinkedList itemDomainCatalogResults; LinkedList itemDomainInventoryResults; LinkedList itemDomainMachineDesignResults; + LinkedList itemDomainMachineDesignIOCResults; + LinkedList itemDomainAppResults; LinkedList itemDomainCableCatalogResults; LinkedList itemDomainCableInventoryResults; LinkedList itemDomainCableDesignResults; @@ -57,6 +59,22 @@ public void setItemDomainMachineDesignResults(LinkedList itemDomai this.itemDomainMachineDesignResults = itemDomainMachineDesignResults; } + public LinkedList getItemDomainMachineDesignIOCResults() { + return itemDomainMachineDesignIOCResults; + } + + public void setItemDomainMachineDesignIOCResults(LinkedList itemDomainMachineDesignIOCResults) { + this.itemDomainMachineDesignIOCResults = itemDomainMachineDesignIOCResults; + } + + public LinkedList getItemDomainAppResults() { + return itemDomainAppResults; + } + + public void setItemDomainAppResults(LinkedList itemDomainAppResults) { + this.itemDomainAppResults = itemDomainAppResults; + } + public LinkedList getItemDomainCableCatalogResults() { return itemDomainCableCatalogResults; } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/routes/SearchRoute.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/routes/SearchRoute.java index e3d0c9c6b..5dd9aa291 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/routes/SearchRoute.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/rest/routes/SearchRoute.java @@ -9,8 +9,10 @@ import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainCableCatalogControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainCableDesignControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainCableInventoryControllerUtility; +import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainAppControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainCatalogControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainInventoryControllerUtility; +import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignIOCControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainLocationControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMAARCControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignControllerUtility; @@ -83,6 +85,16 @@ public SearchEntitiesResults searchEntities(@RequestBody(required = true) Search LinkedList machineDesignResults = machineDesignControllerUtility.performEntitySearch(searchText, true); results.setItemDomainMachineDesignResults(machineDesignResults); } + if (searchEntitiesOptions.isIncludeIOC()) { + ItemDomainMachineDesignIOCControllerUtility iocControllerUtility = new ItemDomainMachineDesignIOCControllerUtility(); + LinkedList iocResults = iocControllerUtility.performEntitySearch(searchText, true); + results.setItemDomainMachineDesignIOCResults(iocResults); + } + if (searchEntitiesOptions.isIncludeApp()) { + ItemDomainAppControllerUtility appControllerUtility = new ItemDomainAppControllerUtility(); + LinkedList appResults = appControllerUtility.performEntitySearch(searchText, true); + results.setItemDomainAppResults(appResults); + } if (searchEntitiesOptions.isIncludeItemLocation()) { ItemDomainLocationControllerUtility locationControllerUtility = new ItemDomainLocationControllerUtility(); LinkedList locationResults = locationControllerUtility.performEntitySearch(searchText, true); diff --git a/src/java/CdbWebPortal/web/resources/css/machineDesignIOC.css b/src/java/CdbWebPortal/web/resources/css/machineDesignIOC.css index 375de94e8..8ec8666d0 100644 --- a/src/java/CdbWebPortal/web/resources/css/machineDesignIOC.css +++ b/src/java/CdbWebPortal/web/resources/css/machineDesignIOC.css @@ -8,6 +8,17 @@ background: linear-gradient(to bottom right, #e8f0f7 0%, #a8d0e6 100%); } +.iocInstructionsPanel.ui-panel { + background: rgba(255, 255, 255, 0.5); + border-radius: 8px; + overflow: hidden; +} + +.iocInstructionsPanel .ui-panel-content { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + html, body { background: linear-gradient( diff --git a/src/java/CdbWebPortal/web/views/domainEntityProperty/private/domainEntityPropertyListDataTable.xhtml b/src/java/CdbWebPortal/web/views/domainEntityProperty/private/domainEntityPropertyListDataTable.xhtml index a7e7de442..46ca5f17e 100644 --- a/src/java/CdbWebPortal/web/views/domainEntityProperty/private/domainEntityPropertyListDataTable.xhtml +++ b/src/java/CdbWebPortal/web/views/domainEntityProperty/private/domainEntityPropertyListDataTable.xhtml @@ -48,7 +48,8 @@ See LICENSE file. - + + + xmlns:ui="http://java.sun.com/jsf/facelets" + xmlns:p="http://primefaces.org/ui"> - + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml index e0c483bea..bc701a48d 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml @@ -28,11 +28,7 @@ See LICENSE file. - - - - - + @@ -52,7 +48,23 @@ See LICENSE file. - + + + + + + + + + + @@ -60,7 +72,13 @@ See LICENSE file. + + diff --git a/src/java/CdbWebPortal/web/views/propertyValue/private/markdownGeneratedContentOutput.xhtml b/src/java/CdbWebPortal/web/views/propertyValue/private/markdownGeneratedContentOutput.xhtml new file mode 100644 index 000000000..807fda00a --- /dev/null +++ b/src/java/CdbWebPortal/web/views/propertyValue/private/markdownGeneratedContentOutput.xhtml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueDetailsDialog.xhtml b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueDetailsDialog.xhtml new file mode 100644 index 000000000..67b8ae1ae --- /dev/null +++ b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueDetailsDialog.xhtml @@ -0,0 +1,71 @@ + + + + + + + + + + + + Type + + + + Tag + + + + Value + + + + + + Description + + + + Category + + + + Units + + + + Entered By + + + + Entered On + + + + Effective Date + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValuePermalinkActionButton.xhtml b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValuePermalinkActionButton.xhtml new file mode 100644 index 000000000..8c91ec973 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValuePermalinkActionButton.xhtml @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueSearchDataTable.xhtml b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueSearchDataTable.xhtml new file mode 100644 index 000000000..4dd2dcc27 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/propertyValue/private/propertyValueSearchDataTable.xhtml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/propertyValue/private/template/propertyValueMarkdownValueDialogTemplate.xhtml b/src/java/CdbWebPortal/web/views/propertyValue/private/template/propertyValueMarkdownValueDialogTemplate.xhtml index 42a3d45a3..077c37c39 100644 --- a/src/java/CdbWebPortal/web/views/propertyValue/private/template/propertyValueMarkdownValueDialogTemplate.xhtml +++ b/src/java/CdbWebPortal/web/views/propertyValue/private/template/propertyValueMarkdownValueDialogTemplate.xhtml @@ -89,9 +89,11 @@ See LICENSE file. - + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/search/private/searchCustomizeDialog.xhtml b/src/java/CdbWebPortal/web/views/search/private/searchCustomizeDialog.xhtml index 19efd7097..f83c56a4e 100644 --- a/src/java/CdbWebPortal/web/views/search/private/searchCustomizeDialog.xhtml +++ b/src/java/CdbWebPortal/web/views/search/private/searchCustomizeDialog.xhtml @@ -29,6 +29,12 @@ See LICENSE file. + + + + + + @@ -59,6 +65,9 @@ See LICENSE file. + + + diff --git a/src/java/CdbWebPortal/web/views/search/private/searchEntityResultPanels.xhtml b/src/java/CdbWebPortal/web/views/search/private/searchEntityResultPanels.xhtml index c54622369..9d31017e6 100644 --- a/src/java/CdbWebPortal/web/views/search/private/searchEntityResultPanels.xhtml +++ b/src/java/CdbWebPortal/web/views/search/private/searchEntityResultPanels.xhtml @@ -15,10 +15,32 @@ See LICENSE file. collapsed="#{!itemDomainMachineDesignController.displaySearchResultList}" toggleable="true"> - + - + + + + + + + + + + + + + + + + + - + + + + + + - +