{"id":23122522,"url":"https://github.com/folio-org/folio-spring-support","last_synced_at":"2025-08-17T01:32:18.573Z","repository":{"id":38310198,"uuid":"316066601","full_name":"folio-org/folio-spring-support","owner":"folio-org","description":"This is a library (jar) that contains the basic functionality and main dependencies required for development FOLIO modules using Spring framework.","archived":false,"fork":false,"pushed_at":"2025-07-28T09:12:30.000Z","size":745,"stargazers_count":1,"open_issues_count":0,"forks_count":6,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-07-31T19:00:02.119Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/folio-org.png","metadata":{"files":{"readme":"README.md","changelog":"NEWS.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-11-25T22:24:27.000Z","updated_at":"2025-07-28T09:12:33.000Z","dependencies_parsed_at":"2023-11-12T22:23:07.867Z","dependency_job_id":"ff3b9140-ea16-4690-b457-f828cb35295d","html_url":"https://github.com/folio-org/folio-spring-support","commit_stats":null,"previous_names":["folio-org/folio-spring-base"],"tags_count":46,"template":false,"template_full_name":null,"purl":"pkg:github/folio-org/folio-spring-support","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/folio-org%2Ffolio-spring-support","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/folio-org%2Ffolio-spring-support/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/folio-org%2Ffolio-spring-support/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/folio-org%2Ffolio-spring-support/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/folio-org","download_url":"https://codeload.github.com/folio-org/folio-spring-support/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/folio-org%2Ffolio-spring-support/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270796223,"owners_count":24647320,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-16T02:00:11.002Z","response_time":91,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-17T07:27:50.177Z","updated_at":"2025-08-17T01:32:18.553Z","avatar_url":"https://github.com/folio-org.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# folio-spring-support\n\nCopyright (C) 2020-2023 The Open Library Foundation\n\nThis software is distributed under the terms of the Apache License,\nVersion 2.0. See the file \"[LICENSE](LICENSE)\" for more information.\n\n## Table of Contents\n\n- [Table of Contents](#table-of-contents)\n- [Introduction](#introduction)\n- [Code structure](#code-structure)\n- [Execution Context](#execution-context)\n- [Properties](#properties)\n- [CQL support](#cql-support)\n- [Logging](#logging)\n  - [Default logging format](#default-logging-format)\n  - [Logging for incoming and outgoing requests](#logging-for-incoming-and-outgoing-requests)\n    - [Log examples:](#log-examples)\n- [Custom `/_/tenant` Logic](#custom-_tenant-logic)\n  - [`TenantService` Event Methods](#tenantservice-event-methods)\n  - [`TenantService` Methods and Fields](#tenantservice-methods-and-fields)\n  - [Event Order](#event-order)\n    - [Upon Creation](#upon-creation)\n    - [Upon Deletion](#upon-deletion)\n  - [Sample](#sample)\n- [Internationalization](#internationalization)\n- [Additional information](#additional-information)\n  - [Issue tracker](#issue-tracker)\n\n## Introduction\n\nThis is a library that contains the basic functionality and main dependencies required for development of FOLIO modules using Spring framework (also known as \"Spring Way\").\n\nPlease find a step-by-step guide on how to create a new FOLIO Spring based module at https://github.com/folio-org/mod-spring-template\n\nAn example of the module based on folio-spring-support could be found at https://github.com/folio-org/folio-sample-modules/tree/master/mod-spring-petstore\n\n## Code structure\n\nThe library comprises several submodules that are built as separate artifacts (jar files) and can be integrated into a project as distinct dependencies. This facilitates more precise dependency management depending on the requirements of each project.\n\nThe library includes the following submodules:\n* **folio-spring-base** - provides fundamental functionality for developing FOLIO modules using the Spring framework.\n* **folio-spring-cql** - facilitates CQL querying (refer to the [CQL support](#cql-support) section below)\n* **folio-spring-system-user** - provides [functionality](folio-spring-system-user/README.md) for system-user creation and utilization \n\n## Execution Context\n\n[FolioExecutionContext](folio-spring-base/src/main/java/org/folio/spring/FolioExecutionContext.java) is used to store\nessential request headers \u003ci\u003e(in thread local)\u003c/i\u003e. Folio Spring Base populates this data\nusing [FolioExecutionScopeFilter](folio-spring-base/src/main/java/org/folio/spring/scope/filter/FolioExecutionScopeFilter.java).\nIt is used by [EnrichUrlAndHeadersClient](folio-spring-base/src/main/java/org/folio/spring/client/EnrichUrlAndHeadersClient.java), to provide right tenant id and other headers for outgoing REST requests.\nIt is also used in [DataSourceSchemaAdvisorBeanPostProcessor](folio-spring-base/src/main/java/org/folio/spring/config/DataSourceSchemaAdvisorBeanPostProcessor.java) for selection of the appropriate schema for sql queries.\n\nFolioExecutionContext is immutable. In order to start new execution context the construct\n\n```\n  try (var x = new FolioExecutionContextSetter(currentFolioExecutionContext)) {\n    chain.doFilter(request, response);\n  }\n```\n\nshould be used (pick any of the available constructors).\n\nUsing try-with-resources is best practice. Not using try-with-resources is error-prone, may result in a wrong tenant and should be avoided. If not using try-with-resources ensure to call `folioExecutionContextSetter.close()` when the execution is finished. Example:\n\n```\n  // Not using try-with-resources is discouraged!\n  var x = new FolioExecutionContextSetter(currentFolioExecutionContext);\n  // do some stuff\n  x.close();\n```\n\n***CAUTION: FolioExecutionContext should not be used in asynchronous code executions (as it is stored in thread local), unless\nthe appropriate data is manually set by using `FolioExecutionContextSetter`.***\n\nExample of asynchronous execution:   \n```\nprivate final FolioModuleMetadata folioModuleMetadata;\n\n@Async\nvoid ayncMethod(Map\u003cString, Collection\u003cString\u003e\u003e headers) {\n  try (var x = new FolioExecutionContextSetter(folioModuleMetadata, httpHeaders)) {\n    _your_code_here_\n  }\n}\n```\nFOLIO scope implementation supports nested FolioExecutionContexts it means that the following code works correctly for\n```\n// Autowired\nprivate final FolioModuleMetadata folioModuleMetadata;\n\n// Autowired\nprotected final FolioExecutionContext context;\n\nvoid someMethod(Map\u003cString, Collection\u003cString\u003e\u003e headers) {\n  Map\u003cString, Collection\u003cString\u003e\u003e headers1 = getHeaderForTenant(\"Tenant1\");\n  try (var x = new FolioExecutionContextSetter(folioModuleMetadata, headers1)) {\n    String tenant1 = context.getTenantId();\n    businessMethod(tenant1);\n\n    Map\u003cString, Collection\u003cString\u003e\u003e headers2 = getHeaderForTenant(\"Tenant2\");\n    try (var x = new FolioExecutionContextSetter(folioModuleMetadata, headers2)) {\n      String tenant2 = context.getTenantId();\n      businessMethod(tenant2);\n    }\n    \n    String tenant1_1 = context.getTenantId();\n    assert tenant1.equals(tenant1_1);\n  }\n}\n\n...\n\nvoid businessMethod(String tenantId) {\n  _do_some_useful_stuff_\n  String tenantId = context.getTenantId();\n\n  assert tenant.equals(tenantId);\n}\n```\n\n\n## Properties\n\n| Property                                              | Description                                                                                                                                                                                                           | Default       | Example                      |\n| ----------------------------------------------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------- | ---------------------------- |\n| `header.validation.x-okapi-tenant.exclude.base-paths` | Specifies base paths to exclude form `x-okapi-tenant` header validation. See [TenantOkapiHeaderValidationFilter.java](folio-spring-base/src/main/java/org/folio/spring/filter/TenantOkapiHeaderValidationFilter.java) | `/admin`      | `/admin,/swagger-ui`         |\n| `folio.jpa.repository.base-packages`                  | Specifies base packages to scan for repositories                                                                                                                                                                      | `org.folio.*` | `org.folio.qm.dao`           |\n| `folio.logging.request.enabled`                       | Turn on logging for incoming requests                                                                                                                                                                                 | `true`        | `true or false`              |\n| `folio.logging.request.level`                         | Specifies logging level for incoming requests                                                                                                                                                                         | `basic`       | `none, basic, headers, full` |\n| `folio.logging.feign.enabled`                         | Turn on logging for outgoing requests in feign clients                                                                                                                                                                | `true`        | `true or false`              |\n| `folio.logging.feign.level`                           | Specifies logging level for outgoing requests                                                                                                                                                                         | `basic`       | `none, basic, headers, full` |\n\n## CQL support\n\nTo have ability to search entities in databases by CQL-queries:\n\n- create repository interface for needed entity\n- extend it from `JpaCqlRepository\u003cT, ID\u003e`, where `T` is entity class and `ID` is entity's id class.\n- the implementation of the repository will be created by Spring\n\n```java\npublic interface PersonRepository extends JpaCqlRepository\u003cPerson, Integer\u003e {\n\n}\n```\n\nTwo methods are available for CQL-queries:\n\n```java\npublic interface JpaCqlRepository\u003cT, ID\u003e extends JpaRepository\u003cT, ID\u003e {\n\n  Page\u003cT\u003e findByCql(String cql, OffsetRequest offset);\n\n  long count(String cql);\n}\n```\n\nBy default a CQL search a `String` field ignores case (= is case insensitive) and ignores accents; this is for consistency with \u003ca href=\"https://github.com/folio-org/raml-module-builder?tab=readme-ov-file#the-post-tenant-api\"\u003eRMB based modules\u003c/a\u003e. Use the annotations `@RespectCase` and/or `@RespectAccents` in the entity class to change the default.\n\n## Logging\n\n### Default logging format\n\nLibrary uses [log4j2](https://logging.apache.org/log4j/2.x/) for logging. There are two default log4j2 configurations:\n\n- `log4j2.properties` console/line based logger and it is the default\n- `log4j2-json.properties` JSON structured logging\n\nTo choose the JSON structured logging by using setting: `-Dlog4j.configurationFile=log4j2-json.properties`\nA module that wants to generate log4J2 logs in a different format can create a `log4j2.properties` file in the /resources directory.\n\n### Logging for incoming and outgoing requests\n\nBy default, logging for incoming and outgoing request enabled. Module could disable it by setting:\n\n- `folio.logging.request.enabled = false`\n- `folio.logging.feign.enabled = false`\n\nAlso, it is possible to specify logging level:\n`none` - no logs\n`basic` - log request method and URI, response status and spent time\n`headers` - log all that `basic` and request headers\n`full` - log all that `headers` and request and response bodies\n\n**_Note:_** _In case you have async requests in your module (DeferredResult, CompletableFuture, etc.) then you should disable default logging for requests._\n\n#### Log examples:\n\n- basic:\n\n```text\n18:41:18 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter ---\u003e PUT /records-editor/records/c9db5d7a-e1d4-11e8-9f32-f2801f1b9fd1 null\n18:41:19 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter \u003c--- 202 in 753ms\n```\n\n- headers:\n\n```text\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter ---\u003e PUT /records-editor/records/c9db5d7a-e1d4-11e8-9f32-f2801f1b9fd1 null\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-url: http://localhost:50017\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-tenant: \u003ctenantId\u003e\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-request-id: \u003crequestId\u003e\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-user-id: \u003cuserId\u003e\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter content-type: application/json; charset=UTF-8\n18:44:23 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter ---\u003e END HTTP\n18:44:24 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter \u003c--- 202 in 786ms\n```\n\n- full:\n\n```text\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter ---\u003e PUT /records-editor/records/c9db5d7a-e1d4-11e8-9f32-f2801f1b9fd1 null\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-url: http://localhost:53146\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-tenant: \u003ctenantId\u003e\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-request-id: \u003crequestId\u003e\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter x-okapi-user-id: \u003cuserId\u003e\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter content-type: application/json; charset=UTF-8\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter Body: {\"parsedRecordId\":\"c9db5d7a-e1d4-11e8-9f32-f2801f1b9fd1\",\"parsedRecordDtoId\":\"c56b70ce-4ef6-47ef-8bc3-c470bafa0b8c\",\"suppressDiscovery\":false}\n18:46:17 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter ---\u003e END HTTP\n18:46:18 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter \u003c--- 202 in 714ms\n18:46:18 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter Body:\n18:46:18 [\u003crequestId\u003e] [\u003ctenantId\u003e] [\u003cuserId\u003e] [\u003cmoduleId\u003e] INFO  LoggingRequestFilter \u003c--- END HTTP\n```\n\n## Custom `/_/tenant` Logic\n\nThere are many cases where you may want to add custom logic to the\n[`/_/tenant` endpoint](https://s3.amazonaws.com/foliodocs/api/folio-spring-base/s/tenant.html),\nsuch as for loading sample data or performing more complex database migration.\n\nIn order to do this, you can extend the `TenantService` within your module and override\nany of the methods listed below.\n\n### `TenantService` Event Methods\n\nThe following methods can be overridden by your module in order to add custom logic around events\nrelating to tenant creation, updates, and deletion. All of these return `void`.\n\nMany of these accept a `TenantAttributes` parameter which can provide information about the\nprevious module (`module_from`), the module being upgraded to (`module_to`), as well as any other\n`parameters` provided.\n\n:warning: Please note that methods with \"update\" in the name will be run on updates _as well as_\nwhen new tenants are created. Be especially careful with methods run before Liquibase -- the\ndatabase schema could potentially be in an unexpected state, particularly when a new tenant is\ncreated.\n\n| Visibility  | Signature                                 | Purpose                                                                                         |\n| ----------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |\n| `public`    | `loadReferenceData()`                     | Load any reference data (requested with `loadReference=true` parameter)                         |\n| `public`    | `loadSampleData()`                        | Load any sample data (requested with `loadSample=true` parameter)                               |\n| `protected` | `beforeTenantUpdate(TenantAttributes)`    | Run custom logic before a tenant is created or updated                                          |\n| `protected` | `beforeLiquibaseUpdate(TenantAttributes)` | Run custom logic immediately before Liquibase updates are started (after `beforeTenantUpdate`)  |\n| `protected` | `afterLiquibaseUpdate(TenantAttributes)`  | Run custom logic immediately before Liquibase updates are finished (before `afterTenantUpdate`) |\n| `protected` | `afterTenantUpdate(TenantAttributes)`     | Run custom logic after all update jobs are completed                                            |\n| `protected` | `beforeTenantDeletion(TenantAttributes)`  | Run custom logic before a tenant is deleted/purged                                              |\n| `protected` | `afterTenantDeletion(TenantAttributes)`   | Run custom logic after a tenant is deleted/purged (the schema will no longer exist)             |\n\n### `TenantService` Methods and Fields\n\nThere are two methods that may be of use in your custom logic:\n\n- `boolean tenantExists()` which will check if the database schema for this tenant exists (this\n  says nothing about if it is up to date)\n- `String getSchemaName()` will construct and return the name of the schema corresponding to the\n  module and tenant\n\nThese fields will also be provided:\n\n- `JdbcTemplate jdbcTemplate`, for running Postgres queries directly\n- `FolioExecutionContext context`, for getting information about the module\n- `FolioSpringLiquibase folioSpringLiquibase`, for interacting with Liquibase directly (this\n  extends `SpringLiquibase` and may be `null` if Liquibase is not enabled!)\n\n### Event Order\n\nThe [events](#tenantservice-event-methods) will be called in the following order:\n\n#### Upon Creation\n\n1. `beforeTenantUpdate`\n2. If Liquibase is enabled:\n   1. `beforeLiquibaseUpdate`\n   2. _Internal logic to apply Liquibase changes_\n   3. `afterLiquibaseUpdate`\n3. `afterTenantUpdate`\n4. `loadReferenceData`, if applicable\n5. `loadSampleData`, if applicable\n\n#### Upon Deletion\n\n1. `beforeTenantDeletion`\n2. _Internal logic to drop the schema_\n3. `afterTenantDeletion`\n\n### Sample\n\nOverriding these methods to add your own custom logic is quite straightforward. Here is an example\nof how to override these in your very own `@Service`:\n\n```java\npackage org.folio.yourmodule.service;\n\nimport org.folio.spring.service.TenantService;\nimport org.folio.tenant.domain.dto.TenantAttributes;\nimport org.folio.yourmodule.SuperCoolDataRepository;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.annotation.Primary;\nimport org.springframework.stereotype.Service;\n\n@Service\n@Primary // required to ensure CustomTenantService will be loaded instead of TenantService\npublic class CustomTenantService extends TenantService {\n\n  protected final SuperCoolDataRepository repository;\n\n  /**\n   * Load reference data\n   */\n  @Override\n  protected void loadReferenceData() {\n    repository.loadReferenceData();\n  }\n\n  /**\n   * Add our custom initial data\n   */\n  @Override\n  protected void beforeTenantUpdate(TenantAttributes attributes) {\n    // some custom logic for potentially migrating data\n  }\n}\n```\n\n## Internationalization\n\nTranslations may be performed in backend modules using the `folio-spring-i18n` library.  For more information, see the [folio-spring-i18n README](folio-spring-i18n/README.md).\n\n## Additional information\n\n### Issue tracker\n\nSee project [FOLSPRINGB](https://issues.folio.org/browse/FOLSPRINGB)\nat the [FOLIO issue tracker](https://dev.folio.org/guidelines/issue-tracker).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffolio-org%2Ffolio-spring-support","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffolio-org%2Ffolio-spring-support","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffolio-org%2Ffolio-spring-support/lists"}