content: package org.alfresco.transform.pii.extractor;
  
  import com.fasterxml.jackson.core.type.TypeReference;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import org.alfresco.transform.base.TransformManager;
  import org.alfresco.transform.base.metadata.AbstractMetadataExtractorEmbedder;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.stereotype.Component;
  
  import java.io.*;
  import java.io.Serializable;
  import java.nio.charset.StandardCharsets;
  import java.util.*;
  
  /**
   * PII Metadata Extractor for PDFs.
   * Produces a flat metadata map suitable for alfresco-metadata-extract mapping.
   *
   * Keys (examples):
   *   pii.hasPII       -> boolean
   *   pii.entities     -> comma-separated list (string)
   *   pii.counts.PERSON -> integer
   *   pii.scoreMax     -> double
   *   pii.scoreAvg     -> double
   */
  @Component
  public class PIIMetadataExtractor extends AbstractMetadataExtractorEmbedder {
  
      private static final Logger logger = LoggerFactory.getLogger(PIIMetadataExtractor.class);
  
      protected PIIMetadataExtractor() {
          super(Type.EXTRACTOR, logger);
      }
  
      @Override
      public String getTransformerName() {
          return "pdf-pii-metadata";
      }
  
      @Override
      public void embedMetadata(String sourceMimetype, InputStream inputStream, String targetMimetype,
                                OutputStream outputStream, Map transformOptions, TransformManager transformManager) {
          // no-op (this class is an EXTRACTOR)
      }
  
      @Override
      @SuppressWarnings("unchecked")
      public Map<String, Serializable> extractMetadata(String sourceMimetype,
                                                       InputStream inputStream,
                                                       String targetMimetype,
                                                       OutputStream outputStream,
                                                       Map transformOptions,
                                                       TransformManager transformManager) throws Exception {
  
          // Persist InputStream to a temp PDF
          File tempPdf = File.createTempFile("pii-extract-", ".pdf");
          try (OutputStream tmp = new FileOutputStream(tempPdf)) {
              inputStream.transferTo(tmp);
          }
  
          // Build analyze.py command with options (same names as transformer)
          List<String> cmd = new ArrayList<>();
          cmd.add("python");
          cmd.add("/opt/redactor/analyze.py");
          cmd.add(tempPdf.getAbsolutePath());
  
          Object ents = transformOptions.get("entities");
          if (ents != null && !String.valueOf(ents).isBlank()) {
              cmd.add("--entities");
              cmd.add(String.valueOf(ents));
          }
          Object thr = transformOptions.get("score-threshold");
          if (thr != null && !String.valueOf(thr).isBlank()) {
              cmd.add("--score-threshold");
              cmd.add(String.valueOf(thr));
          }
          Object lang = transformOptions.get("language");
          if (lang != null && !String.valueOf(lang).isBlank()) {
              cmd.add("--language");
              cmd.add(String.valueOf(lang));
          }
  
          logger.debug("PII analyze command: {}", String.join(" ", cmd));
  
          // Run analyzer and capture stdout
          Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
          String stdout;
          try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
              StringBuilder sb = new StringBuilder();
              String line;
              while ((line = r.readLine()) != null) sb.append(line);
              stdout = sb.toString();
          }
          int exit = p.waitFor();
          if (exit != 0) {
              throw new IllegalStateException("analyze.py exited with code " + exit);
          }
  
          // Parse JSON and flatten inline (arrays -> comma-joined; objects -> key.subKey)
          // stdout is a flat JSON like:
          // {
          //   "pii.hasPII": true,
          //   "pii.entities": ["PERSON","EMAIL_ADDRESS"],
          //   "pii.counts": {"PERSON":2,"EMAIL_ADDRESS":1},
          //   "pii.scoreMax": 0.97,
          //   "pii.scoreAvg": 0.72
          // }
          Map<String, Object> root = new ObjectMapper().readValue(stdout, new TypeReference<Map<String, Object>>() {});
          LinkedHashMap<String, Serializable> meta = new LinkedHashMap<>();
  
          for (Map.Entry<String, Object> e : root.entrySet()) {
              String key = e.getKey();
              Object v = e.getValue();
  
              if (v == null) continue;
  
              if (v instanceof String || v instanceof Boolean || v instanceof Integer || v instanceof Long || v instanceof Double) {
                  meta.put(key, (Serializable) v);
              } else if (v instanceof List) {
                  // e.g., pii.entities, pii.values -> comma-joined
                  String joined = joinList((List<?>) v);
                  if (!joined.isEmpty()) meta.put(key, joined);
              } else if (v instanceof Map) {
                  // e.g., pii.counts (numbers), pii.valuesByType (arrays)
                  Map<?, ?> obj = (Map<?, ?>) v;
                  for (Map.Entry<?, ?> ce : obj.entrySet()) {
                      if (ce.getKey() == null || ce.getValue() == null) continue;
                      String subKey = key + "." + ce.getKey().toString();
                      Object subVal = ce.getValue();
  
                      if (subVal instanceof String || subVal instanceof Boolean || subVal instanceof Integer || subVal instanceof Long || subVal instanceof Double) {
                          meta.put(subKey, (Serializable) subVal);
                      } else if (subVal instanceof List) {
                          String joined = joinList((List<?>) subVal);
                          if (!joined.isEmpty()) meta.put(subKey, joined);
                      } else {
                          // Fallback: store string form
                          meta.put(subKey, subVal.toString());
                      }
                  }
              } else {
                  // Fallback: store string form
                  meta.put(key, v.toString());
              }
          }
  
          return meta;
      }
  
      private static String joinList(List<?> list) {
          if (list.isEmpty()) return "";
          StringBuilder sb = new StringBuilder();
          boolean first = true;
          for (Object o : list) {
              if (o == null) continue;
              if (!first) sb.append(',');
              first = false;
              // basic normalization to keep metadata compact
              String s = String.valueOf(o).trim().replaceAll("\\s+", " ");
              if (s.length() > 200) s = s.substring(0, 197) + "...";
              sb.append(s);
          }
          return sb.toString();
      }
  }
  