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.repository.sequencer;
025    
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.math.BigDecimal;
029    import java.util.Calendar;
030    import java.util.Collections;
031    import java.util.Date;
032    import java.util.HashSet;
033    import java.util.Set;
034    import javax.jcr.Node;
035    import javax.jcr.PathNotFoundException;
036    import javax.jcr.Property;
037    import javax.jcr.PropertyIterator;
038    import javax.jcr.PropertyType;
039    import javax.jcr.RepositoryException;
040    import javax.jcr.Session;
041    import javax.jcr.Value;
042    import org.jboss.dna.common.collection.Problems;
043    import org.jboss.dna.common.util.Logger;
044    import org.jboss.dna.graph.ExecutionContext;
045    import org.jboss.dna.graph.property.Binary;
046    import org.jboss.dna.graph.property.DateTime;
047    import org.jboss.dna.graph.property.Name;
048    import org.jboss.dna.graph.property.NamespaceRegistry;
049    import org.jboss.dna.graph.property.Path;
050    import org.jboss.dna.graph.property.PathFactory;
051    import org.jboss.dna.graph.property.ValueFactories;
052    import org.jboss.dna.graph.sequencer.SequencerContext;
053    import org.jboss.dna.graph.sequencer.StreamSequencer;
054    import org.jboss.dna.repository.RepositoryI18n;
055    import org.jboss.dna.repository.mimetype.MimeType;
056    import org.jboss.dna.repository.observation.NodeChange;
057    import org.jboss.dna.repository.util.JcrExecutionContext;
058    import org.jboss.dna.repository.util.RepositoryNodePath;
059    
060    /**
061     * An adapter class that wraps a {@link StreamSequencer} instance to be a {@link Sequencer}.
062     * 
063     * @author Randall Hauch
064     * @author John Verhaeg
065     */
066    public class StreamSequencerAdapter implements Sequencer {
067    
068        private SequencerConfig configuration;
069        private final StreamSequencer streamSequencer;
070    
071        public StreamSequencerAdapter( StreamSequencer streamSequencer ) {
072            this.streamSequencer = streamSequencer;
073        }
074    
075        /**
076         * {@inheritDoc}
077         */
078        public SequencerConfig getConfiguration() {
079            return this.configuration;
080        }
081    
082        /**
083         * {@inheritDoc}
084         */
085        public void setConfiguration( SequencerConfig configuration ) {
086            this.configuration = configuration;
087        }
088    
089        /**
090         * {@inheritDoc}
091         */
092        public void execute( Node input,
093                             String sequencedPropertyName,
094                             NodeChange changes,
095                             Set<RepositoryNodePath> outputPaths,
096                             JcrExecutionContext execContext,
097                             Problems problems ) throws RepositoryException, SequencerException {
098            // 'sequencedPropertyName' contains the name of the modified property on 'input' that resulted in the call to this
099            // sequencer.
100            // 'changes' contains all of the changes to this node that occurred in the transaction.
101            // 'outputPaths' contains the paths of the node(s) where this sequencer is to save it's data.
102    
103            // Get the property that contains the data, given by 'propertyName' ...
104            Property sequencedProperty = null;
105            try {
106                sequencedProperty = input.getProperty(sequencedPropertyName);
107            } catch (PathNotFoundException e) {
108                String msg = RepositoryI18n.unableToFindPropertyForSequencing.text(sequencedPropertyName, input.getPath());
109                throw new SequencerException(msg, e);
110            }
111    
112            // Get the binary property with the image content, and build the image metadata from the image ...
113            SequencerOutputMap output = new SequencerOutputMap(execContext.getValueFactories());
114            InputStream stream = null;
115            Throwable firstError = null;
116            try {
117                stream = sequencedProperty.getStream();
118                SequencerContext sequencerContext = createSequencerContext(input, sequencedProperty, execContext, problems);
119                this.streamSequencer.sequence(stream, output, sequencerContext);
120            } catch (Throwable t) {
121                // Record the error ...
122                firstError = t;
123            } finally {
124                if (stream != null) {
125                    // Always close the stream, recording the error if we've not yet seen an error
126                    try {
127                        stream.close();
128                    } catch (Throwable t) {
129                        if (firstError == null) firstError = t;
130                    } finally {
131                        stream = null;
132                    }
133                }
134                if (firstError != null) {
135                    // Wrap and throw the first error that we saw ...
136                    throw new SequencerException(firstError);
137                }
138            }
139    
140            // Find each output node and save the image metadata there ...
141            for (RepositoryNodePath outputPath : outputPaths) {
142                Session session = null;
143                try {
144                    // Get the name of the repository workspace and the path to the output node
145                    final String repositoryWorkspaceName = outputPath.getRepositoryWorkspaceName();
146                    final String nodePath = outputPath.getNodePath();
147    
148                    // Create a session to the repository where the data should be written ...
149                    session = execContext.getSessionFactory().createSession(repositoryWorkspaceName);
150    
151                    // Find or create the output node in this session ...
152                    Node outputNode = execContext.getTools().findOrCreateNode(session, nodePath);
153    
154                    // Now save the image metadata to the output node ...
155                    if (saveOutput(outputNode, output, execContext)) {
156                        session.save();
157                    }
158                } finally {
159                    // Always close the session ...
160                    if (session != null) session.logout();
161                }
162            }
163        }
164    
165        /**
166         * Save the sequencing output to the supplied node. This method does not need to save the output, as that is done by the
167         * caller of this method.
168         * 
169         * @param outputNode the existing node onto (or below) which the output is to be written; never null
170         * @param output the (immutable) sequencing output; never null
171         * @param context the execution context for this sequencing operation; never null
172         * @return true if the output was written to the node, or false if no information was written
173         * @throws RepositoryException
174         */
175        protected boolean saveOutput( Node outputNode,
176                                      SequencerOutputMap output,
177                                      JcrExecutionContext context ) throws RepositoryException {
178            if (output.isEmpty()) return false;
179            final PathFactory pathFactory = context.getValueFactories().getPathFactory();
180            final NamespaceRegistry namespaceRegistry = context.getNamespaceRegistry();
181            final Path outputNodePath = pathFactory.create(outputNode.getPath());
182            final Name jcrPrimaryTypePropertyName = context.getValueFactories().getNameFactory().create("jcr:primaryType");
183    
184            // Iterate over the entries in the output, in Path's natural order (shorter paths first and in lexicographical order by
185            // prefix and name)
186            for (SequencerOutputMap.Entry entry : output) {
187                Path targetNodePath = entry.getPath();
188                Name primaryType = entry.getPrimaryTypeValue();
189    
190                // Resolve this path relative to the output node path, handling any parent or self references ...
191                Path absolutePath = targetNodePath.isAbsolute() ? targetNodePath : outputNodePath.resolve(targetNodePath);
192                Path relativePath = absolutePath.relativeTo(outputNodePath);
193    
194                // Find or add the node (which may involve adding intermediate nodes) ...
195                Node targetNode = outputNode;
196                for (int i = 0, max = relativePath.size(); i != max; ++i) {
197                    Path.Segment segment = relativePath.getSegment(i);
198                    String qualifiedName = segment.getString(namespaceRegistry);
199                    if (targetNode.hasNode(qualifiedName)) {
200                        targetNode = targetNode.getNode(qualifiedName);
201                    } else {
202                        // It doesn't exist, so create it ...
203                        if (segment.hasIndex()) {
204                            // Use a name without an index ...
205                            qualifiedName = segment.getName().getString(namespaceRegistry);
206                        }
207                        // We only have the primary type for the final one ...
208                        if (i == (max - 1) && primaryType != null) {
209                            targetNode = targetNode.addNode(qualifiedName, primaryType.getString(namespaceRegistry,
210                                                                                                 Path.NO_OP_ENCODER));
211                        } else {
212                            targetNode = targetNode.addNode(qualifiedName);
213                        }
214                    }
215                    assert targetNode != null;
216                }
217                assert targetNode != null;
218    
219                // Set all of the properties on this
220                for (SequencerOutputMap.PropertyValue property : entry.getPropertyValues()) {
221                    String propertyName = property.getName().getString(namespaceRegistry, Path.NO_OP_ENCODER);
222                    Object value = property.getValue();
223                    if (jcrPrimaryTypePropertyName.equals(property.getName())) {
224                        // Skip the primary type property (which is protected in Jackrabbit 1.5)
225                        Logger.getLogger(this.getClass()).trace("Skipping property {0}/{1}={2}",
226                                                                targetNode.getPath(),
227                                                                propertyName,
228                                                                value);
229                        continue;
230                    }
231                    Logger.getLogger(this.getClass()).trace("Writing property {0}/{1}={2}", targetNode.getPath(), propertyName, value);
232                    if (value instanceof Boolean) {
233                        targetNode.setProperty(propertyName, ((Boolean)value).booleanValue());
234                    } else if (value instanceof String) {
235                        targetNode.setProperty(propertyName, (String)value);
236                    } else if (value instanceof String[]) {
237                        targetNode.setProperty(propertyName, (String[])value);
238                    } else if (value instanceof Integer) {
239                        targetNode.setProperty(propertyName, ((Integer)value).intValue());
240                    } else if (value instanceof Short) {
241                        targetNode.setProperty(propertyName, ((Short)value).shortValue());
242                    } else if (value instanceof Long) {
243                        targetNode.setProperty(propertyName, ((Long)value).longValue());
244                    } else if (value instanceof Float) {
245                        targetNode.setProperty(propertyName, ((Float)value).floatValue());
246                    } else if (value instanceof Double) {
247                        targetNode.setProperty(propertyName, ((Double)value).doubleValue());
248                    } else if (value instanceof Binary) {
249                        Binary binaryValue = (Binary)value;
250                        try {
251                            binaryValue.acquire();
252                            targetNode.setProperty(propertyName, binaryValue.getStream());
253                        } finally {
254                            binaryValue.release();
255                        }
256                    } else if (value instanceof BigDecimal) {
257                        targetNode.setProperty(propertyName, ((BigDecimal)value).doubleValue());
258                    } else if (value instanceof DateTime) {
259                        targetNode.setProperty(propertyName, ((DateTime)value).toCalendar());
260                    } else if (value instanceof Date) {
261                        DateTime instant = context.getValueFactories().getDateFactory().create((Date)value);
262                        targetNode.setProperty(propertyName, instant.toCalendar());
263                    } else if (value instanceof Calendar) {
264                        targetNode.setProperty(propertyName, (Calendar)value);
265                    } else if (value instanceof Name) {
266                        Name nameValue = (Name)value;
267                        String stringValue = nameValue.getString(namespaceRegistry);
268                        targetNode.setProperty(propertyName, stringValue);
269                    } else if (value instanceof Path) {
270                        // Find the path to reference node ...
271                        Path pathToReferencedNode = (Path)value;
272                        if (!pathToReferencedNode.isAbsolute()) {
273                            // Resolve the path relative to the output node ...
274                            pathToReferencedNode = outputNodePath.resolve(pathToReferencedNode);
275                        }
276                        // Find the referenced node ...
277                        try {
278                            Node referencedNode = outputNode.getNode(pathToReferencedNode.getString());
279                            targetNode.setProperty(propertyName, referencedNode);
280                        } catch (PathNotFoundException e) {
281                            String msg = RepositoryI18n.errorGettingNodeRelativeToNode.text(value, outputNode.getPath());
282                            throw new SequencerException(msg, e);
283                        }
284                    } else if (value == null) {
285                        // Remove the property ...
286                        targetNode.setProperty(propertyName, (String)null);
287                    } else {
288                        String msg = RepositoryI18n.unknownPropertyValueType.text(value, value.getClass().getName());
289                        throw new SequencerException(msg);
290                    }
291                }
292            }
293    
294            return true;
295        }
296    
297        protected String[] extractMixinTypes( Object value ) {
298            if (value instanceof String[]) return (String[])value;
299            if (value instanceof String) return new String[] {(String)value};
300            return null;
301        }
302    
303        protected SequencerContext createSequencerContext( Node input,
304                                                           Property sequencedProperty,
305                                                           ExecutionContext context,
306                                                           Problems problems ) throws RepositoryException {
307            assert input != null;
308            assert sequencedProperty != null;
309            assert context != null;
310            assert problems != null;
311            // Translate JCR path and property values to DNA constructs and cache them to improve performance and prevent
312            // RepositoryException from being thrown by getters
313            // Note: getMimeType() will still operate lazily, and thus throw a SequencerException, since it is very intrusive and
314            // potentially slow-running.
315            ValueFactories factories = context.getValueFactories();
316            Path path = factories.getPathFactory().create(input.getPath());
317            Set<org.jboss.dna.graph.property.Property> props = new HashSet<org.jboss.dna.graph.property.Property>();
318            for (PropertyIterator iter = input.getProperties(); iter.hasNext();) {
319                javax.jcr.Property jcrProp = iter.nextProperty();
320                org.jboss.dna.graph.property.Property prop;
321                if (jcrProp.getDefinition().isMultiple()) {
322                    Value[] jcrVals = jcrProp.getValues();
323                    Object[] vals = new Object[jcrVals.length];
324                    int ndx = 0;
325                    for (Value jcrVal : jcrVals) {
326                        vals[ndx++] = convert(factories, jcrProp.getName(), jcrVal);
327                    }
328                    prop = context.getPropertyFactory().create(factories.getNameFactory().create(jcrProp.getName()), vals);
329                } else {
330                    Value jcrVal = jcrProp.getValue();
331                    Object val = convert(factories, jcrProp.getName(), jcrVal);
332                    prop = context.getPropertyFactory().create(factories.getNameFactory().create(jcrProp.getName()), val);
333                }
334                props.add(prop);
335            }
336            props = Collections.unmodifiableSet(props);
337            String mimeType = getMimeType(sequencedProperty, path.getLastSegment().getName().getLocalName());
338            return new SequencerContext(context, path, props, mimeType, problems);
339        }
340    
341        protected Object convert( ValueFactories factories,
342                                  String name,
343                                  Value jcrValue ) throws RepositoryException {
344            switch (jcrValue.getType()) {
345                case PropertyType.BINARY: {
346                    return factories.getBinaryFactory().create(jcrValue.getStream());
347                }
348                case PropertyType.BOOLEAN: {
349                    return factories.getBooleanFactory().create(jcrValue.getBoolean());
350                }
351                case PropertyType.DATE: {
352                    return factories.getDateFactory().create(jcrValue.getDate());
353                }
354                case PropertyType.DOUBLE: {
355                    return factories.getDoubleFactory().create(jcrValue.getDouble());
356                }
357                case PropertyType.LONG: {
358                    return factories.getLongFactory().create(jcrValue.getLong());
359                }
360                case PropertyType.NAME: {
361                    return factories.getNameFactory().create(jcrValue.getString());
362                }
363                case PropertyType.PATH: {
364                    return factories.getPathFactory().create(jcrValue.getString());
365                }
366                case PropertyType.REFERENCE: {
367                    return factories.getReferenceFactory().create(jcrValue.getString());
368                }
369                case PropertyType.STRING: {
370                    return factories.getStringFactory().create(jcrValue.getString());
371                }
372                default: {
373                    throw new RepositoryException(RepositoryI18n.unknownPropertyValueType.text(name, jcrValue.getType()));
374                }
375            }
376        }
377    
378        @SuppressWarnings( "null" )
379        // The need for the SuppressWarnings looks like an Eclipse bug
380        protected String getMimeType( Property sequencedProperty,
381                                      String name ) {
382            SequencerException err = null;
383            String mimeType = null;
384            InputStream stream = null;
385            try {
386                stream = sequencedProperty.getStream();
387                mimeType = MimeType.of(name, stream);
388                return mimeType;
389            } catch (Exception error) {
390                err = new SequencerException(error);
391            } finally {
392                if (stream != null) {
393                    try {
394                        stream.close();
395                    } catch (IOException error) {
396                        // Only throw exception if an exception was not already thrown
397                        if (err == null) err = new SequencerException(error);
398                    }
399                }
400            }
401            if (err != null) throw err;
402            return mimeType;
403        }
404    }