View Javadoc
1   /*
2    * Copyright 2011-2024 Medical Information Systems Research Group (https://medical.zcu.cz),
3    * Department of Computer Science and Engineering, University of West Bohemia.
4    * Address: Univerzitni 8, 306 14 Plzen, Czech Republic.
5    *
6    * Author Petr Vcelak (vcelak@kiv.zcu.cz).
7    *
8    * This file is part of AnonMed project.
9    *
10   * AnonMed is free software: you can redistribute it and/or modify
11   * it under the terms of the GNU General Public License as published by
12   * the Free Software Foundation, either version 3 of the License.
13   *
14   * AnonMed is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17   * GNU General Public License for more details.
18   *
19   * You should have received a copy of the GNU General Public License
20   * along with AnonMed. If not, see <http://www.gnu.org/licenses/>.
21   */
22  package cz.zcu.mre.anonmed.anonymizer;
23  
24  import cz.zcu.mre.anonmed.AnonMedConfiguration;
25  import cz.zcu.mre.anonmed.AnonMedException;
26  import cz.zcu.mre.anonmed.FilenameGenerator;
27  import cz.zcu.mre.anonmed.report.AnonMedReport;
28  import cz.zcu.mre.anonmed.rule.Operation;
29  import cz.zcu.mre.anonmed.rule.Rule;
30  import java.io.File;
31  import java.io.IOException;
32  import java.util.List;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  
36  /**
37   * Abstract de-identification class.
38   *
39   * @author Petr Vcelak (vcelak@kiv.zcu.cz)
40   * @since 2010-01-28
41   */
42  public abstract class AbstractAnonymizer implements Anonymizer {
43  
44      /**
45       * Logger.
46       */
47      private static final Logger LOG = LoggerFactory.getLogger(AbstractAnonymizer.class);
48  
49      /**
50       * Application configuration.
51       */
52      private AnonMedConfiguration config = null;
53  
54      /**
55       * De-identification for a specific file type.
56       */
57      private String fileType = null;
58  
59      /**
60       * De-identification file extension.
61       */
62      private String fileExtension = null;
63  
64      /**
65       * Processed XML file.
66       */
67      private File activeFile = null;
68  
69      /**
70       * Uncertain flag.
71       */
72      private Boolean uncertain = false;
73  
74      /**
75       * The original file last modified time.
76       */
77      protected long originalLastModified = 0L;
78  
79      /**
80       * An anonymous identification.
81       */
82      private String anonymousIdentification;
83  
84      /**
85       * Abstract de-identification class.
86       *
87       * @param conf configuration.
88       */
89      public AbstractAnonymizer(final AnonMedConfiguration conf) {
90          setConfig(conf);
91      }
92  
93      /**
94       * Set configuration.
95       *
96       * @param conf configuration.
97       */
98      protected final void setConfig(
99              final AnonMedConfiguration conf) {
100 
101         if (conf == null) {
102             throw new NullPointerException("Application config is null.");
103         }
104 
105         this.config = conf;
106     }
107 
108     /**
109      * Get configuration.
110      *
111      * @return configuration.
112      */
113     @Override
114     public final AnonMedConfiguration getConfig() {
115         return config;
116     }
117 
118     /**
119      * Set file extension.
120      *
121      * @param extension file extension.
122      */
123     protected final void setFileExtension(
124             final String extension) {
125 
126         this.fileExtension = extension;
127     }
128 
129     /**
130      * Get active (extracted) file that is processed.
131      *
132      * @return file actually extracted file.
133      */
134     public final File getActiveFile() {
135 
136         return activeFile;
137     }
138 
139     /**
140      * Set active (extracted) file that is processed.
141      *
142      * @param value active file.
143      */
144     protected final void setActiveFile(
145             final File value) {
146 
147         this.activeFile = value;
148 
149         // update the original file last modified time
150         if (value != null) {
151             originalLastModified = value.lastModified();
152         }
153     }
154 
155     /**
156      * Get file type as string.
157      *
158      * @return file type.
159      */
160     @Override
161     public final String getFileType() {
162 
163         return this.fileType;
164     }
165 
166     /**
167      * Set file type as String.
168      *
169      * @param type file type
170      */
171     protected final void setFileType(
172             final String type) {
173 
174         this.fileType = type;
175     }
176 
177     /**
178      * Has file uncertain status?
179      *
180      * @return true if uncertain
181      */
182     public final Boolean isUncertain() {
183 
184         return uncertain;
185     }
186 
187     /**
188      * Set uncertain flag.
189      *
190      * @param value Set true if uncertain, false otherwise.
191      */
192     protected final void setUncertain(
193             final Boolean value) {
194 
195         this.uncertain = value;
196     }
197 
198     /**
199      * Create new de-identified file name. Filename is based on date and
200      * sequence number.
201      *
202      * @return A new file name.
203      */
204     protected final String filenameGenerator() {
205 
206         String filename;
207         String output = config.getOutputDirectory().getAbsolutePath();
208 
209         /* Use an uncertain directory as output when any UI method was used. */
210         if (isUncertain()) {
211             output = config.getUncertainDirectory().getAbsolutePath();
212         }
213 
214         /* Set a new file name. */
215         if (config.isChangeFilename()) {
216             filename = FilenameGenerator.generateFileName()
217                     .concat(".").concat(fileExtension);
218         } else {
219             filename = File.separator + activeFile.getName();
220         }
221 
222         LOG.debug("OUTPUT in file {}", output.concat(filename));
223 
224         return output.concat(filename);
225     }
226 
227     /**
228      * De-identify file.
229      *
230      * @param file File for de-identification.
231      * @return De-identified file.
232      * @throws AnonMedException AnonMed exception.
233      */
234     @Override
235     public final File anonymize(
236             final File file)
237             throws AnonMedException {
238 
239         fileOpen(file);
240 
241         if (getActiveFile() != null) {
242 
243             processRules();
244 
245             if (getConfig().isStrictMode()) {
246                 strictMode();
247             }
248 
249             String outputFileName = filenameGenerator();
250             fileWrite(outputFileName);
251 
252             /* Set last last modified time */
253             File anonymousFile = new File(outputFileName);
254 
255             if (!anonymousFile.setLastModified(originalLastModified)) {
256                 LOG.error("ERROR Fail to change last modified time stamp {}", file.getAbsolutePath());
257                 return null;
258             }
259 
260             return anonymousFile;
261         } else {
262             LOG.error("ERROR Fail to anonymise file {}", file.getAbsolutePath());
263         }
264 
265         return null;
266     }
267 
268     /**
269      * Process de-identification rules.
270      */
271     @Override
272     public final void processRules() {
273 
274         List<Rule> profile = config.getProfileBuilder()
275                 .getProfile(getFileType());
276         if (profile == null) {
277             LOG.error("No profile for ''{}'' file type.",
278                     getFileType());
279             return;
280         }
281 
282         for (Rule r : profile) {
283 
284             switch (r.getOperation()) {
285                 case APPEND_AFTER ->
286                     ruleAppendAfter(r);
287 
288                 case APPEND_BEFORE ->
289                     ruleAppendBefore(r);
290 
291                 case CHANGE ->
292                     ruleChange(r);
293 
294                 case DECRYPT ->
295                     ruleDecrypt(r);
296 
297                 case EMPTY ->
298                     ruleEmpty(r);
299 
300                 case ENCRYPT ->
301                     ruleEncrypt(r);
302 
303                 case EXTERNAL -> {
304                     try {
305                         ruleExternal(r);
306                     } catch (AnonMedException e) {
307                         LOG.error("Error during external application call: ", e);
308                     }
309                 }
310 
311                 case IDENTIFICATION ->
312                     ruleIdentification(r);
313 
314                 case KEEP ->
315                     ruleKeep(r);
316 
317                 case LOWERCASE ->
318                     ruleLowercase(r);
319 
320                 case NONE ->
321                     ruleNone(r);
322 
323                 case REMOVE ->
324                     ruleRemove(r);
325 
326                 case SPECIFIC ->
327                     ruleSpecific(r);
328 
329                 case SUBSTRING ->
330                     ruleSubstring(r);
331 
332                 case SUBSTITUTE ->
333                     ruleSubstitute(r);
334 
335                 case UPPERCASE ->
336                     ruleUppercase(r);
337 
338                 default ->
339                     LOG.error("Not supported requested operation {}", r.getOperation());
340 
341             }
342         }
343     }
344 
345     /**
346      * You have to implement your own fileOpen method that is appropriate for
347      * your data file format.
348      *
349      * @param file File to open.
350      */
351     @Override
352     public abstract void fileOpen(final File file) throws AnonMedException;
353 
354     /**
355      * You have to implement your own fileWrite method that is appropriate for
356      * your data file format.
357      *
358      * @param filename fila name.
359      * @throws AnonMedException exception when fail.
360      */
361     @Override
362     public abstract void fileWrite(final String filename) throws AnonMedException;
363 
364     /**
365      * Rule external application call.
366      *
367      * @param rule Rule
368      * @throws AnonMedException exception when exit code wasn't zero.
369      */
370     @Override
371     public final void ruleExternal(final Rule rule) throws AnonMedException {
372 
373         try {
374             Process p = Runtime.getRuntime().exec(
375                     rule.getRule() + " " + getActiveFile()
376                     + " " + rule.getNewValue());
377 
378             if (p.exitValue() != 0) {
379                 throw new AnonMedException(p.exitValue());
380             }
381 
382         } catch (IOException e) {
383             LOG.error(null, e);
384         }
385     }
386 
387     /**
388      * The rule KEEP method is empty. It does nothing because it is used for
389      * building KEEP list only by the buildStrictList.
390      *
391      * @param rule Rule keep.
392      */
393     @Override
394     public void ruleKeep(final Rule rule) {
395 
396         /* Keep rule is used as a marker only. */
397     }
398 
399     /**
400      * Do nothing rule.
401      *
402      * @param rule Rule none.
403      */
404     @Override
405     public void ruleNone(final Rule rule) {
406 
407         /* Rule 'NONE' does nothing and does it excellently. */
408     }
409 
410     /**
411      * Look for the rule for specified tag/path/etc. in the profile.
412      *
413      * @param rule Rule to find out.
414      * @return Result. It is true when it is on the strict mode list of allowed
415      * tags/elements/paths/etc.
416      */
417     @Override
418     public final Boolean isStrictModeRemove(final String rule) {
419 
420         List<Rule> profile
421                 = config.getProfileBuilder().getProfile(getFileType());
422 
423         for (Rule r : profile) {
424 
425             if (testForSameRule(r.getRule(), rule)
426                     && (r.getOperation() == Operation.CHANGE
427                     || r.getOperation() == Operation.EMPTY
428                     || r.getOperation() == Operation.IDENTIFICATION
429                     || r.getOperation() == Operation.KEEP
430                     || r.getOperation() == Operation.ENCRYPT)) {
431                 return Boolean.FALSE;
432             }
433         }
434 
435         return Boolean.TRUE;
436     }
437 
438     /**
439      * Test the same rule.
440      *
441      * @param rule1 Rule 1
442      * @param rule2 Rule 2
443      * @return Result is true when it equals.
444      */
445     private Boolean testForSameRule(final String rule1, final String rule2) {
446 
447         if (rule1.equals(rule2)) {
448             return Boolean.TRUE;
449         }
450 
451         return Boolean.FALSE;
452     }
453 
454     /**
455      * Encrypt content by the configured CipherService.
456      *
457      * @param rule The rule.
458      * @param oldValue Source text content.
459      * @return Encrypted text.
460      */
461     protected String makeEncrypt(final Rule rule, final String oldValue) {
462 
463         if (rule == null) {
464             throw new IllegalArgumentException("The rule cannot be null");
465         }
466 
467         String value = rule.getNewValue();
468         if (value == null || value.isEmpty()) {
469             throw new IllegalArgumentException("Missing or wrong argument(s) for ENCRYPT operation");
470         }
471 
472         String[] settings = value.split(",");
473         switch (settings.length) {
474             case 0 ->
475                 throw new IllegalArgumentException("Missing argument for ENCRYPT operation");
476             case 1 -> {
477                 // aes/rsa
478                 String alg = settings[0].trim().toLowerCase();
479                 // the key alias is person/patient/thing ID dependent
480                 String alias = getAnonymousIdentification();
481 
482                 LOG.debug("Use encrypt algorithm {} and key with alias {}", alg, alias);
483                 return config.getCipherService().encrypt(alg, oldValue, alias);
484             }
485             case 2 -> {
486                 // aes/rsa
487                 String alg = settings[0].trim().toLowerCase();
488                 // key alias is the second argument
489                 String alias = settings[1].trim().toLowerCase();
490 
491                 LOG.debug("Use encrypt algorithm {} and key with alias {}", alg, alias);
492                 return config.getCipherService().encrypt(alg, oldValue, alias);
493             }
494             default ->
495                 throw new IllegalArgumentException("Only two arguments are allowed per ENCRYPT rule");
496         }
497     }
498 
499     /**
500      * Decrypt content by the configured CipherService.
501      *
502      * @param rule The rule.
503      * @param encrypted Encrypted text.
504      * @return Decrypted text content.
505      */
506     protected String makeDecrypt(final Rule rule, String encrypted) {
507 
508         if (rule == null) {
509             throw new IllegalArgumentException("The rule cannot be null");
510         }
511 
512         String value = rule.getNewValue();
513         if (value == null || value.isEmpty()) {
514             throw new IllegalArgumentException("Missing or wrong argument(s) for ENCRYPT operation");
515         }
516 
517         String[] settings = value.split(",");
518 
519         switch (settings.length) {
520             case 0 ->
521                 throw new IllegalArgumentException("Missing argument for ENCRYPT operation");
522             case 1 -> {
523                 // aes/rsa
524                 String alg = settings[0].trim().toLowerCase();
525                 // the key alias is person/patient/thing ID dependent
526                 String alias = getAnonymousIdentification();
527 
528                 LOG.debug("Use decrypt algorithm {} and key with alias {}", alg, alias);
529                 return config.getCipherService().decrypt(alg, encrypted, alias);
530 
531             }
532             case 2 -> {
533                 // aes/rsa
534                 String alg = settings[0].trim().toLowerCase();
535                 // key alias is the second argument
536                 String alias = settings[1].trim().toLowerCase();
537 
538                 LOG.debug("Use decrypt algorithm {} and key with alias {}", alg, alias);
539                 return config.getCipherService().decrypt(alg, encrypted, alias);
540             }
541             default ->
542                 throw new IllegalArgumentException("Only two arguments are allowed per ENCRYPT rule");
543         }
544     }
545 
546     /**
547      * Make report output.
548      *
549      * @param rule The active rule.
550      * @param oldValue The old value.
551      * @param newValue The new value.
552      */
553     protected void report(Rule rule, String oldValue, String newValue) {
554         AnonMedReport.add(activeFile, rule, oldValue, newValue);
555     }
556 
557     /**
558      *
559      * Replace value in a sed-like manner: 's#REGEX#REPLACEMENT#FLAGS'.
560      *
561      * @param rule The rule.
562      * @param oldValue Old value.
563      * @return New string value.
564      */
565     protected String makeSubstitute(Rule rule, String oldValue) {
566 
567         if (rule == null || oldValue == null || oldValue.isEmpty()) {
568             LOG.debug("Empty or null value for SUBSTITUTE {}", rule);
569             return "";
570         }
571 
572         // use rule config
573         String ruleValue = rule.getNewValue();
574         if (ruleValue != null && !ruleValue.isEmpty()) {
575             String[] conf = ruleValue.split("#");
576 
577             switch (conf.length) {
578                 case 2 -> {
579                     return oldValue.replaceFirst(conf[0], conf[1]);
580                 }
581 
582                 case 3 -> {
583                     if (conf[2].equals("g")) {
584                         // global
585                         return oldValue.replaceAll(conf[0], conf[1]);
586                     } else {
587                         return oldValue.replaceFirst(conf[0], conf[1]);
588                     }
589                 }
590 
591                 default ->
592                     LOG.debug("Wrong parameters (e.g. missing '#' separator) for SUBSTITUTE {}", rule);
593             }
594         }
595 
596         return oldValue;
597     }
598 
599     /**
600      * Get and use makeSubstring with start and end positions in the value.
601      *
602      * @param rule The rule.
603      * @param oldValue Old value.
604      * @return New string value.
605      */
606     protected String makeSubstring(Rule rule, String oldValue) {
607 
608         if (rule == null || oldValue == null || oldValue.isEmpty()) {
609             return "";
610         }
611 
612         // initialise variables
613         int beginIndex = 0; // set minimum
614         int endIndex = oldValue.length(); // set maximum
615 
616         // use rule config
617         String ruleValue = rule.getNewValue();
618         if (ruleValue != null) {
619             String[] conf = ruleValue.split(",");
620 
621             switch (conf.length) {
622                 case 0 -> {
623                     LOG.error("Missing configuration for the SUBSTRING {}", rule);
624                     return oldValue;
625                 }
626                 case 1 -> {
627                     try {
628                         beginIndex = Integer.parseInt(conf[0]);
629                         return oldValue.substring(beginIndex);
630                     } catch (NumberFormatException ex) {
631                         LOG.error("Wrong substring number in the rule {}", rule);
632                     }
633                 }
634                 case 2 -> {
635                     // parse beginIndex
636                     if (conf[0].equals("MIN")) {
637                         beginIndex = 0;
638                     } else {
639                         try {
640                             beginIndex = Integer.parseInt(conf[0]);
641                         } catch (NumberFormatException ex) {
642                             LOG.error("Wrong substring number in the rule {}", rule);
643                         }
644                     }
645 
646                     // parse endIndex
647                     if (conf[1].equals("MAX")) {
648                         endIndex = oldValue.length();
649                     } else {
650                         try {
651                             endIndex = Integer.parseInt(conf[1]);
652                         } catch (NumberFormatException ex) {
653                             LOG.error("Wrong substring number in the rule {}", rule);
654                         }
655                     }
656                 }
657                 default -> {
658                     LOG.error("The rule configuration should have two arguments - numbers, MIN or MAX string instead. Wrong parameters for substring: {}", rule);
659                 }
660             }
661         }
662 
663         // fix wrong indexes
664         if (beginIndex < 0) {
665             beginIndex = 0;
666         }
667         if (endIndex > oldValue.length()) {
668             endIndex = oldValue.length();
669         }
670         return oldValue.substring(beginIndex, endIndex);
671     }
672 
673     /**
674      * Change text to lowercase letters.
675      *
676      * @param rule The rule.
677      * @param oldValue Old value.
678      * @return New string value.
679      */
680     protected String makeLowercase(Rule rule, String oldValue) {
681 
682         return oldValue.toLowerCase();
683     }
684 
685     /**
686      * Change text to lowercase letters.
687      *
688      * @param rule The rule.
689      * @param oldValue Old value.
690      * @return New string value.
691      */
692     protected String makeUppercase(Rule rule, String oldValue) {
693 
694         return oldValue.toUpperCase();
695     }
696 
697     @Override
698     public String getAnonymousIdentification() {
699         return anonymousIdentification;
700     }
701 
702     protected void setAnonymousIdentification(String anonymousIdentification) {
703         this.anonymousIdentification = anonymousIdentification;
704     }
705 
706 }