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