]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
9778e13b773cf9238dcb11be8dfa5de8dda78f88
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.batch.nuxeo;
2
3 import java.net.URISyntaxException;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Collections;
7 import java.util.HashMap;
8 import java.util.HashSet;
9 import java.util.Iterator;
10 import java.util.LinkedHashMap;
11 import java.util.LinkedHashSet;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Set;
15
16 import org.apache.commons.lang.StringUtils;
17
18 import org.collectionspace.services.client.PayloadOutputPart;
19 import org.collectionspace.services.client.PoxPayloadOut;
20 import org.collectionspace.services.client.RelationClient;
21 import org.collectionspace.services.client.workflow.WorkflowClient;
22 import org.collectionspace.services.common.NuxeoBasedResource;
23 import org.collectionspace.services.common.api.RefNameUtils;
24 import org.collectionspace.services.common.api.RefNameUtils.AuthorityTermInfo;
25 import org.collectionspace.services.common.authorityref.AuthorityRefDocList;
26 import org.collectionspace.services.common.invocable.InvocationContext.Params.Param;
27 import org.collectionspace.services.common.invocable.InvocationResults;
28 import org.collectionspace.services.common.relation.RelationResource;
29 import org.collectionspace.services.common.vocabulary.AuthorityResource;
30 import org.collectionspace.services.relation.RelationsCommonList;
31
32 import org.dom4j.Document;
33 import org.dom4j.DocumentException;
34 import org.dom4j.DocumentHelper;
35 import org.dom4j.Element;
36 import org.dom4j.Node;
37
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * A batch job that merges authority items. The single and list contexts are
43  * supported.
44  *
45  * The merge target is a record into which one or more source records will be
46  * merged. A merge source is a record that will be merged into the target, as
47  * follows: Each term in a source record is added to the target as a non-
48  * preferred term, if that term does not already exist in the target. If a term
49  * in the source already exists in the target, each non-blank term field is
50  * copied to the target, if that field is empty in the target. If the field is
51  * non-empty in the target, and differs from the source field, a warning is
52  * emitted and no action is taken. If a source is successfully merged into the
53  * target, all references to the source are transferred to the target, and the
54  * source record is soft-deleted.
55  *
56  * The context (singleCSID or listCSIDs of the batch invocation payload
57  * specifies the source record(s).
58  *
59  * The following parameters are allowed:
60  *
61  * targetCSID: The csid of the target record. Only one target may be supplied.
62  *
63  * @author ray
64  */
65 public class MergeAuthorityItemsBatchJob extends AbstractBatchJob {
66         final Logger logger = LoggerFactory.getLogger(MergeAuthorityItemsBatchJob.class);
67
68         public MergeAuthorityItemsBatchJob() {
69                 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
70         }
71
72         @Override
73         public void run() {
74                 setCompletionStatus(STATUS_MIN_PROGRESS);
75
76                 try {
77                         String target = null;
78                         Set<String> sourceCsids = new LinkedHashSet<String>();
79                         String docType = this.getDocType();
80
81                         if (this.requestIsForInvocationModeSingle()) {
82                                 String singleCsid = this.getSingleCsid();
83
84                                 if (singleCsid != null) {
85                                         sourceCsids.add(singleCsid);
86                                 }
87                         } else if (this.requestIsForInvocationModeList()) {
88                                 sourceCsids.addAll(this.getListCsids());
89                         }
90
91                         for (Param param : this.getParams()) {
92                                 String key = param.getKey();
93
94                                 // I don't want this batch job to appear in the UI, since it won't run successfully without parameters.
95                                 // That means it can't be registered with any docType. But if the invocation payload contains a docType,
96                                 // it will be checked against the null registered docType, and will fail. So docType should be passed as a
97                                 // parameter instead.
98
99                                 if (key.equals("docType")) {
100                                         docType = param.getValue();
101                                 }
102                                 else if (key.equals("target")) {
103                                         target = param.getValue();
104                                 }
105                                 else if (key.equals("targetCSID")) {
106                                         target = param.getValue();
107                                 }
108                                 else if (key.equals("sourceCSID")) {
109                                         sourceCsids.add(param.getValue());
110                                 }
111                         }
112
113                         if (target == null || target.equals("")) {
114                                 throw new Exception("a target or targetCSID parameter must be supplied");
115                         }
116
117                         if (sourceCsids.size() == 0) {
118                                 throw new Exception("a source csid must be supplied");
119                         }
120
121                         InvocationResults results = merge(docType, target, sourceCsids);
122
123                         setResults(results);
124                         setCompletionStatus(STATUS_COMPLETE);
125                 }
126                 catch (Exception e) {
127                         setCompletionStatus(STATUS_ERROR);
128                         setErrorInfo(new InvocationError(INT_ERROR_STATUS, e.getMessage()));
129                 }
130         }
131
132         public InvocationResults merge(String docType, String target, String sourceCsid) throws URISyntaxException, DocumentException {
133                 return merge(docType, target, new LinkedHashSet<String>(Arrays.asList(sourceCsid)));
134         }
135
136         public InvocationResults merge(String docType, String target, Set<String> sourceCsids) throws URISyntaxException, DocumentException {
137                 logger.debug("Merging docType=" + docType + " target=" + target + " sourceCsids=" + StringUtils.join(sourceCsids, ","));
138
139                 String serviceName = getAuthorityServiceNameForDocType(docType);
140
141                 PoxPayloadOut targetItemPayload = RefNameUtils.isTermRefname(target)
142                         ? findAuthorityItemByRefName(serviceName, target)
143                         : findAuthorityItemByCsid(serviceName, target);
144
145                 List<PoxPayloadOut> sourceItemPayloads = new ArrayList<PoxPayloadOut>();
146
147                 for (String sourceCsid : sourceCsids) {
148                         sourceItemPayloads.add(findAuthorityItemByCsid(serviceName, sourceCsid));
149                 }
150
151                 return merge(docType, targetItemPayload, sourceItemPayloads);
152         }
153
154         private InvocationResults merge(String docType, PoxPayloadOut targetItemPayload, List<PoxPayloadOut> sourceItemPayloads) throws URISyntaxException, DocumentException {
155                 int numAffected = 0;
156                 List<String> userNotes = new ArrayList<String>();
157
158                 Element targetTermGroupListElement = getTermGroupListElement(targetItemPayload);
159                 Element mergedTermGroupListElement = targetTermGroupListElement.createCopy();
160
161                 String targetCsid = getCsid(targetItemPayload);
162                 String targetRefName = getRefName(targetItemPayload);
163                 String inAuthority = getFieldValue(targetItemPayload, "inAuthority");
164
165                 logger.debug("Merging term groups");
166
167                 for (PoxPayloadOut sourceItemPayload : sourceItemPayloads) {
168                         String sourceCsid = getCsid(sourceItemPayload);
169                         Element sourceTermGroupListElement = getTermGroupListElement(sourceItemPayload);
170
171                         logger.debug("Merging term groups from source " + sourceCsid + " into target " + targetCsid);
172
173                         try {
174                                 mergeTermGroupLists(mergedTermGroupListElement, sourceTermGroupListElement);
175                         }
176                         catch(RuntimeException e) {
177                                 throw new RuntimeException("Error merging source record " + sourceCsid + " into target record " + targetCsid + ": " + e.getMessage(), e);
178                         }
179                 }
180
181                 logger.debug("Updating target: docType=" + docType + " inAuthority=" + inAuthority + " targetCsid=" + targetCsid);
182
183                 updateAuthorityItem(docType, inAuthority, targetCsid, getUpdatePayload(targetTermGroupListElement, mergedTermGroupListElement));
184
185                 String targetDisplayName = RefNameUtils.getDisplayName(targetRefName);
186
187                 userNotes.add("Updated the target record, " + targetDisplayName + ".");
188                 numAffected++;
189
190                 String serviceName = getAuthorityServiceNameForDocType(docType);
191
192                 logger.debug("Updating references");
193
194                 for (PoxPayloadOut sourceItemPayload : sourceItemPayloads) {
195                         String sourceCsid = getCsid(sourceItemPayload);
196                         String sourceRefName = getRefName(sourceItemPayload);
197
198                         InvocationResults results = updateReferences(serviceName, inAuthority, sourceCsid, sourceRefName, targetRefName);
199
200                         userNotes.add(results.getUserNote());
201                         numAffected += results.getNumAffected();
202                 }
203
204                 logger.debug("Deleting source items");
205
206                 for (PoxPayloadOut sourceItemPayload : sourceItemPayloads) {
207                         String sourceCsid = getCsid(sourceItemPayload);
208                         String sourceRefName = getRefName(sourceItemPayload);
209
210                         InvocationResults results = deleteAuthorityItem(docType, getFieldValue(sourceItemPayload, "inAuthority"), sourceCsid, sourceRefName);
211
212                         userNotes.add(results.getUserNote());
213                         numAffected += results.getNumAffected();
214                 }
215
216                 InvocationResults results = new InvocationResults();
217                 results.setNumAffected(numAffected);
218                 results.setUserNote(StringUtils.join(userNotes, "\n"));
219
220                 return results;
221         }
222
223         private InvocationResults updateReferences(String serviceName, String inAuthority, String sourceCsid, String sourceRefName, String targetRefName) throws URISyntaxException, DocumentException {
224                 logger.debug("Updating references: serviceName=" + serviceName + " inAuthority=" + inAuthority + " sourceCsid=" + sourceCsid + " sourceRefName=" + sourceRefName + " targetRefName=" + targetRefName);
225
226                 String sourceDisplayName = RefNameUtils.getDisplayName(sourceRefName);
227
228                 int pageNum = 0;
229                 int pageSize = 100;
230                 List<AuthorityRefDocList.AuthorityRefDocItem> items;
231
232                 int loopCount = 0;
233                 int numUpdated = 0;
234
235                 logger.debug("Looping with pageSize=" + pageSize);
236
237                 do {
238                         loopCount++;
239
240                         // The pageNum/pageSize parameters don't work properly for refobj requests!
241                         // It should be safe to repeatedly fetch page 0 for a large-ish page size,
242                         // and update that page, until no references are left.
243
244                         items = findReferencingFields(serviceName, inAuthority, sourceCsid, null, pageNum, pageSize);
245                         Map<String, ReferencingRecord> referencingRecordsByCsid = new LinkedHashMap<String, ReferencingRecord>();
246
247                         logger.debug("Loop " + loopCount + ": " + items.size() + " items found");
248
249                         for (AuthorityRefDocList.AuthorityRefDocItem item : items) {
250                                 // If a record contains a reference to the record multiple times, multiple items are returned,
251                                 // but only the first has a non-null workflow state. A bug?
252
253                                 String itemCsid = item.getDocId();
254                                 ReferencingRecord record = referencingRecordsByCsid.get(itemCsid);
255
256                                 if (record == null) {
257                                         if (item.getWorkflowState() != null && !item.getWorkflowState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
258                                                 record = new ReferencingRecord(item.getUri());
259                                                 referencingRecordsByCsid.put(itemCsid, record);
260                                         }
261                                 }
262
263                                 if (record != null) {
264                                         String[] sourceFieldElements = item.getSourceField().split(":");
265                                         String partName = sourceFieldElements[0];
266                                         String fieldName = sourceFieldElements[1];
267
268                                         Map<String, Set<String>> fields = record.getFields();
269                                         Set<String> fieldsInPart = fields.get(partName);
270
271                                         if (fieldsInPart == null) {
272                                                 fieldsInPart = new HashSet<String>();
273                                                 fields.put(partName, fieldsInPart);
274                                         }
275
276                                         fieldsInPart.add(fieldName);
277                                 }
278                         }
279
280                         List<ReferencingRecord> referencingRecords = new ArrayList<ReferencingRecord>(referencingRecordsByCsid.values());
281
282                         logger.debug("Loop " + loopCount + ": updating " + referencingRecords.size() + " records");
283
284                         for (ReferencingRecord record : referencingRecords) {
285                                 InvocationResults results = updateReferencingRecord(record, sourceRefName, targetRefName);
286                                 numUpdated += results.getNumAffected();
287                         }
288                 }
289                 while (items.size() > 0);
290
291                 InvocationResults results = new InvocationResults();
292                 results.setNumAffected(numUpdated);
293
294                 if (numUpdated > 0) {
295                         results.setUserNote(
296                                 "Updated "
297                                 + numUpdated
298                                 + (numUpdated == 1 ? " record " : " records ")
299                                 + "that referenced the source record, "
300                                 + sourceDisplayName + "."
301                         );
302                 } else {
303                         results.setUserNote("No records referenced the source record, " + sourceDisplayName + ".");
304                 }
305
306                 return results;
307         }
308
309         private InvocationResults updateReferencingRecord(ReferencingRecord record, String fromRefName, String toRefName) throws URISyntaxException, DocumentException {
310                 String fromRefNameStem = RefNameUtils.stripAuthorityTermDisplayName(fromRefName);
311                 // String toRefNameStem = RefNameUtils.stripAuthorityTermDisplayName(toRefName);
312
313                 logger.debug("Updating references: record.uri=" + record.getUri() + " fromRefName=" + fromRefName + " toRefName=" + toRefName);
314
315                 Map<String, Set<String>> fields = record.getFields();
316
317                 PoxPayloadOut recordPayload = findByUri(record.getUri());
318                 Document recordDocument = recordPayload.getDOMDocument();
319                 Document newDocument = (Document) recordDocument.clone();
320                 Element rootElement = newDocument.getRootElement();
321
322                 for (Element partElement : (List<Element>) rootElement.elements()) {
323                         String partName = partElement.getName();
324
325                         if (fields.containsKey(partName)) {
326                                 for (String fieldName : fields.get(partName)) {
327                                         List<Node> nodes = partElement.selectNodes("descendant::" + fieldName);
328
329                                         for (Node node : nodes) {
330                                                 String text = node.getText();
331                                                 String refNameStem = null;
332
333                                                 try {
334                                                         refNameStem = RefNameUtils.stripAuthorityTermDisplayName(text);
335                                                 }
336                                                 catch(IllegalArgumentException e) {}
337
338                                                 if (refNameStem != null && refNameStem.equals(fromRefNameStem)) {
339                                                         AuthorityTermInfo termInfo = RefNameUtils.parseAuthorityTermInfo(text);
340                                                         // String newRefName = toRefNameStem + "'" + termInfo.displayName + "'";
341                                                         String newRefName = toRefName;
342
343                                                         node.setText(newRefName);
344                                                 }
345                                         }
346                                 }
347                         }
348                         else {
349                                 rootElement.remove(partElement);
350                         }
351                 }
352
353                 String payload = newDocument.asXML();
354
355                 return updateUri(record.getUri(), payload);
356         }
357
358         private InvocationResults updateUri(String uri, String payload) throws URISyntaxException {
359                 String[] uriParts = uri.split("/");
360
361                 if (uriParts.length == 3) {
362                         String serviceName = uriParts[1];
363                         String csid = uriParts[2];
364
365                         NuxeoBasedResource resource = (NuxeoBasedResource) getResourceMap().get(serviceName);
366
367                         resource.update(getResourceMap(), createUriInfo(), csid, payload);
368                 }
369                 else if (uriParts.length == 5) {
370                         String serviceName = uriParts[1];
371                         String vocabularyCsid = uriParts[2];
372                         String items = uriParts[3];
373                         String csid = uriParts[4];
374
375                         if (items.equals("items")) {
376                                 AuthorityResource<?, ?> resource = (AuthorityResource<?, ?>) getResourceMap().get(serviceName);
377
378                                 resource.updateAuthorityItem(getResourceMap(), createUriInfo(), vocabularyCsid, csid, payload);
379                         }
380                 }
381                 else {
382                         throw new IllegalArgumentException("Invalid uri " + uri);
383                 }
384
385                 logger.debug("Updated referencing record " + uri);
386
387                 InvocationResults results = new InvocationResults();
388                 results.setNumAffected(1);
389                 results.setUserNote("Updated referencing record " + uri);
390
391                 return results;
392         }
393
394         private void updateAuthorityItem(String docType, String inAuthority, String csid, String payload) throws URISyntaxException {
395                 String serviceName = getAuthorityServiceNameForDocType(docType);
396                 AuthorityResource<?, ?> resource = (AuthorityResource<?, ?>) getResourceMap().get(serviceName);
397
398                 resource.updateAuthorityItem(getResourceMap(), createUriInfo(), inAuthority, csid, payload);
399         }
400
401         private InvocationResults deleteAuthorityItem(String docType, String inAuthority, String csid, String refName) throws URISyntaxException {
402                 int numAffected = 0;
403                 List<String> userNotes = new ArrayList<String>();
404                 String displayName = RefNameUtils.getDisplayName(refName);
405
406                 // If the item is the broader context of any items, warn and do nothing.
407
408                 List<String> narrowerItemCsids = findNarrower(csid);
409
410                 if (narrowerItemCsids.size() > 0) {
411                         logger.debug("Item " + csid + " has narrower items -- not deleting");
412
413                         userNotes.add("The source record, " + displayName + ", was not deleted because it has narrower items in its hierarchy.");
414                 }
415                 else {
416                         // If the item has a broader context, delete the relation.
417
418                         List<RelationsCommonList.RelationListItem> relationItems = new ArrayList<RelationsCommonList.RelationListItem>();
419
420                         for (RelationsCommonList.RelationListItem item : findRelated(csid, null, "hasBroader", null, null)) {
421                                 relationItems.add(item);
422                         }
423
424                         if (relationItems.size() > 0) {
425                                 RelationResource relationResource = (RelationResource) getResourceMap().get(RelationClient.SERVICE_NAME);
426
427                                 for (RelationsCommonList.RelationListItem item : relationItems) {
428                                         String relationCsid = item.getCsid();
429
430                                         String subjectRefName = item.getSubject().getRefName();
431                                         String subjectDisplayName = RefNameUtils.getDisplayName(subjectRefName);
432
433                                         String objectRefName = item.getObject().getRefName();
434                                         String objectDisplayName = RefNameUtils.getDisplayName(objectRefName);
435
436                                         logger.debug("Deleting hasBroader relation " + relationCsid);
437
438                                         relationResource.delete(relationCsid);
439
440                                         userNotes.add("Deleted the \"has broader\" relation from " + subjectDisplayName + " to " + objectDisplayName + ".");
441                                         numAffected++;
442                                 }
443                         }
444
445                         String serviceName = getAuthorityServiceNameForDocType(docType);
446                         AuthorityResource<?, ?> resource = (AuthorityResource<?, ?>) getResourceMap().get(serviceName);
447
448                         logger.debug("Soft deleting: docType=" + docType + " inAuthority=" + inAuthority + " csid=" + csid);
449
450                         resource.updateItemWorkflowWithTransition(null, inAuthority, csid, "delete");
451
452                         userNotes.add("Deleted the source record, " + displayName + ".");
453                         numAffected++;
454                 }
455
456                 InvocationResults results = new InvocationResults();
457                 results.setNumAffected(numAffected);
458                 results.setUserNote(StringUtils.join(userNotes, "\n"));
459
460                 return results;
461         }
462
463         /**
464          * @param Returns a map of the term groups in term group list, keyed by display name.
465          *        If multiple groups have the same display name, an exception is thrown.
466          * @return The term groups.
467          */
468         private Map<String, Element> getTermGroups(Element termGroupListElement) {
469                 Map<String, Element> termGroups = new LinkedHashMap<String, Element>();
470                 Iterator<Element> childIterator = termGroupListElement.elementIterator();
471
472                 while (childIterator.hasNext()) {
473                         Element termGroupElement = childIterator.next();
474                         String displayName = getDisplayName(termGroupElement);
475
476                         if (termGroups.containsKey(displayName)) {
477                                 // Two term groups in the same item have identical display names.
478
479                                 throw new RuntimeException("multiple terms have display name \"" + displayName + "\"");
480                         }
481                         else {
482                                 termGroups.put(displayName, termGroupElement);
483                         }
484                 }
485
486                 return termGroups;
487         }
488
489         private String getDisplayName(Element termGroupElement) {
490                 Node displayNameNode = termGroupElement.selectSingleNode("termDisplayName");
491                 String displayName = (displayNameNode == null) ? "" : displayNameNode.getText();
492
493                 return displayName;
494         }
495
496         private Element getTermGroupListElement(PoxPayloadOut itemPayload) {
497                 Element termGroupListElement = null;
498                 Element commonPartElement = findCommonPartElement(itemPayload);
499
500                 if (commonPartElement != null) {
501                         termGroupListElement = findTermGroupListElement(commonPartElement);
502                 }
503
504                 return termGroupListElement;
505         }
506
507         private Element findCommonPartElement(PoxPayloadOut itemPayload) {
508                 Element commonPartElement = null;
509
510                 for (PayloadOutputPart candidatePart : itemPayload.getParts()) {
511                         Element candidatePartElement = candidatePart.asElement();
512
513                         if (candidatePartElement.getName().endsWith("_common")) {
514                                 commonPartElement = candidatePartElement;
515                                 break;
516                         }
517                 }
518
519                 return commonPartElement;
520         }
521
522         private Element findTermGroupListElement(Element contextElement) {
523                 Element termGroupListElement = null;
524                 Iterator<Element> childIterator = contextElement.elementIterator();
525
526                 while (childIterator.hasNext()) {
527                         Element candidateElement = childIterator.next();
528
529                         if (candidateElement.getName().endsWith("TermGroupList")) {
530                                 termGroupListElement = candidateElement;
531                                 break;
532                         }
533                 }
534
535                 return termGroupListElement;
536         }
537
538         private void mergeTermGroupLists(Element targetTermGroupListElement, Element sourceTermGroupListElement) {
539                 Map<String, Element> sourceTermGroups;
540
541                 try {
542                         sourceTermGroups = getTermGroups(sourceTermGroupListElement);
543                 }
544                 catch(RuntimeException e) {
545                         throw new RuntimeException("a problem was found in the source record: " + e.getMessage(), e);
546                 }
547
548                 for (Element targetTermGroupElement : (List<Element>) targetTermGroupListElement.elements()) {
549                         String displayName = getDisplayName(targetTermGroupElement);
550
551                         if (sourceTermGroups.containsKey(displayName)) {
552                                 logger.debug("Merging in existing term \"" + displayName + "\"");
553
554                                 try {
555                                         mergeTermGroups(targetTermGroupElement, sourceTermGroups.get(displayName));
556                                 }
557                                 catch(RuntimeException e) {
558                                         throw new RuntimeException("could not merge term groups with display name \"" + displayName + "\": " + e.getMessage(), e);
559                                 }
560
561                                 sourceTermGroups.remove(displayName);
562                         }
563                 }
564
565                 for (Element sourceTermGroupElement : sourceTermGroups.values()) {
566                         logger.debug("Adding new term \"" + getDisplayName(sourceTermGroupElement) + "\"");
567
568                         targetTermGroupListElement.add(sourceTermGroupElement.createCopy());
569                 }
570         }
571
572         private void mergeTermGroups(Element targetTermGroupElement, Element sourceTermGroupElement) {
573                 // This function assumes there are no nested repeating groups.
574
575                 for (Element sourceChildElement : (List<Element>) sourceTermGroupElement.elements()) {
576                         String sourceValue = sourceChildElement.getText();
577
578                         if (sourceValue == null) {
579                                 sourceValue = "";
580                         }
581
582                         if (sourceValue.length() > 0) {
583                                 String name = sourceChildElement.getName();
584                                 Element targetChildElement = targetTermGroupElement.element(name);
585
586                                 if (targetChildElement == null) {
587                                         targetTermGroupElement.add(sourceChildElement.createCopy());
588                                 }
589                                 else {
590                                         String targetValue = targetChildElement.getText();
591
592                                         if (targetValue == null) {
593                                                 targetValue = "";
594                                         }
595
596                                         if (!targetValue.equals(sourceValue)) {
597                                                 if (targetValue.length() > 0) {
598                                                         throw new RuntimeException("merge conflict in field " + name + ": source value \"" + sourceValue + "\" differs from target value \"" + targetValue +"\"");
599                                                 }
600
601                                                 targetTermGroupElement.remove(targetChildElement);
602                                                 targetTermGroupElement.add(sourceChildElement.createCopy());
603                                         }
604                                 }
605                         }
606                 }
607         }
608
609         private String getUpdatePayload(Element originalTermGroupListElement, Element updatedTermGroupListElement) {
610                 List<Element> parents = new ArrayList<Element>();
611
612                 for (Element e = originalTermGroupListElement; e != null; e = e.getParent()) {
613                         parents.add(e);
614                 }
615
616                 Collections.reverse(parents);
617
618                 // Remove the original termGroupList element
619                 parents.remove(parents.size() - 1);
620
621                 // Remove the root
622                 Element rootElement = parents.remove(0);
623
624                 // Copy the root to a new document
625                 Document document = DocumentHelper.createDocument(copyElement(rootElement));
626                 Element current = document.getRootElement();
627
628                 // Copy the remaining parents
629                 for (Element parent : parents) {
630                         Element parentCopy = copyElement(parent);
631
632                         current.add(parentCopy);
633                         current = parentCopy;
634                 }
635
636                 // Add the updated termGroupList element
637
638                 current.add(updatedTermGroupListElement);
639
640                 String payload = document.asXML();
641
642                 return payload;
643         }
644
645         private Element copyElement(Element element) {
646                 Element copy = DocumentHelper.createElement(element.getQName());
647                 copy.appendAttributes(element);
648
649                 return copy;
650         }
651
652         private class ReferencingRecord {
653                 private String uri;
654                 private Map<String, Set<String>> fields;
655
656                 public ReferencingRecord(String uri) {
657                         this.uri = uri;
658                         this.fields = new HashMap<String, Set<String>>();
659                 }
660
661                 public String getUri() {
662                         return uri;
663                 }
664
665                 public void setUri(String uri) {
666                         this.uri = uri;
667                 }
668
669                 public Map<String, Set<String>> getFields() {
670                         return fields;
671                 }
672         }
673 }