View Javadoc

1   /*
2    * Copyright 2015 Data Archiving and Networked Services (an institute of
3    * Koninklijke Nederlandse Akademie van Wetenschappen), King's College London,
4    * Georg-August-Universitaet Goettingen Stiftung Oeffentlichen Rechts
5    *
6    * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
7    * the European Commission - subsequent versions of the EUPL (the "Licence");
8    * You may not use this work except in compliance with the Licence.
9    * You may obtain a copy of the Licence at:
10   *
11   * https://joinup.ec.europa.eu/software/page/eupl
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the Licence is distributed on an "AS IS" basis,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the Licence for the specific language governing
17   * permissions and limitations under the Licence.
18   */
19  
20  package eu.ehri.project.tools;
21  
22  import com.google.common.collect.Lists;
23  import com.tinkerpop.frames.FramedGraph;
24  import eu.ehri.project.acl.SystemScope;
25  import eu.ehri.project.core.GraphManager;
26  import eu.ehri.project.core.GraphManagerFactory;
27  import eu.ehri.project.definitions.Ontology;
28  import eu.ehri.project.exceptions.SerializationError;
29  import eu.ehri.project.models.EntityClass;
30  import eu.ehri.project.models.base.Accessible;
31  import eu.ehri.project.models.base.Described;
32  import eu.ehri.project.models.base.Description;
33  import eu.ehri.project.models.base.Entity;
34  import eu.ehri.project.models.base.PermissionScope;
35  import eu.ehri.project.models.base.Versioned;
36  import eu.ehri.project.models.events.Version;
37  import eu.ehri.project.models.idgen.IdGenerator;
38  import eu.ehri.project.persistence.Bundle;
39  import eu.ehri.project.persistence.Serializer;
40  
41  import java.util.Collection;
42  import java.util.List;
43  import java.util.Optional;
44  
45  /**
46   * Util class for re-generating the IDs for a given
47   * set of items. Hierarchical IDs are used to enforce uniqueness
48   * of identifiers within a particular parent-child scope. In
49   * theory, since identifiers are generally very stable this works
50   * fine, but occasionally they change, invalidating the identifiers
51   * of their child items.
52   * <p>
53   * We also sometimes tweak the ID generation algorithm itself, which
54   * necessitates bulk ID re-generation.
55   */
56  public class IdRegenerator {
57      private final FramedGraph<?> graph;
58      private final GraphManager manager;
59      private final Serializer depSerializer;
60      private final boolean dryrun;
61      private final boolean skipCollisions;
62      private final boolean collisionMode;
63  
64      public static class IdCollisionError extends Exception {
65          IdCollisionError(String from, String to) {
66              super(String.format("Renaming %s to %s would collide with existing item",
67                      from, to));
68          }
69      }
70  
71      public IdRegenerator(FramedGraph<?> graph) {
72          this(graph, true, false, false);
73      }
74  
75      private IdRegenerator(FramedGraph<?> graph, boolean dryrun, boolean skipCollisions,
76              boolean collisionMode) {
77          this.graph = graph;
78          this.manager = GraphManagerFactory.getInstance(graph);
79          this.depSerializer = new Serializer.Builder(graph).dependentOnly().build();
80          this.dryrun = dryrun;
81          this.skipCollisions = skipCollisions;
82          this.collisionMode = collisionMode;
83      }
84  
85      public List<List<String>> reGenerateIds(PermissionScope scope, Iterable<? extends Entity> items) throws
86              IdCollisionError {
87          List<List<String>> remaps = Lists.newArrayList();
88          for (Entity item : items) {
89              Optional<List<String>> optionalRemap = reGenerateId(scope, item);
90              optionalRemap.ifPresent(remaps::add);
91          }
92          return remaps;
93      }
94  
95      public List<List<String>> reGenerateIds(Iterable<? extends Accessible> items) throws IdCollisionError {
96          List<List<String>> remaps = Lists.newArrayList();
97          for (Accessible item : items) {
98              Optional<List<String>> optionalRemap = reGenerateId(item);
99              optionalRemap.ifPresent(remaps::add);
100         }
101         return remaps;
102     }
103 
104     public Optional<List<String>> reGenerateId(Accessible item) throws IdCollisionError {
105         return reGenerateId(item.getPermissionScope(), item);
106     }
107 
108     Optional<List<String>> reGenerateId(PermissionScope permissionScope, Entity item)
109             throws IdCollisionError {
110         String currentId = item.getId();
111         Collection<String> idChain = Lists.newArrayList();
112         if (permissionScope != null && !permissionScope.equals(SystemScope.getInstance())) {
113             idChain.addAll(permissionScope.idPath());
114         }
115 
116         EntityClass entityClass = manager.getEntityClass(item);
117         try {
118             IdGenerator idgen = entityClass.getIdGen();
119             Bundle itemBundle = depSerializer.entityToBundle(item);
120             String newId = idgen.generateId(idChain, itemBundle);
121             if (collisionMode) {
122                 if (!newId.equals(currentId) && manager.exists(newId)) {
123                     List<String> collision = Lists.newArrayList(currentId, newId);
124                     return Optional.of(collision);
125                 } else {
126                     return Optional.empty();
127                 }
128             } else {
129                 if (!newId.equals(currentId)) {
130                     if (manager.exists(newId)) {
131                         if (!skipCollisions) {
132                             throw new IdCollisionError(currentId, newId);
133                         } else {
134                             return Optional.empty();
135                         }
136                     } else {
137                         if (!dryrun) {
138                             manager.renameVertex(item.asVertex(), currentId, newId);
139 
140                             // Rename all the descriptions
141                             String idBase = idgen.getIdBase(itemBundle);
142                             Collection<String> descIdChain = Lists.newArrayList(idChain);
143                             descIdChain.add(idBase);
144                             for (Description d : item.as(Described.class).getDescriptions()) {
145                                 Bundle desc = depSerializer.entityToBundle(d);
146                                 String newDescriptionId = desc.getType().getIdGen().generateId(descIdChain, desc);
147                                 manager.renameVertex(d.asVertex(), d.getId(), newDescriptionId);
148                             }
149 
150                             // Change the ID on any versions...
151                             for (Version v : item.as(Versioned.class).getAllPriorVersions()) {
152                                 manager.setProperty(v.asVertex(), Ontology.VERSION_ENTITY_ID, newId);
153                             }
154                         }
155                         List<String> remap = Lists.newArrayList(currentId, newId);
156                         return Optional.of(remap);
157                     }
158                 } else {
159                     return Optional.empty();
160                 }
161             }
162         } catch (SerializationError e) {
163             throw new RuntimeException(e);
164         }
165     }
166 
167     /**
168      * Obtain a re-generator that will actually perform the rename
169      * step.
170      *
171      * @param doIt whether to actually commit changes
172      * @return a new, more dangerous, re-generator
173      */
174     public IdRegenerator withActualRename(boolean doIt) {
175         return new IdRegenerator(graph, !doIt, skipCollisions, collisionMode);
176     }
177 
178     /**
179      * Obtain a re-generator that will skip items that would collide
180      * with existing items.
181      *
182      * @param skipCollisions whether or not to error on an ID collision
183      * @return a new, more tolerant, re-generator
184      */
185     public IdRegenerator skippingCollisions(boolean skipCollisions) {
186         return new IdRegenerator(graph, dryrun, skipCollisions, collisionMode);
187     }
188 
189     /**
190      * Obtain a re-generator that only outputs items with IDs that if renamed
191      * would collide with another item's ID.
192      *
193      * @param collisionMode only output items that would collide if renamed
194      * @return a new, more tolerant, re-generator
195      */
196     public IdRegenerator collisionMode(boolean collisionMode) {
197         return new IdRegenerator(graph, dryrun, skipCollisions, collisionMode);
198     }
199 }