Skip to content

Commit 6effc2a

Browse files
committed
TRUNK-6469: Add support for automatic initialization of Envers audit tables (#5468) (#5831)
1 parent eab4d6a commit 6effc2a

File tree

6 files changed

+392
-8
lines changed

6 files changed

+392
-8
lines changed

api/src/main/java/org/openmrs/ConceptReferenceRange.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class ConceptReferenceRange extends BaseReferenceRange implements Openmrs
4646
@GeneratedValue(strategy = GenerationType.IDENTITY)
4747
private Integer conceptReferenceRangeId;
4848

49-
@Column(name = "criteria", length = 65535)
49+
@Column(name = "criteria", length = 255)
5050
private String criteria;
5151

5252
@ManyToOne(fetch = FetchType.LAZY)

api/src/main/java/org/openmrs/api/db/hibernate/HibernateSessionFactoryBean.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727
import org.hibernate.boot.Metadata;
2828
import org.hibernate.engine.spi.SessionFactoryImplementor;
2929
import org.hibernate.integrator.spi.Integrator;
30+
import org.hibernate.service.ServiceRegistry;
3031
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
32+
import org.jspecify.annotations.NonNull;
33+
import org.openmrs.api.APIException;
3134
import org.openmrs.api.cache.CacheConfig;
3235
import org.openmrs.api.context.Context;
3336
import org.openmrs.module.Module;
3437
import org.openmrs.module.ModuleFactory;
38+
import org.openmrs.util.EnversAuditTableInitializer;
3539
import org.openmrs.util.OpenmrsUtil;
3640
import org.slf4j.Logger;
3741
import org.slf4j.LoggerFactory;
@@ -75,7 +79,7 @@ public class HibernateSessionFactoryBean extends LocalSessionFactoryBean impleme
7579
* as 'private' instead of 'protected'
7680
*/
7781
@Override
78-
public void setMappingResources(String... mappingResources) {
82+
public void setMappingResources(String @NonNull ... mappingResources) {
7983
Collections.addAll(this.mappingResources, mappingResources);
8084

8185
super.setMappingResources(this.mappingResources.toArray(new String[] {}));
@@ -87,7 +91,7 @@ public void setMappingResources(String... mappingResources) {
8791
* It adds to the set instead of overwriting it with each call.
8892
*/
8993
@Override
90-
public void setPackagesToScan(String... packagesToScan) {
94+
public void setPackagesToScan(String @NonNull ... packagesToScan) {
9195
this.packagesToScan.addAll(Arrays.asList(packagesToScan));
9296

9397
super.setPackagesToScan(this.packagesToScan.toArray(new String[0]));
@@ -129,7 +133,7 @@ public void afterPropertiesSet() throws IOException {
129133
Object key = entry.getKey();
130134
String prop = (String) key;
131135
String value = (String) entry.getValue();
132-
log.trace("Setting module property: " + prop + ":" + value);
136+
log.trace("Setting module property: {}:{}", prop, value);
133137
config.setProperty(prop, value);
134138
if (!prop.startsWith("hibernate")) {
135139
config.setProperty("hibernate." + prop, value);
@@ -143,7 +147,7 @@ public void afterPropertiesSet() throws IOException {
143147
Object key = entry.getKey();
144148
String prop = (String) key;
145149
String value = (String) entry.getValue();
146-
log.trace("Setting property: " + prop + ":" + value);
150+
log.trace("Setting property: {}:{}", prop, value);
147151
config.setProperty(prop, value);
148152
if (!prop.startsWith("hibernate")) {
149153
config.setProperty("hibernate." + prop, value);
@@ -186,8 +190,8 @@ public void afterPropertiesSet() throws IOException {
186190
value = value.replace("%APPLICATION_DATA_DIRECTORY%", applicationDataDirectory);
187191
entry.setValue(value);
188192
}
189-
190-
log.debug("Setting global Hibernate Session Interceptor for SessionFactory, Interceptor: " + chainingInterceptor);
193+
194+
log.debug("Setting global Hibernate Session Interceptor for SessionFactory, Interceptor: {}", chainingInterceptor);
191195

192196
// make sure all autowired interceptors are put onto our chaining interceptor
193197
// sort on the keys so that the devs/modules have some sort of control over the order of the interceptors
@@ -221,6 +225,7 @@ public void destroy() throws HibernateException {
221225
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory,
222226
SessionFactoryServiceRegistry serviceRegistry) {
223227
this.metadata = metadata;
228+
generateEnversAuditTables(metadata, serviceRegistry);
224229
}
225230

226231
@Override
@@ -234,4 +239,13 @@ public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactor
234239
public Metadata getMetadata() {
235240
return metadata;
236241
}
242+
243+
private void generateEnversAuditTables(Metadata metadata, ServiceRegistry serviceRegistry) {
244+
try {
245+
Properties hibernateProperties = getHibernateProperties();
246+
EnversAuditTableInitializer.initialize(metadata, hibernateProperties, serviceRegistry);
247+
} catch (Exception e) {
248+
throw new APIException("An error occurred while initializing the Envers audit tables", e);
249+
}
250+
}
237251
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.util;
11+
12+
import java.util.EnumSet;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
import java.util.Properties;
16+
import java.util.concurrent.atomic.AtomicBoolean;
17+
18+
import org.hibernate.boot.Metadata;
19+
import org.hibernate.boot.model.relational.Namespace;
20+
import org.hibernate.boot.model.relational.Sequence;
21+
import org.hibernate.mapping.Table;
22+
import org.hibernate.service.ServiceRegistry;
23+
import org.hibernate.tool.schema.TargetType;
24+
import org.hibernate.tool.schema.spi.ExceptionHandler;
25+
import org.hibernate.tool.schema.spi.ExecutionOptions;
26+
import org.hibernate.tool.schema.spi.SchemaFilter;
27+
import org.hibernate.tool.schema.spi.SchemaFilterProvider;
28+
import org.hibernate.tool.schema.spi.SchemaManagementTool;
29+
import org.hibernate.tool.schema.spi.SchemaMigrator;
30+
import org.hibernate.tool.schema.spi.ScriptTargetOutput;
31+
import org.hibernate.tool.schema.spi.TargetDescriptor;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
34+
35+
/**
36+
* Initializes Hibernate Envers audit tables when auditing is enabled. This class is responsible for
37+
* conditionally creating audit tables only when hibernate.integration.envers.enabled=true.
38+
*/
39+
public class EnversAuditTableInitializer {
40+
41+
private static final Logger log = LoggerFactory.getLogger(EnversAuditTableInitializer.class);
42+
43+
private EnversAuditTableInitializer() {
44+
45+
}
46+
47+
/**
48+
* Checks if Envers is enabled and creates/updates audit tables as needed. This will Create or
49+
* Update audit tables if they don't exist - Update existing audit tables if the schema has
50+
* changed
51+
*
52+
* @param metadata Hibernate metadata containing entity mappings
53+
* @param hibernateProperties properties containing Envers configuration
54+
* @param serviceRegistry Hibernate service registry
55+
*/
56+
public static void initialize(Metadata metadata, Properties hibernateProperties,
57+
ServiceRegistry serviceRegistry) {
58+
59+
if (!isEnversEnabled(hibernateProperties)) {
60+
log.debug("Hibernate Envers is not enabled. Skipping audit table initialization.");
61+
return;
62+
}
63+
64+
updateAuditTables(metadata, hibernateProperties, serviceRegistry);
65+
}
66+
67+
/**
68+
* Checks if Hibernate Envers is enabled in the configuration.
69+
*
70+
* @param properties Hibernate properties
71+
* @return true if Envers is enabled, false otherwise
72+
*/
73+
private static boolean isEnversEnabled(Properties properties) {
74+
String enversEnabled = properties.getProperty("hibernate.integration.envers.enabled");
75+
return "true".equalsIgnoreCase(enversEnabled);
76+
}
77+
78+
/**
79+
* Creates or updates audit tables using Hibernate's {@link SchemaMigrator}. This method filters
80+
* to only process audit tables.
81+
*
82+
* @param metadata Hibernate metadata containing entity mappings (includes Envers audit
83+
* entities)
84+
* @param hibernateProperties Hibernate configuration properties
85+
* @param serviceRegistry Hibernate service registry
86+
*/
87+
private static void updateAuditTables(Metadata metadata, Properties hibernateProperties,
88+
ServiceRegistry serviceRegistry) {
89+
String auditTablePrefix = hibernateProperties.getProperty("org.hibernate.envers.audit_table_prefix", "");
90+
String auditTableSuffix = hibernateProperties.getProperty("org.hibernate.envers.audit_table_suffix", "_audit");
91+
92+
Map<String, Object> settings = new HashMap<>((Map) hibernateProperties);
93+
settings.put("hibernate.hbm2ddl.schema_filter_provider", buildSchemaFilterProvider(auditTablePrefix, auditTableSuffix));
94+
95+
AtomicBoolean hasErrors = new AtomicBoolean(false);
96+
ExecutionOptions executionOptions = getExecutionOptions(settings, hasErrors);
97+
SchemaMigrator schemaMigrator = serviceRegistry.getService(SchemaManagementTool.class).getSchemaMigrator(settings);
98+
99+
schemaMigrator.doMigration(metadata, executionOptions, getTargetDescriptor());
100+
101+
if (hasErrors.get()) {
102+
log.warn("Envers audit table migration completed with errors.");
103+
} else {
104+
log.info("Successfully created/updated Envers audit tables using Hibernate SchemaManagementTool.");
105+
}
106+
}
107+
108+
private static SchemaFilterProvider buildSchemaFilterProvider(String auditTablePrefix, String auditTableSuffix) {
109+
String lowerPrefix = auditTablePrefix.toLowerCase();
110+
String lowerSuffix = auditTableSuffix.toLowerCase();
111+
112+
SchemaFilter auditFilter = new SchemaFilter() {
113+
@Override
114+
public boolean includeNamespace(Namespace namespace) {
115+
return true;
116+
}
117+
118+
@Override
119+
public boolean includeTable(Table table) {
120+
String tableName = table.getName();
121+
if (tableName == null) {
122+
return false;
123+
}
124+
125+
String lowerTableName = tableName.toLowerCase();
126+
127+
if (lowerTableName.contains("revision") || lowerTableName.equals("revinfo")) {
128+
return true;
129+
}
130+
131+
boolean hasPrefix = lowerPrefix.isEmpty() || lowerTableName.startsWith(lowerPrefix);
132+
boolean hasSuffix = lowerSuffix.isEmpty() || lowerTableName.endsWith(lowerSuffix);
133+
134+
return hasPrefix && hasSuffix;
135+
}
136+
137+
@Override
138+
public boolean includeSequence(Sequence sequence) {
139+
return false;
140+
}
141+
};
142+
143+
return new SchemaFilterProvider() {
144+
@Override public SchemaFilter getCreateFilter() { return auditFilter; }
145+
@Override public SchemaFilter getDropFilter() { return auditFilter; }
146+
@Override public SchemaFilter getMigrateFilter() { return auditFilter; }
147+
@Override public SchemaFilter getValidateFilter() { return auditFilter; }
148+
};
149+
}
150+
151+
private static TargetDescriptor getTargetDescriptor() {
152+
return new TargetDescriptor() {
153+
@Override
154+
public EnumSet<TargetType> getTargetTypes() {
155+
return EnumSet.of(TargetType.DATABASE);
156+
}
157+
158+
@Override
159+
public ScriptTargetOutput getScriptTargetOutput() {
160+
return null;
161+
}
162+
};
163+
}
164+
165+
private static ExecutionOptions getExecutionOptions(Map<String, Object> settings, AtomicBoolean hasErrors) {
166+
return new ExecutionOptions() {
167+
@Override
168+
public Map<String, Object> getConfigurationValues() {
169+
return settings;
170+
}
171+
172+
@Override
173+
public boolean shouldManageNamespaces() {
174+
return false;
175+
}
176+
177+
@Override
178+
public ExceptionHandler getExceptionHandler() {
179+
return throwable -> {
180+
hasErrors.set(true);
181+
log.warn("Schema migration encountered an issue: {}", throwable.getMessage());
182+
};
183+
}
184+
};
185+
}
186+
}

api/src/main/resources/hibernate.default.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ hibernate.id.new_generator_mappings=false
6161
# Hibernate envers options
6262
hibernate.integration.envers.enabled=false
6363
org.hibernate.envers.revision_listener=org.openmrs.api.db.hibernate.envers.OpenmrsRevisionEntityListener
64+
org.hibernate.envers.audit_table_suffix=_audit

api/src/test/java/org/openmrs/util/DatabaseIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class DatabaseIT implements LiquibaseProvider {
4444
protected static final String PASSWORD = "test";
4545

4646
@BeforeEach
47-
public void setup() throws SQLException, ClassNotFoundException {
47+
public void setup() throws Exception {
4848
this.initializeDatabase();
4949
}
5050

0 commit comments

Comments
 (0)