View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.connector.store.jpa.model.simple;
25  
26  import java.io.ByteArrayInputStream;
27  import java.io.ByteArrayOutputStream;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.ObjectInputStream;
31  import java.io.ObjectOutputStream;
32  import java.io.OutputStream;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.Collection;
36  import java.util.Collections;
37  import java.util.HashMap;
38  import java.util.HashSet;
39  import java.util.Iterator;
40  import java.util.LinkedList;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Set;
44  import java.util.UUID;
45  import java.util.zip.GZIPInputStream;
46  import java.util.zip.GZIPOutputStream;
47  import javax.persistence.EntityManager;
48  import javax.persistence.EntityTransaction;
49  import javax.persistence.NoResultException;
50  import javax.persistence.Query;
51  import net.jcip.annotations.NotThreadSafe;
52  import org.modeshape.common.util.IoUtil;
53  import org.modeshape.common.util.StringUtil;
54  import org.modeshape.connector.store.jpa.JpaConnectorI18n;
55  import org.modeshape.connector.store.jpa.model.common.WorkspaceEntity;
56  import org.modeshape.connector.store.jpa.util.Namespaces;
57  import org.modeshape.connector.store.jpa.util.Serializer;
58  import org.modeshape.connector.store.jpa.util.Workspaces;
59  import org.modeshape.connector.store.jpa.util.Serializer.LargeValues;
60  import org.modeshape.graph.ExecutionContext;
61  import org.modeshape.graph.Location;
62  import org.modeshape.graph.ModeShapeLexicon;
63  import org.modeshape.graph.connector.LockFailedException;
64  import org.modeshape.graph.connector.map.AbstractMapWorkspace;
65  import org.modeshape.graph.connector.map.MapNode;
66  import org.modeshape.graph.connector.map.MapRepository;
67  import org.modeshape.graph.connector.map.MapRepositoryTransaction;
68  import org.modeshape.graph.connector.map.MapWorkspace;
69  import org.modeshape.graph.property.Binary;
70  import org.modeshape.graph.property.Name;
71  import org.modeshape.graph.property.NameFactory;
72  import org.modeshape.graph.property.Path;
73  import org.modeshape.graph.property.PathFactory;
74  import org.modeshape.graph.property.Property;
75  import org.modeshape.graph.property.PropertyFactory;
76  import org.modeshape.graph.property.PropertyType;
77  import org.modeshape.graph.property.Reference;
78  import org.modeshape.graph.property.UuidFactory;
79  import org.modeshape.graph.property.ValueFactories;
80  import org.modeshape.graph.property.ValueFactory;
81  import org.modeshape.graph.property.Path.Segment;
82  import org.modeshape.graph.request.CompositeRequest;
83  import org.modeshape.graph.request.LockBranchRequest.LockScope;
84  
85  /**
86   * Implementation of {@link MapRepository} for the {@link SimpleModel Simple JPA connector model}. This class exposes a map of
87   * workspace names to {@link Workspace workspaces} and each workspace provides a logical mapping of node UUIDs to {@link JpaNode
88   * nodes}. The {@code JpaNode} class functions as an adapter between the {@link NodeEntity persistent entity for nodes} and the
89   * {@link MapNode map repository interface for nodes}.
90   * <p>
91   * This class differs slightly from the other {@link MapRepository} implementations in that it exists only within the lifetime of
92   * a single {@link EntityManager} (which itself is opened and closed within the lifetime of a single {@link SimpleJpaConnection}.
93   * The other map repository implementations all outlive any particular connection and generally survive for the lifetime of the
94   * ModeShape server.
95   * </p>
96   */
97  public class SimpleJpaRepository extends MapRepository {
98  
99      protected final EntityManager entityManager;
100     protected final Workspaces workspaceEntities;
101     protected final Namespaces namespaceEntities;
102     protected final ExecutionContext context;
103     protected final PathFactory pathFactory;
104     protected final NameFactory nameFactory;
105     private final List<String> predefinedWorkspaceNames;
106     protected final boolean compressData;
107     protected final boolean creatingWorkspacesAllowed;
108     protected final long minimumSizeOfLargeValuesInBytes;
109     protected final String dialect;
110 
111     public SimpleJpaRepository( String sourceName,
112                                 UUID rootNodeUuid,
113                                 String defaultWorkspaceName,
114                                 String[] predefinedWorkspaceNames,
115                                 EntityManager entityManager,
116                                 ExecutionContext context,
117                                 boolean compressData,
118                                 boolean creatingWorkspacesAllowed,
119                                 long minimumSizeOfLargeValuesInBytes,
120                                 String dialect ) {
121         super(sourceName, rootNodeUuid, defaultWorkspaceName);
122 
123         this.context = context;
124         ValueFactories valueFactories = context.getValueFactories();
125         this.nameFactory = valueFactories.getNameFactory();
126         this.pathFactory = valueFactories.getPathFactory();
127         this.predefinedWorkspaceNames = Arrays.asList(predefinedWorkspaceNames);
128         this.compressData = compressData;
129         this.creatingWorkspacesAllowed = creatingWorkspacesAllowed;
130         this.minimumSizeOfLargeValuesInBytes = minimumSizeOfLargeValuesInBytes;
131         this.dialect = dialect;
132 
133         this.entityManager = entityManager;
134         workspaceEntities = new Workspaces(entityManager);
135         namespaceEntities = new Namespaces(entityManager);
136         super.initialize();
137     }
138 
139     public SimpleJpaRepository( String sourceName,
140                                 UUID rootNodeUuid,
141                                 EntityManager entityManager,
142                                 ExecutionContext context,
143                                 boolean compressData,
144                                 boolean creatingWorkspacesAllowed,
145                                 long minimumSizeOfLargeValuesInBytes,
146                                 String dialect ) {
147         super(sourceName, rootNodeUuid);
148 
149         this.context = context;
150         ValueFactories valueFactories = context.getValueFactories();
151         this.nameFactory = valueFactories.getNameFactory();
152         this.pathFactory = valueFactories.getPathFactory();
153         this.predefinedWorkspaceNames = Collections.emptyList();
154         this.compressData = compressData;
155         this.creatingWorkspacesAllowed = creatingWorkspacesAllowed;
156         this.minimumSizeOfLargeValuesInBytes = minimumSizeOfLargeValuesInBytes;
157         this.dialect = dialect;
158 
159         this.entityManager = entityManager;
160         workspaceEntities = new Workspaces(entityManager);
161         namespaceEntities = new Namespaces(entityManager);
162         super.initialize();
163     }
164 
165     /**
166      * Determine whether creating workspaces is allowed.
167      * 
168      * @return true if creating workspace is allowed, or false otherwise
169      * @see org.modeshape.connector.store.jpa.JpaSource#isCreatingWorkspacesAllowed()
170      */
171     final boolean creatingWorkspacesAllowed() {
172         return this.creatingWorkspacesAllowed;
173     }
174 
175     /*
176      * (non-Javadoc)
177      * @see org.modeshape.graph.connector.map.MapRepository#createWorkspace(org.modeshape.graph.ExecutionContext, java.lang.String)
178      */
179     @Override
180     protected MapWorkspace createWorkspace( ExecutionContext context,
181                                             String name ) {
182 
183         WorkspaceEntity entity = workspaceEntities.get(name, false);
184 
185         if (entity != null) {
186             return new Workspace(this, name, entity.getId().intValue());
187         }
188 
189         entity = workspaceEntities.create(name);
190 
191         // Flush to ensure that the entity ID is set
192         entityManager.flush();
193 
194         Workspace workspace = new Workspace(this, name, entity.getId().intValue());
195         workspace.createRootNode();
196 
197         return workspace;
198     }
199 
200     /*
201      * (non-Javadoc)
202      * @see org.modeshape.graph.connector.map.MapRepository#getWorkspace(java.lang.String)
203      */
204     @Override
205     public MapWorkspace getWorkspace( String name ) {
206         MapWorkspace workspace = super.getWorkspace(name);
207         if (workspace != null) return workspace;
208 
209         // There's no such workspace in the local cache, check if one exists in the DB
210         if (name == null) name = getDefaultWorkspaceName();
211         WorkspaceEntity entity = workspaceEntities.get(name, false);
212         if (entity == null) {
213             if (this.predefinedWorkspaceNames.contains(name)) {
214                 return createWorkspace(context, name);
215             }
216 
217             return null;
218         }
219 
220         return new Workspace(this, name, entity.getId());
221     }
222 
223     /*
224      * (non-Javadoc)
225      * @see org.modeshape.graph.connector.map.MapRepository#getWorkspaceNames()
226      */
227     @Override
228     public Set<String> getWorkspaceNames() {
229         Set<String> workspaceNames = new HashSet<String>(super.getWorkspaceNames());
230         workspaceNames.addAll(predefinedWorkspaceNames);
231 
232         return workspaceNames;
233     }
234 
235     /**
236      * {@inheritDoc}
237      * 
238      * @see org.modeshape.graph.connector.map.MapRepository#startTransaction(boolean)
239      */
240     @Override
241     public MapRepositoryTransaction startTransaction( boolean readonly ) {
242         EntityTransaction txn = entityManager.getTransaction();
243         return new SimpleJpaTransaction(txn);
244     }
245 
246     /**
247      * This class provides a logical mapping of UUIDs to {@link JpaNode nodes} within a named workspace.
248      * <p>
249      * Like its enclosing class, this class only survives for the lifetime of a single request (which may be a
250      * {@link CompositeRequest}).
251      * </p>
252      */
253     @SuppressWarnings( "synthetic-access" )
254     protected class Workspace extends AbstractMapWorkspace {
255         private final long workspaceId;
256         private final Map<Path, MapNode> nodesByPath = new HashMap<Path, MapNode>();
257 
258         public Workspace( MapRepository repository,
259                           String name,
260                           long workspaceId ) {
261             super(repository, name);
262 
263             this.workspaceId = workspaceId;
264 
265             // This gets called from the repository for this connector since the repository
266             // already knows whether this workspace existed in the database before this call.
267             // initialize();
268         }
269 
270         void createRootNode() {
271             initialize();
272         }
273 
274         /**
275          * This should copy the subgraph given by the original node and place the new copy under the supplied new parent. Note
276          * that internal references between nodes within the original subgraph must be reflected as internal nodes within the new
277          * subgraph.
278          * 
279          * @param context the context; may not be null
280          * @param original the node to be copied; may not be null
281          * @param newWorkspace the workspace containing the new parent node; may not be null
282          * @param newParent the parent where the copy is to be placed; may not be null
283          * @param desiredName the desired name for the node; if null, the name will be obtained from the original node
284          * @param recursive true if the copy should be recursive
285          * @return the new node, which is the top of the new subgraph
286          */
287         @Override
288         public MapNode copyNode( ExecutionContext context,
289                                  MapNode original,
290                                  MapWorkspace newWorkspace,
291                                  MapNode newParent,
292                                  Name desiredName,
293                                  boolean recursive ) {
294 
295             Map<UUID, UUID> oldToNewUuids = new HashMap<UUID, UUID>();
296             MapNode copyRoot = copyNode(context, original, newWorkspace, newParent, desiredName, true, oldToNewUuids);
297 
298             // Now, adjust any references in the new subgraph to objects in the original subgraph
299             // (because they were internal references, and need to be internal to the new subgraph)
300             PropertyFactory propertyFactory = context.getPropertyFactory();
301             UuidFactory uuidFactory = context.getValueFactories().getUuidFactory();
302             ValueFactory<Reference> referenceFactory = context.getValueFactories().getReferenceFactory();
303             boolean refChanged = false;
304             for (Map.Entry<UUID, UUID> oldToNew : oldToNewUuids.entrySet()) {
305                 MapNode oldNode = this.getNode(oldToNew.getKey());
306                 MapNode newNode = newWorkspace.getNode(oldToNew.getValue());
307                 assert oldNode != null;
308                 assert newNode != null;
309                 // Iterate over the properties of the new ...
310                 for (Map.Entry<Name, Property> entry : newNode.getProperties().entrySet()) {
311                     Property property = entry.getValue();
312                     // Now see if any of the property values are references ...
313                     List<Object> newValues = new ArrayList<Object>();
314                     boolean foundReference = false;
315                     for (Iterator<?> iter = property.getValues(); iter.hasNext();) {
316                         Object value = iter.next();
317                         PropertyType type = PropertyType.discoverType(value);
318                         if (type == PropertyType.REFERENCE) {
319                             UUID oldReferencedUuid = uuidFactory.create(value);
320                             UUID newReferencedUuid = oldToNewUuids.get(oldReferencedUuid);
321                             if (newReferencedUuid != null) {
322                                 newValues.add(referenceFactory.create(newReferencedUuid));
323                                 foundReference = true;
324                                 refChanged = true;
325                             }
326                         } else {
327                             newValues.add(value);
328                         }
329                     }
330                     // If we found at least one reference, we have to build a new Property object ...
331                     if (foundReference) {
332                         Property newProperty = propertyFactory.create(property.getName(), newValues);
333                         entry.setValue(newProperty);
334                     }
335                 }
336 
337                 if (refChanged) {
338                     ((JpaNode)newNode).serializeProperties();
339                 }
340             }
341             return copyRoot;
342         }
343 
344         /*
345          * (non-Javadoc)
346          * @see org.modeshape.graph.connector.map.AbstractMapWorkspace#correctSameNameSiblingIndexes(org.modeshape.graph.ExecutionContext, org.modeshape.graph.connector.map.MapNode, org.modeshape.graph.property.Name)
347          */
348         @Override
349         protected void correctSameNameSiblingIndexes( ExecutionContext context,
350                                                       MapNode parentNode,
351                                                       Name name ) {
352             int snsIndex = 1;
353             int parentIndex = 0;
354             List<MapNode> children = parentNode.getChildren();
355 
356             for (MapNode child : children) {
357                 NodeEntity childNode = ((JpaNode)child).entity;
358                 if (parentIndex != childNode.getIndexInParent()) {
359                     childNode.setIndexInParent(parentIndex);
360                 }
361 
362                 if (name.equals(child.getName().getName())) {
363                     if (snsIndex != childNode.getSameNameSiblingIndex()) {
364                         childNode.setSameNameSiblingIndex(snsIndex);
365                     }
366                     snsIndex++;
367 
368                 }
369                 parentIndex++;
370             }
371 
372         }
373 
374         /**
375          * Adds the given node to the persistent store, replacing any node already in the persistent store with the same UUID.
376          * <p>
377          * Invoking this method causes a database INSERT statement to execute immediately.
378          * </p>
379          * 
380          * @param node the node to add to the persistent store; may not be null
381          */
382         @Override
383         protected void addNodeToMap( MapNode node ) {
384             assert node != null;
385 
386             NodeEntity nodeEntity = ((JpaNode)node).entity;
387             nodeEntity.setWorkspaceId(this.workspaceId);
388             nodeEntity.setReferentialIntegrityEnforced(false);
389 
390             entityManager.persist(nodeEntity);
391         }
392 
393         @Override
394         protected MapNode removeNodeFromMap( UUID nodeUuid ) {
395             throw new IllegalStateException("This code should be unreachable");
396         }
397 
398         /**
399          * Removes the given node and its children from the persistent store using the
400          * {@link SubgraphQuery#deleteSubgraph(boolean) subgraph bulk delete method}.
401          * 
402          * @param node the root of the branch to be removed
403          */
404         @Override
405         protected void removeUuidReference( MapNode node ) {
406             SubgraphQuery branch = SubgraphQuery.create(entityManager, workspaceId, node.getUuid(), 0);
407             // Delete in bulk except when using MySql ...
408             branch.deleteSubgraph(true);
409             branch.close();
410 
411             // Delete unused large values ...
412             LargeValueEntity.deleteUnused(entityManager, dialect);
413 
414             // And clean up the local cache by paths by brute force ...
415             this.nodesByPath.clear();
416         }
417 
418         /*
419          * (non-Javadoc)
420          * @see org.modeshape.graph.connector.map.AbstractMapWorkspace#createMapNode(java.util.UUID)
421          */
422         @Override
423         protected MapNode createMapNode( UUID uuid ) {
424             return new JpaNode(uuid);
425         }
426 
427         /**
428          * Removes all of the nodes in this workspace from the persistent store with a single query.
429          */
430         @Override
431         protected void removeAllNodesFromMap() {
432             Query query = entityManager.createQuery("NodeEntity.deleteAllInWorkspace");
433             query.setParameter("workspaceId", workspaceId);
434             query.executeUpdate();
435 
436             // Delete unused large values ...
437             LargeValueEntity.deleteUnused(entityManager, dialect);
438         }
439 
440         /*
441          * (non-Javadoc)
442          * @see org.modeshape.graph.connector.map.AbstractMapWorkspace#getNode(java.util.UUID)
443          */
444         @Override
445         public JpaNode getNode( UUID nodeUuid ) {
446             assert nodeUuid != null;
447 
448             Query query = entityManager.createNamedQuery("NodeEntity.findByNodeUuid");
449             query.setParameter("workspaceId", workspaceId);
450             query.setParameter("nodeUuidString", nodeUuid.toString());
451             try {
452                 // Find the parent of the UUID ...
453                 NodeEntity result = (NodeEntity)query.getSingleResult();
454                 return new JpaNode(result);
455             } catch (NoResultException e) {
456                 return null;
457             }
458         }
459 
460         /*
461          * (non-Javadoc)
462          * @see org.modeshape.graph.connector.map.AbstractMapWorkspace#getNode(org.modeshape.graph.property.Path)
463          */
464         @Override
465         public MapNode getNode( Path path ) {
466             MapNode node = nodesByPath.get(path);
467             if (node != null) return node;
468 
469             node = super.getNode(path);
470             nodesByPath.put(path, node);
471             return node;
472         }
473 
474         /**
475          * Retrieves the branch of nodes rooted at the given location using the {@link SubgraphQuery#getNodes(boolean, boolean)
476          * subgraph bulk accessor method}.
477          * 
478          * @param rootLocation the root of the branch of nodes to retrieve
479          * @param maximumDepth the maximum depth to retrieve; a negative number indicates that the entire branch should be
480          *        retrieved
481          * @return the list of nodes in the branch rooted at {@code rootLocation}
482          */
483         public List<MapNode> getBranch( Location rootLocation,
484                                         int maximumDepth ) {
485             assert rootLocation.getUuid() != null || rootLocation.getPath() != null;
486             UUID subgraphRootUuid = rootLocation.getUuid();
487 
488             if (subgraphRootUuid == null) {
489                 MapNode rootNode = getNode(rootLocation.getPath());
490                 subgraphRootUuid = rootNode.getUuid();
491                 assert subgraphRootUuid != null;
492             }
493 
494             SubgraphQuery subgraph = SubgraphQuery.create(entityManager, workspaceId, subgraphRootUuid, maximumDepth);
495 
496             List<NodeEntity> entities = subgraph.getNodes(true, true);
497             List<MapNode> nodes = new ArrayList<MapNode>(entities.size());
498 
499             for (NodeEntity entity : entities) {
500                 nodes.add(new JpaNode(entity));
501             }
502 
503             subgraph.close();
504 
505             return nodes;
506         }
507 
508         /**
509          * This connector does not support connector-level, persistent locking of nodes.
510          * 
511          * @param node
512          * @param lockScope
513          * @param lockTimeoutInMillis
514          * @throws LockFailedException
515          */
516         public void lockNode( MapNode node,
517                               LockScope lockScope,
518                               long lockTimeoutInMillis ) throws LockFailedException {
519             // Locking is not supported by this connector
520         }
521 
522         /**
523          * This connector does not support connector-level, persistent locking of nodes.
524          * 
525          * @param node the node to be unlocked
526          */
527         public void unlockNode( MapNode node ) {
528             // Locking is not supported by this connector
529         }
530 
531     }
532 
533     /**
534      * Adapter between the {@link NodeEntity persistent entity for nodes} and the {@link MapNode map repository interface for
535      * nodes}.
536      */
537     @SuppressWarnings( "synthetic-access" )
538     @NotThreadSafe
539     protected class JpaNode implements MapNode {
540         private final NodeEntity entity;
541         private Map<Name, Property> properties = null;
542 
543         protected JpaNode( NodeEntity entity ) {
544             this.entity = entity;
545         }
546 
547         public JpaNode( UUID uuid ) {
548             this.entity = new NodeEntity();
549             entity.setNodeUuidString(uuid.toString());
550         }
551 
552         private final JpaNode jpaNodeFor( MapNode node ) {
553             if (!(node instanceof JpaNode)) {
554                 throw new IllegalStateException();
555             }
556             return (JpaNode)node;
557         }
558 
559         public void addChild( int index,
560                               MapNode child ) {
561             entity.addChild(index, jpaNodeFor(child).entity);
562         }
563 
564         public void addChild( MapNode child ) {
565             entity.addChild(jpaNodeFor(child).entity);
566         }
567 
568         public List<MapNode> getChildren() {
569             List<MapNode> children = new ArrayList<MapNode>(entity.getChildren().size());
570 
571             for (NodeEntity child : entity.getChildren()) {
572                 children.add(new JpaNode(child));
573             }
574 
575             return Collections.unmodifiableList(children);
576         }
577 
578         public Segment getName() {
579             return pathFactory.createSegment(nameFactory.create(entity.getChildNamespace().getUri(), entity.getChildName()),
580                                              entity.getSameNameSiblingIndex());
581         }
582 
583         public MapNode getParent() {
584             if (entity.getParent() == null) return null;
585             return new JpaNode(entity.getParent());
586         }
587 
588         private void ensurePropertiesLoaded() {
589             if (properties != null) return;
590 
591             Collection<Property> propsCollection = new LinkedList<Property>();
592 
593             if (entity.getData() != null) {
594                 Serializer serializer = new Serializer(context, true);
595                 ObjectInputStream ois = null;
596 
597                 try {
598                     LargeValueSerializer largeValues = new LargeValueSerializer(entity);
599                     ois = new ObjectInputStream(new ByteArrayInputStream(entity.getData()));
600                     serializer.deserializeAllProperties(ois, propsCollection, largeValues);
601 
602                 } catch (IOException ioe) {
603                     throw new IllegalStateException(ioe);
604                 } catch (ClassNotFoundException cnfe) {
605                     throw new IllegalStateException(cnfe);
606                 } finally {
607                     try {
608                         if (ois != null) ois.close();
609                     } catch (Exception ex) {
610                     }
611                 }
612             }
613 
614             PropertyFactory propertyFactory = context.getPropertyFactory();
615             Map<Name, Property> properties = new HashMap<Name, Property>();
616             properties.put(ModeShapeLexicon.UUID, propertyFactory.create(ModeShapeLexicon.UUID, getUuid()));
617             for (Property prop : propsCollection) {
618                 properties.put(prop.getName(), prop);
619             }
620 
621             this.properties = properties;
622         }
623 
624         private void serializeProperties() {
625             Serializer serializer = new Serializer(context, true);
626             ObjectOutputStream oos = null;
627 
628             try {
629                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
630                 oos = new ObjectOutputStream(baos);
631 
632                 LargeValueSerializer largeValues = new LargeValueSerializer(entity);
633                 // dna:uuid prop is in collection but won't be serialized
634                 int numberOfPropertiesToSerialize = properties.size() - 1;
635                 serializer.serializeProperties(oos,
636                                                numberOfPropertiesToSerialize,
637                                                properties.values(),
638                                                largeValues,
639                                                Serializer.NO_REFERENCES_VALUES);
640                 oos.flush();
641                 entity.setData(baos.toByteArray());
642                 entity.setPropertyCount(properties.size());
643             } catch (IOException ioe) {
644                 throw new IllegalStateException(ioe);
645             } finally {
646                 try {
647                     if (oos != null) oos.close();
648                 } catch (Exception ignore) {
649                 }
650             }
651         }
652 
653         public MapNode removeProperty( Name propertyName ) {
654             ensurePropertiesLoaded();
655 
656             if (properties.containsKey(propertyName)) {
657                 properties.remove(propertyName);
658                 serializeProperties();
659             }
660             return this;
661         }
662 
663         public Map<Name, Property> getProperties() {
664             ensurePropertiesLoaded();
665             return properties;
666         }
667 
668         public Property getProperty( ExecutionContext context,
669                                      String name ) {
670             return getProperty(context.getValueFactories().getNameFactory().create(name));
671         }
672 
673         public Property getProperty( Name name ) {
674             ensurePropertiesLoaded();
675             return properties.get(name);
676         }
677 
678         public Set<Name> getUniqueChildNames() {
679             List<NodeEntity> children = entity.getChildren();
680             Set<Name> uniqueNames = new HashSet<Name>(children.size());
681 
682             for (NodeEntity child : children) {
683                 uniqueNames.add(nameFactory.create(child.getChildNamespace().getUri(), child.getChildName()));
684             }
685 
686             return uniqueNames;
687         }
688 
689         public UUID getUuid() {
690             if (entity.getNodeUuidString() == null) return null;
691             return UUID.fromString(entity.getNodeUuidString());
692         }
693 
694         public boolean removeChild( MapNode child ) {
695 
696             /*
697              * The NodeEntity.equals method compares on the Hibernate identifier to avoid
698              * confusing Hibernate.  However, different nodes can be loaded in the same 
699              * session for the same UUID in the same workspace, forcing us to roll our own
700              * implementation of indexOf that tests for the equality of the NodeEntity UUIDs, 
701              * rather than their Hibernate identifiers.
702              */
703             List<NodeEntity> children = entity.getChildren();
704 
705             int index = -1;
706             String childUuidString = jpaNodeFor(child).entity.getNodeUuidString();
707             for (int i = 0; i < children.size(); i++) {
708                 if (childUuidString.equals(children.get(i).getNodeUuidString())) {
709                     index = i;
710                     break;
711                 }
712             }
713 
714             // int index = entity.getChildren().indexOf(jpaNodeFor(child).entity);
715             // assert entity.getChildren().contains(jpaNodeFor(child).entity);
716             if (index < 0) return false;
717 
718             entity.removeChild(index);
719 
720             assert !entity.getChildren().contains(child);
721             assert child.getParent() == null;
722 
723             return true;
724         }
725 
726         public void clearChildren() {
727             entity.getChildren().clear();
728         }
729 
730         public void setName( Segment name ) {
731             entity.setChildNamespace(namespaceEntities.get(name.getName().getNamespaceUri(), true));
732             // entity.setChildNamespace(NamespaceEntity.findByUri(entityManager, name.getName().getNamespaceUri(), true));
733             entity.setChildName(name.getName().getLocalName());
734             entity.setSameNameSiblingIndex(name.getIndex());
735         }
736 
737         public void setParent( MapNode parent ) {
738             if (parent == null) {
739                 entity.setParent(null);
740             } else {
741                 entity.setParent(jpaNodeFor(parent).entity);
742             }
743         }
744 
745         public MapNode setProperty( ExecutionContext context,
746                                     String name,
747                                     Object... values ) {
748             PropertyFactory propertyFactory = context.getPropertyFactory();
749 
750             return this.setProperty(propertyFactory.create(nameFactory.create(name), values));
751         }
752 
753         public MapNode setProperty( Property property ) {
754             ensurePropertiesLoaded();
755 
756             properties.put(property.getName(), property);
757             serializeProperties();
758 
759             return this;
760         }
761 
762         public MapNode setProperties( Iterable<Property> properties ) {
763             ensurePropertiesLoaded();
764 
765             for (Property property : properties) {
766                 this.properties.put(property.getName(), property);
767             }
768 
769             serializeProperties();
770 
771             return this;
772         }
773 
774         @Override
775         public String toString() {
776             if (entity.getNodeUuidString().equals(rootNodeUuid.toString())) return "<root>";
777             return getName().getString() + " (" + entity.getNodeUuidString() + ")";
778         }
779 
780         @Override
781         public boolean equals( Object obj ) {
782             if (!(obj instanceof JpaNode)) return false;
783 
784             JpaNode other = (JpaNode)obj;
785             return entity.getNodeUuidString().equals(other.entity.getNodeUuidString());
786         }
787 
788         @Override
789         public int hashCode() {
790             return entity.getNodeUuidString().hashCode();
791         }
792 
793     }
794 
795     protected class LargeValueSerializer implements LargeValues {
796         private final NodeEntity node;
797         private final Set<String> written;
798 
799         public LargeValueSerializer( NodeEntity entity ) {
800             this.node = entity;
801             this.written = null;
802         }
803 
804         public LargeValueSerializer( NodeEntity entity,
805                                      Set<String> written ) {
806             this.node = entity;
807             this.written = written;
808         }
809 
810         /**
811          * {@inheritDoc}
812          * 
813          * @see org.modeshape.connector.store.jpa.util.Serializer.LargeValues#getMinimumSize()
814          */
815         public long getMinimumSize() {
816             return minimumSizeOfLargeValuesInBytes;
817         }
818 
819         /**
820          * {@inheritDoc}
821          * 
822          * @see org.modeshape.connector.store.jpa.util.Serializer.LargeValues#read(org.modeshape.graph.property.ValueFactories,
823          *      byte[], long)
824          */
825         public Object read( ValueFactories valueFactories,
826                             byte[] hash,
827                             long length ) throws IOException {
828             String hashStr = StringUtil.getHexString(hash);
829             // Find the large value ...
830             LargeValueEntity entity = entityManager.find(LargeValueEntity.class, hashStr);
831             if (entity != null) {
832                 // Find the large value from the existing property entity ...
833                 byte[] data = entity.getData();
834                 if (entity.isCompressed()) {
835                     InputStream stream = new GZIPInputStream(new ByteArrayInputStream(data));
836                     try {
837                         data = IoUtil.readBytes(stream);
838                     } finally {
839                         stream.close();
840                     }
841                 }
842                 return valueFactories.getValueFactory(entity.getType()).create(data);
843             }
844             throw new IOException(JpaConnectorI18n.unableToReadLargeValue.text(getSourceName(), hashStr));
845         }
846 
847         /**
848          * {@inheritDoc}
849          * 
850          * @see org.modeshape.connector.store.jpa.util.Serializer.LargeValues#write(byte[], long,
851          *      org.modeshape.graph.property.PropertyType, java.lang.Object)
852          */
853         public void write( byte[] hash,
854                            long length,
855                            PropertyType type,
856                            Object value ) throws IOException {
857             if (value == null) return;
858             String hashStr = StringUtil.getHexString(hash);
859             if (written != null) written.add(hashStr);
860 
861             // Look for an existing value in the collection ...
862             for (LargeValueEntity existing : node.getLargeValues()) {
863                 if (existing.getHash().equals(hashStr)) {
864                     // Already associated with this properties entity
865                     return;
866                 }
867             }
868             LargeValueEntity entity = entityManager.find(LargeValueEntity.class, hashStr);
869             if (entity == null) {
870                 // We have to create the large value entity ...
871                 entity = new LargeValueEntity();
872                 entity.setCompressed(compressData);
873                 entity.setHash(hashStr);
874                 entity.setLength(length);
875                 entity.setType(type);
876                 ValueFactories factories = context.getValueFactories();
877                 byte[] bytes = null;
878                 switch (type) {
879                     case BINARY:
880                         Binary binary = factories.getBinaryFactory().create(value);
881                         InputStream stream = null;
882                         try {
883                             binary.acquire();
884                             stream = binary.getStream();
885                             if (compressData) stream = new GZIPInputStream(stream);
886                             bytes = IoUtil.readBytes(stream);
887                         } finally {
888                             try {
889                                 if (stream != null) stream.close();
890                             } finally {
891                                 binary.release();
892                             }
893                         }
894                         break;
895                     case URI:
896                         // This will be treated as a string ...
897                     default:
898                         String str = factories.getStringFactory().create(value);
899                         if (compressData) {
900                             ByteArrayOutputStream bs = new ByteArrayOutputStream();
901                             OutputStream strStream = new GZIPOutputStream(bs);
902                             try {
903                                 IoUtil.write(str, strStream);
904                             } finally {
905                                 strStream.close();
906                             }
907                             bytes = bs.toByteArray();
908                         } else {
909                             bytes = str.getBytes();
910                         }
911                         break;
912                 }
913                 entity.setData(bytes);
914                 entityManager.persist(entity);
915             }
916             // Now associate the large value with the properties entity ...
917             assert entity.getHash() != null;
918             node.getLargeValues().add(entity);
919         }
920 
921     }
922 
923 }