Return-Path: Delivered-To: apmail-jackrabbit-commits-archive@www.apache.org Received: (qmail 67546 invoked from network); 28 Jan 2009 13:36:23 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.2) by minotaur.apache.org with SMTP; 28 Jan 2009 13:36:23 -0000 Received: (qmail 83045 invoked by uid 500); 28 Jan 2009 13:36:23 -0000 Delivered-To: apmail-jackrabbit-commits-archive@jackrabbit.apache.org Received: (qmail 83011 invoked by uid 500); 28 Jan 2009 13:36:23 -0000 Mailing-List: contact commits-help@jackrabbit.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@jackrabbit.apache.org Delivered-To: mailing list commits@jackrabbit.apache.org Received: (qmail 83002 invoked by uid 99); 28 Jan 2009 13:36:23 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 28 Jan 2009 05:36:23 -0800 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 28 Jan 2009 13:36:12 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 5AECE2388A23; Wed, 28 Jan 2009 13:35:49 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r738480 [2/3] - in /jackrabbit/sandbox/spi: ./ spi2davex/ spi2davex/src/ spi2davex/src/main/ spi2davex/src/main/java/ spi2davex/src/main/java/org/ spi2davex/src/main/java/org/apache/ spi2davex/src/main/java/org/apache/jackrabbit/ spi2davex/... Date: Wed, 28 Jan 2009 13:35:48 -0000 To: commits@jackrabbit.apache.org From: angela@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20090128133549.5AECE2388A23@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java?rev=738480&view=auto ============================================================================== --- jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java (added) +++ jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java Wed Jan 28 13:35:47 2009 @@ -0,0 +1,1088 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.spi2davex; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PathFactory; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.QValueFactory; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.util.ISO8601; +import org.apache.jackrabbit.util.TransientFileFactory; +import org.apache.jackrabbit.uuid.UUID; +import org.apache.jackrabbit.value.ValueFactoryImpl; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.property.ValuesProperty; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.DavException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.TimeZone; + +/** + * ValueFactoryImpl... + */ +class QValueFactoryImpl implements QValueFactory { + + private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance(); + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + private static final String DEFAULT_ENCODING = "UTF-8"; + + private final NamePathResolver resolver; + private final ValueLoader loader; + + public QValueFactoryImpl() { + this(null, null); + } + + QValueFactoryImpl(NamePathResolver resolver, ValueLoader loader) { + this.resolver = resolver; + this.loader = loader; + } + + /** + * Create a BINARY QValue with the given length and the given uri used + * to retrieve the value. + * + * @param length Length of the binary value. + * @param uri Uri from which the the binary value can be accessed. + * @param index The index of the value within the values array. + * @return a new BINARY QValue. + */ + QValue create(long length, String uri, int index) { + if (loader == null) { + throw new IllegalStateException(); + } + return new BinaryQValue(length, uri, index); + } + + /** + * + * @param uri The Uri from which the type info can be retrieved. + * @return the type of the property with the given uri. + * @throws IOException If an error occurs. + * @throws RepositoryException If an error occurs. + */ + int retrieveType(String uri) throws IOException, RepositoryException { + return loader.loadType(uri); + } + + //------------------------------------------------------< QValueFactory >--- + /** + * @see QValueFactory#create(String, int) + */ + public QValue create(String value, int type) throws RepositoryException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + try { + switch (type) { + case PropertyType.BOOLEAN: + return new QValueImpl(Boolean.valueOf(value)); + case PropertyType.DATE: { + Calendar cal = ISO8601.parse(value); + if (cal == null) { + throw new ValueFormatException("not a valid date: " + value); + } + return new DateQValue(cal); + } + case PropertyType.DOUBLE: + return new QValueImpl(Double.valueOf(value)); + case PropertyType.LONG: + return new QValueImpl(Long.valueOf(value)); + case PropertyType.PATH: + return new QValueImpl(PATH_FACTORY.create(value)); + case PropertyType.NAME: + return new QValueImpl(NAME_FACTORY.create(value)); + case PropertyType.STRING: + case PropertyType.REFERENCE: + return new QValueImpl(value, type); + case PropertyType.BINARY: + return new BinaryQValue(value.getBytes(DEFAULT_ENCODING)); + default: + throw new IllegalArgumentException("illegal type"); + } + } catch (NumberFormatException ex) { + throw new ValueFormatException(ex); + } catch (UnsupportedEncodingException ex) { + throw new RepositoryException(ex); + } + } + + /** + * @see QValueFactory#create(Calendar) + */ + public QValue create(Calendar value) { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + // Calendar is not constant, must create a clone + return new DateQValue((Calendar) value.clone()); + } + + /** + * @see QValueFactory#create(double) + */ + public QValue create(double value) { + return new QValueImpl(new Double(value)); + } + + /** + * @see QValueFactory#create(long) + */ + public QValue create(long value) { + return new QValueImpl(new Long(value)); + } + + public QValue create(boolean value) throws RepositoryException { + return new QValueImpl(new Boolean(value)); + } + + /** + * @see QValueFactory#create(Name) + */ + public QValue create(Name value) { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new QValueImpl(value); + } + + /** + * @see QValueFactory#create(Path) + */ + public QValue create(Path value) { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new QValueImpl(value); + } + + /** + * @see QValueFactory#create(byte[]) + */ + public QValue create(byte[] value) { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + /** + * @see QValueFactory#create(InputStream) + */ + public QValue create(InputStream value) throws IOException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + /** + * @see QValueFactory#create(File) + */ + public QValue create(File value) throws IOException { + if (value == null) { + throw new IllegalArgumentException("Cannot create QValue from null value."); + } + return new BinaryQValue(value); + } + + /** + * @see QValueFactory#computeAutoValues(QPropertyDefinition) + */ + public QValue[] computeAutoValues(QPropertyDefinition propertyDefinition) throws RepositoryException { + Name nodeType = propertyDefinition.getDeclaringNodeType(); + Name name = propertyDefinition.getName(); + + if (NameConstants.NT_HIERARCHYNODE.equals(nodeType) && NameConstants.JCR_CREATED.equals(name)) { + return new QValue[] { create(Calendar.getInstance()) }; + } else if (NameConstants.NT_RESOURCE.equals(nodeType) && NameConstants.JCR_LASTMODIFIED.equals(name)) { + return new QValue[] { create(Calendar.getInstance()) }; + } else if (NameConstants.MIX_REFERENCEABLE.equals(nodeType) && NameConstants.JCR_UUID.equals(name)) { + return new QValue[] { create(UUID.randomUUID().toString(), PropertyType.STRING) }; + } else { + throw new RepositoryException("createFromDefinition not implemented for: " + name); + } + } + + //--------------------------------------------------------< Inner Class >--- + /** + * QValue implementation for all valid PropertyTypes + * except for BINARY. + * @see QValueFactoryImpl.BinaryQValue + */ + private class QValueImpl implements QValue, Serializable { + + private final Object val; + private final int type; + + private QValueImpl(String value, int type) { + val = value; + this.type = type; + } + + private QValueImpl(Long value) { + val = value; + type = PropertyType.LONG; + } + + private QValueImpl(Double value) { + val = value; + type = PropertyType.DOUBLE; + } + + private QValueImpl(Boolean value) { + val = value; + type = PropertyType.BOOLEAN; + } + + private QValueImpl(Calendar value) { + val = value; + this.type = PropertyType.DATE; + } + + private QValueImpl(Name value) { + val = value; + type = PropertyType.NAME; + } + + private QValueImpl(Path value) { + val = value; + type = PropertyType.PATH; + } + + protected String getQString(int type) throws RepositoryException { + return getString(); + } + //---------------------------------------------------------< QValue >--- + /** + * @see QValue#getType() + */ + public int getType() { + return type; + } + + /** + * @see QValue#getLength() + */ + public long getLength() throws RepositoryException { + return getString().length(); + } + + /** + * @see QValue#getString() + */ + public String getString() { + return val.toString(); + } + + /** + * @see QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + try { + // convert via string + return new ByteArrayInputStream(getString().getBytes(QValueFactoryImpl.DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(QValueFactoryImpl.DEFAULT_ENCODING + " is not supported encoding on this platform", e); + } + } + + /** + * @see QValue#getCalendar() + */ + public Calendar getCalendar() throws RepositoryException { + if (val instanceof Calendar) { + return (Calendar) ((Calendar) val).clone(); + } else if (val instanceof Double) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(((Double) val).longValue()); + return cal; + } else if (val instanceof Long) { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(((Long) val).longValue()); + return cal; + } else { + String str = getString(); + Calendar cal = ISO8601.parse(str); + if (cal == null) { + int type = getType(); + if (type == PropertyType.LONG) { + cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(new Long(str).longValue()); + } else if (type == PropertyType.DOUBLE) { + cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+00:00")); + cal.setTimeInMillis(new Double(str).longValue()); + } else { + throw new ValueFormatException("not a date string: " + getString()); + } + } + return cal; + } + } + + /** + * @see QValue#getDouble() + */ + public double getDouble() throws RepositoryException { + if (val instanceof Double) { + return ((Double) val).doubleValue(); + } else if (val instanceof Calendar) { + return ((Calendar) val).getTimeInMillis(); + } else { + try { + return Double.parseDouble(getString()); + } catch (NumberFormatException ex) { + int type = getType(); + if (type == PropertyType.DATE) { + Calendar cal = ISO8601.parse(getString()); + if (cal != null) { + return cal.getTimeInMillis(); + } + } + throw new ValueFormatException("not a double: " + getString(), ex); + } + } + } + + /** + * @see QValue#getLong() + */ + public long getLong() throws RepositoryException { + if (val instanceof Long) { + return ((Long) val).longValue(); + } else if (val instanceof Double) { + return ((Double) val).longValue(); + } else if (val instanceof Calendar) { + return ((Calendar) val).getTimeInMillis(); + } else { + String str = getString(); + try { + return Long.parseLong(str); + } catch (NumberFormatException ex) { + int type = getType(); + if (type == PropertyType.DOUBLE) { + return new Double(str).longValue(); + } else if (type == PropertyType.DATE) { + Calendar cal = ISO8601.parse(getString()); + if (cal != null) { + return cal.getTimeInMillis(); + } + } + throw new ValueFormatException("not a long: " + getString(), ex); + } + } + } + + public boolean getBoolean() throws RepositoryException { + if (val instanceof Boolean) { + return ((Boolean) val).booleanValue(); + } else { + try { + return new Boolean(getString()).booleanValue(); + } catch (NumberFormatException ex) { + throw new ValueFormatException("not a long: " + getString(), ex); + } + } + } + + /** + * @see QValue#getName() + */ + public Name getName() throws RepositoryException { + if (val instanceof Name) { + return (Name) val; + } else { + return NAME_FACTORY.create(getString()); + } + } + + /** + * @see QValue#getPath() + */ + public Path getPath() throws RepositoryException { + if (val instanceof Path) { + return (Path) val; + } else { + return PATH_FACTORY.create(getString()); + } + } + + /** + * @see QValue#discard() + */ + public void discard() { + // nothing to do + } + + //---------------------------------------------------------< Object >--- + /** + * Returns the string representation of this internal value. + * + * @return string representation of this internal value. + * @see Object#toString() + */ + public String toString() { + return val.toString(); + } + + /** + * @see Object#equals(Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof QValueImpl) { + QValueImpl other = (QValueImpl) obj; + if (type == other.type && type != PropertyType.UNDEFINED) { + return getString().equals(other.getString()); + } + try { + int type = getType(); + return type == other.getType() && getQString(type).equals(other.getQString(type)); + } catch (RepositoryException e) { + // should never get here. return false. + } + } + return false; + } + + /** + * @return the hashCode of the internal value object. + * @see Object#hashCode() + */ + public int hashCode() { + return val.hashCode(); + } + } + + //--------------------------------------------------------< Inner Class >--- + /** + * Extension for values of type {@link PropertyType#DATE}. + */ + private class DateQValue extends QValueImpl { + + private final String formattedStr; + + private DateQValue(Calendar value) { + super(value); + formattedStr = ISO8601.format(value); + } + + /** + * @return The formatted String of the internal Calendar value. + * @see QValue#getString() + * @see ISO8601#format(Calendar) + */ + public String getString() { + return formattedStr; + } + + //---------------------------------------------------------< Object >--- + /** + * @param obj The object to be checked for equality. + * @return true if the given Object is a DateQValue with an + * equal String representation. + * @see Object#equals(Object) + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DateQValue) { + DateQValue other = (DateQValue) obj; + return formattedStr.equals(other.formattedStr); + } else if (obj instanceof QValueImpl) { + QValueImpl other = (QValueImpl) obj; + return formattedStr.equals(other.getString()) && + other.getType() == PropertyType.DATE; + } + return false; + } + + /** + * @return the hashCode of the formatted String of the Calender value. + * @see Object#hashCode() + */ + public int hashCode() { + return formattedStr.hashCode(); + } + } + + //--------------------------------------------------------< Inner Class >--- + /** + * BinaryQValue represents a binary Value which is + * backed by a resource or byte[]. Unlike BinaryValue it has no + * state, i.e. the getStream() method always returns a fresh + * InputStream instance. + */ + private class BinaryQValue implements QValue, Serializable, ValueLoader.Target { + /** + * empty array + */ + private final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * max size for keeping tmp data in memory + */ + private static final int MAX_BUFFER_SIZE = 0x10000; + + /** + * underlying file + */ + private transient File file; + + /** + * flag indicating if this instance represents a temporary value + * whose dynamically allocated resources can be explicitly freed on + * {@link #discard()}. + */ + private transient boolean temp; + + /** + * Buffer for small-sized data + */ + private byte[] buffer; + + /** + * Converted text + */ + private transient String text = null; + + /** + * URI to retrieve the value from + */ + private String uri; + private long length; + private int index = -1; + private boolean initialized = true; + + private BinaryQValue(long length, String uri, int index) { + this.length = length; + this.uri = uri; + this.index = index; + initialized = false; + } + + /** + * Creates a new BinaryQValue instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

+ * The new instance represents a temporary value whose dynamically + * allocated resources will be freed explicitly on {@link #discard()}. + * + * @param in stream to be represented as a BinaryQValue instance + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + private BinaryQValue(InputStream in) throws IOException { + init(in, true); + } + + + /** + * Creates a new BinaryQValue instance from a + * byte[] array. + * + * @param bytes byte array to be represented as a BinaryQValue + * instance + */ + private BinaryQValue(byte[] bytes) { + buffer = bytes; + file = null; + // this instance is not backed by a temporarily allocated buffer + temp = false; + } + + /** + * Creates a new BinaryQValue instance from a File. + * + * @param file file to be represented as a BinaryQValue instance + * @throws IOException if the file can not be read + */ + private BinaryQValue(File file) throws IOException { + String path = file.getCanonicalPath(); + if (!file.isFile()) { + throw new IOException(path + ": the specified file does not exist"); + } + if (!file.canRead()) { + throw new IOException(path + ": the specified file can not be read"); + } + // this instance is backed by a 'real' file + this.file = file; + // this instance is not backed by temporarily allocated resource/buffer + temp = false; + } + + /** + * Creates a new BinaryQValue instance from an + * InputStream. The contents of the stream is spooled + * to a temporary file or to a byte buffer if its size is smaller than + * {@link #MAX_BUFFER_SIZE}. + *

+ * The temp parameter governs whether dynamically allocated + * resources will be freed explicitly on {@link #discard()}. Note that any + * dynamically allocated resources (temp file/buffer) will be freed + * implicitly once this instance has been gc'ed. + * + * @param in stream to be represented as a BinaryQValue instance + * @param temp flag indicating whether this instance represents a + * temporary value whose resources can be explicitly freed + * on {@link #discard()}. + * @throws IOException if an error occurs while reading from the stream or + * writing to the temporary file + */ + private void init(InputStream in, boolean temp) throws IOException { + byte[] spoolBuffer = new byte[0x2000]; + int read; + int len = 0; + OutputStream out = null; + File spoolFile = null; + try { + while ((read = in.read(spoolBuffer)) > 0) { + if (out != null) { + // spool to temp file + out.write(spoolBuffer, 0, read); + len += read; + } else if (len + read > BinaryQValue.MAX_BUFFER_SIZE) { + // threshold for keeping data in memory exceeded; + // create temp file and spool buffer contents + TransientFileFactory fileFactory = TransientFileFactory.getInstance(); + spoolFile = fileFactory.createTransientFile("bin", null, null); + out = new FileOutputStream(spoolFile); + out.write(buffer, 0, len); + out.write(spoolBuffer, 0, read); + buffer = null; + len += read; + } else { + // reallocate new buffer and spool old buffer contents + if (buffer == null) { + buffer = EMPTY_BYTE_ARRAY; + } + byte[] newBuffer = new byte[len + read]; + System.arraycopy(buffer, 0, newBuffer, 0, len); + System.arraycopy(spoolBuffer, 0, newBuffer, len, read); + buffer = newBuffer; + len += read; + } + } + } finally { + if (out != null) { + out.close(); + } + } + + if (spoolFile == null && buffer == null) { + // input stream was empty -> initialize an empty binary value + this.temp = false; + buffer = EMPTY_BYTE_ARRAY; + } else { + // init vars + file = spoolFile; + this.temp = temp; + } + initialized = true; + } + + //---------------------------------------------------------< QValue >--- + /** + * @see QValue#getType() + */ + public int getType() { + return PropertyType.BINARY; + } + + /** + * Returns the length of this BinaryQValue. + * + * @return The length, in bytes, of this BinaryQValue, + * or -1L if the length can't be determined. + * @see QValue#getLength() + */ + public long getLength() { + if (file != null) { + // this instance is backed by a 'real' file + if (file.exists()) { + return file.length(); + } else { + return -1; + } + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + return buffer.length; + } else { + // value has not yet been read from the server. + return length; + } + } + + /** + * @see QValue#getString() + */ + public String getString() throws RepositoryException { + if (text == null) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + spool(out); + byte[] data = out.toByteArray(); + text = new String(data, QValueFactoryImpl.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + throw new RepositoryException(QValueFactoryImpl.DEFAULT_ENCODING + + " not supported on this platform", e); + } catch (IOException e) { + throw new ValueFormatException("conversion from stream to string failed", e); + } finally { + try { + out.close(); + } catch (IOException e) { + // ignore + } + } + } + return text; + } + + /** + * @see QValue#getStream() + */ + public InputStream getStream() throws RepositoryException { + // if the value has not yet been loaded -> retrieve it first in + // order to make sure that either 'file' or 'buffer' is set. + if (file == null && buffer == null) { + try { + loadBinary(); + } catch (IOException e) { + throw new RepositoryException(e); + } + } + + // always return a 'fresh' stream + if (file != null) { + // this instance is backed by a 'real' file + try { + return new FileInputStream(file); + } catch (FileNotFoundException fnfe) { + throw new RepositoryException("file backing binary value not found", + fnfe); + } + } else { + return new ByteArrayInputStream(buffer); + } + } + + /** + * @see QValue#getName() + */ + public Name getName() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * @see QValue#getCalendar() + */ + public Calendar getCalendar() throws RepositoryException { + Calendar cal = ISO8601.parse(getString()); + if (cal == null) { + throw new ValueFormatException("not a date string: " + getString()); + } else { + return cal; + } + } + + /** + * @see QValue#getDouble() + */ + public double getDouble() throws RepositoryException { + try { + return Double.parseDouble(getString()); + } catch (NumberFormatException ex) { + throw new ValueFormatException(ex); + } + } + + /** + * @see QValue#getLong() + */ + public long getLong() throws RepositoryException { + try { + return Long.parseLong(getString()); + } catch (NumberFormatException ex) { + throw new ValueFormatException(ex); + } + } + + public boolean getBoolean() throws RepositoryException { + return new Boolean(getString()).booleanValue(); + } + + /** + * @see QValue#getPath() + */ + public Path getPath() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + /** + * Frees temporarily allocated resources such as temporary file, buffer, etc. + * If this BinaryQValue is backed by a persistent resource + * calling this method will have no effect. + * @see QValue#discard() + */ + public void discard() { + if (!temp) { + // do nothing if this instance is not backed by temporarily + // allocated resource/buffer + return; + } + if (file != null) { + // this instance is backed by a temp file + file.delete(); + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + buffer = EMPTY_BYTE_ARRAY; + } + } + + //-----------------------------------------------< java.lang.Object >--- + /** + * Returns a string representation of this BinaryQValue + * instance. The string representation of a resource backed value is + * the path of the underlying resource. If this instance is backed by an + * in-memory buffer the generic object string representation of the byte + * array will be used instead. + * + * @return A string representation of this BinaryQValue instance. + */ + public String toString() { + if (file != null) { + // this instance is backed by a 'real' file + return file.toString(); + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + return buffer.toString(); + } else { + return super.toString(); + } + } + + /** + * {@inheritDoc} + */ + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BinaryQValue) { + BinaryQValue other = (BinaryQValue) obj; + // for both the value has not been loaded yet + if (!initialized) { + if (other.uri != null) { + return uri.equals(uri); + } else { + // need to load the binary value in order to be able + // to compare the 2 values. + try { + loadBinary(); + } catch (RepositoryException e) { + return false; + } catch (IOException e) { + return false; + } + } + } + // both have been loaded + return ((file == null ? other.file == null : file.equals(other.file)) + && Arrays.equals(buffer, other.buffer)); + } + return false; + } + + /** + * Returns zero to satisfy the Object equals/hashCode contract. + * This class is mutable and not meant to be used as a hash key. + * + * @return always zero + * @see Object#hashCode() + */ + public int hashCode() { + return 0; + } + + //---------------------------------------------------------------------- + /** + * Spools the contents of this BinaryQValue to the given + * output stream. + * + * @param out output stream + * @throws RepositoryException if the input stream for this + * BinaryQValue could not be obtained + * @throws IOException if an error occurs while while spooling + */ + private void spool(OutputStream out) throws RepositoryException, IOException { + InputStream in; + if (file != null) { + // this instance is backed by a 'real' file + try { + in = new FileInputStream(file); + } catch (FileNotFoundException fnfe) { + throw new RepositoryException("file backing binary value not found", + fnfe); + } + } else if (buffer != null) { + // this instance is backed by an in-memory buffer + in = new ByteArrayInputStream(buffer); + } else { + // only uri present: + loadBinary(); + if (buffer == null) { + in = new FileInputStream(file); + } else { + in = new ByteArrayInputStream(buffer); + } + } + try { + byte[] buffer = new byte[0x2000]; + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + } finally { + try { + in.close(); + } catch (IOException ignore) { + } + } + } + + private synchronized void loadBinary() throws RepositoryException, IOException { + if (uri == null) { + throw new IllegalStateException(); + } + loader.loadBinary(uri, index, this); + } + + //-----------------------------< Serializable >------------------------- + private void writeObject(ObjectOutputStream out) + throws IOException { + out.defaultWriteObject(); + // write hasFile marker + out.writeBoolean(file != null); + // then write file if necessary + if (file != null) { + byte[] buffer = new byte[4096]; + int bytes; + InputStream stream = new FileInputStream(file); + while ((bytes = stream.read(buffer)) >= 0) { + // Write a segment of the input stream + if (bytes > 0) { + // just to ensure that no 0 is written + out.writeInt(bytes); + out.write(buffer, 0, bytes); + } + } + // Write the end of stream marker + out.writeInt(0); + // close stream + stream.close(); + } + } + + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + boolean hasFile = in.readBoolean(); + if (hasFile) { + file = File.createTempFile("binary-qvalue", "bin"); + + OutputStream out = new FileOutputStream(file); + byte[] buffer = new byte[4096]; + for (int bytes = in.readInt(); bytes > 0; bytes = in.readInt()) { + if (buffer.length < bytes) { + buffer = new byte[bytes]; + } + in.readFully(buffer, 0, bytes); + out.write(buffer, 0, bytes); + } + out.close(); + } + // deserialized value is always temp + temp = true; + } + + //---------------------------------------------------------< Target >--- + public void setStream(InputStream in) throws IOException { + if (index == -1) { + init(in, true); + } else { + // TODO: improve. jcr-server sends XML for multivalued properties + try { + DocumentBuilder db = DomUtil.BUILDER_FACTORY.newDocumentBuilder(); + Document doc = db.parse(in); + Element prop = DomUtil.getChildElement(doc, ItemResourceConstants.JCR_VALUES.getName(), ItemResourceConstants.JCR_VALUES.getNamespace()); + DavProperty p = DefaultDavProperty.createFromXml(prop); + ValuesProperty vp = new ValuesProperty(p, PropertyType.BINARY, ValueFactoryImpl.getInstance()); + + Value[] jcrVs = vp.getJcrValues(); + init(jcrVs[index].getStream(), true); + } catch (RepositoryException e) { + throw new IOException(e.getMessage()); + }catch (DavException e) { + throw new IOException(e.getMessage()); + } catch (SAXException e) { + throw new IOException(e.getMessage()); + } catch (ParserConfigurationException e) { + throw new IOException(e.getMessage()); + } + } + } + } +} \ No newline at end of file Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/QValueFactoryImpl.java ------------------------------------------------------------------------------ svn:keywords = author date id revision url Added: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java?rev=738480&view=auto ============================================================================== --- jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java (added) +++ jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java Wed Jan 28 13:35:47 2009 @@ -0,0 +1,770 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.spi2davex; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.methods.RequestEntity; +import org.apache.commons.httpclient.methods.multipart.FilePart; +import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; +import org.apache.commons.httpclient.methods.multipart.Part; +import org.apache.commons.httpclient.methods.multipart.PartBase; +import org.apache.commons.httpclient.methods.multipart.StringPart; +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.spi.Batch; +import org.apache.jackrabbit.spi.ItemId; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NodeId; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.PropertyId; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.commons.identifier.IdFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.PathBuilder; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi2dav.ExceptionConverter; +import org.apache.jackrabbit.util.Text; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.header.IfHeader; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.jcr.JcrValueType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.jcr.Credentials; +import javax.jcr.ItemNotFoundException; +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * RepositoryServiceImpl... + */ +public class RepositoryServiceImpl extends org.apache.jackrabbit.spi2dav.RepositoryServiceImpl { + + private static Logger log = LoggerFactory.getLogger(RepositoryServiceImpl.class); + + private static final String PARAM_DIFF = ":diff"; + private static final String PARAM_COPY = ":copy"; + private static final String PARAM_CLONE = ":clone"; + + private static final char SYMBOL_ADD_NODE = '+'; + private static final char SYMBOL_MOVE = '>'; + private static final char SYMBOL_REMOVE = '-'; + private static final char SYMBOL_SET_PROPERTY = '^'; + + private static final String ORDER_POSITION_LAST = "#last"; + private static final String ORDER_POSITION_BEFORE = "#before"; + + /** + * base uri to the extended jcr-server that can handle the GET and POST + * (or PATCH) requests sent by this service implementation. + */ + private final String jcrServerURI; + + /** + * the name of the default workspace or null. + * NOTE: with JCR-1842 the RepositoryConfiguration doesn't provide the + * default workspace name any more. In order to provide backwards + * compatibility with jcr-server < 1.5.0 the workspace name can be + * passed to the RepositoryService implementation. + */ + private final String defaultWorkspaceName; + + /** + * The configuration map used to determine the maximal depth of child + * items to be accessed upon a call to {@link #getNodeInfo(SessionInfo, NodeId)}. + */ + private final BatchReadConfig batchReadConfig; + + private final Map qvFactories = new HashMap(); + + public RepositoryServiceImpl(String jcrServerURI, BatchReadConfig batchReadConfig) throws RepositoryException { + this(jcrServerURI, null, batchReadConfig); + } + + public RepositoryServiceImpl(String jcrServerURI, String defaultWorkspaceName, BatchReadConfig batchReadConfig) throws RepositoryException { + super(jcrServerURI, IdFactoryImpl.getInstance(), NameFactoryImpl.getInstance(), PathFactoryImpl.getInstance(), new QValueFactoryImpl()); + + this.jcrServerURI = jcrServerURI.endsWith("/") ? jcrServerURI : jcrServerURI + "/"; + this.defaultWorkspaceName = defaultWorkspaceName; + if (batchReadConfig == null) { + this.batchReadConfig = new BatchReadConfig() { + public int getDepth(Path path, PathResolver resolver) + throws NamespaceException { + return 0; + } + }; + } else { + this.batchReadConfig = batchReadConfig; + } + } + + private Path getPath(ItemId itemId, SessionInfo sessionInfo) throws RepositoryException { + if (itemId.denotesNode()) { + Path p = itemId.getPath(); + String uid = itemId.getUniqueID(); + if (uid == null) { + return p; + } else { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String uri = super.getItemUri(itemId, sessionInfo); + String rootUri = getRootURI(sessionInfo); + String jcrPath; + if (uri.startsWith(rootUri)) { + jcrPath = uri.substring(rootUri.length()); + } else { + log.warn("ItemURI " + uri + " doesn't start with rootURI (" + rootUri + ")."); + // fallback: + // calculated uri does not start with the rootURI + // -> search /jcr:root and start substring behind. + String rootSegment = Text.escapePath(ItemResourceConstants.ROOT_ITEM_RESOURCEPATH); + jcrPath = uri.substring(uri.indexOf(rootSegment) + rootSegment.length()); + } + jcrPath = Text.unescape(jcrPath); + return resolver.getQPath(jcrPath); + } + } else { + PropertyId pId = (PropertyId) itemId; + Path parentPath = getPath(pId.getParentId(), sessionInfo); + return getPathFactory().create(parentPath, pId.getName(), true); + } + } + + private String getURI(Path path, SessionInfo sessionInfo) throws RepositoryException { + StringBuffer sb = new StringBuffer(getRootURI(sessionInfo)); + String jcrPath = getNamePathResolver(sessionInfo).getJCRPath(path); + sb.append(Text.escapePath(jcrPath)); + return sb.toString(); + } + + private String getURI(ItemId itemId, SessionInfo sessionInfo) throws RepositoryException { + Path p = getPath(itemId, sessionInfo); + if (p == null) { + return super.getItemUri(itemId, sessionInfo); + } else { + return getURI(p, sessionInfo); + } + } + + private String getRootURI(SessionInfo sessionInfo) { + StringBuffer sb = new StringBuffer(getWorkspaceURI(sessionInfo)); + sb.append(Text.escapePath(ItemResourceConstants.ROOT_ITEM_RESOURCEPATH)); + return sb.toString(); + } + + private String getWorkspaceURI(SessionInfo sessionInfo) { + StringBuffer sb = new StringBuffer(); + sb.append(jcrServerURI); + sb.append(Text.escape(sessionInfo.getWorkspaceName())); + return sb.toString(); + } + + /** + * @see RepositoryService#getQValueFactory() + */ + public QValueFactoryImpl getQValueFactory(SessionInfo sessionInfo) throws RepositoryException { + QValueFactoryImpl qv; + if (qvFactories.containsKey(sessionInfo)) { + qv = (QValueFactoryImpl) qvFactories.get(sessionInfo); + } else { + ValueLoader loader = new ValueLoader(getClient(sessionInfo)); + qv = new QValueFactoryImpl(getNamePathResolver(sessionInfo), loader); + qvFactories.put(sessionInfo, qv); + } + return qv; + } + + //--------------------------------------------------< RepositoryService >--- + + // exists && getPropertyInfo -> to be done + // getNodeInfo: omitted for requires list of 'references' + + /** + * If the specified workspaceName the default workspace name + * is used for backwards compatibility with jackrabbit-jcr-server < 1.5.0 + * + * @see RepositoryService#obtain(Credentials, String) + */ + public SessionInfo obtain(Credentials credentials, String workspaceName) + throws RepositoryException { + // for backwards compatibility with jcr-server < 1.5.0 + String wspName = (workspaceName == null) ? defaultWorkspaceName : workspaceName; + return super.obtain(credentials, wspName); + } + + /** + * If the specified workspaceName the default workspace name + * is used for backwards compatibility with jackrabbit-jcr-server < 1.5.0 + * + * @see RepositoryService#obtain(SessionInfo, String) + */ + public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName) + throws RepositoryException { + // for backwards compatibility with jcr-server < 1.5.0 + String wspName = (workspaceName == null) ? defaultWorkspaceName : workspaceName; + return super.obtain(sessionInfo, wspName); + } + + /** + * @see RepositoryService#dispose(SessionInfo) + */ + public void dispose(SessionInfo sessionInfo) throws RepositoryException { + super.dispose(sessionInfo); + // remove the qvalue factory created for the given SessionInfo from the + // map of valuefactories. + qvFactories.remove(sessionInfo); + } + + /** + * @see RepositoryService#getItemInfos(SessionInfo, NodeId) + */ + public Iterator getItemInfos(SessionInfo sessionInfo, NodeId nodeId) throws ItemNotFoundException, RepositoryException { + Path path = getPath(nodeId, sessionInfo); + String uri = getURI(path, sessionInfo); + int depth = batchReadConfig.getDepth(path, this.getNamePathResolver(sessionInfo)); + + GetMethod method = new GetMethod(uri + "." + depth + ".json"); + try { + int statusCode = getClient(sessionInfo).executeMethod(method); + if (statusCode == DavServletResponse.SC_OK) { + if (method.getResponseContentLength() == 0) { + // no json response -> no such node on the server + throw new ItemNotFoundException("No such node " + nodeId); + } + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + NodeInfoImpl nInfo = new NodeInfoImpl(nodeId, path); + + ItemInfoJSONHandler handler = new ItemInfoJSONHandler(resolver, nInfo, getRootURI(sessionInfo), getQValueFactory(sessionInfo), getPathFactory(), getIdFactory()); + JSONParser ps = new JSONParser(handler); + ps.parse(method.getResponseBodyAsStream(), method.getResponseCharSet()); + + Iterator it = handler.getItemInfos(); + if (!it.hasNext()) { + throw new ItemNotFoundException("No such node " + uri); + } + return handler.getItemInfos(); + } else { + throw ExceptionConverter.generate(new DavException(statusCode, "Unable to retrieve NodeInfo for " + uri), method); + } + } catch (HttpException e) { + throw ExceptionConverter.generate(new DavException(method.getStatusCode(), "Unable to retrieve NodeInfo for " + uri)); + } catch (IOException e) { + log.error("Internal error while retrieving NodeInfo.",e); + throw new RepositoryException(e); + } catch (Exception e) { + log.error("Internal error while retrieving NodeInfo.",e); + throw new RepositoryException(e); + } finally { + method.releaseConnection(); + } + } + + public Batch createBatch(SessionInfo sessionInfo, ItemId itemId) throws RepositoryException { + return new BatchImpl(itemId, sessionInfo); + } + + public void submit(Batch batch) throws RepositoryException { + if (!(batch instanceof BatchImpl)) { + throw new RepositoryException("Unknown Batch implementation."); + } + BatchImpl batchImpl = (BatchImpl) batch; + try { + if (!batchImpl.isEmpty()) { + batchImpl.start(); + } + } finally { + batchImpl.dispose(); + } + } + + public void copy(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + if (srcWorkspaceName.equals(sessionInfo.getWorkspaceName())) { + super.copy(sessionInfo, srcWorkspaceName, srcNodeId, destParentNodeId, destName); + return; + } + PostMethod method = null; + try { + method = new PostMethod(getWorkspaceURI(sessionInfo)); + NamePathResolver resolver = getNamePathResolver(sessionInfo); + + StringBuffer args = new StringBuffer(); + args.append(srcWorkspaceName); + args.append(","); + args.append(resolver.getJCRPath(getPath(srcNodeId, sessionInfo))); + args.append(","); + String destParentPath = resolver.getJCRPath(getPath(destParentNodeId, sessionInfo)); + String destPath = destParentPath + "/" + resolver.getJCRName(destName); + args.append(destPath); + + method.addParameter(PARAM_COPY, args.toString()); + addIfHeader(sessionInfo, method); + getClient(sessionInfo).executeMethod(method); + + method.checkSuccess(); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + + public void clone(SessionInfo sessionInfo, String srcWorkspaceName, NodeId srcNodeId, NodeId destParentNodeId, Name destName, boolean removeExisting) throws RepositoryException { + PostMethod method = null; + try { + method = new PostMethod(getWorkspaceURI(sessionInfo)); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + StringBuffer args = new StringBuffer(); + args.append(srcWorkspaceName); + args.append(","); + args.append(resolver.getJCRPath(getPath(srcNodeId, sessionInfo))); + args.append(","); + String destParentPath = resolver.getJCRPath(getPath(destParentNodeId, sessionInfo)); + String destPath = destParentPath + "/" + resolver.getJCRName(destName); + args.append(destPath); + args.append(","); + args.append(Boolean.toString(removeExisting)); + + method.addParameter(PARAM_CLONE, args.toString()); + addIfHeader(sessionInfo, method); + getClient(sessionInfo).executeMethod(method); + + method.checkSuccess(); + + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + + private static void addIfHeader(SessionInfo sInfo, HttpMethod method) { + String[] locktokens = sInfo.getLockTokens(); + if (locktokens != null && locktokens.length > 0) { + IfHeader ifH = new IfHeader(locktokens); + method.setRequestHeader(ifH.getHeaderName(), ifH.getHeaderValue()); + } + } + + //-------------------------------------------------------------------------- + + private class BatchImpl implements Batch { + + private final ItemId targetId; + private final SessionInfo sessionInfo; + private final List parts; + private final List diff; + /* + If this batch needs to remove multiple same-name-siblings starting + from lower index, the index of the following siblings must be reset + in order to avoid PathNotFoundException. + */ + private final Map removed = new HashMap(); + + private PostMethod method; // TODO: use PATCH request instead. + private boolean isConsumed; + + private BatchImpl(ItemId targetId, SessionInfo sessionInfo) { + this.targetId = targetId; + this.sessionInfo = sessionInfo; + parts = new ArrayList(); + diff = new ArrayList(); + } + + private void start() throws RepositoryException { + checkConsumed(); + + // add locktokens + addIfHeader(sessionInfo, method); + + // insert the content of 'batchMap' part containing the ordered list + // of methods to be executed: + StringBuffer buf = new StringBuffer(); + for (Iterator it = diff.iterator(); it.hasNext();) { + buf.append(it.next().toString()); + if (it.hasNext()) { + buf.append("\r"); + } + } + + if (parts.isEmpty()) { + // only a diff part. no multipart required. + method.addParameter(PARAM_DIFF, buf.toString()); + } else { + // other parts are present -> add the diff part + addPart(PARAM_DIFF, buf.toString()); + // ... and create multipart-entity (and set it to method) + Part[] partArr = (Part[]) parts.toArray(new Part[parts.size()]); + RequestEntity entity = new MultipartRequestEntity(partArr, method.getParams()); + method.setRequestEntity(entity); + } + + HttpClient client = getClient(sessionInfo); + try { + client.executeMethod(method); + method.checkSuccess(); + } catch (IOException e) { + throw new RepositoryException(e); + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + method.releaseConnection(); + } + } + + private void dispose() { + method = null; + isConsumed = true; + } + + private void checkConsumed() { + if (isConsumed) { + throw new IllegalStateException("Batch has already been consumed."); + } + } + + private boolean isEmpty() { + return method == null; + } + + private void assertMethod() throws RepositoryException { + if (method == null) { + String uri = getURI(targetId, sessionInfo); + method = new PostMethod(uri); + // ship lock-tokens as if-header to cirvumvent problems with + // locks created by this session. + String[] locktokens = sessionInfo.getLockTokens(); + if (locktokens != null && locktokens.length > 0) { + IfHeader ifH = new IfHeader(locktokens); + method.setRequestHeader(ifH.getHeaderName(), ifH.getHeaderValue()); + } + } + } + + //----------------------------------------------------------< Batch >--- + /** + * @inheritDoc + */ + public void addNode(NodeId parentId, Name nodeName, Name nodetypeName, + String uuid) throws RepositoryException { + assertMethod(); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), nodeName, true); + String jcrPath = resolver.getJCRPath(p); + + StringWriter wr = new StringWriter(); + wr.write('{'); + wr.write(getJSONKey(JcrConstants.JCR_PRIMARYTYPE)); + wr.write(Text.getJSONString(getNamePathResolver(sessionInfo).getJCRName(nodetypeName))); + if (uuid != null) { + wr.write(','); + wr.write(getJSONKey(JcrConstants.JCR_UUID)); + wr.write(Text.getJSONString(uuid)); + } + wr.write('}'); + appendDiff(SYMBOL_ADD_NODE, jcrPath, wr.toString()); + } + + /** + * @inheritDoc + */ + public void addProperty(NodeId parentId, Name propertyName, QValue value) throws RepositoryException { + assertMethod(); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), propertyName, true); + setProperty(p, value); + } + + /** + * @inheritDoc + */ + public void addProperty(NodeId parentId, Name propertyName, QValue[] values) throws RepositoryException { + assertMethod(); + Path p = getPathFactory().create(getPath(parentId, sessionInfo), propertyName, true); + setProperty(p, values); + } + + /** + * @inheritDoc + */ + public void setValue(PropertyId propertyId, QValue value) throws RepositoryException { + assertMethod(); + setProperty(getPath(propertyId, sessionInfo), value); + } + + /** + * @inheritDoc + */ + public void setValue(PropertyId propertyId, QValue[] values) throws RepositoryException { + assertMethod(); + Path p = getPath(propertyId, sessionInfo); + setProperty(p, values); + } + + /** + * @inheritDoc + */ + public void remove(ItemId itemId) throws RepositoryException { + assertMethod(); + + Path rmPath = getPath(itemId, sessionInfo); + if (itemId.denotesNode()) { + rmPath = calcRemovePath(rmPath); + } + String rmJcrPath = getNamePathResolver(sessionInfo).getJCRPath(rmPath); + appendDiff(SYMBOL_REMOVE, rmJcrPath, null); + } + + /** + * @inheritDoc + */ + public void reorderNodes(NodeId parentId, NodeId srcNodeId, NodeId beforeNodeId) throws RepositoryException { + assertMethod(); + + // TODO: multiple reorder of SNS nodes requires readjustment of path -> see remove() + String srcPath = getNamePathResolver(sessionInfo).getJCRPath(getPath(srcNodeId, sessionInfo)); + + StringBuffer val = new StringBuffer(); + if (beforeNodeId != null) { + Path beforePath = getPath(beforeNodeId, sessionInfo); + String beforeJcrPath = getNamePathResolver(sessionInfo).getJCRPath(beforePath); + val.append(Text.getName(beforeJcrPath)); + val.append(ORDER_POSITION_BEFORE); + } else { + val.append(ORDER_POSITION_LAST); + } + appendDiff(SYMBOL_MOVE, srcPath, val.toString()); + } + + /** + * @inheritDoc + */ + public void setMixins(NodeId nodeId, Name[] mixinNodeTypeNames) throws RepositoryException { + assertMethod(); + + NamePathResolver resolver = getNamePathResolver(sessionInfo); + QValue[] vs = new QValue[mixinNodeTypeNames.length]; + for (int i = 0; i < mixinNodeTypeNames.length; i++) { + vs[i] = getQValueFactory(sessionInfo).create(mixinNodeTypeNames[i]); + } + addProperty(nodeId, resolver.getQName(JcrConstants.JCR_MIXINTYPES), vs); + } + + /** + * @inheritDoc + */ + public void move(NodeId srcNodeId, NodeId destParentNodeId, Name destName) throws RepositoryException { + assertMethod(); + + String srcPath = getNamePathResolver(sessionInfo).getJCRPath(getPath(srcNodeId, sessionInfo)); + Path destPath = getPathFactory().create(getPath(destParentNodeId, sessionInfo), destName, true); + String destJcrPath = getNamePathResolver(sessionInfo).getJCRPath(destPath); + + appendDiff(SYMBOL_MOVE, srcPath, destJcrPath); + } + + /** + * + * @param symbol + * @param targetPath + * @param value + */ + private void appendDiff(char symbol, String targetPath, String value) { + StringBuffer bf = new StringBuffer(); + bf.append(symbol).append(targetPath).append(" : "); + if (value != null) { + bf.append(value); + } + diff.add(bf.toString()); + } + + /** + * + * @param propPath + * @param value + * @throws RepositoryException + */ + private void setProperty(Path propPath, QValue value) throws RepositoryException { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String jcrPropPath = resolver.getJCRPath(propPath); + clearPreviousSetProperty(jcrPropPath); + + String strValue = getJSONString(value); + appendDiff(SYMBOL_SET_PROPERTY, jcrPropPath, strValue); + if (strValue == null) { + addPart(jcrPropPath, value, resolver); + } + } + + private void setProperty(Path propPath, QValue[] values) throws RepositoryException { + NamePathResolver resolver = getNamePathResolver(sessionInfo); + String jcrPropPath = resolver.getJCRPath(propPath); + clearPreviousSetProperty(jcrPropPath); + + StringBuffer strVal = new StringBuffer("["); + for (int i = 0; i < values.length; i++) { + String str = getJSONString(values[i]); + if (str == null) { + addPart(jcrPropPath, values[i], resolver); + } else { + String delim = (i == 0) ? "" : ","; + strVal.append(delim).append(str); + } + } + strVal.append("]"); + appendDiff(SYMBOL_SET_PROPERTY, jcrPropPath, strVal.toString()); + } + + private void clearPreviousSetProperty(String jcrPropPath) { + String key = SYMBOL_SET_PROPERTY + jcrPropPath + " : "; + // make sure that multiple calls to setProperty for a given path + // are only reflected once in the multipart, otherwise this will + // cause consistency problems as the various calls cannot be separated + // (missing unique identifier for the parts). + for (Iterator it = diff.iterator(); it.hasNext();) { + String entry = it.next().toString(); + if (entry.startsWith(key)) { + it.remove(); + removeParts(jcrPropPath); + return; + } + } + } + + /** + * + * @param paramName + * @param value + */ + private void addPart(String paramName, String value) { + parts.add(new StringPart(paramName, value)); + } + + /** + * + * @param paramName + * @param value + * @param resolver + * @throws RepositoryException + */ + private void addPart(String paramName, QValue value, NamePathResolver resolver) throws RepositoryException { + Part part; + switch (value.getType()) { + case PropertyType.BINARY: + part = new FilePart(paramName, new BinaryPartSource(value)); + break; + case PropertyType.NAME: + part = new StringPart(paramName, resolver.getJCRName(value.getName())); + break; + case PropertyType.PATH: + part = new StringPart(paramName, resolver.getJCRPath(value.getPath())); + break; + default: + part = new StringPart(paramName, value.getString()); + } + String ctype = JcrValueType.contentTypeFromType(value.getType()); + ((PartBase) part).setContentType(ctype); + + parts.add(part); + } + + private void removeParts(String paramName) { + for (Iterator it = parts.iterator(); it.hasNext();) { + Part part = (Part) it.next(); + if (part.getName().equals(paramName)) { + it.remove(); + } + } + } + + private String getJSONKey(String str) { + return Text.getJSONString(str) + ":"; + } + + private String getJSONString(QValue value) throws RepositoryException { + String str; + switch (value.getType()) { + case PropertyType.STRING: + str = Text.getJSONString(value.getString()); + break; + case PropertyType.BOOLEAN: + case PropertyType.LONG: + str = value.getString(); + break; + case PropertyType.DOUBLE: + str = value.getString(); + if (str.indexOf('.') == -1) { + str += ".0"; + } + break; + default: + // JSON cannot specifically handle this property type... + str = null; + } + return str; + } + + private Path calcRemovePath(Path removedNodePath) throws RepositoryException { + removed.put(removedNodePath, removedNodePath); + Name name = removedNodePath.getNameElement().getName(); + int index = removedNodePath.getNameElement().getNormalizedIndex(); + if (index > Path.INDEX_DEFAULT) { + Path.Element[] elems = removedNodePath.getElements(); + PathBuilder pb = new PathBuilder(); + for (int i = 0; i <= elems.length - 2; i++) { + pb.addLast(elems[i]); + } + Path parent = pb.getPath(); + while (index > Path.INDEX_UNDEFINED) { + Path siblingP = getPathFactory().create(parent, name, --index, true); + if (removed.containsKey(siblingP)) { + // as the previous sibling has been remove -> the same index + // must be used to remove the node at removedNodePath. + siblingP = (Path) removed.get(siblingP); + removed.put(removedNodePath, siblingP); + return siblingP; + } + } + } + // first of siblings or no sibling at all + return removedNodePath; + } + } +} Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/RepositoryServiceImpl.java ------------------------------------------------------------------------------ svn:keywords = author date id revision url Added: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java URL: http://svn.apache.org/viewvc/jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java?rev=738480&view=auto ============================================================================== --- jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java (added) +++ jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java Wed Jan 28 13:35:47 2009 @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.spi.spi2davex; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.jackrabbit.spi2dav.ExceptionConverter; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.DavMethodBase; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.jcr.ItemResourceConstants; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + +import javax.jcr.ItemNotFoundException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import java.io.IOException; +import java.io.InputStream; + +/** + * ValueLoader... + */ +class ValueLoader { + + private final HttpClient client; + + ValueLoader(HttpClient client) { + this.client = client; + } + + void loadBinary(String uri, int index, Target target) throws RepositoryException, IOException { + GetMethod method = new GetMethod(uri); + try { + int statusCode = client.executeMethod(method); + if (statusCode == DavServletResponse.SC_OK) { + target.setStream(method.getResponseBodyAsStream()); + } else { + throw ExceptionConverter.generate(new DavException(statusCode, ("Unable to load binary. Status line = " + method.getStatusLine().toString()))); + } + } finally { + method.releaseConnection(); + } + } + + int loadType(String uri) throws RepositoryException, IOException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(ItemResourceConstants.JCR_TYPE); + + DavMethodBase method = null; + try { + method = new PropFindMethod(uri, nameSet, DavConstants.DEPTH_0); + client.executeMethod(method); + method.checkSuccess(); + + MultiStatusResponse[] responses = method.getResponseBodyAsMultiStatus().getResponses(); + if (responses.length == 1) { + DavPropertySet props = responses[0].getProperties(DavServletResponse.SC_OK); + DavProperty type = props.get(ItemResourceConstants.JCR_TYPE); + if (type != null) { + return PropertyType.valueFromName(type.getValue().toString()); + } else { + throw new RepositoryException("Internal error. Cannot retrieve property type at " + uri); + } + } else { + throw new ItemNotFoundException("Internal error. Cannot retrieve property type at " + uri); + } + } catch (DavException e) { + throw ExceptionConverter.generate(e); + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + + //-------------------------------------------------------------------------- + /** + * Internal inteface + */ + interface Target { + /** + * @param in + * @throws IOException + */ + void setStream(InputStream in) throws IOException; + } +} \ No newline at end of file Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: jackrabbit/sandbox/spi/spi2davex/src/main/java/org/apache/jackrabbit/spi/spi2davex/ValueLoader.java ------------------------------------------------------------------------------ svn:keywords = author date id revision url