001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.connector.store.jpa.model.basic;
025    
026    import java.io.ByteArrayInputStream;
027    import java.io.ByteArrayOutputStream;
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.io.ObjectInputStream;
031    import java.io.ObjectOutputStream;
032    import java.io.OutputStream;
033    import java.util.ArrayList;
034    import java.util.Arrays;
035    import java.util.Collection;
036    import java.util.Collections;
037    import java.util.HashMap;
038    import java.util.HashSet;
039    import java.util.Iterator;
040    import java.util.LinkedList;
041    import java.util.List;
042    import java.util.ListIterator;
043    import java.util.Map;
044    import java.util.Set;
045    import java.util.UUID;
046    import java.util.zip.GZIPInputStream;
047    import java.util.zip.GZIPOutputStream;
048    import javax.persistence.EntityManager;
049    import javax.persistence.EntityTransaction;
050    import javax.persistence.NoResultException;
051    import javax.persistence.Query;
052    import net.jcip.annotations.Immutable;
053    import net.jcip.annotations.NotThreadSafe;
054    import org.jboss.dna.common.util.IoUtil;
055    import org.jboss.dna.common.util.Logger;
056    import org.jboss.dna.common.util.StringUtil;
057    import org.jboss.dna.connector.store.jpa.JpaConnectorI18n;
058    import org.jboss.dna.connector.store.jpa.model.common.NamespaceEntity;
059    import org.jboss.dna.connector.store.jpa.model.common.WorkspaceEntity;
060    import org.jboss.dna.connector.store.jpa.util.Namespaces;
061    import org.jboss.dna.connector.store.jpa.util.RequestProcessorCache;
062    import org.jboss.dna.connector.store.jpa.util.Serializer;
063    import org.jboss.dna.connector.store.jpa.util.Workspaces;
064    import org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues;
065    import org.jboss.dna.graph.DnaLexicon;
066    import org.jboss.dna.graph.ExecutionContext;
067    import org.jboss.dna.graph.JcrLexicon;
068    import org.jboss.dna.graph.Location;
069    import org.jboss.dna.graph.observe.Observer;
070    import org.jboss.dna.graph.property.Binary;
071    import org.jboss.dna.graph.property.Name;
072    import org.jboss.dna.graph.property.NameFactory;
073    import org.jboss.dna.graph.property.Path;
074    import org.jboss.dna.graph.property.PathFactory;
075    import org.jboss.dna.graph.property.PathNotFoundException;
076    import org.jboss.dna.graph.property.Property;
077    import org.jboss.dna.graph.property.PropertyType;
078    import org.jboss.dna.graph.property.Reference;
079    import org.jboss.dna.graph.property.ReferentialIntegrityException;
080    import org.jboss.dna.graph.property.UuidFactory;
081    import org.jboss.dna.graph.property.ValueFactories;
082    import org.jboss.dna.graph.property.ValueFactory;
083    import org.jboss.dna.graph.property.ValueFormatException;
084    import org.jboss.dna.graph.request.CloneWorkspaceRequest;
085    import org.jboss.dna.graph.request.CopyBranchRequest;
086    import org.jboss.dna.graph.request.CreateNodeRequest;
087    import org.jboss.dna.graph.request.CreateWorkspaceRequest;
088    import org.jboss.dna.graph.request.DeleteBranchRequest;
089    import org.jboss.dna.graph.request.DeleteChildrenRequest;
090    import org.jboss.dna.graph.request.DestroyWorkspaceRequest;
091    import org.jboss.dna.graph.request.GetWorkspacesRequest;
092    import org.jboss.dna.graph.request.InvalidRequestException;
093    import org.jboss.dna.graph.request.InvalidWorkspaceException;
094    import org.jboss.dna.graph.request.MoveBranchRequest;
095    import org.jboss.dna.graph.request.ReadAllChildrenRequest;
096    import org.jboss.dna.graph.request.ReadAllPropertiesRequest;
097    import org.jboss.dna.graph.request.ReadBlockOfChildrenRequest;
098    import org.jboss.dna.graph.request.ReadBranchRequest;
099    import org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest;
100    import org.jboss.dna.graph.request.ReadNodeRequest;
101    import org.jboss.dna.graph.request.ReadPropertyRequest;
102    import org.jboss.dna.graph.request.Request;
103    import org.jboss.dna.graph.request.UpdatePropertiesRequest;
104    import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
105    import org.jboss.dna.graph.request.processor.RequestProcessor;
106    
107    /**
108     * @author Randall Hauch
109     */
110    @NotThreadSafe
111    public class BasicRequestProcessor extends RequestProcessor {
112    
113        protected final EntityManager entities;
114        protected final ValueFactory<String> stringFactory;
115        protected final PathFactory pathFactory;
116        protected final NameFactory nameFactory;
117        protected final UuidFactory uuidFactory;
118        protected final Namespaces namespaces;
119        protected final Workspaces workspaces;
120        protected final UUID rootNodeUuid;
121        protected final String rootNodeUuidString;
122        protected final String nameOfDefaultWorkspace;
123        protected final String[] predefinedWorkspaceNames;
124        protected final boolean creatingWorkspacesAllowed;
125        protected final Serializer serializer;
126        protected final long largeValueMinimumSizeInBytes;
127        protected final boolean compressData;
128        protected final Logger logger;
129        protected final RequestProcessorCache cache;
130        protected final boolean enforceReferentialIntegrity;
131        private final Set<Long> workspaceIdsWithChangedReferences = new HashSet<Long>();
132    
133        /**
134         * @param sourceName
135         * @param context
136         * @param observer
137         * @param entityManager
138         * @param rootNodeUuid
139         * @param nameOfDefaultWorkspace
140         * @param predefinedWorkspaceNames
141         * @param largeValueMinimumSizeInBytes
142         * @param creatingWorkspacesAllowed
143         * @param compressData
144         * @param enforceReferentialIntegrity
145         */
146        public BasicRequestProcessor( String sourceName,
147                                      ExecutionContext context,
148                                      Observer observer,
149                                      EntityManager entityManager,
150                                      UUID rootNodeUuid,
151                                      String nameOfDefaultWorkspace,
152                                      String[] predefinedWorkspaceNames,
153                                      long largeValueMinimumSizeInBytes,
154                                      boolean creatingWorkspacesAllowed,
155                                      boolean compressData,
156                                      boolean enforceReferentialIntegrity ) {
157            super(sourceName, context, observer);
158            assert entityManager != null;
159            assert rootNodeUuid != null;
160            assert predefinedWorkspaceNames != null;
161            this.entities = entityManager;
162            ValueFactories valuesFactory = context.getValueFactories();
163            this.stringFactory = valuesFactory.getStringFactory();
164            this.pathFactory = valuesFactory.getPathFactory();
165            this.nameFactory = valuesFactory.getNameFactory();
166            this.uuidFactory = valuesFactory.getUuidFactory();
167            this.namespaces = new Namespaces(entityManager);
168            this.workspaces = new Workspaces(entityManager);
169            this.rootNodeUuid = rootNodeUuid;
170            this.rootNodeUuidString = this.rootNodeUuid.toString();
171            this.nameOfDefaultWorkspace = nameOfDefaultWorkspace;
172            this.creatingWorkspacesAllowed = creatingWorkspacesAllowed;
173            this.largeValueMinimumSizeInBytes = largeValueMinimumSizeInBytes;
174            this.compressData = compressData;
175            this.enforceReferentialIntegrity = enforceReferentialIntegrity;
176            this.serializer = new Serializer(context, true);
177            this.logger = getExecutionContext().getLogger(getClass());
178            this.cache = new RequestProcessorCache(this.pathFactory);
179            this.predefinedWorkspaceNames = predefinedWorkspaceNames;
180    
181            // Start the transaction ...
182            this.entities.getTransaction().begin();
183        }
184    
185        /**
186         * {@inheritDoc}
187         * 
188         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateNodeRequest)
189         */
190        @Override
191        public void process( CreateNodeRequest request ) {
192            logger.trace(request.toString());
193            Location actualLocation = null;
194            try {
195                // Find the workspace ...
196                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
197                if (workspace == null) return;
198                Long workspaceId = workspace.getId();
199                assert workspaceId != null;
200    
201                // Create nodes have to be defined via a path ...
202                Location parentLocation = request.under();
203                ActualLocation actual = getActualLocation(workspace.getId(), parentLocation);
204                String parentUuidString = actual.uuid;
205                assert parentUuidString != null;
206    
207                // We need to look for an existing UUID property in the request,
208                // so since we have to iterate through the properties, go ahead an serialize them right away ...
209                String uuidString = null;
210                for (Property property : request.properties()) {
211                    if (property.getName().equals(DnaLexicon.UUID)) {
212                        uuidString = stringFactory.create(property.getFirstValue());
213                        break;
214                    }
215                }
216                if (uuidString == null) uuidString = UUID.randomUUID().toString();
217                assert uuidString != null;
218                createProperties(workspaceId, uuidString, request.properties());
219    
220                // Find or create the namespace for the child ...
221                Name childName = request.named();
222                String childNsUri = childName.getNamespaceUri();
223                NamespaceEntity ns = namespaces.get(childNsUri, true);
224                assert ns != null;
225                final Path parentPath = actual.location.getPath();
226                assert parentPath != null;
227    
228                // Figure out the next SNS index and index-in-parent for this new child ...
229                actualLocation = addNewChild(workspaceId, actual, uuidString, childName, true);
230    
231                // Since we've just created this node, we know about all the children (actually, there are none).
232                cache.setAllChildren(workspace.getId(), actualLocation.getPath(), new LinkedList<Location>());
233    
234                // Flush the entities ...
235                // entities.flush();
236    
237            } catch (Throwable e) { // Includes PathNotFoundException
238                request.setError(e);
239                logger.trace(e, "Problem " + request);
240                return;
241            }
242            request.setActualLocationOfNode(actualLocation);
243        }
244    
245        /**
246         * Create a new child with the supplied UUID and name under the supplied parent. If the parent is null, then the child will be
247         * the root node.
248         * 
249         * @param workspaceId the ID of the workspace in which the child is to be created
250         * @param parent the actual location of the parent, or null if the child is to be the root of the workspace
251         * @param childUuid the UUID of the child
252         * @param childName the name of the child
253         * @param allowSameNameChildrenInNewNode
254         * @return the location of the new child
255         */
256        protected Location addNewChild( Long workspaceId,
257                                        ActualLocation parent,
258                                        String childUuid,
259                                        Name childName,
260                                        boolean allowSameNameChildrenInNewNode ) {
261            int nextSnsIndex = 1; // SNS index is 1-based
262            int nextIndexInParent = 0; // index-in-parent is 0-based
263            String childNsUri = childName.getNamespaceUri();
264            NamespaceEntity ns = namespaces.get(childNsUri, true);
265            assert ns != null;
266    
267            // If the parent is null, the create a root node ...
268            Path parentPath = null;
269            String parentUuid = null;
270            ChildEntity parentEntity = null;
271            if (parent == null) {
272                return Location.create(pathFactory.createRootPath(), UUID.fromString(childUuid));
273            }
274            parentPath = parent.location.getPath();
275            parentUuid = parent.uuid;
276            parentEntity = parent.childEntity; // may be null
277    
278            assert workspaceId != null;
279    
280            ChildId id = new ChildId(workspaceId, parentUuid, childUuid);
281            ChildEntity entity = null;
282    
283            // Look in the cache for the children of the parent node.
284            LinkedList<Location> childrenOfParent = cache.getAllChildren(workspaceId, parentPath);
285    
286            // Now create the entity ...
287            if (parentEntity == null || parentEntity.getAllowsSameNameChildren()) {
288                // The parent DOES allow same-name-siblings, so we need to find the SNS index ...
289    
290                if (childrenOfParent != null) {
291                    // The cache had the complete list of children for the parent node, which means
292                    // we know about all of the children and can walk the children to figure out the next indexes.
293                    nextIndexInParent = childrenOfParent.size();
294                    if (nextIndexInParent > 1) {
295                        // Since we want the last indexes, process the list backwards ...
296                        ListIterator<Location> iter = childrenOfParent.listIterator(childrenOfParent.size());
297                        while (iter.hasPrevious()) {
298                            Location existing = iter.previous();
299                            Path.Segment segment = existing.getPath().getLastSegment();
300                            if (!segment.getName().equals(childName)) continue;
301                            // Otherwise the name matched, so get the indexes ...
302                            nextSnsIndex = segment.getIndex() + 1;
303                        }
304                    }
305                } else {
306                    // The cache did not have the complete list of children for the parent node,
307                    // so we need to look the values up by querying the database ...
308    
309                    // Find the largest SNS index in the existing ChildEntity objects with the same name ...
310                    String childLocalName = childName.getLocalName();
311                    Query query = entities.createNamedQuery("ChildEntity.findMaximumSnsIndex");
312                    query.setParameter("workspaceId", workspaceId);
313                    query.setParameter("parentUuid", parentUuid);
314                    query.setParameter("ns", ns.getId());
315                    query.setParameter("childName", childLocalName);
316                    try {
317                        Integer result = (Integer)query.getSingleResult();
318                        nextSnsIndex = result != null ? result + 1 : 1; // SNS index is 1-based
319                    } catch (NoResultException e) {
320                    }
321    
322                    // Find the largest child index in the existing ChildEntity objects ...
323                    query = entities.createNamedQuery("ChildEntity.findMaximumChildIndex");
324                    query.setParameter("workspaceId", workspaceId);
325                    query.setParameter("parentUuid", parentUuid);
326                    try {
327                        Integer result = (Integer)query.getSingleResult();
328                        nextIndexInParent = result != null ? result + 1 : 0; // index-in-parent is 0-based
329                    } catch (NoResultException e) {
330                    }
331                }
332    
333                // Create the new ChildEntity ...
334                entity = new ChildEntity(id, nextIndexInParent, ns, childName.getLocalName(), nextSnsIndex);
335            } else {
336                // The parent does not allow same-name-siblings, so we only need to find the next index ...
337                // Find the largest child index in the existing ChildEntity objects ...
338                if (childrenOfParent != null) {
339                    // The cache had the complete list of children for the parent node, which means
340                    // we know about all of the children and can walk the children to figure out the next indexes.
341                    nextIndexInParent = childrenOfParent.size();
342                } else {
343                    // We don't have the children cached, so we need to do a query ...
344                    Query query = entities.createNamedQuery("ChildEntity.findMaximumChildIndex");
345                    query.setParameter("workspaceId", workspaceId);
346                    query.setParameter("parentUuid", parentUuid);
347                    try {
348                        Integer result = (Integer)query.getSingleResult();
349                        nextIndexInParent = result != null ? result + 1 : 0; // index-in-parent is 0-based
350                    } catch (NoResultException e) {
351                    }
352                }
353    
354                // Create the new child entity ...
355                entity = new ChildEntity(id, nextIndexInParent, ns, childName.getLocalName(), 1);
356            }
357    
358            // Persist the new child entity ...
359            entity.setAllowsSameNameChildren(allowSameNameChildrenInNewNode);
360            entities.persist(entity);
361    
362            // Set the actual path, regardless of the supplied path...
363            Path path = pathFactory.create(parentPath, childName, nextSnsIndex);
364            Location actualLocation = Location.create(path, UUID.fromString(childUuid));
365    
366            // Finally, update the cache with the information we know ...
367            if (childrenOfParent != null) {
368                // Add to the cached list of children ...
369                childrenOfParent.add(actualLocation);
370            } else {
371                cache.setAllChildren(workspaceId, parentPath, null);
372            }
373            return actualLocation;
374        }
375    
376        protected class NextChildIndexes {
377            protected final int nextIndexInParent;
378            protected final int nextSnsIndex;
379    
380            protected NextChildIndexes( int nextIndexInParent,
381                                        int nextSnsIndex ) {
382                this.nextIndexInParent = nextIndexInParent;
383                this.nextSnsIndex = nextSnsIndex;
384            }
385        }
386    
387        /**
388         * {@inheritDoc}
389         * 
390         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNodeRequest)
391         */
392        @Override
393        public void process( ReadNodeRequest request ) {
394            logger.trace(request.toString());
395            Location actualLocation = null;
396            try {
397                // Find the workspace ...
398                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
399                if (workspace == null) return;
400                Long workspaceId = workspace.getId();
401                assert workspaceId != null;
402    
403                Location location = request.at();
404                ActualLocation actual = getActualLocation(workspaceId, location);
405                String parentUuidString = actual.uuid;
406                actualLocation = actual.location;
407    
408                // Record the UUID as a property, since it's not stored in the serialized properties...
409                request.addProperty(actualLocation.getIdProperty(DnaLexicon.UUID));
410    
411                // Find the properties entity for this node ...
412                Query query = entities.createNamedQuery("PropertiesEntity.findByUuid");
413                query.setParameter("workspaceId", workspaceId);
414                query.setParameter("uuid", parentUuidString);
415                try {
416                    PropertiesEntity entity = (PropertiesEntity)query.getSingleResult();
417    
418                    // Deserialize the properties ...
419                    boolean compressed = entity.isCompressed();
420                    Collection<Property> properties = new LinkedList<Property>();
421                    byte[] data = entity.getData();
422                    if (data != null) {
423                        LargeValueSerializer largeValues = new LargeValueSerializer(entity);
424                        ByteArrayInputStream bais = new ByteArrayInputStream(data);
425                        InputStream is = compressed ? new GZIPInputStream(bais) : bais;
426                        ObjectInputStream ois = new ObjectInputStream(is);
427                        try {
428                            serializer.deserializeAllProperties(ois, properties, largeValues);
429                            for (Property property : properties) {
430                                request.addProperty(property);
431                            }
432                        } finally {
433                            ois.close();
434                        }
435                    }
436    
437                } catch (NoResultException e) {
438                    // No properties, but that's okay...
439                }
440    
441                // Get the children for this node ...
442                for (Location childLocation : getAllChildren(workspaceId, actual)) {
443                    request.addChild(childLocation);
444                }
445            } catch (NoResultException e) {
446                // there are no properties (probably not expected, but still okay) ...
447            } catch (Throwable e) { // Includes PathNotFoundException
448                request.setError(e);
449                return;
450            }
451            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
452            setCacheableInfo(request);
453        }
454    
455        /**
456         * {@inheritDoc}
457         * 
458         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllChildrenRequest)
459         */
460        @Override
461        public void process( ReadAllChildrenRequest request ) {
462            logger.trace(request.toString());
463            Location actualLocation = null;
464            try {
465                // Find the workspace ...
466                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
467                if (workspace == null) return;
468                Long workspaceId = workspace.getId();
469                assert workspaceId != null;
470    
471                Location location = request.of();
472                ActualLocation actual = getActualLocation(workspaceId, location);
473                actualLocation = actual.location;
474    
475                // Get the children for this node ...
476                for (Location childLocation : getAllChildren(workspaceId, actual)) {
477                    request.addChild(childLocation);
478                }
479            } catch (NoResultException e) {
480                // there are no properties (probably not expected, but still okay) ...
481            } catch (Throwable e) { // Includes PathNotFoundException
482                request.setError(e);
483                return;
484            }
485            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
486            setCacheableInfo(request);
487        }
488    
489        /**
490         * Utility method to obtain all of the children for a node, either from the cache (if all children are known to this
491         * processor) or by querying the database (and caching the list of children).
492         * 
493         * @param workspaceId the ID of the workspace; may not be null
494         * @param parent the actual location of the parent node; may not be null
495         * @return the list of child locations
496         */
497        protected LinkedList<Location> getAllChildren( Long workspaceId,
498                                                       ActualLocation parent ) {
499            assert parent != null;
500            Path parentPath = parent.location.getPath();
501            assert parentPath != null;
502            LinkedList<Location> cachedChildren = cache.getAllChildren(workspaceId, parentPath);
503            if (cachedChildren != null) {
504                // The cache has all of the children for the node ...
505                return cachedChildren;
506            }
507    
508            // Not found in the cache, so query the database ...
509            Query query = entities.createNamedQuery("ChildEntity.findAllUnderParent");
510            query.setParameter("workspaceId", workspaceId);
511            query.setParameter("parentUuidString", parent.uuid);
512            LinkedList<Location> childLocations = new LinkedList<Location>();
513            @SuppressWarnings( "unchecked" )
514            List<ChildEntity> children = query.getResultList();
515            for (ChildEntity child : children) {
516                String namespaceUri = child.getChildNamespace().getUri();
517                String localName = child.getChildName();
518                Name childName = nameFactory.create(namespaceUri, localName);
519                int sns = child.getSameNameSiblingIndex();
520                Path childPath = pathFactory.create(parentPath, childName, sns);
521                String childUuidString = child.getId().getChildUuidString();
522                Location childLocation = Location.create(childPath, UUID.fromString(childUuidString));
523                childLocations.add(childLocation);
524            }
525            // Update the cache ...
526            cache.setAllChildren(workspaceId, parentPath, childLocations);
527            return childLocations;
528        }
529    
530        /**
531         * {@inheritDoc}
532         * 
533         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBlockOfChildrenRequest)
534         */
535        @Override
536        public void process( ReadBlockOfChildrenRequest request ) {
537            logger.trace(request.toString());
538            Location actualLocation = null;
539            final int startingIndex = request.startingAtIndex();
540            try {
541                // Find the workspace ...
542                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
543                if (workspace == null) return;
544                Long workspaceId = workspace.getId();
545                assert workspaceId != null;
546    
547                Location parentLocation = request.of();
548                ActualLocation actualParent = getActualLocation(workspaceId, parentLocation);
549                actualLocation = actualParent.location;
550    
551                Path parentPath = actualParent.location.getPath();
552                assert parentPath != null;
553                LinkedList<Location> cachedChildren = cache.getAllChildren(workspaceId, parentPath);
554                if (cachedChildren != null) {
555                    // The cache has all of the children for the node ...
556                    if (startingIndex < cachedChildren.size()) {
557                        ListIterator<Location> iter = cachedChildren.listIterator(startingIndex);
558                        for (int i = 0; i != request.count() && iter.hasNext(); ++i) {
559                            Location child = iter.next();
560                            request.addChild(child);
561                        }
562                    }
563                } else {
564                    // Nothing was cached, so we need to search the database for the children ...
565                    Query query = entities.createNamedQuery("ChildEntity.findRangeUnderParent");
566                    query.setParameter("workspaceId", workspaceId);
567                    query.setParameter("parentUuidString", actualParent.uuid);
568                    query.setParameter("firstIndex", startingIndex);
569                    query.setParameter("afterIndex", startingIndex + request.count());
570                    @SuppressWarnings( "unchecked" )
571                    List<ChildEntity> children = query.getResultList();
572                    for (ChildEntity child : children) {
573                        String namespaceUri = child.getChildNamespace().getUri();
574                        String localName = child.getChildName();
575                        Name childName = nameFactory.create(namespaceUri, localName);
576                        int sns = child.getSameNameSiblingIndex();
577                        Path childPath = pathFactory.create(parentPath, childName, sns);
578                        String childUuidString = child.getId().getChildUuidString();
579                        Location childLocation = Location.create(childPath, UUID.fromString(childUuidString));
580                        request.addChild(childLocation);
581                    }
582                    // Do not update the cache, since we don't know all of the children.
583                }
584    
585            } catch (NoResultException e) {
586                // there are no properties (probably not expected, but still okay) ...
587            } catch (Throwable e) { // Includes PathNotFoundException
588                request.setError(e);
589                return;
590            }
591            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
592            setCacheableInfo(request);
593        }
594    
595        /**
596         * {@inheritDoc}
597         * 
598         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadNextBlockOfChildrenRequest)
599         */
600        @Override
601        public void process( ReadNextBlockOfChildrenRequest request ) {
602            logger.trace(request.toString());
603            Location actualLocation = null;
604            final Location previousSibling = request.startingAfter();
605            final int count = request.count();
606            try {
607                // Find the workspace ...
608                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
609                if (workspace == null) return;
610                Long workspaceId = workspace.getId();
611                assert workspaceId != null;
612    
613                ActualLocation actualSibling = getActualLocation(workspaceId, previousSibling);
614                actualLocation = actualSibling.location;
615                if (!actualLocation.getPath().isRoot()) {
616                    // First look in the cache for the children of the parent ...
617                    Path parentPath = actualSibling.location.getPath().getParent();
618                    assert parentPath != null;
619                    LinkedList<Location> cachedChildren = cache.getAllChildren(workspaceId, parentPath);
620                    if (cachedChildren != null) {
621                        // The cache has all of the children for the node.
622                        // First find the location of the previous sibling ...
623                        boolean accumulate = false;
624                        int counter = 0;
625                        for (Location child : cachedChildren) {
626                            if (accumulate) {
627                                // We're accumulating children ...
628                                request.addChild(child);
629                                ++counter;
630                                if (counter <= count) continue;
631                                break;
632                            }
633                            // Haven't found the previous sibling yet ...
634                            if (child.isSame(previousSibling)) {
635                                accumulate = true;
636                            }
637                        }
638                    } else {
639                        // The children were not found in the cache, so we have to search the database.
640                        // We don't know the UUID of the parent, so find the previous sibling and
641                        // then get the starting index and the parent UUID ...
642                        ChildEntity previousChild = actualSibling.childEntity;
643                        if (previousChild == null) {
644                            Query query = entities.createNamedQuery("ChildEntity.findByChildUuid");
645                            query.setParameter("workspaceId", workspaceId);
646                            query.setParameter("childUuidString", actualSibling.uuid);
647                            previousChild = (ChildEntity)query.getSingleResult();
648                        }
649                        int startingIndex = previousChild.getIndexInParent() + 1;
650                        String parentUuid = previousChild.getId().getParentUuidString();
651    
652                        // Now search the database for the children ...
653                        Query query = entities.createNamedQuery("ChildEntity.findRangeUnderParent");
654                        query.setParameter("workspaceId", workspaceId);
655                        query.setParameter("parentUuidString", parentUuid);
656                        query.setParameter("firstIndex", startingIndex);
657                        query.setParameter("afterIndex", startingIndex + request.count());
658                        @SuppressWarnings( "unchecked" )
659                        List<ChildEntity> children = query.getResultList();
660                        LinkedList<Location> allChildren = null;
661                        if (startingIndex == 1 && children.size() < request.count()) {
662                            // The previous child was the first sibling, and we got fewer children than
663                            // the max count. This means we know all of the children, so accumulate the locations
664                            // so they can be cached ...
665                            allChildren = new LinkedList<Location>();
666                            allChildren.add(actualSibling.location);
667                        }
668                        for (ChildEntity child : children) {
669                            String namespaceUri = child.getChildNamespace().getUri();
670                            String localName = child.getChildName();
671                            Name childName = nameFactory.create(namespaceUri, localName);
672                            int sns = child.getSameNameSiblingIndex();
673                            Path childPath = pathFactory.create(parentPath, childName, sns);
674                            String childUuidString = child.getId().getChildUuidString();
675                            Location childLocation = Location.create(childPath, UUID.fromString(childUuidString));
676                            request.addChild(childLocation);
677                            if (allChildren != null) {
678                                // We're going to cache the results, so add this child ...
679                                allChildren.add(childLocation);
680                            }
681                        }
682    
683                        if (allChildren != null) {
684                            cache.setAllChildren(workspaceId, parentPath, allChildren);
685                        }
686                    }
687                }
688    
689            } catch (NoResultException e) {
690                // there are no properties (probably not expected, but still okay) ...
691            } catch (Throwable e) { // Includes PathNotFoundException
692                request.setError(e);
693                return;
694            }
695            if (actualLocation != null) request.setActualLocationOfStartingAfterNode(actualLocation);
696            setCacheableInfo(request);
697        }
698    
699        /**
700         * {@inheritDoc}
701         * 
702         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadAllPropertiesRequest)
703         */
704        @Override
705        public void process( ReadAllPropertiesRequest request ) {
706            logger.trace(request.toString());
707            Location actualLocation = null;
708            try {
709                // Find the workspace ...
710                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
711                if (workspace == null) return;
712                Long workspaceId = workspace.getId();
713                assert workspaceId != null;
714    
715                Location location = request.at();
716                ActualLocation actual = getActualLocation(workspaceId, location);
717                String uuidString = actual.uuid;
718                actualLocation = actual.location;
719    
720                // Record the UUID as a property, since it's not stored in the serialized properties...
721                request.addProperty(actualLocation.getIdProperty(DnaLexicon.UUID));
722    
723                // Find the properties entity for this node ...
724                Query query = entities.createNamedQuery("PropertiesEntity.findByUuid");
725                query.setParameter("workspaceId", workspaceId);
726                query.setParameter("uuid", uuidString);
727                PropertiesEntity entity = (PropertiesEntity)query.getSingleResult();
728    
729                // Deserialize the properties ...
730                boolean compressed = entity.isCompressed();
731                int propertyCount = entity.getPropertyCount();
732                Collection<Property> properties = new ArrayList<Property>(propertyCount);
733                byte[] data = entity.getData();
734                if (data != null) {
735                    LargeValueSerializer largeValues = new LargeValueSerializer(entity);
736                    ByteArrayInputStream bais = new ByteArrayInputStream(data);
737                    InputStream is = compressed ? new GZIPInputStream(bais) : bais;
738                    ObjectInputStream ois = new ObjectInputStream(is);
739                    try {
740                        serializer.deserializeAllProperties(ois, properties, largeValues);
741                        for (Property property : properties) {
742                            request.addProperty(property);
743                        }
744                    } finally {
745                        ois.close();
746                    }
747                }
748            } catch (NoResultException e) {
749                // there are no properties (probably not expected, but still okay) ...
750            } catch (Throwable e) { // Includes PathNotFoundException
751                request.setError(e);
752                return;
753            }
754            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
755            setCacheableInfo(request);
756        }
757    
758        /**
759         * {@inheritDoc}
760         * 
761         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadPropertyRequest)
762         */
763        @Override
764        public void process( ReadPropertyRequest request ) {
765            logger.trace(request.toString());
766            // Process the one property that's requested ...
767            Location actualLocation = null;
768            try {
769                // Find the workspace ...
770                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
771                if (workspace == null) return;
772                Long workspaceId = workspace.getId();
773                assert workspaceId != null;
774    
775                // Small optimization ...
776                final Name propertyName = request.named();
777                if (DnaLexicon.UUID.equals(propertyName)) {
778                    try {
779                        // Just get the UUID ...
780                        Location location = request.on();
781                        ActualLocation actual = getActualLocation(workspaceId, location); // verifies the UUID
782                        UUID uuid = actual.location.getUuid();
783                        Property uuidProperty = getExecutionContext().getPropertyFactory().create(propertyName, uuid);
784                        request.setProperty(uuidProperty);
785                        request.setActualLocationOfNode(actual.location);
786                        setCacheableInfo(request);
787                    } catch (Throwable e) { // Includes PathNotFoundException
788                        request.setError(e);
789                    }
790                    return;
791                }
792    
793                Location location = request.on();
794                ActualLocation actual = getActualLocation(workspaceId, location);
795                String uuidString = actual.uuid;
796                actualLocation = actual.location;
797    
798                // Find the properties entity for this node ...
799                Query query = entities.createNamedQuery("PropertiesEntity.findByUuid");
800                query.setParameter("workspaceId", workspaceId);
801                query.setParameter("uuid", uuidString);
802                PropertiesEntity entity = (PropertiesEntity)query.getSingleResult();
803    
804                // Deserialize the stream of properties, but only materialize the one property ...
805                boolean compressed = entity.isCompressed();
806                int propertyCount = entity.getPropertyCount();
807                Collection<Property> properties = new ArrayList<Property>(propertyCount);
808                byte[] data = entity.getData();
809                if (data != null) {
810                    LargeValueSerializer largeValues = new LargeValueSerializer(entity);
811                    ByteArrayInputStream bais = new ByteArrayInputStream(data);
812                    InputStream is = compressed ? new GZIPInputStream(bais) : bais;
813                    ObjectInputStream ois = new ObjectInputStream(is);
814                    try {
815                        Serializer.LargeValues skippedLargeValues = Serializer.NO_LARGE_VALUES;
816                        serializer.deserializeSomeProperties(ois, properties, largeValues, skippedLargeValues, propertyName);
817                        for (Property property : properties) {
818                            request.setProperty(property); // should be only one property
819                        }
820                    } finally {
821                        ois.close();
822                    }
823                }
824            } catch (NoResultException e) {
825                // there are no properties (probably not expected, but still okay) ...
826            } catch (Throwable e) { // Includes PathNotFoundException
827                request.setError(e);
828                return;
829            }
830            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
831            setCacheableInfo(request);
832        }
833    
834        /**
835         * {@inheritDoc}
836         * 
837         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.UpdatePropertiesRequest)
838         */
839        @Override
840        public void process( UpdatePropertiesRequest request ) {
841            logger.trace(request.toString());
842            Location actualLocation = null;
843            try {
844                // Find the workspace ...
845                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
846                if (workspace == null) return;
847                Long workspaceId = workspace.getId();
848                assert workspaceId != null;
849    
850                Location location = request.on();
851                ActualLocation actual = getActualLocation(workspaceId, location);
852                actualLocation = actual.location;
853    
854                // Find the properties entity for this node ...
855                Query query = entities.createNamedQuery("PropertiesEntity.findByUuid");
856                query.setParameter("workspaceId", workspaceId);
857                query.setParameter("uuid", actual.uuid);
858                PropertiesEntity entity = null;
859                try {
860                    entity = (PropertiesEntity)query.getSingleResult();
861    
862                    // Prepare the streams so we can deserialize all existing properties and reserialize the old and updated
863                    // properties ...
864                    boolean compressed = entity.isCompressed();
865                    byte[] originalData = entity.getData();
866                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
867                    OutputStream os = compressed ? new GZIPOutputStream(baos) : baos;
868                    ObjectOutputStream oos = new ObjectOutputStream(os);
869                    int numProps = 0;
870                    LargeValueSerializer largeValues = null;
871                    Map<Name, Property> props = request.properties();
872                    References refs = enforceReferentialIntegrity ? new References() : null;
873                    if (originalData == null) {
874                        largeValues = new LargeValueSerializer(entity);
875                        numProps = props.size();
876                        serializer.serializeProperties(oos, numProps, props.values(), largeValues, refs);
877                    } else {
878                        boolean hadLargeValues = !entity.getLargeValues().isEmpty();
879                        Set<String> largeValueHashesWritten = hadLargeValues ? new HashSet<String>() : null;
880                        largeValues = new LargeValueSerializer(entity, largeValueHashesWritten);
881                        ByteArrayInputStream bais = new ByteArrayInputStream(originalData);
882                        InputStream is = compressed ? new GZIPInputStream(bais) : bais;
883                        ObjectInputStream ois = new ObjectInputStream(is);
884                        SkippedLargeValues removedValues = new SkippedLargeValues(largeValues);
885                        try {
886                            Serializer.ReferenceValues refValues = refs != null ? refs : Serializer.NO_REFERENCES_VALUES;
887                            numProps = serializer.reserializeProperties(ois, oos, props, largeValues, removedValues, refValues);
888                        } finally {
889                            try {
890                                ois.close();
891                            } finally {
892                                oos.close();
893                            }
894                        }
895                        // The new large values were recorded and associated with the properties entity during reserialization.
896                        // However, any values no longer used now need to be removed ...
897                        if (hadLargeValues) {
898                            // Remove any large value from the 'skipped' list that was also written ...
899                            removedValues.skippedKeys.removeAll(largeValueHashesWritten);
900                            for (String oldHexKey : removedValues.skippedKeys) {
901                                LargeValueId id = new LargeValueId(oldHexKey);
902                                entity.getLargeValues().remove(id);
903                            }
904                        }
905    
906                        if (refs != null) {
907                            // Remove any existing references ...
908                            if (refs.hasRemoved()) {
909                                for (Reference reference : refs.getRemoved()) {
910                                    String toUuid = resolveToUuid(workspaceId, reference);
911                                    if (toUuid != null) {
912                                        ReferenceId id = new ReferenceId(workspaceId, actual.uuid, toUuid);
913                                        ReferenceEntity refEntity = entities.find(ReferenceEntity.class, id);
914                                        if (refEntity != null) {
915                                            entities.remove(refEntity);
916                                            workspaceIdsWithChangedReferences.add(workspaceId);
917                                        }
918                                    }
919                                }
920                            }
921                        }
922                    }
923                    entity.setPropertyCount(numProps);
924                    entity.setData(baos.toByteArray());
925                    entity.setCompressed(compressData);
926    
927                    if (refs != null && refs.hasWritten()) {
928                        // If there were references from the updated node ...
929                        Set<Reference> newReferences = refs.getWritten();
930                        // Remove any reference that was written (and not removed) ...
931                        newReferences.removeAll(refs.getRead());
932                        if (newReferences.size() != 0) {
933                            // Now save the new references ...
934                            for (Reference reference : newReferences) {
935                                String toUuid = resolveToUuid(workspaceId, reference);
936                                if (toUuid != null) {
937                                    ReferenceId id = new ReferenceId(workspaceId, actual.uuid, toUuid);
938                                    ReferenceEntity refEntity = new ReferenceEntity(id);
939                                    entities.persist(refEntity);
940                                    workspaceIdsWithChangedReferences.add(workspaceId);
941                                }
942                            }
943                        }
944                    }
945                } catch (NoResultException e) {
946                    // there are no properties yet ...
947                    createProperties(workspaceId, actual.uuid, request.properties().values());
948                }
949    
950            } catch (Throwable e) { // Includes PathNotFoundException
951                request.setError(e);
952                return;
953            }
954            if (actualLocation != null) request.setActualLocationOfNode(actualLocation);
955            recordChange(request);
956        }
957    
958        /**
959         * {@inheritDoc}
960         * 
961         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.ReadBranchRequest)
962         */
963        @Override
964        public void process( ReadBranchRequest request ) {
965            logger.trace(request.toString());
966            Location actualLocation = null;
967            try {
968                // Find the workspace ...
969                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
970                if (workspace == null) return;
971                Long workspaceId = workspace.getId();
972                assert workspaceId != null;
973    
974                Location location = request.at();
975                ActualLocation actual = getActualLocation(workspaceId, location);
976                actualLocation = actual.location;
977                Path path = actualLocation.getPath();
978    
979                // Record the location of each node by its UUID; we'll use this when processing the properties ...
980                Map<String, Location> locationsByUuid = new HashMap<String, Location>();
981                locationsByUuid.put(actual.uuid, location);
982    
983                // Compute the subgraph, including the root ...
984                int maxDepth = request.maximumDepth();
985                SubgraphQuery query = SubgraphQuery.create(getExecutionContext(),
986                                                           entities,
987                                                           workspaceId,
988                                                           actualLocation.getUuid(),
989                                                           path,
990                                                           maxDepth);
991    
992                try {
993                    // Record all of the children ...
994                    Path parent = path;
995                    String parentUuid = actual.uuid;
996                    Location parentLocation = actualLocation;
997                    List<Location> children = new LinkedList<Location>();
998                    Map<Location, List<Location>> childrenByParentLocation = new HashMap<Location, List<Location>>();
999                    childrenByParentLocation.put(parentLocation, children);
1000                    boolean includeChildrenOfNodesAtMaxDepth = true;
1001                    for (ChildEntity child : query.getNodes(false, includeChildrenOfNodesAtMaxDepth)) {
1002                        String namespaceUri = child.getChildNamespace().getUri();
1003                        String localName = child.getChildName();
1004                        Name childName = nameFactory.create(namespaceUri, localName);
1005                        int sns = child.getSameNameSiblingIndex();
1006                        // Figure out who the parent is ...
1007                        String childParentUuid = child.getId().getParentUuidString();
1008                        if (!parentUuid.equals(childParentUuid)) {
1009                            // Find the correct parent ...
1010                            parentLocation = locationsByUuid.get(childParentUuid);
1011                            parent = parentLocation.getPath();
1012                            parentUuid = childParentUuid;
1013                            // See if there is already a list of children for this parent ...
1014                            children = childrenByParentLocation.get(parentLocation);
1015                            if (children == null) {
1016                                children = new LinkedList<Location>();
1017                                childrenByParentLocation.put(parentLocation, children);
1018                            }
1019                        }
1020                        assert children != null;
1021                        Path childPath = pathFactory.create(parent, childName, sns);
1022                        String childUuidString = child.getId().getChildUuidString();
1023                        Location childLocation = Location.create(childPath, UUID.fromString(childUuidString));
1024                        locationsByUuid.put(childUuidString, childLocation);
1025                        children.add(childLocation);
1026                    }
1027                    // Now add the list of children to the results ...
1028                    for (Map.Entry<Location, List<Location>> entry : childrenByParentLocation.entrySet()) {
1029                        // Don't add if there are no children ...
1030                        if (!entry.getValue().isEmpty()) {
1031                            request.setChildren(entry.getKey(), entry.getValue());
1032                        }
1033                    }
1034    
1035                    // Note that we've found children for nodes that are at the maximum depth. This is so that the nodes
1036                    // in the subgraph all have the correct children. However, we don't want to store the properties for
1037                    // any node whose depth is greater than the maximum depth. Therefore, only get the properties that
1038                    // include nodes within the maximum depth...
1039                    includeChildrenOfNodesAtMaxDepth = false;
1040    
1041                    // Now record all of the properties ...
1042                    for (PropertiesEntity props : query.getProperties(true, includeChildrenOfNodesAtMaxDepth)) {
1043                        boolean compressed = props.isCompressed();
1044                        int propertyCount = props.getPropertyCount();
1045                        Collection<Property> properties = new ArrayList<Property>(propertyCount);
1046                        Location nodeLocation = locationsByUuid.get(props.getId().getUuidString());
1047                        assert nodeLocation != null;
1048                        // Record the UUID as a property, since it's not stored in the serialized properties...
1049                        properties.add(actualLocation.getIdProperty(DnaLexicon.UUID));
1050                        // Deserialize all the properties (except the UUID)...
1051                        byte[] data = props.getData();
1052                        if (data != null) {
1053                            LargeValueSerializer largeValues = new LargeValueSerializer(props);
1054                            ByteArrayInputStream bais = new ByteArrayInputStream(data);
1055                            InputStream is = compressed ? new GZIPInputStream(bais) : bais;
1056                            ObjectInputStream ois = new ObjectInputStream(is);
1057                            try {
1058                                serializer.deserializeAllProperties(ois, properties, largeValues);
1059                                request.setProperties(nodeLocation, properties);
1060                            } finally {
1061                                ois.close();
1062                            }
1063                        }
1064                    }
1065                } finally {
1066                    // Close and release the temporary data used for this operation ...
1067                    query.close();
1068                }
1069    
1070            } catch (Throwable e) { // Includes PathNotFoundException
1071                request.setError(e);
1072                return;
1073            }
1074            request.setActualLocationOfNode(actualLocation);
1075            setCacheableInfo(request);
1076        }
1077    
1078        /**
1079         * {@inheritDoc}
1080         * 
1081         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CopyBranchRequest)
1082         */
1083        @Override
1084        public void process( CopyBranchRequest request ) {
1085            logger.trace(request.toString());
1086            Location actualFromLocation = null;
1087            Location actualToLocation = null;
1088            try {
1089                // Find the workspaces ...
1090                WorkspaceEntity fromWorkspace = getExistingWorkspace(request.fromWorkspace(), request);
1091                if (fromWorkspace == null) return;
1092                WorkspaceEntity intoWorkspace = getExistingWorkspace(request.intoWorkspace(), request);
1093                if (intoWorkspace == null) return;
1094                Long fromWorkspaceId = fromWorkspace.getId();
1095                Long intoWorkspaceId = intoWorkspace.getId();
1096                assert fromWorkspaceId != null;
1097                assert intoWorkspaceId != null;
1098                final boolean isSameWorkspace = fromWorkspaceId == intoWorkspaceId;
1099    
1100                Location fromLocation = request.from();
1101                ActualLocation actualFrom = getActualLocation(fromWorkspaceId, fromLocation);
1102                actualFromLocation = actualFrom.location;
1103                Path fromPath = actualFromLocation.getPath();
1104    
1105                Location newParentLocation = request.into();
1106                ActualLocation actualNewParent = getActualLocation(intoWorkspaceId, newParentLocation);
1107                assert actualNewParent != null;
1108    
1109                // Create a map that we'll use to record the new UUID for each of the original nodes ...
1110                Map<String, String> originalToNewUuid = new HashMap<String, String>();
1111    
1112                // Compute the subgraph, including the top node in the subgraph ...
1113                SubgraphQuery query = SubgraphQuery.create(getExecutionContext(),
1114                                                           entities,
1115                                                           fromWorkspaceId,
1116                                                           actualFromLocation.getUuid(),
1117                                                           fromPath,
1118                                                           0);
1119                try {
1120                    // Walk through the original nodes, creating new ChildEntity object (i.e., copy) for each original ...
1121                    List<ChildEntity> originalNodes = query.getNodes(true, true);
1122                    Iterator<ChildEntity> originalIter = originalNodes.iterator();
1123    
1124                    // Start with the original (top-level) node first, since we need to add it to the list of children ...
1125                    if (originalIter.hasNext()) {
1126                        ChildEntity original = originalIter.next();
1127    
1128                        // Create a new UUID for the copy ...
1129                        String copyUuid = original.getId().getChildUuidString();
1130                        if (isSameWorkspace) {
1131                            copyUuid = UUID.randomUUID().toString();
1132                            originalToNewUuid.put(original.getId().getChildUuidString(), copyUuid);
1133                        }
1134    
1135                        // Now add the new copy of the original ...
1136                        Name childName = request.desiredName();
1137                        if (childName == null) childName = fromPath.getLastSegment().getName();
1138                        boolean allowSnS = original.getAllowsSameNameChildren();
1139                        actualToLocation = addNewChild(intoWorkspaceId, actualNewParent, copyUuid, childName, allowSnS);
1140                    }
1141    
1142                    // Now create copies of all children in the subgraph.
1143                    // We assign new UUIDs to each new child only if the 'from' and 'into' workspaces are the same ...
1144                    while (originalIter.hasNext()) {
1145                        ChildEntity original = originalIter.next();
1146                        String newParentUuidOfCopy = original.getId().getParentUuidString();
1147                        if (isSameWorkspace) newParentUuidOfCopy = originalToNewUuid.get(newParentUuidOfCopy);
1148                        assert newParentUuidOfCopy != null;
1149    
1150                        // Create a new UUID for the copy ...
1151                        String copyUuid = original.getId().getChildUuidString();
1152                        if (isSameWorkspace) {
1153                            copyUuid = UUID.randomUUID().toString();
1154                            originalToNewUuid.put(original.getId().getChildUuidString(), copyUuid);
1155                        }
1156    
1157                        // Create the copy ...
1158                        ChildEntity copy = new ChildEntity(new ChildId(intoWorkspaceId, newParentUuidOfCopy, copyUuid),
1159                                                           original.getIndexInParent(), original.getChildNamespace(),
1160                                                           original.getChildName(), original.getSameNameSiblingIndex());
1161                        entities.persist(copy);
1162                    }
1163                    entities.flush();
1164    
1165                    Set<String> newNodesWithReferenceProperties = new HashSet<String>();
1166                    if (isSameWorkspace) {
1167                        // Now create copies of all the intra-subgraph references, replacing the UUIDs on both ends ...
1168                        for (ReferenceEntity reference : query.getInternalReferences()) {
1169                            String newFromUuid = originalToNewUuid.get(reference.getId().getFromUuidString());
1170                            assert newFromUuid != null;
1171                            String newToUuid = originalToNewUuid.get(reference.getId().getToUuidString());
1172                            assert newToUuid != null;
1173                            ReferenceEntity copy = new ReferenceEntity(new ReferenceId(intoWorkspaceId, newFromUuid, newToUuid));
1174                            entities.persist(copy);
1175                            newNodesWithReferenceProperties.add(newFromUuid);
1176                        }
1177    
1178                        // Now create copies of all the references owned by the subgraph but pointing to non-subgraph nodes,
1179                        // so we only replaced the 'from' UUID ...
1180                        for (ReferenceEntity reference : query.getOutwardReferences()) {
1181                            String oldToUuid = reference.getId().getToUuidString();
1182                            String newFromUuid = originalToNewUuid.get(reference.getId().getFromUuidString());
1183                            assert newFromUuid != null;
1184                            ReferenceEntity copy = new ReferenceEntity(new ReferenceId(intoWorkspaceId, newFromUuid, oldToUuid));
1185                            entities.persist(copy);
1186                            newNodesWithReferenceProperties.add(newFromUuid);
1187                        }
1188                    }
1189    
1190                    // Now process the properties, creating a copy (note references are not changed) ...
1191                    for (PropertiesEntity original : query.getProperties(true, true)) {
1192                        // Find the UUID of the copy ...
1193                        String copyUuid = original.getId().getUuidString();
1194                        if (isSameWorkspace) copyUuid = originalToNewUuid.get(copyUuid);
1195                        assert copyUuid != null;
1196    
1197                        // Create the copy ...
1198                        boolean compressed = original.isCompressed();
1199                        byte[] originalData = original.getData();
1200                        PropertiesEntity copy = new PropertiesEntity(new NodeId(intoWorkspaceId, copyUuid));
1201                        copy.setCompressed(compressed);
1202                        if (newNodesWithReferenceProperties.contains(copyUuid)) {
1203    
1204                            // This node has internal or outward references that must be adjusted ...
1205                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
1206                            OutputStream os = compressed ? new GZIPOutputStream(baos) : baos;
1207                            ObjectOutputStream oos = new ObjectOutputStream(os);
1208                            ByteArrayInputStream bais = new ByteArrayInputStream(originalData);
1209                            InputStream is = compressed ? new GZIPInputStream(bais) : bais;
1210                            ObjectInputStream ois = new ObjectInputStream(is);
1211                            try {
1212                                serializer.adjustReferenceProperties(ois, oos, originalToNewUuid);
1213                            } finally {
1214                                try {
1215                                    ois.close();
1216                                } finally {
1217                                    oos.close();
1218                                }
1219                            }
1220                            copy.setData(baos.toByteArray());
1221                        } else {
1222                            // No references to adjust, so just copy the original data ...
1223                            copy.setData(originalData);
1224                        }
1225                        copy.setPropertyCount(original.getPropertyCount());
1226                        copy.setReferentialIntegrityEnforced(original.isReferentialIntegrityEnforced());
1227                        entities.persist(copy);
1228                    }
1229                    entities.flush();
1230    
1231                } finally {
1232                    // Close and release the temporary data used for this operation ...
1233                    query.close();
1234                }
1235    
1236            } catch (Throwable e) { // Includes PathNotFoundException
1237                request.setError(e);
1238                return;
1239            }
1240            request.setActualLocations(actualFromLocation, actualToLocation);
1241            recordChange(request);
1242        }
1243    
1244        /**
1245         * {@inheritDoc}
1246         * 
1247         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteBranchRequest)
1248         */
1249        @Override
1250        public void process( DeleteBranchRequest request ) {
1251            logger.trace(request.toString());
1252            Location location = delete(request, request.at(), request.inWorkspace(), true);
1253            if (location != null) {
1254                request.setActualLocationOfNode(location);
1255                recordChange(request);
1256            }
1257        }
1258    
1259        /**
1260         * {@inheritDoc}
1261         * 
1262         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DeleteChildrenRequest)
1263         */
1264        @Override
1265        public void process( DeleteChildrenRequest request ) {
1266            logger.trace(request.toString());
1267            Location location = delete(request, request.at(), request.inWorkspace(), false);
1268            if (location != null) {
1269                request.setActualLocationOfNode(location);
1270                recordChange(request);
1271            }
1272        }
1273    
1274        protected Location delete( Request request,
1275                                   Location location,
1276                                   String workspaceName,
1277                                   boolean deleteTopOfBranch ) {
1278            Location actualLocation = null;
1279            try {
1280                // Find the workspace ...
1281                WorkspaceEntity workspace = getExistingWorkspace(workspaceName, request);
1282                if (workspace == null) return null;
1283                Long workspaceId = workspace.getId();
1284                assert workspaceId != null;
1285    
1286                ActualLocation actual = getActualLocation(workspaceId, location);
1287                actualLocation = actual.location;
1288                Path path = actualLocation.getPath();
1289    
1290                // Compute the subgraph, including the top node in the subgraph ...
1291                SubgraphQuery query = SubgraphQuery.create(getExecutionContext(),
1292                                                           entities,
1293                                                           workspaceId,
1294                                                           actualLocation.getUuid(),
1295                                                           path,
1296                                                           0);
1297                try {
1298                    ChildEntity deleted = query.getNode();
1299                    String parentUuidString = deleted.getId().getParentUuidString();
1300                    String childName = deleted.getChildName();
1301                    long nsId = deleted.getChildNamespace().getId();
1302                    int indexInParent = deleted.getIndexInParent();
1303    
1304                    // Get the locations of all deleted nodes, which will be required by events ...
1305                    List<Location> deletedLocations = query.getNodeLocations(true, true);
1306    
1307                    // Now delete the subgraph ...
1308                    query.deleteSubgraph(deleteTopOfBranch);
1309    
1310                    // Verify referential integrity: that none of the deleted nodes are referenced by nodes not being deleted.
1311                    List<ReferenceEntity> invalidReferences = query.getInwardReferences();
1312                    if (invalidReferences.size() > 0) {
1313                        // Some of the references that remain will be invalid, since they point to nodes that
1314                        // have just been deleted. Build up the information necessary to produce a useful exception ...
1315                        ValueFactory<Reference> refFactory = getExecutionContext().getValueFactories().getReferenceFactory();
1316                        Map<Location, List<Reference>> invalidRefs = new HashMap<Location, List<Reference>>();
1317                        for (ReferenceEntity entity : invalidReferences) {
1318                            UUID fromUuid = UUID.fromString(entity.getId().getFromUuidString());
1319                            ActualLocation actualFromLocation = getActualLocation(workspaceId, Location.create(fromUuid));
1320                            Location fromLocation = actualFromLocation.location;
1321                            List<Reference> refs = invalidRefs.get(fromLocation);
1322                            if (refs == null) {
1323                                refs = new ArrayList<Reference>();
1324                                invalidRefs.put(fromLocation, refs);
1325                            }
1326                            UUID toUuid = UUID.fromString(entity.getId().getToUuidString());
1327                            refs.add(refFactory.create(toUuid));
1328                        }
1329                        String msg = JpaConnectorI18n.unableToDeleteBecauseOfReferences.text();
1330                        throw new ReferentialIntegrityException(invalidRefs, msg);
1331                    }
1332    
1333                    if (deleteTopOfBranch) {
1334                        // And adjust the SNS index and indexes ...
1335                        ChildEntity.adjustSnsIndexesAndIndexesAfterRemoving(entities,
1336                                                                            workspaceId,
1337                                                                            parentUuidString,
1338                                                                            childName,
1339                                                                            nsId,
1340                                                                            indexInParent);
1341                        entities.flush();
1342                    }
1343    
1344                    // Remove from the cache of children locations all entries for deleted nodes ...
1345                    cache.removeBranch(workspaceId, deletedLocations);
1346                } finally {
1347                    // Close and release the temporary data used for this operation ...
1348                    query.close();
1349                }
1350    
1351            } catch (Throwable e) { // Includes PathNotFoundException
1352                request.setError(e);
1353                return null;
1354            }
1355            return actualLocation;
1356        }
1357    
1358        /**
1359         * {@inheritDoc}
1360         * 
1361         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest)
1362         */
1363        @SuppressWarnings( "unchecked" )
1364        @Override
1365        public void process( MoveBranchRequest request ) {
1366            logger.trace(request.toString());
1367            Location actualOldLocation = null;
1368            Location actualNewLocation = null;
1369            try {
1370                // Find the workspaces ...
1371                WorkspaceEntity workspace = getExistingWorkspace(request.inWorkspace(), request);
1372                if (workspace == null) return;
1373                Long workspaceId = workspace.getId();
1374                assert workspaceId != null;
1375    
1376                Location fromLocation = request.from();
1377                ActualLocation actualLocation = getActualLocation(workspaceId, fromLocation);
1378                String fromUuidString = actualLocation.uuid;
1379                actualOldLocation = actualLocation.location;
1380                Path oldPath = actualOldLocation.getPath();
1381    
1382                // It's not possible to move the root node
1383                if (oldPath.isRoot()) {
1384                    String msg = JpaConnectorI18n.unableToMoveRootNode.text(getSourceName());
1385                    throw new InvalidRequestException(msg);
1386                }
1387    
1388                // Find the ChildEntity of the existing 'from' node ...
1389                ChildEntity fromEntity = actualLocation.childEntity;
1390                final String oldParentUuid = fromEntity.getId().getParentUuidString();
1391    
1392                // Find the actual new location ...
1393                Location toLocation = request.into();
1394                Location beforeLocation = request.before();
1395    
1396                if (beforeLocation != null) {
1397                    if (beforeLocation.hasPath()) {
1398                        toLocation = Location.create(beforeLocation.getPath().getParent());
1399                    } else {
1400                        ActualLocation actualBeforeLocation = getActualLocation(workspaceId, beforeLocation);
1401    
1402                        // Ensure that the beforeLocation has a path - actualBeforeLocation has a path
1403                        beforeLocation = actualBeforeLocation.location;
1404                        toLocation = Location.create(actualBeforeLocation.location.getPath().getParent());
1405                    }
1406                }
1407    
1408                String toUuidString = null;
1409                if (request.hasNoEffect()) {
1410                    actualNewLocation = actualOldLocation;
1411                } else {
1412                    // We have to proceed as normal ...
1413                    ActualLocation actualIntoLocation = getActualLocation(workspaceId, toLocation);
1414                    toUuidString = actualIntoLocation.uuid;
1415                    if (!toUuidString.equals(oldParentUuid)) {
1416                        // Now we know that the new parent is not the existing parent ...
1417                        final int oldIndex = fromEntity.getIndexInParent();
1418    
1419                        // Make sure the child name is set correctly ...
1420                        String childOldLocalName = fromEntity.getChildName();
1421                        String childLocalName = null;
1422                        NamespaceEntity ns = null;
1423                        Name childName = request.desiredName();
1424                        if (childName != null) {
1425                            childLocalName = request.desiredName().getLocalName();
1426                            String childNsUri = childName.getNamespaceUri();
1427                            ns = namespaces.get(childNsUri, true);
1428                        } else {
1429                            childName = oldPath.getLastSegment().getName();
1430                            childLocalName = fromEntity.getChildName();
1431                            ns = fromEntity.getChildNamespace();
1432                        }
1433    
1434                        int nextSnsIndex = 1;
1435                        int nextIndexInParent = 1;
1436                        if (beforeLocation == null) {
1437                            // Find the largest SNS index in the existing ChildEntity objects with the same name ...
1438                            Query query = entities.createNamedQuery("ChildEntity.findMaximumSnsIndex");
1439                            query.setParameter("workspaceId", workspaceId);
1440                            query.setParameter("parentUuid", toUuidString);
1441                            query.setParameter("ns", ns.getId());
1442                            query.setParameter("childName", childLocalName);
1443                            try {
1444                                Integer index = (Integer)query.getSingleResult();
1445                                if (index != null) nextSnsIndex = index.intValue() + 1;
1446                            } catch (NoResultException e) {
1447                            }
1448    
1449                            // Find the largest child index in the existing ChildEntity objects ...
1450                            query = entities.createNamedQuery("ChildEntity.findMaximumChildIndex");
1451                            query.setParameter("workspaceId", workspaceId);
1452                            query.setParameter("parentUuid", toUuidString);
1453                            try {
1454                                Integer index = (Integer)query.getSingleResult();
1455                                if (index != null) nextIndexInParent = index + 1;
1456                            } catch (NoResultException e) {
1457                            }
1458                        } else {
1459                            /*
1460                             * This is a sub-optimal approach, particularly for inserts to the front
1461                             * of a long list of child nodes, but it guarantees that we won't have
1462                             * the JPA-cached entities and the database out of sync.
1463                             */
1464    
1465                            Query query = entities.createNamedQuery("ChildEntity.findAllUnderParent");
1466                            query.setParameter("workspaceId", workspaceId);
1467                            query.setParameter("parentUuidString", toUuidString);
1468                            try {
1469                                List<ChildEntity> children = query.getResultList();
1470                                Path beforePath = beforeLocation.getPath();
1471                                Path.Segment beforeSegment = beforePath.getLastSegment();
1472    
1473                                boolean foundBefore = false;
1474                                for (ChildEntity child : children) {
1475                                    NamespaceEntity namespace = child.getChildNamespace();
1476                                    if (namespace.getUri().equals(ns.getUri())
1477                                        && child.getChildName().equals(childLocalName)
1478                                        && child.getSameNameSiblingIndex() == beforeSegment.getIndex()) {
1479                                        foundBefore = true;
1480                                        nextIndexInParent = child.getIndexInParent();
1481                                        nextSnsIndex = beforeSegment.getIndex();                                    
1482                                    }
1483    
1484                                    if (foundBefore) {
1485                                        child.setIndexInParent(child.getIndexInParent() + 1);
1486                                        if (child.getChildName().equals(childLocalName)
1487                                            && namespace.getUri().equals(ns.getUri())) {
1488                                            child.setSameNameSiblingIndex(child.getSameNameSiblingIndex() + 1);
1489                                        }
1490                                        entities.persist(child);
1491                                    }
1492                                }
1493    
1494                            } catch (NoResultException e) {
1495                            }
1496    
1497                        }
1498    
1499                        ChildId movedId = new ChildId(workspaceId, toUuidString, fromUuidString);
1500                        if (fromEntity.getId().equals(movedId)) {
1501                            // The node is being renamed, but not moved ...
1502                            fromEntity.setChildName(childLocalName);
1503                            fromEntity.setChildNamespace(ns);
1504                            fromEntity.setIndexInParent(nextIndexInParent);
1505                            fromEntity.setSameNameSiblingIndex(nextSnsIndex);
1506                        } else {
1507                            // We won't be able to move the entity to a different parent, because that would involve
1508                            // changing the PK for the entity, which is not possible. Instead, we have to create a
1509                            // new entity with the same identity information, then delete 'fromEntity'
1510                            ChildEntity movedEntity = new ChildEntity(movedId, nextIndexInParent, ns, childLocalName, nextSnsIndex);
1511                            movedEntity.setAllowsSameNameChildren(fromEntity.getAllowsSameNameChildren());
1512                            entities.persist(movedEntity);
1513                            entities.remove(fromEntity);
1514                        }
1515    
1516                        // Flush the entities to the database ...
1517                        entities.flush();
1518    
1519                        // Determine the new location ...
1520                        Path newParentPath = actualIntoLocation.location.getPath();
1521                        Path newPath = pathFactory.create(newParentPath, childName, nextSnsIndex);
1522                        actualNewLocation = actualOldLocation.with(newPath);
1523    
1524                        // And adjust the SNS index and indexes ...
1525                        ChildEntity.adjustSnsIndexesAndIndexesAfterRemoving(entities,
1526                                                                            workspaceId,
1527                                                                            oldParentUuid,
1528                                                                            childOldLocalName,
1529                                                                            ns.getId(),
1530                                                                            oldIndex);
1531    
1532                        // Update the cache ...
1533                        cache.moveNode(workspaceId, actualOldLocation, oldIndex, actualNewLocation);
1534                    }
1535    
1536                }
1537    
1538            } catch (Throwable e) { // Includes PathNotFoundException
1539                request.setError(e);
1540                return;
1541            }
1542            request.setActualLocations(actualOldLocation, actualNewLocation);
1543            recordChange(request);
1544        }
1545    
1546        /**
1547         * {@inheritDoc}
1548         * 
1549         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.VerifyWorkspaceRequest)
1550         */
1551        @Override
1552        public void process( VerifyWorkspaceRequest request ) {
1553            // Find the workspace ...
1554            String workspaceName = request.workspaceName();
1555            if (workspaceName == null) workspaceName = nameOfDefaultWorkspace;
1556            WorkspaceEntity workspace = getExistingWorkspace(workspaceName, request);
1557            if (workspace != null) {
1558                Long workspaceId = workspace.getId();
1559                assert workspaceId != null;
1560                ActualLocation actual = getActualLocation(workspaceId, Location.create(pathFactory.createRootPath()));
1561                request.setActualRootLocation(actual.location);
1562                request.setActualWorkspaceName(workspace.getName());
1563            }
1564        }
1565    
1566        /**
1567         * {@inheritDoc}
1568         * 
1569         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.GetWorkspacesRequest)
1570         */
1571        @Override
1572        public void process( GetWorkspacesRequest request ) {
1573            // Return the set of available workspace names, even if new workspaces can be created ...
1574            Set<String> names = workspaces.getWorkspaceNames();
1575            // Add in the names of the predefined workspaces (in case they weren't yet accessed) ...
1576            for (String name : this.predefinedWorkspaceNames) {
1577                names.add(name);
1578            }
1579            request.setAvailableWorkspaceNames(Collections.unmodifiableSet(names));
1580            setCacheableInfo(request);
1581        }
1582    
1583        /**
1584         * {@inheritDoc}
1585         * 
1586         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CreateWorkspaceRequest)
1587         */
1588        @Override
1589        public void process( CreateWorkspaceRequest request ) {
1590            String name = request.desiredNameOfNewWorkspace();
1591            if (!creatingWorkspacesAllowed) {
1592                String msg = JpaConnectorI18n.unableToCreateWorkspaces.text(getSourceName());
1593                request.setError(new InvalidRequestException(msg));
1594                return;
1595            }
1596            Set<String> existingNames = workspaces.getWorkspaceNames();
1597            int counter = 0;
1598            while (existingNames.contains(name)) {
1599                switch (request.conflictBehavior()) {
1600                    case CREATE_WITH_ADJUSTED_NAME:
1601                        name = request.desiredNameOfNewWorkspace() + ++counter;
1602                        break;
1603                    case DO_NOT_CREATE:
1604                    default:
1605                        String msg = JpaConnectorI18n.workspaceAlreadyExists.text(getSourceName(), name);
1606                        request.setError(new InvalidWorkspaceException(msg));
1607                        return;
1608                }
1609            }
1610            // Create the workspace ...
1611            WorkspaceEntity entity = workspaces.create(name);
1612            request.setActualWorkspaceName(entity.getName());
1613            // Create the root node ...
1614            Location root = Location.create(pathFactory.createRootPath());
1615            request.setActualRootLocation(getActualLocation(entity.getId(), root).location);
1616            recordChange(request);
1617        }
1618    
1619        /**
1620         * {@inheritDoc}
1621         * 
1622         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.CloneWorkspaceRequest)
1623         */
1624        @SuppressWarnings( "unchecked" )
1625        @Override
1626        public void process( CloneWorkspaceRequest request ) {
1627            if (!creatingWorkspacesAllowed) {
1628                String msg = JpaConnectorI18n.unableToCreateWorkspaces.text(getSourceName());
1629                request.setError(new InvalidRequestException(msg));
1630                return;
1631            }
1632            Set<String> existingNames = workspaces.getWorkspaceNames();
1633            String name = request.desiredNameOfTargetWorkspace();
1634            int counter = 0;
1635            while (existingNames.contains(name)) {
1636                switch (request.targetConflictBehavior()) {
1637                    case CREATE_WITH_ADJUSTED_NAME:
1638                        name = request.desiredNameOfTargetWorkspace() + ++counter;
1639                        break;
1640                    case DO_NOT_CREATE:
1641                    default:
1642                        String msg = JpaConnectorI18n.workspaceAlreadyExists.text(getSourceName(), name);
1643                        request.setError(new InvalidWorkspaceException(msg));
1644                        return;
1645                }
1646            }
1647            String fromWorkspaceName = request.nameOfWorkspaceToBeCloned();
1648            WorkspaceEntity fromWorkspace = workspaces.get(fromWorkspaceName, false);
1649            if (fromWorkspace == null) {
1650                switch (request.cloneConflictBehavior()) {
1651                    case SKIP_CLONE:
1652                        break;
1653                    case DO_NOT_CLONE:
1654                    default:
1655                        String msg = JpaConnectorI18n.workspaceDoesNotExist.text(getSourceName(), fromWorkspaceName);
1656                        request.setError(new InvalidRequestException(msg));
1657                        return;
1658                }
1659            }
1660    
1661            // Create the workspace ...
1662            WorkspaceEntity intoWorkspace = workspaces.create(name);
1663            String newWorkspaceName = intoWorkspace.getName();
1664            request.setActualWorkspaceName(newWorkspaceName);
1665    
1666            if (fromWorkspace != null) {
1667                // Copy the workspace into the new workspace, via bulk insert statements ..
1668                Long fromWorkspaceId = fromWorkspace.getId();
1669                Long intoWorkspaceId = intoWorkspace.getId();
1670                Query query = entities.createNamedQuery("ChildEntity.findInWorkspace");
1671                query.setParameter("workspaceId", fromWorkspaceId);
1672                List<ChildEntity> childEntities = query.getResultList();
1673                for (ChildEntity child : childEntities) {
1674                    ChildId origId = child.getId();
1675                    ChildId copyId = new ChildId(intoWorkspaceId, origId.getParentUuidString(), origId.getChildUuidString());
1676                    ChildEntity copy = new ChildEntity(copyId, child.getIndexInParent(), child.getChildNamespace(),
1677                                                       child.getChildName());
1678                    copy.setAllowsSameNameChildren(child.getAllowsSameNameChildren());
1679                    copy.setSameNameSiblingIndex(child.getSameNameSiblingIndex());
1680                    entities.persist(copy);
1681                }
1682                entities.flush();
1683    
1684                query = entities.createNamedQuery("PropertiesEntity.findInWorkspace");
1685                query.setParameter("workspaceId", fromWorkspaceId);
1686                List<PropertiesEntity> properties = query.getResultList();
1687                for (PropertiesEntity property : properties) {
1688                    NodeId copyId = new NodeId(intoWorkspaceId, property.getId().getUuidString());
1689                    PropertiesEntity copy = new PropertiesEntity(copyId);
1690                    copy.setCompressed(property.isCompressed());
1691                    copy.setData(property.getData());
1692                    copy.setPropertyCount(property.getPropertyCount());
1693                    copy.setReferentialIntegrityEnforced(property.isReferentialIntegrityEnforced());
1694                    Collection<LargeValueId> ids = property.getLargeValues();
1695                    if (ids.size() != 0) {
1696                        copy.getLargeValues().addAll(ids);
1697                    }
1698                    entities.persist(copy);
1699                }
1700                entities.flush();
1701    
1702                query = entities.createNamedQuery("ReferenceEntity.findInWorkspace");
1703                query.setParameter("workspaceId", fromWorkspaceId);
1704                List<ReferenceEntity> references = query.getResultList();
1705                for (ReferenceEntity reference : references) {
1706                    ReferenceId from = reference.getId();
1707                    ReferenceId copy = new ReferenceId(fromWorkspaceId, from.getFromUuidString(), from.getToUuidString());
1708                    entities.persist(new ReferenceEntity(copy));
1709                }
1710                entities.flush();
1711            }
1712    
1713            // Finish up the request ...
1714            Location root = Location.create(pathFactory.createRootPath(), rootNodeUuid);
1715            request.setActualRootLocation(getActualLocation(intoWorkspace.getId(), root).location);
1716            recordChange(request);
1717        }
1718    
1719        /**
1720         * {@inheritDoc}
1721         * 
1722         * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.DestroyWorkspaceRequest)
1723         */
1724        @Override
1725        public void process( DestroyWorkspaceRequest request ) {
1726            // Verify the workspace exists ...
1727            WorkspaceEntity workspace = getExistingWorkspace(request.workspaceName(), request);
1728            if (workspace == null) return;
1729            Long workspaceId = workspace.getId();
1730            assert workspaceId != null;
1731    
1732            // Get the actual location of the root node ...
1733            ActualLocation actual = getActualLocation(workspaceId, Location.create(pathFactory.createRootPath()));
1734    
1735            // Delete the workspace ...
1736            workspaces.destroy(workspace.getName());
1737    
1738            // Delete all the entities from this workspace ...
1739            Query delete = entities.createQuery("delete PropertiesEntity entity where entity.id.workspaceId = :workspaceId");
1740            delete.setParameter("workspaceId", workspaceId);
1741            delete.executeUpdate();
1742    
1743            delete = entities.createQuery("delete ChildEntity entity where entity.id.workspaceId = :workspaceId");
1744            delete.setParameter("workspaceId", workspaceId);
1745            delete.executeUpdate();
1746    
1747            delete = entities.createQuery("delete ReferenceEntity entity where entity.id.workspaceId = :workspaceId");
1748            delete.setParameter("workspaceId", workspaceId);
1749            delete.executeUpdate();
1750    
1751            // Delete unused large values ...
1752            LargeValueEntity.deleteUnused(entities);
1753    
1754            // Finish the request ...
1755            request.setActualRootLocation(actual.location);
1756            recordChange(request);
1757        }
1758    
1759        /**
1760         * {@inheritDoc}
1761         * 
1762         * @see org.jboss.dna.graph.request.processor.RequestProcessor#close()
1763         */
1764        @Override
1765        public void close() {
1766            // Verify that the references are valid so far ...
1767            verifyReferences();
1768    
1769            // Now commit the transaction ...
1770            EntityTransaction txn = entities.getTransaction();
1771            if (txn != null) txn.commit();
1772            super.close();
1773        }
1774    
1775        protected WorkspaceEntity getExistingWorkspace( String workspaceName,
1776                                                        Request request ) {
1777            WorkspaceEntity workspace = workspaces.get(workspaceName, false);
1778            if (workspace == null) {
1779                // Is this a predefined workspace?
1780                for (String name : predefinedWorkspaceNames) {
1781                    if (workspaceName.equals(name)) {
1782                        // Create it anyway ...
1783                        return workspaces.create(workspaceName);
1784                    }
1785                }
1786                String msg = JpaConnectorI18n.workspaceDoesNotExist.text(getSourceName(), workspaceName);
1787                request.setError(new InvalidWorkspaceException(msg));
1788            }
1789            return workspace;
1790        }
1791    
1792        /**
1793         * {@link ReferenceEntity Reference entities} are added and removed in the appropriate <code>process(...)</code> methods.
1794         * However, this method is typically called in {@link BasicRequestProcessor#close()} and performs the following steps:
1795         * <ol>
1796         * <li>Remove all references that have a "from" node that is under the versions branch.</li>
1797         * <li>Verify that all remaining references have a valid and existing "to" node</li>
1798         * </ol>
1799         */
1800        protected void verifyReferences() {
1801            if (!enforceReferentialIntegrity) return;
1802            if (!workspaceIdsWithChangedReferences.isEmpty()) {
1803    
1804                Map<Location, List<Reference>> invalidRefs = new HashMap<Location, List<Reference>>();
1805                for (Long workspaceId : workspaceIdsWithChangedReferences) {
1806    
1807                    // Remove all references that have a "from" node that doesn't support referential integrity ...
1808                    ReferenceEntity.deleteUnenforcedReferences(workspaceId, entities);
1809    
1810                    // Verify that all references are resolved to existing nodes ...
1811                    int numUnresolved = ReferenceEntity.countAllReferencesResolved(workspaceId, entities);
1812                    if (numUnresolved != 0) {
1813                        List<ReferenceEntity> references = ReferenceEntity.verifyAllReferencesResolved(workspaceId, entities);
1814                        ValueFactory<Reference> refFactory = getExecutionContext().getValueFactories().getReferenceFactory();
1815                        for (ReferenceEntity entity : references) {
1816                            ReferenceId id = entity.getId();
1817                            UUID fromUuid = UUID.fromString(id.getFromUuidString());
1818                            Location location = Location.create(fromUuid);
1819                            location = getActualLocation(id.getWorkspaceId(), location).location;
1820                            List<Reference> refs = invalidRefs.get(location);
1821                            if (refs == null) {
1822                                refs = new ArrayList<Reference>();
1823                                invalidRefs.put(location, refs);
1824                            }
1825                            UUID toUuid = UUID.fromString(id.getToUuidString());
1826                            refs.add(refFactory.create(toUuid));
1827                        }
1828                    }
1829                }
1830    
1831                workspaceIdsWithChangedReferences.clear();
1832    
1833                if (!invalidRefs.isEmpty()) {
1834                    String msg = JpaConnectorI18n.invalidReferences.text(getSourceName());
1835                    throw new ReferentialIntegrityException(invalidRefs, msg);
1836                }
1837            }
1838        }
1839    
1840        protected String createProperties( Long workspaceId,
1841                                           String uuidString,
1842                                           Collection<Property> properties ) throws IOException {
1843            assert uuidString != null;
1844    
1845            // Create the PropertiesEntity ...
1846            NodeId nodeId = new NodeId(workspaceId, uuidString);
1847            PropertiesEntity props = new PropertiesEntity(nodeId);
1848    
1849            // If there are properties ...
1850            boolean processProperties = true;
1851            if (properties.isEmpty()) processProperties = false;
1852            else if (properties.size() == 1 && properties.iterator().next().getName().equals(JcrLexicon.NAME)) processProperties = false;
1853    
1854            if (processProperties) {
1855                References refs = enforceReferentialIntegrity ? new References() : null;
1856                LargeValueSerializer largeValues = new LargeValueSerializer(props);
1857                ByteArrayOutputStream baos = new ByteArrayOutputStream();
1858                OutputStream os = compressData ? new GZIPOutputStream(baos) : baos;
1859                ObjectOutputStream oos = new ObjectOutputStream(os);
1860                int numProperties = properties.size();
1861                try {
1862                    Serializer.ReferenceValues refValues = refs != null ? refs : Serializer.NO_REFERENCES_VALUES;
1863                    serializer.serializeProperties(oos, numProperties, properties, largeValues, refValues);
1864                } finally {
1865                    oos.close();
1866                }
1867    
1868                props.setData(baos.toByteArray());
1869                props.setPropertyCount(numProperties);
1870    
1871                // Record the changes to the references ...
1872                if (refs != null && refs.hasWritten()) {
1873                    for (Reference reference : refs.getWritten()) {
1874                        String toUuid = resolveToUuid(workspaceId, reference);
1875                        if (toUuid != null) {
1876                            ReferenceId id = new ReferenceId(workspaceId, uuidString, toUuid);
1877                            ReferenceEntity refEntity = new ReferenceEntity(id);
1878                            entities.persist(refEntity);
1879                            workspaceIdsWithChangedReferences.add(workspaceId);
1880                        }
1881                    }
1882                }
1883            } else {
1884                props.setData(null);
1885                props.setPropertyCount(0);
1886            }
1887            props.setCompressed(compressData);
1888            props.setReferentialIntegrityEnforced(true);
1889    
1890            entities.persist(props);
1891    
1892            // References will be persisted in the commit ...
1893            return uuidString;
1894        }
1895    
1896        /**
1897         * Attempt to resolve the reference.
1898         * 
1899         * @param workspaceId the ID of the workspace in which the reference occurs; may not be null
1900         * @param reference the reference
1901         * @return the UUID of the node to which the reference points, or null if the reference could not be resolved
1902         */
1903        protected String resolveToUuid( Long workspaceId,
1904                                        Reference reference ) {
1905            // See if the reference is by UUID ...
1906            try {
1907                UUID uuid = uuidFactory.create(reference);
1908                ActualLocation actualLocation = getActualLocation(workspaceId, Location.create(uuid));
1909                return actualLocation.uuid;
1910            } catch (ValueFormatException e) {
1911                // Unknown kind of reference, which we don't track
1912            } catch (PathNotFoundException e) {
1913                // Unable to resolve reference ...
1914            }
1915            // Unable to resolve reference ...
1916            return null;
1917        }
1918    
1919        /**
1920         * Utility method to look up the actual information given a supplied location. This method verifies that the location actually
1921         * represents an existing node, or it throws a {@link PathNotFoundException}. In all cases, the resulting information contains
1922         * the correct path and the correct UUID.
1923         * <p>
1924         * Note that this method sometimes performs "unnecessary" work when the location contains both a path to a node and the node's
1925         * corresponding UUID. Strictly speaking, this method would need to do very little. However, in such cases, this method does
1926         * verify that the information is still correct (ensuring that calls to use the {@link ChildEntity} will be correct). So,
1927         * while this work <i>may</i> be unnecessary, it does ensure that the location is consistent and correct (something that is
1928         * not unnecessary).
1929         * </p>
1930         * <p>
1931         * There are cases when a request containing a Path and a UUID are no longer correct. The node may have been just moved by
1932         * another request (perhaps from a different client), or there may be an error in the component making the request. In these
1933         * cases, this method assumes that the path is incorrect (since paths may change) and finds the <i>correct path</i> given the
1934         * UUID.
1935         * </p>
1936         * <p>
1937         * This method will also find the path when the location contains just the UUID.
1938         * </p>
1939         * 
1940         * @param workspaceId the ID of the workspace; may not be null
1941         * @param original the original location; may not be null
1942         * @return the actual location, which includes the verified location and additional information needed by this method that may
1943         *         be usable after this method is called
1944         * @throws PathNotFoundException if the location does not represent a location that could be found
1945         */
1946        protected ActualLocation getActualLocation( Long workspaceId,
1947                                                    Location original ) throws PathNotFoundException {
1948            assert original != null;
1949    
1950            // Look for the UUID in the original ...
1951            Property uuidProperty = original.getIdProperty(DnaLexicon.UUID);
1952            String uuidString = uuidProperty != null && !uuidProperty.isEmpty() ? stringFactory.create(uuidProperty.getFirstValue()) : null;
1953    
1954            Path path = original.getPath();
1955            if (path != null) {
1956                // See if the location is already in the cache ...
1957                Location cached = cache.getLocationFor(workspaceId, path);
1958                if (cached != null) {
1959                    return new ActualLocation(cached, cached.getUuid().toString(), null);
1960                }
1961            }
1962    
1963            // If the original location has a UUID, then use that to find the child entity that represents the location ...
1964            if (uuidString != null) {
1965                // The original has a UUID, so use that to find the child entity.
1966                // Then walk up the ancestors and build the path.
1967                String nodeUuidString = uuidString;
1968                LinkedList<Path.Segment> segments = new LinkedList<Path.Segment>();
1969                ChildEntity entity = null;
1970                ChildEntity originalEntity = null;
1971                while (uuidString != null && !uuidString.equals(this.rootNodeUuidString)) {
1972                    Query query = entities.createNamedQuery("ChildEntity.findByChildUuid");
1973                    query.setParameter("workspaceId", workspaceId);
1974                    query.setParameter("childUuidString", uuidString);
1975                    try {
1976                        // Find the parent of the UUID ...
1977                        entity = (ChildEntity)query.getSingleResult();
1978                        if (originalEntity == null) originalEntity = entity;
1979                        String localName = entity.getChildName();
1980                        String uri = entity.getChildNamespace().getUri();
1981                        int sns = entity.getSameNameSiblingIndex();
1982                        Name name = nameFactory.create(uri, localName);
1983                        segments.addFirst(pathFactory.createSegment(name, sns));
1984                        uuidString = entity.getId().getParentUuidString();
1985                    } catch (NoResultException e) {
1986                        uuidString = null;
1987                    }
1988                }
1989                Path fullPath = pathFactory.createAbsolutePath(segments);
1990                Location newLocation = Location.create(fullPath, uuidProperty);
1991                cache.addNewNode(workspaceId, newLocation);
1992                return new ActualLocation(newLocation, nodeUuidString, originalEntity);
1993            }
1994    
1995            // There is no UUID, so look for a path ...
1996            if (path == null) {
1997                String propName = DnaLexicon.UUID.getString(getExecutionContext().getNamespaceRegistry());
1998                String msg = JpaConnectorI18n.locationShouldHavePathAndOrProperty.text(getSourceName(), propName);
1999                throw new PathNotFoundException(original, pathFactory.createRootPath(), msg);
2000            }
2001    
2002            // Walk the child entities, starting at the root, down the to the path ...
2003            if (path.isRoot()) {
2004                Location newLocation = original.with(rootNodeUuid);
2005                cache.addNewNode(workspaceId, newLocation);
2006                return new ActualLocation(newLocation, rootNodeUuidString, null);
2007            }
2008            // See if the parent location is known in the cache ...
2009            Location cachedParent = cache.getLocationFor(workspaceId, path.getParent());
2010            if (cachedParent != null) {
2011                // We know the UUID of the parent, so we can find the child a little faster ...
2012                ChildEntity child = findByPathSegment(workspaceId, cachedParent.getUuid().toString(), path.getLastSegment());
2013                uuidString = child.getId().getChildUuidString();
2014                Location newLocation = original.with(UUID.fromString(uuidString));
2015                cache.addNewNode(workspaceId, newLocation);
2016                return new ActualLocation(newLocation, uuidString, child);
2017            }
2018    
2019            // We couldn't find the parent, so we need to search by path ...
2020            String parentUuid = this.rootNodeUuidString;
2021            ChildEntity child = null;
2022            for (Path.Segment segment : path) {
2023                child = findByPathSegment(workspaceId, parentUuid, segment);
2024                if (child == null) {
2025                    // Unable to complete the path, so prepare the exception by determining the lowest path that exists ...
2026                    Path lowest = path;
2027                    while (lowest.getLastSegment() != segment) {
2028                        lowest = lowest.getParent();
2029                    }
2030                    lowest = lowest.getParent();
2031                    throw new PathNotFoundException(original, lowest);
2032                }
2033                parentUuid = child.getId().getChildUuidString();
2034            }
2035            assert child != null;
2036            uuidString = child.getId().getChildUuidString();
2037            Location newLocation = original.with(UUID.fromString(uuidString));
2038            cache.addNewNode(workspaceId, newLocation);
2039            return new ActualLocation(newLocation, uuidString, child);
2040        }
2041    
2042        /**
2043         * Find the node with the supplied path segment that is a child of the supplied parent.
2044         * 
2045         * @param workspaceId the ID of the workspace
2046         * @param parentUuid the UUID of the parent node, in string form
2047         * @param pathSegment the path segment of the child
2048         * @return the existing namespace, or null if one does not exist
2049         * @throws IllegalArgumentException if the manager or URI are null
2050         */
2051        protected ChildEntity findByPathSegment( Long workspaceId,
2052                                                 String parentUuid,
2053                                                 Path.Segment pathSegment ) {
2054            assert namespaces != null;
2055            assert parentUuid != null;
2056            assert pathSegment != null;
2057            assert workspaceId != null;
2058            Name name = pathSegment.getName();
2059            String localName = name.getLocalName();
2060            String nsUri = name.getNamespaceUri();
2061            NamespaceEntity ns = namespaces.get(nsUri, false);
2062            if (ns == null) {
2063                // The namespace can't be found, then certainly the node won't be found ...
2064                return null;
2065            }
2066            int snsIndex = pathSegment.hasIndex() ? pathSegment.getIndex() : 1;
2067            Query query = entities.createNamedQuery("ChildEntity.findByPathSegment");
2068            query.setParameter("workspaceId", workspaceId);
2069            query.setParameter("parentUuidString", parentUuid);
2070            query.setParameter("ns", ns.getId());
2071            query.setParameter("childName", localName);
2072            query.setParameter("sns", snsIndex);
2073            try {
2074                return (ChildEntity)query.getSingleResult();
2075            } catch (NoResultException e) {
2076                return null;
2077            }
2078        }
2079    
2080        protected String createHexValuesString( Collection<String> hexValues ) {
2081            if (hexValues == null || hexValues.isEmpty()) return null;
2082            StringBuilder sb = new StringBuilder();
2083            boolean first = true;
2084            for (String hexValue : hexValues) {
2085                if (first) {
2086                    first = false;
2087                } else {
2088                    sb.append(',');
2089                }
2090                sb.append(hexValue);
2091            }
2092            return sb.toString();
2093        }
2094    
2095        protected Collection<String> createHexValues( String hexValuesString ) {
2096            return Arrays.asList(hexValuesString.split(","));
2097        }
2098    
2099        protected class LargeValueSerializer implements LargeValues {
2100            private final PropertiesEntity properties;
2101            private Set<String> written;
2102    
2103            public LargeValueSerializer( PropertiesEntity entity ) {
2104                this.properties = entity;
2105                this.written = null;
2106            }
2107    
2108            public LargeValueSerializer( PropertiesEntity entity,
2109                                         Set<String> written ) {
2110                this.properties = entity;
2111                this.written = written;
2112            }
2113    
2114            /**
2115             * {@inheritDoc}
2116             * 
2117             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#getMinimumSize()
2118             */
2119            public long getMinimumSize() {
2120                return largeValueMinimumSizeInBytes;
2121            }
2122    
2123            /**
2124             * {@inheritDoc}
2125             * 
2126             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#read(org.jboss.dna.graph.property.ValueFactories,
2127             *      byte[], long)
2128             */
2129            public Object read( ValueFactories valueFactories,
2130                                byte[] hash,
2131                                long length ) throws IOException {
2132                String hashStr = StringUtil.getHexString(hash);
2133                // Find the large value ...
2134                LargeValueId largeValueId = new LargeValueId(hashStr);
2135                LargeValueEntity entity = entities.find(LargeValueEntity.class, largeValueId);
2136                if (entity != null) {
2137                    // Find the large value from the existing property entity ...
2138                    byte[] data = entity.getData();
2139                    if (entity.isCompressed()) {
2140                        InputStream stream = new GZIPInputStream(new ByteArrayInputStream(data));
2141                        try {
2142                            data = IoUtil.readBytes(stream);
2143                        } finally {
2144                            stream.close();
2145                        }
2146                    }
2147                    return valueFactories.getValueFactory(entity.getType()).create(data);
2148                }
2149                throw new IOException(JpaConnectorI18n.unableToReadLargeValue.text(getSourceName(), hashStr));
2150            }
2151    
2152            /**
2153             * {@inheritDoc}
2154             * 
2155             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#write(byte[], long,
2156             *      org.jboss.dna.graph.property.PropertyType, java.lang.Object)
2157             */
2158            public void write( byte[] hash,
2159                               long length,
2160                               PropertyType type,
2161                               Object value ) throws IOException {
2162                if (value == null) return;
2163                String hashStr = StringUtil.getHexString(hash);
2164                if (written != null) written.add(hashStr);
2165    
2166                // Look for an existing value in the collection ...
2167                final LargeValueId id = new LargeValueId(hashStr);
2168                for (LargeValueId existing : properties.getLargeValues()) {
2169                    if (existing.equals(id)) {
2170                        // Already associated with this properties entity
2171                        return;
2172                    }
2173                }
2174                LargeValueEntity entity = entities.find(LargeValueEntity.class, id);
2175                if (entity == null) {
2176                    // We have to create the large value entity ...
2177                    entity = new LargeValueEntity();
2178                    entity.setCompressed(true);
2179                    entity.setId(id);
2180                    entity.setLength(length);
2181                    entity.setType(type);
2182                    ValueFactories factories = getExecutionContext().getValueFactories();
2183                    byte[] bytes = null;
2184                    switch (type) {
2185                        case BINARY:
2186                            Binary binary = factories.getBinaryFactory().create(value);
2187                            InputStream stream = null;
2188                            try {
2189                                binary.acquire();
2190                                stream = binary.getStream();
2191                                if (compressData) stream = new GZIPInputStream(stream);
2192                                bytes = IoUtil.readBytes(stream);
2193                            } finally {
2194                                try {
2195                                    if (stream != null) stream.close();
2196                                } finally {
2197                                    binary.release();
2198                                }
2199                            }
2200                            break;
2201                        case URI:
2202                            // This will be treated as a string ...
2203                        default:
2204                            String str = factories.getStringFactory().create(value);
2205                            if (compressData) {
2206                                ByteArrayOutputStream bs = new ByteArrayOutputStream();
2207                                OutputStream strStream = new GZIPOutputStream(bs);
2208                                try {
2209                                    IoUtil.write(str, strStream);
2210                                } finally {
2211                                    strStream.close();
2212                                }
2213                                bytes = bs.toByteArray();
2214                            } else {
2215                                bytes = str.getBytes();
2216                            }
2217                            break;
2218                    }
2219                    entity.setData(bytes);
2220                    entities.persist(entity);
2221                }
2222                // Now associate the large value with the properties entity ...
2223                assert id.getHash() != null;
2224                properties.getLargeValues().add(id);
2225            }
2226    
2227        }
2228    
2229        protected class RecordingLargeValues implements LargeValues {
2230            protected final Collection<String> readKeys = new HashSet<String>();
2231            protected final Collection<String> writtenKeys = new HashSet<String>();
2232            protected final LargeValues delegate;
2233    
2234            RecordingLargeValues( LargeValues delegate ) {
2235                assert delegate != null;
2236                this.delegate = delegate;
2237            }
2238    
2239            /**
2240             * {@inheritDoc}
2241             * 
2242             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#getMinimumSize()
2243             */
2244            public long getMinimumSize() {
2245                return delegate.getMinimumSize();
2246            }
2247    
2248            /**
2249             * {@inheritDoc}
2250             * 
2251             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#read(org.jboss.dna.graph.property.ValueFactories,
2252             *      byte[], long)
2253             */
2254            public Object read( ValueFactories valueFactories,
2255                                byte[] hash,
2256                                long length ) throws IOException {
2257                String key = StringUtil.getHexString(hash);
2258                readKeys.add(key);
2259                return delegate.read(valueFactories, hash, length);
2260            }
2261    
2262            public void write( byte[] hash,
2263                               long length,
2264                               PropertyType type,
2265                               Object value ) throws IOException {
2266                String key = StringUtil.getHexString(hash);
2267                writtenKeys.add(key);
2268                delegate.write(hash, length, type, value);
2269            }
2270        }
2271    
2272        protected class SkippedLargeValues implements LargeValues {
2273            protected Collection<String> skippedKeys = new HashSet<String>();
2274            protected final LargeValues delegate;
2275    
2276            SkippedLargeValues( LargeValues delegate ) {
2277                assert delegate != null;
2278                this.delegate = delegate;
2279            }
2280    
2281            /**
2282             * {@inheritDoc}
2283             * 
2284             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#getMinimumSize()
2285             */
2286            public long getMinimumSize() {
2287                return delegate.getMinimumSize();
2288            }
2289    
2290            /**
2291             * {@inheritDoc}
2292             * 
2293             * @see org.jboss.dna.connector.store.jpa.util.Serializer.LargeValues#read(org.jboss.dna.graph.property.ValueFactories,
2294             *      byte[], long)
2295             */
2296            public Object read( ValueFactories valueFactories,
2297                                byte[] hash,
2298                                long length ) throws IOException {
2299                String key = StringUtil.getHexString(hash);
2300                skippedKeys.add(key);
2301                return null;
2302            }
2303    
2304            public void write( byte[] hash,
2305                               long length,
2306                               PropertyType type,
2307                               Object value ) {
2308                throw new UnsupportedOperationException();
2309            }
2310        }
2311    
2312        @Immutable
2313        protected static class ActualLocation {
2314            /** The actual location */
2315            protected final Location location;
2316            /** The string-form of the UUID, supplied as a convenience. */
2317            protected final String uuid;
2318            /** The ChildEntity that represents the location, which may be null if the location represents the root node */
2319            protected final ChildEntity childEntity;
2320    
2321            protected ActualLocation( Location location,
2322                                      String uuid,
2323                                      ChildEntity childEntity ) {
2324                assert location != null;
2325                assert uuid != null;
2326                this.location = location;
2327                this.uuid = uuid;
2328                this.childEntity = childEntity;
2329            }
2330    
2331            /**
2332             * {@inheritDoc}
2333             * 
2334             * @see java.lang.Object#toString()
2335             */
2336            @Override
2337            public String toString() {
2338                return this.location.toString() + " (uuid=" + uuid + ") " + childEntity;
2339            }
2340        }
2341    
2342        protected class References implements Serializer.ReferenceValues {
2343            private Set<Reference> read;
2344            private Set<Reference> removed;
2345            private Set<Reference> written;
2346    
2347            protected References() {
2348            }
2349    
2350            /**
2351             * {@inheritDoc}
2352             * 
2353             * @see org.jboss.dna.connector.store.jpa.util.Serializer.ReferenceValues#read(org.jboss.dna.graph.property.Reference)
2354             */
2355            public void read( Reference reference ) {
2356                if (read == null) read = new HashSet<Reference>();
2357                read.add(reference);
2358            }
2359    
2360            /**
2361             * {@inheritDoc}
2362             * 
2363             * @see org.jboss.dna.connector.store.jpa.util.Serializer.ReferenceValues#remove(org.jboss.dna.graph.property.Reference)
2364             */
2365            public void remove( Reference reference ) {
2366                if (removed == null) removed = new HashSet<Reference>();
2367                removed.add(reference);
2368            }
2369    
2370            /**
2371             * {@inheritDoc}
2372             * 
2373             * @see org.jboss.dna.connector.store.jpa.util.Serializer.ReferenceValues#write(org.jboss.dna.graph.property.Reference)
2374             */
2375            public void write( Reference reference ) {
2376                if (written == null) written = new HashSet<Reference>();
2377                written.add(reference);
2378            }
2379    
2380            public boolean hasRead() {
2381                return read != null;
2382            }
2383    
2384            public boolean hasRemoved() {
2385                return removed != null;
2386            }
2387    
2388            public boolean hasWritten() {
2389                return written != null;
2390            }
2391    
2392            /**
2393             * @return read
2394             */
2395            public Set<Reference> getRead() {
2396                if (read != null) return read;
2397                return Collections.emptySet();
2398            }
2399    
2400            /**
2401             * @return removed
2402             */
2403            public Set<Reference> getRemoved() {
2404                if (removed != null) return removed;
2405                return Collections.emptySet();
2406            }
2407    
2408            /**
2409             * @return written
2410             */
2411            public Set<Reference> getWritten() {
2412                if (written != null) return written;
2413                return Collections.emptySet();
2414            }
2415        }
2416    }