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