Skip to content

Commit 6bbc254

Browse files
committed
text normalizer for jasper
1 parent 4c6a494 commit 6bbc254

5 files changed

Lines changed: 233 additions & 0 deletions

File tree

api/src/main/java/ca/bc/gov/educ/api/graduation/service/DistrictReportService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ca.bc.gov.educ.api.graduation.model.dto.DistrictReport;
1010
import ca.bc.gov.educ.api.graduation.model.dto.ReportGradStudentData;
1111
import ca.bc.gov.educ.api.graduation.util.EducGraduationApiConstants;
12+
import ca.bc.gov.educ.api.graduation.util.TextNormalizer;
1213
import lombok.extern.slf4j.Slf4j;
1314
import org.apache.commons.lang3.StringUtils;
1415
import org.springframework.beans.factory.annotation.Autowired;
@@ -334,16 +335,19 @@ private String getReportOrgCode(String mincode) {
334335
}
335336

336337
private byte[] getDistrictYearEndReportJasper(ReportRequest reportRequest) {
338+
TextNormalizer.normalizeObject(reportRequest.getData());
337339
return restService.post(educGraduationApiConstants.getDistrictDistributionYearEnd(),
338340
reportRequest, byte[].class, graduationApiClient);
339341
}
340342

341343
private byte[] getDistrictYearEndNonGradReportJasper(ReportRequest reportRequest) {
344+
TextNormalizer.normalizeObject(reportRequest.getData());
342345
return restService.post(educGraduationApiConstants.getDistrictDistributionYearEndNonGrad(),
343346
reportRequest, byte[].class, graduationApiClient);
344347
}
345348

346349
private byte[] getDistrictLabelsReportJasper(ReportRequest reportRequest) {
350+
TextNormalizer.normalizeObject(reportRequest.getData());
347351
return restService.post(educGraduationApiConstants.getSchoolLabels(), reportRequest, byte[].class, graduationApiClient);
348352
}
349353
}

api/src/main/java/ca/bc/gov/educ/api/graduation/service/GraduationService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ public byte[] prepareTranscriptReport(String pen, String interim, String preview
139139
options.setPreview(isPreview);
140140
ReportRequest reportParams = new ReportRequest();
141141
reportParams.setOptions(options);
142+
TextNormalizer.normalizeObject(reportData);
142143
reportParams.setData(reportData);
143144

144145
try {
@@ -401,6 +402,7 @@ private byte[] getSchoolReportGradRegReport(ReportData data, String mincode) {
401402
options.setReportName(String.format("%s_%s00_GRADREG.pdf", mincode, LocalDate.now().getYear()));
402403
ReportRequest reportParams = new ReportRequest();
403404
reportParams.setOptions(options);
405+
TextNormalizer.normalizeObject(data);
404406
reportParams.setData(data);
405407

406408
return this.restService.post(educGraduationApiConstants.getSchoolGraduation(), reportParams, byte[].class, graduationApiClient);
@@ -432,6 +434,7 @@ private byte[] getSchoolReportNonGradRegReport(ReportData data, String mincode)
432434
options.setReportName(String.format("%s_%s00_NONGRADREG.pdf", mincode, LocalDate.now().getYear()));
433435
ReportRequest reportParams = new ReportRequest();
434436
reportParams.setOptions(options);
437+
TextNormalizer.normalizeObject(data);
435438
reportParams.setData(data);
436439

437440
return this.restService.post(educGraduationApiConstants.getSchoolNonGraduation(), reportParams, byte[].class, graduationApiClient);
@@ -453,6 +456,7 @@ private byte[] getSchoolReportStudentNonGradPrjReport(ReportData data, String mi
453456
options.setReportName(String.format("%s_%s00_NONGRADPRJ.pdf", mincode, LocalDate.now().getYear()));
454457
ReportRequest reportParams = new ReportRequest();
455458
reportParams.setOptions(options);
459+
TextNormalizer.normalizeObject(data);
456460
reportParams.setData(data);
457461

458462
return this.restService.post(educGraduationApiConstants.getStudentNonGradProjected(), reportParams, byte[].class, graduationApiClient);
@@ -467,6 +471,7 @@ private byte[] getSchoolReportStudentNonGradReport(ReportData data, String minco
467471
options.setReportName(String.format("%s_%s00_NONGRADDISTREP_SC.pdf", mincode, LocalDate.now().getYear()));
468472
ReportRequest reportParams = new ReportRequest();
469473
reportParams.setOptions(options);
474+
TextNormalizer.normalizeObject(data);
470475
reportParams.setData(data);
471476

472477
return this.restService.post(educGraduationApiConstants.getStudentNonGrad(), reportParams, byte[].class, graduationApiClient);

api/src/main/java/ca/bc/gov/educ/api/graduation/service/ReportService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ private String generateStudentTranscriptReportJasper(ReportData sample, Exceptio
971971
options.setReportName("Transcript Report.pdf");
972972
ReportRequest reportParams = new ReportRequest();
973973
reportParams.setOptions(options);
974+
TextNormalizer.normalizeObject(sample);
974975
reportParams.setData(sample);
975976
try {
976977
byte[] bytes = restService.post(educGraduationApiConstants.getTranscriptReport(), reportParams,
@@ -1109,6 +1110,7 @@ private String generateStudentCertificateReportJasper(ReportData sample) {
11091110
options.setReportName("Certificate.pdf");
11101111
ReportRequest reportParams = new ReportRequest();
11111112
reportParams.setOptions(options);
1113+
TextNormalizer.normalizeObject(sample);
11121114
reportParams.setData(sample);
11131115
try {
11141116
byte[] bytesSAR = restService.post(educGraduationApiConstants.getCertificateReport(),
@@ -1129,6 +1131,7 @@ private String generateStudentAchievementReportJasper(ReportData data) {
11291131
options.setReportName("Student Achievement Report.pdf");
11301132
ReportRequest reportParams = new ReportRequest();
11311133
reportParams.setOptions(options);
1134+
TextNormalizer.normalizeObject(data);
11321135
reportParams.setData(data);
11331136
try {
11341137
byte[] bytesSAR = restService.post(educGraduationApiConstants.getAchievementReport(),

api/src/main/java/ca/bc/gov/educ/api/graduation/service/SchoolReportsService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,12 @@ private void updateSchoolReport(SchoolReports requestObj) {
416416
}
417417

418418
private byte[] getSchoolYearEndReportJasper(ReportRequest reportRequest) {
419+
TextNormalizer.normalizeObject(reportRequest.getData());
419420
return restService.post(educGraduationApiConstants.getSchoolDistributionYearEnd(), reportRequest, byte[].class, graduationApiClient);
420421
}
421422

422423
private byte[] getSchoolLabelsReportJasper(ReportRequest reportRequest) {
424+
TextNormalizer.normalizeObject(reportRequest.getData());
423425
return restService.post(educGraduationApiConstants.getSchoolLabels(), reportRequest, byte[].class, graduationApiClient);
424426
}
425427

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package ca.bc.gov.educ.api.graduation.util;
2+
3+
import java.lang.reflect.Field;
4+
import java.text.Normalizer;
5+
import java.util.*;
6+
7+
/**
8+
* Utility to recursively normalize all String fields in an object and its children.
9+
* This ensures Indigenous language combining diacritics (like ̓) are preserved.
10+
*
11+
* Usage:
12+
* MyReportData data = createReportData();
13+
* TextNormalizer.normalizeObject(data);
14+
* String json = objectMapper.writeValueAsString(data);
15+
*/
16+
public class TextNormalizer {
17+
18+
private static final Set<Class<?>> WRAPPER_TYPES = new HashSet<>(Arrays.asList(
19+
Boolean.class, Character.class, Byte.class, Short.class,
20+
Integer.class, Long.class, Float.class, Double.class, Void.class
21+
));
22+
23+
/**
24+
* Normalize all String fields in an object and its children recursively.
25+
* Handles: POJOs, Collections, Maps, Arrays
26+
*
27+
* @param obj The object to normalize (modified in-place)
28+
* @return The same object (for chaining)
29+
*/
30+
public static <T> T normalizeObject(T obj) {
31+
if (obj == null) {
32+
return null;
33+
}
34+
35+
try {
36+
normalizeRecursive(obj, new HashSet<>());
37+
} catch (Exception e) {
38+
throw new RuntimeException("Failed to normalize object", e);
39+
}
40+
41+
return obj;
42+
}
43+
44+
/**
45+
* Internal recursive normalization with cycle detection
46+
*/
47+
private static void normalizeRecursive(Object obj, Set<Object> visited) throws Exception {
48+
if (obj == null || visited.contains(obj)) {
49+
return; // Prevent infinite loops
50+
}
51+
52+
Class<?> clazz = obj.getClass();
53+
54+
// Skip primitives and wrappers
55+
if (clazz.isPrimitive() || WRAPPER_TYPES.contains(clazz)) {
56+
return;
57+
}
58+
59+
// Skip Java internal classes (UUID, LocalDate, etc.)
60+
String className = clazz.getName();
61+
if (className.startsWith("java.") || className.startsWith("javax.") ||
62+
className.startsWith("jdk.") || className.startsWith("sun.")) {
63+
return;
64+
}
65+
66+
// Handle String
67+
if (obj instanceof String) {
68+
// Can't modify String in-place, caller must handle
69+
return;
70+
}
71+
72+
visited.add(obj);
73+
74+
// Handle Collections
75+
if (obj instanceof Collection) {
76+
normalizeCollection((Collection<?>) obj, visited);
77+
return;
78+
}
79+
80+
// Handle Maps
81+
if (obj instanceof Map) {
82+
normalizeMap((Map<?, ?>) obj, visited);
83+
return;
84+
}
85+
86+
// Handle Arrays
87+
if (clazz.isArray()) {
88+
normalizeArray(obj, visited);
89+
return;
90+
}
91+
92+
// Handle POJOs - normalize all fields
93+
normalizeFields(obj, visited);
94+
}
95+
96+
/**
97+
* Normalize all fields in a POJO
98+
*/
99+
private static void normalizeFields(Object obj, Set<Object> visited) throws Exception {
100+
Class<?> clazz = obj.getClass();
101+
102+
// Walk up the inheritance hierarchy
103+
while (clazz != null && clazz != Object.class) {
104+
for (Field field : clazz.getDeclaredFields()) {
105+
field.setAccessible(true);
106+
107+
Object value = field.get(obj);
108+
if (value == null) {
109+
continue;
110+
}
111+
112+
// If it's a String, normalize and set back
113+
if (value instanceof String) {
114+
String normalized = normalize((String) value);
115+
field.set(obj, normalized);
116+
}
117+
// Otherwise recurse into the object
118+
else {
119+
normalizeRecursive(value, visited);
120+
}
121+
}
122+
123+
clazz = clazz.getSuperclass();
124+
}
125+
}
126+
127+
/**
128+
* Normalize strings in a Collection
129+
*/
130+
private static void normalizeCollection(Collection<?> collection, Set<Object> visited) throws Exception {
131+
// For Lists, we can replace elements
132+
if (collection instanceof List) {
133+
List<Object> list = (List<Object>) collection;
134+
for (int i = 0; i < list.size(); i++) {
135+
Object item = list.get(i);
136+
if (item instanceof String) {
137+
list.set(i, normalize((String) item));
138+
} else if (item != null) {
139+
normalizeRecursive(item, visited);
140+
}
141+
}
142+
} else {
143+
// For other collections, just recurse (can't modify elements)
144+
for (Object item : collection) {
145+
if (item != null && !(item instanceof String)) {
146+
normalizeRecursive(item, visited);
147+
}
148+
}
149+
}
150+
}
151+
152+
/**
153+
* Normalize strings in a Map
154+
*/
155+
private static void normalizeMap(Map<?, ?> map, Set<Object> visited) throws Exception {
156+
Map<Object, Object> mutableMap = (Map<Object, Object>) map;
157+
158+
// Normalize both keys and values
159+
List<Object> keysToUpdate = new ArrayList<>();
160+
Map<Object, Object> updates = new HashMap<>();
161+
162+
for (Map.Entry<?, ?> entry : map.entrySet()) {
163+
Object key = entry.getKey();
164+
Object value = entry.getValue();
165+
166+
// Normalize key if it's a string
167+
Object normalizedKey = key;
168+
if (key instanceof String) {
169+
normalizedKey = normalize((String) key);
170+
if (!normalizedKey.equals(key)) {
171+
keysToUpdate.add(key);
172+
updates.put(normalizedKey, value);
173+
}
174+
}
175+
176+
// Normalize value
177+
if (value instanceof String) {
178+
mutableMap.put(key, normalize((String) value));
179+
} else if (value != null) {
180+
normalizeRecursive(value, visited);
181+
}
182+
}
183+
184+
// Update keys that changed (remove old, add new)
185+
for (Object oldKey : keysToUpdate) {
186+
mutableMap.remove(oldKey);
187+
}
188+
mutableMap.putAll(updates);
189+
}
190+
191+
/**
192+
* Normalize strings in an Array
193+
*/
194+
private static void normalizeArray(Object array, Set<Object> visited) throws Exception {
195+
int length = java.lang.reflect.Array.getLength(array);
196+
197+
for (int i = 0; i < length; i++) {
198+
Object item = java.lang.reflect.Array.get(array, i);
199+
200+
if (item instanceof String) {
201+
String normalized = normalize((String) item);
202+
java.lang.reflect.Array.set(array, i, normalized);
203+
} else if (item != null) {
204+
normalizeRecursive(item, visited);
205+
}
206+
}
207+
}
208+
209+
/**
210+
* Normalize a single string to NFC form
211+
* NFC = Canonical Composition (keeps combining marks attached to base characters)
212+
*/
213+
private static String normalize(String text) {
214+
if (text == null || text.isEmpty()) {
215+
return text;
216+
}
217+
return Normalizer.normalize(text, Normalizer.Form.NFC);
218+
}
219+
}

0 commit comments

Comments
 (0)