View Javadoc

1   package eu.ehri.project.tools;
2   
3   import com.google.common.base.Preconditions;
4   import com.google.common.collect.Lists;
5   import com.tinkerpop.blueprints.CloseableIterable;
6   import com.tinkerpop.frames.FramedGraph;
7   import eu.ehri.project.core.GraphManager;
8   import eu.ehri.project.core.GraphManagerFactory;
9   import eu.ehri.project.definitions.EventTypes;
10  import eu.ehri.project.exceptions.ItemNotFound;
11  import eu.ehri.project.exceptions.SerializationError;
12  import eu.ehri.project.exceptions.ValidationError;
13  import eu.ehri.project.models.EntityClass;
14  import eu.ehri.project.models.base.Accessible;
15  import eu.ehri.project.models.base.Actioner;
16  import eu.ehri.project.persistence.ActionManager;
17  import eu.ehri.project.persistence.Bundle;
18  import eu.ehri.project.persistence.BundleManager;
19  import eu.ehri.project.persistence.Serializer;
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.util.List;
24  import java.util.Optional;
25  
26  /**
27   * Find and replace text in item properties.
28   * <p>
29   * Unlike a Cypher query, this class takes care of managing
30   * the audit log to record changes, and also tries to prevent
31   * accidental misuse.
32   */
33  public class FindReplace {
34  
35      private static final Logger logger = LoggerFactory.getLogger(FindReplace.class);
36  
37      private final FramedGraph<?> graph;
38      private final boolean commit;
39      private final int maxItems;
40      private final GraphManager manager;
41      private final ActionManager actionManager;
42      private final Serializer depSerializer;
43      private final BundleManager dao;
44  
45      public FindReplace(FramedGraph<?> graph, boolean commit, int maxItems) {
46          this.graph = graph;
47          this.commit = commit;
48          this.maxItems = maxItems;
49          this.manager = GraphManagerFactory.getInstance(graph);
50          this.actionManager = new ActionManager(graph);
51          this.depSerializer = new Serializer.Builder(graph).dependentOnly().build();
52          this.dao = new BundleManager(graph);
53      }
54  
55      /**
56       * Find and replace a string
57       *
58       * @param contentType the content type of the top-level item
59       * @param entityType  the entity type of the property-holding node
60       * @param property    the name of the property to search
61       * @param textToFind  the text to find
62       * @param replacement the replacement text
63       * @param actioner    the current user
64       * @param logMessage  a mandatory log message
65       * @return a list of lists each comprising: the parent item ID, the
66       * child item ID, and the current text value in which a match was found
67       */
68      public List<List<String>> findAndReplace(
69              EntityClass contentType, EntityClass entityType,
70              String property, String textToFind, String replacement,
71              Actioner actioner, String logMessage) throws ValidationError {
72  
73          Preconditions.checkNotNull(entityType, "Entity type cannot be null.");
74          Preconditions.checkNotNull(property, "Property name cannot be null.");
75          Preconditions.checkNotNull(textToFind, "Text to find cannot be null.");
76          Preconditions.checkArgument(!commit || replacement != null,
77                  "Replacement text cannot be null if committing a replacement value.");
78          Preconditions.checkArgument(!commit || logMessage != null,
79                  "Log message cannot be null if committing a replacement value.");
80  
81          logger.info("Find:    '{}'", textToFind);
82          logger.info("Replace: '{}'", replacement);
83  
84          List<List<String>> todo = Lists.newArrayList();
85  
86          EventTypes eventType = contentType.equals(entityType)
87                  ? EventTypes.modification
88                  : EventTypes.modifyDependent;
89          ActionManager.EventContext context = actionManager
90                  .newEventContext(actioner, eventType, Optional.ofNullable(logMessage));
91  
92          try (CloseableIterable<Accessible> entities = manager.getEntities(contentType, Accessible.class)) {
93              for (Accessible entity : entities) {
94                  if (todo.size() >= maxItems) {
95                      break;
96                  }
97  
98                  Bundle bundle = depSerializer.entityToBundle(entity);
99                  List<List<String>> matches = Lists.newArrayList();
100                 bundle.map(d -> {
101                     if (d.getType().equals(entityType)) {
102                         Object v = d.getDataValue(property);
103                         if (find(textToFind, v)) {
104                             matches.add(Lists.newArrayList(entity.getId(),
105                                     d.getId(), v.toString()));
106                         }
107                     }
108                     return d;
109                 });
110 
111                 if (!matches.isEmpty()) {
112                     todo.addAll(matches);
113                     logger.info("Found in {}", entity.getId());
114 
115                     if (commit) {
116                         context.createVersion(entity);
117                         context.addSubjects(entity);
118 
119                         Bundle newBundle = bundle.map(d -> {
120                             if (d.getType().equals(entityType)) {
121                                 Object newValue = replace(textToFind, replacement, d.getDataValue(property));
122                                 return d.withDataValue(property, newValue);
123                             }
124                             return d;
125                         });
126                         dao.update(newBundle, Accessible.class);
127                     }
128                 }
129             }
130         } catch (SerializationError | ItemNotFound e) {
131             throw new RuntimeException(e);
132         }
133 
134         if (commit && !context.getSubjects().isEmpty()) {
135             context.commit();
136         }
137 
138         return todo;
139     }
140 
141     private boolean find(String needle, Object data) {
142         if (data != null) {
143             if (data instanceof List) {
144                 for (Object v : ((List) data)) {
145                     if (find(needle, v)) {
146                         return true;
147                     }
148                 }
149                 return false;
150             } else if (data instanceof String) {
151                 return ((String) data).contains(needle);
152             }
153         }
154         return false;
155     }
156 
157     private Object replace(String original, String replacement, Object data) {
158         if (data != null) {
159             if (data instanceof List) {
160                 List<Object> newList = Lists.newArrayListWithExpectedSize(((List) data).size());
161                 for (Object v : ((List) data)) {
162                     newList.add(replace(original, replacement, v));
163                 }
164                 return newList;
165             } else if (data instanceof String) {
166                 return ((String) data).replace(original, replacement);
167             }
168         }
169         return data;
170     }
171 }