) (Set) props.keySet());
+ propNames.addAll(this.systemProperties);
+ for (String systemProperty : propNames)
+ {
+ resolveMergedProperty(systemProperty, props);
+ if (this.systemPropertiesMode == PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_FALLBACK
+ && props.containsKey(systemProperty))
+ {
+ // It's already there
+ continue;
+ }
+ // Get the system value and assign if present
+ String systemPropertyValue = System.getProperty(systemProperty);
+ if (systemPropertyValue != null)
+ {
+ props.put(systemProperty, systemPropertyValue);
+ }
+ }
+ }
+ return props;
+ }
+
+ /**
+ * Override hook. Allows subclasses to resolve a merged property from an alternative source, whilst still respecting
+ * the chosen system property fallback path.
+ *
+ * @param systemProperty String
+ * @param props Properties
+ */
+ protected void resolveMergedProperty(String systemProperty, Properties props)
+ {
+ }
+}
diff --git a/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java b/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java
new file mode 100644
index 0000000000..a00fe869d5
--- /dev/null
+++ b/src/main/java/org/alfresco/config/SystemPropertiesSetterBean.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Takes a set of properties and pushes them into the Java environment. Usually, VM properties
+ * are required by the system (see {@link SystemPropertiesFactoryBean} and
+ * Spring's PropertyPlaceholderConfigurer ); sometimes it is necessary to take properties
+ * available to Spring and push them onto the VM.
+ *
+ * For simplicity, the system property, if present, will NOT be modified. Also, property placeholders
+ * (${...} ), empty values and null values will be ignored.
+ *
+ * Use the {@link #init()} method to push the properties.
+ *
+ * @author Derek Hulley
+ * @since V3.1
+ */
+public class SystemPropertiesSetterBean
+{
+ private static Log logger = LogFactory.getLog(SystemPropertiesSetterBean.class);
+
+ private Map propertyMap;
+
+ SystemPropertiesSetterBean()
+ {
+ propertyMap = new HashMap(3);
+ }
+
+ /**
+ * Set the properties that will be pushed onto the JVM.
+ *
+ * @param propertyMap a map of property name to property value
+ */
+ public void setPropertyMap(Map propertyMap)
+ {
+ this.propertyMap = propertyMap;
+ }
+
+ public void init()
+ {
+ for (Map.Entry entry : propertyMap.entrySet())
+ {
+ String name = entry.getKey();
+ String value = entry.getValue();
+ // Some values can be ignored
+ if (value == null || value.length() == 0)
+ {
+ continue;
+ }
+ if (value.startsWith("${") && value.endsWith("}"))
+ {
+ continue;
+ }
+ // Check the system properties
+ if (System.getProperty(name) != null)
+ {
+ // It was already there
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("\n" +
+ "Not pushing up system property: \n" +
+ " Property: " + name + "\n" +
+ " Value already present: " + System.getProperty(name) + "\n" +
+ " Value provided: " + value);
+ }
+ continue;
+ }
+ System.setProperty(name, value);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java b/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java
new file mode 100644
index 0000000000..cceb32c944
--- /dev/null
+++ b/src/main/java/org/alfresco/encoding/AbstractCharactersetFinder.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public abstract class AbstractCharactersetFinder implements CharactersetFinder
+{
+ private static Log logger = LogFactory.getLog(AbstractCharactersetFinder.class);
+ private static boolean isDebugEnabled = logger.isDebugEnabled();
+
+ private int bufferSize;
+
+ public AbstractCharactersetFinder()
+ {
+ this.bufferSize = 8192;
+ }
+
+ /**
+ * Set the maximum number of bytes to read ahead when attempting to determine the characterset.
+ * Most characterset detectors are efficient and can process 8K of buffered data very quickly.
+ * Some, may need to be constrained a bit.
+ *
+ * @param bufferSize the number of bytes - default 8K.
+ */
+ public void setBufferSize(int bufferSize)
+ {
+ this.bufferSize = bufferSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The input stream is checked to ensure that it supports marks, after which
+ * a buffer is extracted, leaving the stream in its original state.
+ */
+ public final Charset detectCharset(InputStream is)
+ {
+ // Only support marking streams
+ if (!is.markSupported())
+ {
+ throw new IllegalArgumentException("The InputStream must support marks. Wrap the stream in a BufferedInputStream.");
+ }
+ try
+ {
+ int bufferSize = getBufferSize();
+ if (bufferSize < 0)
+ {
+ throw new RuntimeException("The required buffer size may not be negative: " + bufferSize);
+ }
+ // Mark the stream for just a few more than we actually will need
+ is.mark(bufferSize);
+ // Create a buffer to hold the data
+ byte[] buffer = new byte[bufferSize];
+ // Fill it
+ int read = is.read(buffer);
+ // Create an appropriately sized buffer
+ if (read > -1 && read < buffer.length)
+ {
+ byte[] copyBuffer = new byte[read];
+ System.arraycopy(buffer, 0, copyBuffer, 0, read);
+ buffer = copyBuffer;
+ }
+ // Detect
+ return detectCharset(buffer);
+ }
+ catch (IOException e)
+ {
+ // Attempt a reset
+ throw new AlfrescoRuntimeException("IOException while attempting to detect charset encoding.", e);
+ }
+ finally
+ {
+ try { is.reset(); } catch (Throwable ee) {}
+ }
+ }
+
+ public final Charset detectCharset(byte[] buffer)
+ {
+ try
+ {
+ Charset charset = detectCharsetImpl(buffer);
+ // Done
+ if (isDebugEnabled)
+ {
+ if (charset == null)
+ {
+ // Read a few characters for debug purposes
+ logger.debug("\n" +
+ "Failed to identify stream character set: \n" +
+ " Guessed 'chars': " + Arrays.toString(buffer));
+ }
+ else
+ {
+ // Read a few characters for debug purposes
+ logger.debug("\n" +
+ "Identified character set from stream:\n" +
+ " Charset: " + charset + "\n" +
+ " Detected chars: " + new String(buffer, charset.name()));
+ }
+ }
+ return charset;
+ }
+ catch (Throwable e)
+ {
+ logger.error("IOException while attempting to detect charset encoding.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Some implementations may only require a few bytes to do detect the stream type,
+ * whilst others may be more efficient with larger buffers. In either case, the
+ * number of bytes actually present in the buffer cannot be enforced.
+ *
+ * Only override this method if there is a very compelling reason to adjust the buffer
+ * size, and then consider handling the {@link #setBufferSize(int)} method by issuing a
+ * warning. This will prevent users from setting the buffer size when it has no effect.
+ *
+ * @return Returns the maximum desired size of the buffer passed
+ * to the {@link CharactersetFinder#detectCharset(byte[])} method.
+ *
+ * @see #setBufferSize(int)
+ */
+ protected int getBufferSize()
+ {
+ return bufferSize;
+ }
+
+ /**
+ * Worker method for implementations to override. All exceptions will be reported and
+ * absorbed and null returned.
+ *
+ * The interface contract is that the data buffer must not be altered in any way.
+ *
+ * @param buffer the buffer of data no bigger than the requested
+ * {@linkplain #getBufferSize() best buffer size}. This can,
+ * very efficiently, be turned into an InputStream using a
+ * ByteArrayInputStream.
+ * @return Returns the charset or null if an accurate conclusion
+ * is not possible
+ * @throws Exception Any exception, checked or not
+ */
+ protected abstract Charset detectCharsetImpl(byte[] buffer) throws Exception;
+}
diff --git a/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java b/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java
new file mode 100644
index 0000000000..d4908c0320
--- /dev/null
+++ b/src/main/java/org/alfresco/encoding/BomCharactersetFinder.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Byte Order Marker encoding detection.
+ *
+ * @since 2.1
+ * @author Pacific Northwest National Lab
+ * @author Derek Hulley
+ */
+public class BomCharactersetFinder extends AbstractCharactersetFinder
+{
+ private static Log logger = LogFactory.getLog(BomCharactersetFinder.class);
+
+ @Override
+ public void setBufferSize(int bufferSize)
+ {
+ logger.warn("Setting the buffersize has no effect for charset finder: " + BomCharactersetFinder.class.getName());
+ }
+
+ /**
+ * @return Returns 64
+ */
+ @Override
+ protected int getBufferSize()
+ {
+ return 64;
+ }
+
+ /**
+ * Just searches the Byte Order Marker, i.e. the first three characters for a sign of
+ * the encoding.
+ */
+ protected Charset detectCharsetImpl(byte[] buffer) throws Exception
+ {
+ Charset charset = null;
+ ByteArrayInputStream bis = null;
+ try
+ {
+ bis = new ByteArrayInputStream(buffer);
+ bis.mark(3);
+ char[] byteHeader = new char[3];
+ InputStreamReader in = new InputStreamReader(bis);
+ int bytesRead = in.read(byteHeader);
+ bis.reset();
+
+ if (bytesRead < 2)
+ {
+ // ASCII
+ charset = Charset.forName("Cp1252");
+ }
+ else if (
+ byteHeader[0] == 0xFE &&
+ byteHeader[1] == 0xFF)
+ {
+ // UCS-2 Big Endian
+ charset = Charset.forName("UTF-16BE");
+ }
+ else if (
+ byteHeader[0] == 0xFF &&
+ byteHeader[1] == 0xFE)
+ {
+ // UCS-2 Little Endian
+ charset = Charset.forName("UTF-16LE");
+ }
+ else if (
+ bytesRead >= 3 &&
+ byteHeader[0] == 0xEF &&
+ byteHeader[1] == 0xBB &&
+ byteHeader[2] == 0xBF)
+ {
+ // UTF-8
+ charset = Charset.forName("UTF-8");
+ }
+ else
+ {
+ // No idea
+ charset = null;
+ }
+ // Done
+ return charset;
+ }
+ finally
+ {
+ if (bis != null)
+ {
+ try { bis.close(); } catch (Throwable e) {}
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encoding/CharactersetFinder.java b/src/main/java/org/alfresco/encoding/CharactersetFinder.java
new file mode 100644
index 0000000000..365b55bcf0
--- /dev/null
+++ b/src/main/java/org/alfresco/encoding/CharactersetFinder.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Interface for classes that are able to read a text-based input stream and determine
+ * the character encoding.
+ *
+ * There are quite a few libraries that do this, but none are perfect. It is therefore
+ * necessary to abstract the implementation to allow these finders to be configured in
+ * as required.
+ *
+ * Implementations should have a default constructor and be completely thread safe and
+ * stateless. This will allow them to be constructed and held indefinitely to do the
+ * decoding work.
+ *
+ * Where the encoding cannot be determined, it is left to the client to decide what to do.
+ * Some implementations may guess and encoding or use a default guess - it is up to the
+ * implementation to specify the behaviour.
+ *
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public interface CharactersetFinder
+{
+ /**
+ * Attempt to detect the character set encoding for the give input stream. The input
+ * stream will not be altered or closed by this method, and must therefore support
+ * marking. If the input stream available doesn't support marking, then it can be wrapped with
+ * a {@link BufferedInputStream}.
+ *
+ * The current state of the stream will be restored before the method returns.
+ *
+ * @param is an input stream that must support marking
+ * @return Returns the encoding of the stream,
+ * or null if encoding cannot be identified
+ */
+ public Charset detectCharset(InputStream is);
+
+ /**
+ * Attempt to detect the character set encoding for the given buffer.
+ *
+ * @param buffer the first n bytes of the character stream
+ * @return Returns the encoding of the buffer,
+ * or null if encoding cannot be identified
+ */
+ public Charset detectCharset(byte[] buffer);
+}
diff --git a/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java b/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java
new file mode 100644
index 0000000000..92cd35f1c6
--- /dev/null
+++ b/src/main/java/org/alfresco/encoding/GuessEncodingCharsetFinder.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encoding;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+
+import com.glaforge.i18n.io.CharsetToolkit;
+
+/**
+ * Uses the Guess Encoding
+ * library.
+ *
+ * @since 2.1
+ * @author Derek Hulley
+ */
+public class GuessEncodingCharsetFinder extends AbstractCharactersetFinder
+{
+ /** Dummy charset to detect the default guess */
+ private static final Charset DUMMY_CHARSET = new DummyCharset();
+
+ @Override
+ protected Charset detectCharsetImpl(byte[] buffer) throws Exception
+ {
+ CharsetToolkit charsetToolkit = new CharsetToolkit(buffer, DUMMY_CHARSET);
+ charsetToolkit.setEnforce8Bit(true); // Force the default instead of a guess
+ Charset charset = charsetToolkit.guessEncoding();
+ if (charset == DUMMY_CHARSET)
+ {
+ return null;
+ }
+ else
+ {
+ return charset;
+ }
+ }
+
+ /**
+ * A dummy charset to detect a default hit.
+ */
+ public static class DummyCharset extends Charset
+ {
+ DummyCharset()
+ {
+ super("dummy", new String[] {});
+ }
+
+ @Override
+ public boolean contains(Charset cs)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CharsetDecoder newDecoder()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CharsetEncoder newEncoder()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/AbstractEncryptor.java b/src/main/java/org/alfresco/encryption/AbstractEncryptor.java
new file mode 100644
index 0000000000..865df534a4
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/AbstractEncryptor.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+import java.security.InvalidKeyException;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SealedObject;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.Pair;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Basic support for encryption engines.
+ *
+ * @since 4.0
+ */
+public abstract class AbstractEncryptor implements Encryptor
+{
+ protected static final Log logger = LogFactory.getLog(Encryptor.class);
+ protected String cipherAlgorithm;
+ protected String cipherProvider;
+
+ protected KeyProvider keyProvider;
+
+ /**
+ * Constructs with defaults
+ */
+ protected AbstractEncryptor()
+ {
+ }
+
+ /**
+ * @param keyProvider provides encryption keys based on aliases
+ */
+ public void setKeyProvider(KeyProvider keyProvider)
+ {
+ this.keyProvider = keyProvider;
+ }
+
+ public KeyProvider getKeyProvider()
+ {
+ return keyProvider;
+ }
+
+ public void init()
+ {
+ PropertyCheck.mandatory(this, "keyProvider", keyProvider);
+ }
+
+ /**
+ * Factory method to be written by implementations to construct and initialize
+ * physical ciphering objects.
+ *
+ * @param keyAlias the key alias
+ * @param params algorithm-specific parameters
+ * @param mode the cipher mode
+ * @return Cipher
+ */
+ protected abstract Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE);
+ if (cipher == null)
+ {
+ return new Pair(input, null);
+ }
+ try
+ {
+ byte[] output = cipher.doFinal(input);
+ params = cipher.getParameters();
+ return new Pair(output, params);
+ }
+ catch (Throwable e)
+ {
+// cipher.init(Cipher.ENCRYPT_MODE, key, params);
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ protected void resetCipher()
+ {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+ try
+ {
+ return cipher.doFinal(input);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream decrypt(String keyAlias, AlgorithmParameters params, InputStream input)
+ {
+ Cipher cipher = getCipher(keyAlias, params, Cipher.DECRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+
+ try
+ {
+ return new CipherInputStream(input, cipher);
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Decryption failed for key alias: " + keyAlias, e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Serializes and {@link #encrypt(String, AlgorithmParameters, byte[]) encrypts} the input data.
+ */
+ @Override
+ public Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input)
+ {
+ try
+ {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
+ ObjectOutputStream oos = new ObjectOutputStream(bos);
+ oos.writeObject(input);
+ byte[] unencrypted = bos.toByteArray();
+ return encrypt(keyAlias, params, unencrypted);
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to serialize or encrypt object", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * {@link #decrypt(String, AlgorithmParameters, byte[]) Decrypts} and deserializes the input data
+ */
+ @Override
+ public Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input)
+ {
+ try
+ {
+ byte[] unencrypted = decrypt(keyAlias, params, input);
+ ByteArrayInputStream bis = new ByteArrayInputStream(unencrypted);
+ ObjectInputStream ois = new ObjectInputStream(bis);
+ Object obj = ois.readObject();
+ return obj;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to deserialize or decrypt object", e);
+ }
+ }
+
+ @Override
+ public Serializable sealObject(String keyAlias, AlgorithmParameters params, Serializable input)
+ {
+ if (input == null)
+ {
+ return null;
+ }
+ Cipher cipher = getCipher(keyAlias, params, Cipher.ENCRYPT_MODE);
+ if (cipher == null)
+ {
+ return input;
+ }
+ try
+ {
+ return new SealedObject(input, cipher);
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to seal object", e);
+ }
+ }
+
+ @Override
+ public Serializable unsealObject(String keyAlias, Serializable input) throws InvalidKeyException
+ {
+ if (input == null)
+ {
+ return input;
+ }
+ // Don't unseal it if it is not sealed
+ if (!(input instanceof SealedObject))
+ {
+ return input;
+ }
+ // Get the Key, rather than a Cipher
+ Key key = keyProvider.getKey(keyAlias);
+ if (key == null)
+ {
+ // The client will be expecting to unseal the object
+ throw new IllegalStateException("No key matching " + keyAlias + ". Cannot unseal object.");
+ }
+ // Unseal it using the key
+ SealedObject sealedInput = (SealedObject) input;
+ try
+ {
+ Serializable output = (Serializable) sealedInput.getObject(key);
+ // Done
+ return output;
+ }
+ catch(InvalidKeyException e)
+ {
+ // let these through, can be useful to client code to know this is the cause
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to unseal object", e);
+ }
+ }
+
+ public void setCipherAlgorithm(String cipherAlgorithm)
+ {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ public String getCipherAlgorithm()
+ {
+ return this.cipherAlgorithm;
+ }
+
+ public void setCipherProvider(String cipherProvider)
+ {
+ this.cipherProvider = cipherProvider;
+ }
+
+ public String getCipherProvider()
+ {
+ return this.cipherProvider;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AlgorithmParameters decodeAlgorithmParameters(byte[] encoded)
+ {
+ try
+ {
+ AlgorithmParameters p = null;
+ String algorithm = "DESede";
+ if(getCipherProvider() != null)
+ {
+ p = AlgorithmParameters.getInstance(algorithm, getCipherProvider());
+ }
+ else
+ {
+ p = AlgorithmParameters.getInstance(algorithm);
+ }
+ p.init(encoded);
+ return p;
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("", e);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java b/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java
new file mode 100644
index 0000000000..297f28c355
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/AbstractKeyProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+/**
+ * Basic support for key providers
+ *
+ * TODO: This class will provide the alias name mapping so that use-cases can be mapped
+ * to different alias names in the keystore.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractKeyProvider implements KeyProvider
+{
+ /*
+ * Not a useless class.
+ */
+}
diff --git a/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java b/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java
new file mode 100644
index 0000000000..a017ce984a
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/AlfrescoKeyStore.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+import java.util.Set;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.TrustManager;
+
+/**
+ * Manages a Java Keystore for Alfresco, including caching keys where appropriate.
+ *
+ * @since 4.0
+ *
+ */
+public interface AlfrescoKeyStore
+{
+ public static final String KEY_KEYSTORE_PASSWORD = "keystore.password";
+
+ /**
+ * The name of the keystore.
+ *
+ * @return the name of the keystore.
+ */
+ public String getName();
+
+ /**
+ * Backup the keystore to the backup location. Write the keys to the backup keystore.
+ */
+ public void backup();
+
+ /**
+ * The key store parameters.
+ *
+ * @return KeyStoreParameters
+ */
+ public KeyStoreParameters getKeyStoreParameters();
+
+ /**
+ * The backup key store parameters.
+ *
+ * @return * @return
+
+ */
+ public KeyStoreParameters getBackupKeyStoreParameters();
+
+ /**
+ * Does the underlying key store exist?
+ *
+ * @return true if it exists, false otherwise
+ */
+ public boolean exists();
+
+ /**
+ * Return the key with the given key alias.
+ *
+ * @param keyAlias String
+ * @return Key
+ */
+ public Key getKey(String keyAlias);
+
+ /**
+ * Return the timestamp (in ms) of when the key was last loaded from the keystore on disk.
+ *
+ * @param keyAlias String
+ * @return long
+ */
+ public long getKeyTimestamp(String keyAlias);
+
+ /**
+ * Return the backup key with the given key alias.
+ *
+ * @param keyAlias String
+ * @return Key
+ */
+ public Key getBackupKey(String keyAlias);
+
+ /**
+ * Return all key aliases in the key store.
+ *
+ * @return Set
+ */
+ public Set getKeyAliases();
+
+ /**
+ * Create an array of key managers from keys in the key store.
+ *
+ * @return KeyManager[]
+ */
+ public KeyManager[] createKeyManagers();
+
+ /**
+ * Create an array of trust managers from certificates in the key store.
+ *
+ * @return TrustManager[]
+ */
+ public TrustManager[] createTrustManagers();
+
+ /**
+ * Create the key store if it doesn't exist.
+ * A key for each key alias will be written to the keystore on disk, either from the cached keys or, if not present, a key will be generated.
+ */
+ public void create();
+
+ /**
+ * Reload the keys from the key store.
+ */
+ public void reload() throws InvalidKeystoreException, MissingKeyException;
+
+ /**
+ * Check that the keys in the key store are valid i.e. that they match those registered.
+ */
+ public void validateKeys() throws InvalidKeystoreException, MissingKeyException;
+
+}
diff --git a/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java b/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java
new file mode 100644
index 0000000000..085bec231a
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/AlfrescoKeyStoreImpl.java
@@ -0,0 +1,1100 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESedeKeySpec;
+import javax.management.openmbean.KeyAlreadyExistsException;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+import org.alfresco.encryption.EncryptionKeysRegistry.KEY_STATUS;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.PropertyCheck;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * This wraps a Java Keystore and caches the encryption keys. It manages the loading and caching of the encryption keys
+ * and their registration with and validation against the encryption key registry.
+ *
+ * @since 4.0
+ *
+ */
+public class AlfrescoKeyStoreImpl implements AlfrescoKeyStore
+{
+ private static final Log logger = LogFactory.getLog(AlfrescoKeyStoreImpl.class);
+
+ protected KeyStoreParameters keyStoreParameters;
+ protected KeyStoreParameters backupKeyStoreParameters;
+ protected KeyResourceLoader keyResourceLoader;
+ protected EncryptionKeysRegistry encryptionKeysRegistry;
+
+ protected KeyMap keys;
+ protected KeyMap backupKeys;
+ protected final WriteLock writeLock;
+ protected final ReadLock readLock;
+
+ private Set keysToValidate;
+ protected boolean validateKeyChanges = false;
+
+ public AlfrescoKeyStoreImpl()
+ {
+ ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+ writeLock = lock.writeLock();
+ readLock = lock.readLock();
+ this.keys = new KeyMap();
+ this.backupKeys = new KeyMap();
+ }
+
+ public AlfrescoKeyStoreImpl(KeyStoreParameters keyStoreParameters, KeyResourceLoader keyResourceLoader)
+ {
+ this();
+
+ this.keyResourceLoader = keyResourceLoader;
+ this.keyStoreParameters = keyStoreParameters;
+
+ safeInit();
+ }
+
+ public void init()
+ {
+ writeLock.lock();
+ try
+ {
+ safeInit();
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ public void setEncryptionKeysRegistry(
+ EncryptionKeysRegistry encryptionKeysRegistry)
+ {
+ this.encryptionKeysRegistry = encryptionKeysRegistry;
+ }
+
+ public void setValidateKeyChanges(boolean validateKeyChanges)
+ {
+ this.validateKeyChanges = validateKeyChanges;
+ }
+
+ public void setKeysToValidate(Set keysToValidate)
+ {
+ this.keysToValidate = keysToValidate;
+ }
+
+ public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters)
+ {
+ this.keyStoreParameters = keyStoreParameters;
+ }
+
+ public void setBackupKeyStoreParameters(
+ KeyStoreParameters backupKeyStoreParameters)
+ {
+ this.backupKeyStoreParameters = backupKeyStoreParameters;
+ }
+
+ public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader)
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ }
+
+ public KeyStoreParameters getKeyStoreParameters()
+ {
+ return keyStoreParameters;
+ }
+
+ public KeyStoreParameters getBackupKeyStoreParameters()
+ {
+ return backupKeyStoreParameters;
+ }
+
+ public KeyResourceLoader getKeyResourceLoader()
+ {
+ return keyResourceLoader;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getName()
+ {
+ return keyStoreParameters.getName();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void validateKeys() throws InvalidKeystoreException, MissingKeyException
+ {
+ validateKeys(keys, backupKeys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean exists()
+ {
+ return keyStoreExists(getKeyStoreParameters().getLocation());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void reload() throws InvalidKeystoreException, MissingKeyException
+ {
+ KeyMap keys = loadKeyStore(getKeyStoreParameters());
+ KeyMap backupKeys = loadKeyStore(getBackupKeyStoreParameters());
+
+ validateKeys(keys, backupKeys);
+
+ // all ok, reload the keys
+ writeLock.lock();
+ try
+ {
+ this.keys = keys;
+ this.backupKeys = backupKeys;
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set getKeyAliases()
+ {
+ return new HashSet(keys.getKeyAliases());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void backup()
+ {
+ writeLock.lock();
+ try
+ {
+ for(String keyAlias : keys.getKeyAliases())
+ {
+ backupKeys.setKey(keyAlias, keys.getKey(keyAlias));
+ }
+ createKeyStore(backupKeyStoreParameters, backupKeys);
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void create()
+ {
+ createKeyStore(keyStoreParameters, keys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Key getKey(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ return keys.getCachedKey(keyAlias).getKey();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long getKeyTimestamp(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ CachedKey cachedKey = keys.getCachedKey(keyAlias);
+ return cachedKey.getTimestamp();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Key getBackupKey(String keyAlias)
+ {
+ readLock.lock();
+ try
+ {
+ return backupKeys.getCachedKey(keyAlias).getKey();
+ }
+ finally
+ {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public KeyManager[] createKeyManagers()
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(keyStoreParameters, keyInfoManager);
+
+ logger.debug("Initializing key managers");
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+ String keyStorePassword = keyInfoManager.getKeyStorePassword();
+ kmfactory.init(ks, keyStorePassword != null ? keyStorePassword.toCharArray(): null);
+ return kmfactory.getKeyManagers();
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create key manager", e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public TrustManager[] createTrustManagers()
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+
+ logger.debug("Initializing trust managers");
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(ks);
+ return tmfactory.getTrustManagers();
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create key manager", e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ protected String getKeyMetaDataFileLocation()
+ {
+ return keyStoreParameters.getKeyMetaDataFileLocation();
+ }
+
+ protected InputStream getKeyStoreStream(String location) throws FileNotFoundException
+ {
+ if(location == null)
+ {
+ return null;
+ }
+ return keyResourceLoader.getKeyStore(location);
+ }
+
+ protected OutputStream getKeyStoreOutStream() throws FileNotFoundException
+ {
+ return new FileOutputStream(getKeyStoreParameters().getLocation());
+ }
+
+ protected KeyInfoManager getKeyInfoManager(String metadataFileLocation) throws FileNotFoundException, IOException
+ {
+ return new KeyInfoManager(metadataFileLocation, keyResourceLoader);
+ }
+
+ protected KeyMap cacheKeys(KeyStore ks, KeyInfoManager keyInfoManager)
+ throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException
+ {
+ KeyMap keys = new KeyMap();
+
+ // load and cache the keys
+ for(Entry keyEntry : keyInfoManager.getKeyInfo().entrySet())
+ {
+ String keyAlias = keyEntry.getKey();
+
+ KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias);
+ String passwordStr = keyInfo != null ? keyInfo.getPassword() : null;
+
+ // Null is an acceptable value (means no key)
+ Key key = null;
+
+ // Attempt to get the key
+ key = ks.getKey(keyAlias, passwordStr == null ? null : passwordStr.toCharArray());
+ if(key != null)
+ {
+ keys.setKey(keyAlias, key);
+ }
+ // Key loaded
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Retrieved key from keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n" +
+ " Alias: " + keyAlias + "\n" +
+ " Password?: " + (passwordStr != null));
+
+ Certificate[] certs = ks.getCertificateChain(keyAlias);
+ if(certs != null)
+ {
+ logger.debug("Certificate chain '" + keyAlias + "':");
+ for(int c = 0; c < certs.length; c++)
+ {
+ if(certs[c] instanceof X509Certificate)
+ {
+ X509Certificate cert = (X509Certificate)certs[c];
+ logger.debug(" Certificate " + (c + 1) + ":");
+ logger.debug(" Subject DN: " + cert.getSubjectDN());
+ logger.debug(" Signature Algorithm: " + cert.getSigAlgName());
+ logger.debug(" Valid from: " + cert.getNotBefore() );
+ logger.debug(" Valid until: " + cert.getNotAfter());
+ logger.debug(" Issuer: " + cert.getIssuerDN());
+ }
+ }
+ }
+ }
+ }
+
+ return keys;
+ }
+
+ protected KeyStore initialiseKeyStore(String type, String provider)
+ {
+ KeyStore ks = null;
+
+ try
+ {
+ if(provider == null || provider.equals(""))
+ {
+ ks = KeyStore.getInstance(type);
+ }
+ else
+ {
+ ks = KeyStore.getInstance(type, provider);
+ }
+
+ ks.load(null, null);
+
+ return ks;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to intialise key store", e);
+ }
+ }
+
+ protected KeyStore loadKeyStore(KeyStoreParameters keyStoreParameters, KeyInfoManager keyInfoManager)
+ {
+ String pwdKeyStore = null;
+
+ try
+ {
+ KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider());
+
+ // Load it up
+ InputStream is = getKeyStoreStream(keyStoreParameters.getLocation());
+ if (is != null)
+ {
+ try
+ {
+ // Get the keystore password
+ pwdKeyStore = keyInfoManager.getKeyStorePassword();
+ ks.load(is, pwdKeyStore == null ? null : pwdKeyStore.toCharArray());
+ }
+ finally
+ {
+ try {is.close(); } catch (Throwable e) {}
+ }
+ }
+ else
+ {
+ // this is ok, the keystore will contain no keys.
+ logger.warn("Keystore file doesn't exist: " + keyStoreParameters.getLocation());
+ }
+
+ return ks;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to load key store: " + keyStoreParameters.getLocation(), e);
+ }
+ finally
+ {
+ pwdKeyStore = null;
+ }
+ }
+
+ /**
+ * Initializes class
+ */
+ private void safeInit()
+ {
+ PropertyCheck.mandatory(this, "location", getKeyStoreParameters().getLocation());
+
+ // Make sure we choose the default type, if required
+ if(getKeyStoreParameters().getType() == null)
+ {
+ keyStoreParameters.setType(KeyStore.getDefaultType());
+ }
+
+ writeLock.lock();
+ try
+ {
+ keys = loadKeyStore(keyStoreParameters);
+ backupKeys = loadKeyStore(backupKeyStoreParameters);
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ private KeyMap loadKeyStore(KeyStoreParameters keyStoreParameters)
+ {
+ InputStream is = null;
+ KeyInfoManager keyInfoManager = null;
+ KeyStore ks = null;
+
+ if(keyStoreParameters == null)
+ {
+ // empty key map
+ return new KeyMap();
+ }
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation());
+ ks = loadKeyStore(keyStoreParameters, keyInfoManager);
+ // Loaded
+ }
+ catch (Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to initialize keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clearKeyStorePassword();
+ }
+
+ if (is != null)
+ {
+ try
+ {
+ is.close();
+ }
+ catch (Throwable e)
+ {
+
+ }
+ }
+ }
+
+ try
+ {
+ // cache the keys from the keystore
+ KeyMap keys = cacheKeys(ks, keyInfoManager);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Initialized keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n" +
+ keys.numKeys() + " keys found");
+ }
+
+ return keys;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to retrieve keys from keystore: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType() + "\n",
+ e);
+ }
+ finally
+ {
+ // Clear key information
+ keyInfoManager.clear();
+ }
+ }
+
+ protected void createKey(String keyAlias)
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ Key key = getSecretKey(keyInfoManager.getKeyInformation(keyAlias));
+ encryptionKeysRegistry.registerKey(keyAlias, key);
+ keys.setKey(keyAlias, key);
+
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+ ks.setKeyEntry(keyAlias, key, keyInfoManager.getKeyInformation(keyAlias).getPassword().toCharArray(), null);
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyInfoManager.getKeyStorePassword().toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+
+ logger.info("Created key: " + keyAlias + "\n in key store: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType());
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to create key: " + keyAlias + "\n in key store: \n" +
+ " Location: " + getKeyStoreParameters().getLocation() + "\n" +
+ " Provider: " + getKeyStoreParameters().getProvider() + "\n" +
+ " Type: " + getKeyStoreParameters().getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ protected void createKeyStore(KeyStoreParameters keyStoreParameters, KeyMap keys)
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ try
+ {
+ if(!keyStoreExists(keyStoreParameters.getLocation()))
+ {
+ keyInfoManager = getKeyInfoManager(keyStoreParameters.getKeyMetaDataFileLocation());
+ KeyStore ks = initialiseKeyStore(keyStoreParameters.getType(), keyStoreParameters.getProvider());
+
+ String keyStorePassword = keyInfoManager.getKeyStorePassword();
+ if(keyStorePassword == null)
+ {
+ throw new AlfrescoRuntimeException("Key store password is null for keystore at location "
+ + getKeyStoreParameters().getLocation()
+ + ", key store meta data location" + getKeyMetaDataFileLocation());
+ }
+
+ for(String keyAlias : keys.getKeyAliases())
+ {
+ KeyInformation keyInfo = keyInfoManager.getKeyInformation(keyAlias);
+
+ Key key = keys.getKey(keyAlias);
+ if(key == null)
+ {
+ logger.warn("Key with alias " + keyAlias + " is null when creating keystore at location " + keyStoreParameters.getLocation());
+ }
+ else
+ {
+ ks.setKeyEntry(keyAlias, key, keyInfo.getPassword().toCharArray(), null);
+ }
+ }
+
+// try
+// {
+// throw new Exception("Keystore creation: " + );
+// }
+// catch(Throwable e)
+// {
+// logger.debug(e.getMessage());
+// e.printStackTrace();
+// }
+
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyStorePassword.toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+ }
+ else
+ {
+ logger.warn("Can't create key store " + keyStoreParameters.getLocation() + ", already exists.");
+ }
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to create keystore: \n" +
+ " Location: " + keyStoreParameters.getLocation() + "\n" +
+ " Provider: " + keyStoreParameters.getProvider() + "\n" +
+ " Type: " + keyStoreParameters.getType(),
+ e);
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+ }
+ }
+
+ /*
+ * For testing
+ */
+// void createBackup()
+// {
+// createKeyStore(backupKeyStoreParameters, backupKeys);
+// }
+
+ private byte[] generateKeyData()
+ {
+ try
+ {
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN];
+ random.nextBytes(bytes);
+ return bytes;
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException("Unable to generate secret key", e);
+ }
+ }
+
+ protected Key getSecretKey(KeyInformation keyInformation) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException
+ {
+ byte[] keyData = keyInformation.getKeyData();
+
+ if(keyData == null)
+ {
+ if(keyInformation.getKeyAlgorithm().equals("DESede"))
+ {
+ // no key data provided, generate key data automatically
+ keyData = generateKeyData();
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Unable to generate secret key: key algorithm is not DESede and no keyData provided");
+ }
+ }
+
+ DESedeKeySpec keySpec = new DESedeKeySpec(keyData);
+ SecretKeyFactory kf = SecretKeyFactory.getInstance(keyInformation.getKeyAlgorithm());
+ SecretKey secretKey = kf.generateSecret(keySpec);
+ return secretKey;
+ }
+
+ void importPrivateKey(String keyAlias, String keyPassword, InputStream fl, InputStream certstream)
+ throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, CertificateException, KeyStoreException
+ {
+ KeyInfoManager keyInfoManager = null;
+
+ writeLock.lock();
+ try
+ {
+ keyInfoManager = getKeyInfoManager(getKeyMetaDataFileLocation());
+ KeyStore ks = loadKeyStore(getKeyStoreParameters(), keyInfoManager);
+
+ // loading Key
+ byte[] keyBytes = new byte[fl.available()];
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ fl.read(keyBytes, 0, fl.available());
+ fl.close();
+ PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(keyBytes);
+ PrivateKey key = kf.generatePrivate(keysp);
+
+ // loading CertificateChain
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ @SuppressWarnings("rawtypes")
+ Collection c = cf.generateCertificates(certstream) ;
+ Certificate[] certs = new Certificate[c.toArray().length];
+
+ certs = (Certificate[])c.toArray(new Certificate[0]);
+
+ // storing keystore
+ ks.setKeyEntry(keyAlias, key, keyPassword.toCharArray(), certs);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Key and certificate stored.");
+ logger.debug("Alias:"+ keyAlias);
+ }
+ OutputStream keyStoreOutStream = getKeyStoreOutStream();
+ ks.store(keyStoreOutStream, keyPassword.toCharArray());
+ // Workaround for MNT-15005
+ keyStoreOutStream.close();
+ }
+ finally
+ {
+ if(keyInfoManager != null)
+ {
+ keyInfoManager.clear();
+ }
+
+ writeLock.unlock();
+ }
+ }
+
+ public boolean backupExists()
+ {
+ return keyStoreExists(getBackupKeyStoreParameters().getLocation());
+ }
+
+ protected boolean keyStoreExists(String location)
+ {
+ try
+ {
+ InputStream is = getKeyStoreStream(location);
+ if (is == null)
+ {
+ return false;
+ }
+ else
+ {
+ try { is.close(); } catch (Throwable e) {}
+ return true;
+ }
+ }
+ catch(FileNotFoundException e)
+ {
+ return false;
+ }
+ }
+
+ /*
+ * Validates the keystore keys against the key registry, throwing exceptions if the keys have been unintentionally changed.
+ *
+ * For each key to validate:
+ *
+ * (i) no main key, no backup key, the key is registered for the main keystore -> error, must re-instate the keystore
+ * (ii) no main key, no backup key, the key is not registered -> create the main key store and register the key
+ * (iii) main key exists but is not registered -> register the key
+ * (iv) main key exists, no backup key, the key is registered -> check that the key has not changed - if it has, throw an exception
+ * (v) main key exists, backup key exists, the key is registered -> check in the registry that the backup key has not changed and then re-register main key
+ */
+ protected void validateKeys(KeyMap keys, KeyMap backupKeys) throws InvalidKeystoreException, MissingKeyException
+ {
+ if(!validateKeyChanges)
+ {
+ return;
+ }
+
+ writeLock.lock();
+ try
+ {
+ // check for the existence of a key store first
+ for(String keyAlias : keysToValidate)
+ {
+ if(keys.getKey(keyAlias) == null)
+ {
+ if(backupKeys.getKey(keyAlias) == null)
+ {
+ if(encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // The key is registered and neither key nor backup key exist -> throw
+ // an exception indicating that the key is missing and the keystore should
+ // be re-instated.
+ throw new MissingKeyException(keyAlias, getKeyStoreParameters().getLocation());
+ }
+ else
+ {
+ // Neither the key nor the backup key exist, so create the key
+ createKey(keyAlias);
+ }
+ }
+ }
+ else
+ {
+ if(!encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // The key is not registered, so register it
+ encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias));
+ }
+ else if(backupKeys.getKey(keyAlias) == null && encryptionKeysRegistry.checkKey(keyAlias, keys.getKey(keyAlias)) == KEY_STATUS.CHANGED)
+ {
+ // A key has been changed, indicating that the keystore has been un-intentionally changed.
+ // Note: this will halt the application bootstrap.
+ throw new InvalidKeystoreException("The key with alias " + keyAlias + " has been changed, re-instate the previous keystore");
+ }
+ else if(backupKeys.getKey(keyAlias) != null && encryptionKeysRegistry.isKeyRegistered(keyAlias))
+ {
+ // Both key and backup key exist and the key is registered.
+ if(encryptionKeysRegistry.checkKey(keyAlias, backupKeys.getKey(keyAlias)) == KEY_STATUS.OK)
+ {
+ // The registered key is the backup key so lets re-register the key in the main key store.
+ // Unregister the existing (now backup) key and re-register the main key.
+ encryptionKeysRegistry.unregisterKey(keyAlias);
+ encryptionKeysRegistry.registerKey(keyAlias, keys.getKey(keyAlias));
+ }
+ }
+ }
+ }
+ }
+ finally
+ {
+ writeLock.unlock();
+ }
+ }
+
+ public static class KeyInformation
+ {
+ protected String alias;
+ protected byte[] keyData;
+ protected String password;
+ protected String keyAlgorithm;
+
+ public KeyInformation(String alias, byte[] keyData, String password, String keyAlgorithm)
+ {
+ super();
+ this.alias = alias;
+ this.keyData = keyData;
+ this.password = password;
+ this.keyAlgorithm = keyAlgorithm;
+ }
+
+ public String getAlias()
+ {
+ return alias;
+ }
+
+ public byte[] getKeyData()
+ {
+ return keyData;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public String getKeyAlgorithm()
+ {
+ return keyAlgorithm;
+ }
+ }
+
+ /*
+ * Caches key meta data information such as password, seed.
+ *
+ */
+ public static class KeyInfoManager
+ {
+ private KeyResourceLoader keyResourceLoader;
+ private String metadataFileLocation;
+ private Properties keyProps;
+ private String keyStorePassword = null;
+ private Map keyInfo;
+
+ /**
+ * For testing.
+ *
+ * @param passwords
+ */
+ KeyInfoManager(Map passwords, KeyResourceLoader keyResourceLoader)
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ keyInfo = new HashMap(2);
+ for(Map.Entry password : passwords.entrySet())
+ {
+ keyInfo.put(password.getKey(), new KeyInformation(password.getKey(), null, password.getValue(), null));
+ }
+ }
+
+ KeyInfoManager(String metadataFileLocation, KeyResourceLoader keyResourceLoader) throws IOException, FileNotFoundException
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ this.metadataFileLocation = metadataFileLocation;
+ keyInfo = new HashMap(2);
+ loadKeyMetaData();
+ }
+
+ public Map getKeyInfo()
+ {
+ // TODO defensively copy
+ return keyInfo;
+ }
+
+ /**
+ * Set the map of key meta data (including passwords to access the keystore).
+ *
+ * Where required, null values must be inserted into the map to indicate the presence
+ * of a key that is not protected by a password. They entry for {@link #KEY_KEYSTORE_PASSWORD}
+ * is required if the keystore is password protected.
+ */
+ protected void loadKeyMetaData() throws IOException, FileNotFoundException
+ {
+ keyProps = keyResourceLoader.loadKeyMetaData(metadataFileLocation);
+ if(keyProps != null)
+ {
+ String aliases = keyProps.getProperty("aliases");
+ if(aliases == null)
+ {
+ throw new AlfrescoRuntimeException("Passwords file must contain an aliases key");
+ }
+
+ this.keyStorePassword = keyProps.getProperty(KEY_KEYSTORE_PASSWORD);
+
+ StringTokenizer st = new StringTokenizer(aliases, ",");
+ while(st.hasMoreTokens())
+ {
+ String keyAlias = st.nextToken();
+ keyInfo.put(keyAlias, loadKeyInformation(keyAlias));
+ }
+ }
+ else
+ {
+ // TODO
+ //throw new FileNotFoundException("Cannot find key metadata file " + getKeyMetaDataFileLocation());
+ }
+ }
+
+ public void clear()
+ {
+ this.keyStorePassword = null;
+ if(this.keyProps != null)
+ {
+ this.keyProps.clear();
+ }
+ }
+
+ public void removeKeyInformation(String keyAlias)
+ {
+ this.keyProps.remove(keyAlias);
+ }
+
+ protected KeyInformation loadKeyInformation(String keyAlias)
+ {
+ String keyPassword = keyProps.getProperty(keyAlias + ".password");
+ String keyData = keyProps.getProperty(keyAlias + ".keyData");
+ String keyAlgorithm = keyProps.getProperty(keyAlias + ".algorithm");
+
+ byte[] keyDataBytes = null;
+ if(keyData != null && !keyData.equals(""))
+ {
+ keyDataBytes = Base64.decodeBase64(keyData);
+ }
+ KeyInformation keyInfo = new KeyInformation(keyAlias, keyDataBytes, keyPassword, keyAlgorithm);
+ return keyInfo;
+ }
+
+ public String getKeyStorePassword()
+ {
+ return keyStorePassword;
+ }
+
+ public void clearKeyStorePassword()
+ {
+ this.keyStorePassword = null;
+ }
+
+ public KeyInformation getKeyInformation(String keyAlias)
+ {
+ return keyInfo.get(keyAlias);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/CachedKey.java b/src/main/java/org/alfresco/encryption/CachedKey.java
new file mode 100644
index 0000000000..9d24ee0598
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/CachedKey.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+
+/**
+ *
+ * Represents a loaded, cached encryption key. The key can be null .
+ *
+ * @since 4.0
+ *
+ */
+public class CachedKey
+{
+ public static CachedKey NULL = new CachedKey(null, null);
+
+ private Key key;
+ private long timestamp;
+
+ CachedKey(Key key, Long timestamp)
+ {
+ this.key = key;
+ this.timestamp = (timestamp != null ? timestamp.longValue() : -1);
+ }
+
+ public CachedKey(Key key)
+ {
+ super();
+ this.key = key;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ public Key getKey()
+ {
+ return key;
+ }
+
+ public long getTimestamp()
+ {
+ return timestamp;
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/DecryptingInputStream.java b/src/main/java/org/alfresco/encryption/DecryptingInputStream.java
new file mode 100644
index 0000000000..97a0e5bf7f
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/DecryptingInputStream.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An input stream that encrypts data produced by a {@link EncryptingOutputStream}. A lightweight yet secure hybrid
+ * encryption scheme is used. A random symmetric key is decrypted using the receiver's private key. The supplied data is
+ * then decrypted using the symmetric key and read on a streaming basis. When the end of the stream is reached or the
+ * stream is closed, a HMAC checksum of the entire stream contents is validated.
+ */
+public class DecryptingInputStream extends InputStream
+{
+
+ /** The wrapped stream. */
+ private final DataInputStream wrapped;
+
+ /** The input cipher. */
+ private final Cipher inputCipher;
+
+ /** The MAC generator. */
+ private final Mac mac;
+
+ /** Internal buffer for MAC computation. */
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
+
+ /** A DataOutputStream on top of our interal buffer. */
+ private final DataOutputStream dataStr = new DataOutputStream(this.buffer);
+
+ /** The current unencrypted data block. */
+ private byte[] currentDataBlock;
+
+ /** The next encrypted data block. (could be the HMAC checksum) */
+ private byte[] nextDataBlock;
+
+ /** Have we read to the end of the underlying stream?. */
+ private boolean isAtEnd;
+
+ /** Our current position within currentDataBlock. */
+ private int currentDataPos;
+
+ /**
+ * Constructs a DecryptingInputStream using default symmetric encryption parameters.
+ *
+ * @param wrapped
+ * the input stream to decrypt
+ * @param privKey
+ * the receiver's private key for decrypting the symmetric key
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws InvalidAlgorithmParameterException
+ * the invalid algorithm parameter exception
+ * @throws NoSuchProviderException
+ * the no such provider exception
+ */
+ public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey) throws IOException,
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
+ BadPaddingException, InvalidAlgorithmParameterException, NoSuchProviderException
+ {
+ this(wrapped, privKey, "AES", "CBC", "PKCS5PADDING");
+ }
+
+ /**
+ * Constructs a DecryptingInputStream.
+ *
+ * @param wrapped
+ * the input stream to decrypt
+ * @param privKey
+ * the receiver's private key for decrypting the symmetric key
+ * @param algorithm
+ * encryption algorithm (e.g. "AES")
+ * @param mode
+ * encryption mode (e.g. "CBC")
+ * @param padding
+ * padding scheme (e.g. "PKCS5PADDING")
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws InvalidAlgorithmParameterException
+ * the invalid algorithm parameter exception
+ * @throws NoSuchProviderException
+ * the no such provider exception
+ */
+ public DecryptingInputStream(final InputStream wrapped, final PrivateKey privKey, final String algorithm,
+ final String mode, final String padding) throws IOException, NoSuchAlgorithmException,
+ NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
+ InvalidAlgorithmParameterException, NoSuchProviderException
+ {
+ // Initialise a secure source of randomness
+ this.wrapped = new DataInputStream(wrapped);
+ final SecureRandom secRand = SecureRandom.getInstance("SHA1PRNG");
+
+ // Set up RSA
+ final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
+ rsa.init(Cipher.DECRYPT_MODE, privKey, secRand);
+
+ // Read and decrypt the symmetric key
+ final SecretKey symKey = new SecretKeySpec(rsa.doFinal(readBlock()), algorithm);
+
+ // Read and decrypt initialisation vector
+ final byte[] keyIV = rsa.doFinal(readBlock());
+
+ // Set up cipher for decryption
+ this.inputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding);
+ this.inputCipher.init(Cipher.DECRYPT_MODE, symKey, new IvParameterSpec(keyIV));
+
+ // Read and decrypt the MAC key
+ final SecretKey macKey = new SecretKeySpec(this.inputCipher.doFinal(readBlock()), "HMACSHA1");
+
+ // Set up HMAC
+ this.mac = Mac.getInstance("HMACSHA1");
+ this.mac.init(macKey);
+
+ // Always read a block ahead so we can intercept the HMAC block
+ this.nextDataBlock = readBlock(false);
+ }
+
+ /**
+ * Reads the next block of data, adding it to the HMAC checksum. Strips the header recording the number of bytes in
+ * the block.
+ *
+ * @return the data block, or null
if the end of the stream has been reached
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private byte[] readBlock() throws IOException
+ {
+ return readBlock(true);
+ }
+
+ /**
+ * Reads the next block of data, optionally adding it to the HMAC checksum. Strips the header recording the number
+ * of bytes in the block.
+ *
+ * @param updateMac
+ * should the block be added to the HMAC checksum?
+ * @return the data block, or null
if the end of the stream has been reached
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private byte[] readBlock(final boolean updateMac) throws IOException
+ {
+ int len;
+ try
+ {
+ len = this.wrapped.readInt();
+ }
+ catch (final EOFException e)
+ {
+ return null;
+ }
+ final byte[] in = new byte[len];
+ this.wrapped.readFully(in);
+ if (updateMac)
+ {
+ macBlock(in);
+ }
+ return in;
+ }
+
+ /**
+ * Updates the HMAC checksum with the given data block.
+ *
+ * @param block
+ * the block
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private void macBlock(final byte[] block) throws IOException
+ {
+ this.dataStr.writeInt(block.length);
+ this.dataStr.write(block);
+ // If we don't have the MAC key yet, buffer up until we do
+ if (this.mac != null)
+ {
+ this.dataStr.flush();
+ final byte[] bytes = this.buffer.toByteArray();
+ this.buffer.reset();
+ this.mac.update(bytes);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read()
+ */
+ @Override
+ public int read() throws IOException
+ {
+ final byte[] buf = new byte[1];
+ int bytesRead;
+ while ((bytesRead = read(buf)) == 0)
+ {
+ ;
+ }
+ return bytesRead == -1 ? -1 : buf[0] & 0xFF;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read(byte[])
+ */
+ @Override
+ public int read(final byte b[]) throws IOException
+ {
+ return read(b, 0, b.length);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#read(byte[], int, int)
+ */
+ @Override
+ public int read(final byte b[], int off, final int len) throws IOException
+ {
+ if (b == null)
+ {
+ throw new NullPointerException();
+ }
+ else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0)
+ {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0)
+ {
+ return 0;
+ }
+
+ int bytesToRead = len;
+ OUTER: while (bytesToRead > 0)
+ {
+ // Fetch another block if necessary
+ while (this.currentDataBlock == null || this.currentDataPos >= this.currentDataBlock.length)
+ {
+ byte[] newDataBlock;
+ // We're right at the end of the last block so finish
+ if (this.isAtEnd)
+ {
+ this.currentDataBlock = this.nextDataBlock = null;
+ break OUTER;
+ }
+ // We've already read the last block so validate the MAC code
+ else if ((newDataBlock = readBlock(false)) == null)
+ {
+ if (!MessageDigest.isEqual(this.mac.doFinal(), this.nextDataBlock))
+ {
+ throw new IOException("Invalid HMAC");
+ }
+ // We still have what's left in the cipher to read
+ try
+ {
+ this.currentDataBlock = this.inputCipher.doFinal();
+ }
+ catch (final GeneralSecurityException e)
+ {
+ throw new RuntimeException(e);
+ }
+ this.isAtEnd = true;
+ }
+ // We have an ordinary data block to MAC and decrypt
+ else
+ {
+ macBlock(this.nextDataBlock);
+ this.currentDataBlock = this.inputCipher.update(this.nextDataBlock);
+ this.nextDataBlock = newDataBlock;
+ }
+ this.currentDataPos = 0;
+ }
+ final int bytesRead = Math.min(bytesToRead, this.currentDataBlock.length - this.currentDataPos);
+ System.arraycopy(this.currentDataBlock, this.currentDataPos, b, off, bytesRead);
+ bytesToRead -= bytesRead;
+ off += bytesRead;
+ this.currentDataPos += bytesRead;
+ }
+ return bytesToRead == len ? -1 : len - bytesToRead;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#available()
+ */
+ @Override
+ public int available() throws IOException
+ {
+ return this.currentDataBlock == null ? 0 : this.currentDataBlock.length - this.currentDataPos;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.InputStream#close()
+ */
+ @Override
+ public void close() throws IOException
+ {
+ // Read right to the end, just to ensure the MAC code is valid!
+ if (this.nextDataBlock != null)
+ {
+ final byte[] skipBuff = new byte[1024];
+ while (read(skipBuff) != -1)
+ {
+ ;
+ }
+ }
+ this.wrapped.close();
+ this.dataStr.close();
+ }
+
+}
diff --git a/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java b/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java
new file mode 100644
index 0000000000..6bceef2a9d
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/DefaultEncryptionUtils.java
@@ -0,0 +1,480 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.AlgorithmParameters;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.alfresco.encryption.MACUtils.MACInput;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.IPUtils;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.extensions.surf.util.Base64;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Various encryption utility methods.
+ *
+ * @since 4.0
+ */
+public class DefaultEncryptionUtils implements EncryptionUtils
+{
+ // Logger
+ protected static Log logger = LogFactory.getLog(Encryptor.class);
+
+ protected static String HEADER_ALGORITHM_PARAMETERS = "XAlfresco-algorithmParameters";
+ protected static String HEADER_MAC = "XAlfresco-mac";
+ protected static String HEADER_TIMESTAMP = "XAlfresco-timestamp";
+
+ protected Encryptor encryptor;
+ protected MACUtils macUtils;
+ protected long messageTimeout; // ms
+ protected String remoteIP;
+ protected String localIP;
+
+ public DefaultEncryptionUtils()
+ {
+ try
+ {
+ this.localIP = InetAddress.getLocalHost().getHostAddress();
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("Unable to initialise EncryptionUtils", e);
+ }
+ }
+
+ public String getRemoteIP()
+ {
+ return remoteIP;
+ }
+
+ public void setRemoteIP(String remoteIP)
+ {
+ try
+ {
+ this.remoteIP = IPUtils.getRealIPAddress(remoteIP);
+ }
+ catch (UnknownHostException e)
+ {
+ throw new AlfrescoRuntimeException("Failed to get server IP address", e);
+ }
+ }
+
+ /**
+ * Get the local registered IP address for authentication purposes
+ *
+ * @return String
+ */
+ protected String getLocalIPAddress()
+ {
+ return localIP;
+ }
+
+ public void setMessageTimeout(long messageTimeout)
+ {
+ this.messageTimeout = messageTimeout;
+ }
+
+ public void setEncryptor(Encryptor encryptor)
+ {
+ this.encryptor = encryptor;
+ }
+
+ public void setMacUtils(MACUtils macUtils)
+ {
+ this.macUtils = macUtils;
+ }
+
+ protected void setRequestMac(HttpMethod method, byte[] mac)
+ {
+ if(mac == null)
+ {
+ throw new AlfrescoRuntimeException("Mac cannot be null");
+ }
+ method.setRequestHeader(HEADER_MAC, Base64.encodeBytes(mac));
+ }
+
+ /**
+ * Set the MAC on the HTTP response
+ *
+ * @param response HttpServletResponse
+ * @param mac byte[]
+ */
+ protected void setMac(HttpServletResponse response, byte[] mac)
+ {
+ if(mac == null)
+ {
+ throw new AlfrescoRuntimeException("Mac cannot be null");
+ }
+
+ response.setHeader(HEADER_MAC, Base64.encodeBytes(mac));
+ }
+
+ /**
+ * Get the MAC (Message Authentication Code) on the HTTP request
+ *
+ * @param req HttpServletRequest
+ * @return the MAC
+ * @throws IOException
+ */
+ protected byte[] getMac(HttpServletRequest req) throws IOException
+ {
+ String header = req.getHeader(HEADER_MAC);
+ if(header != null)
+ {
+ return Base64.decode(header);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get the MAC (Message Authentication Code) on the HTTP response
+ *
+ * @param res HttpMethod
+ * @return the MAC
+ * @throws IOException
+ */
+ protected byte[] getResponseMac(HttpMethod res) throws IOException
+ {
+ Header header = res.getResponseHeader(HEADER_MAC);
+ if(header != null)
+ {
+ return Base64.decode(header.getValue());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Set the timestamp on the HTTP request
+ * @param method HttpMethod
+ * @param timestamp (ms, in UNIX time)
+ */
+ protected void setRequestTimestamp(HttpMethod method, long timestamp)
+ {
+ method.setRequestHeader(HEADER_TIMESTAMP, String.valueOf(timestamp));
+ }
+
+ /**
+ * Set the timestamp on the HTTP response
+ * @param res HttpServletResponse
+ * @param timestamp (ms, in UNIX time)
+ */
+ protected void setTimestamp(HttpServletResponse res, long timestamp)
+ {
+ res.setHeader(HEADER_TIMESTAMP, String.valueOf(timestamp));
+ }
+
+ /**
+ * Get the timestamp on the HTTP response
+ *
+ * @param method HttpMethod
+ * @return timestamp (ms, in UNIX time)
+ * @throws IOException
+ */
+ protected Long getResponseTimestamp(HttpMethod method) throws IOException
+ {
+ Header header = method.getResponseHeader(HEADER_TIMESTAMP);
+ if(header != null)
+ {
+ return Long.valueOf(header.getValue());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Get the timestamp on the HTTP request
+ *
+ * @param method HttpServletRequest
+ * @return timestamp (ms, in UNIX time)
+ * @throws IOException
+ */
+ protected Long getTimestamp(HttpServletRequest method) throws IOException
+ {
+ String header = method.getHeader(HEADER_TIMESTAMP);
+ if(header != null)
+ {
+ return Long.valueOf(header);
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRequestAlgorithmParameters(HttpMethod method, AlgorithmParameters params) throws IOException
+ {
+ if(params != null)
+ {
+ method.setRequestHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded()));
+ }
+ }
+
+ /**
+ * Set the algorithm parameters header on the HTTP response
+ *
+ * @param response HttpServletResponse
+ * @param params AlgorithmParameters
+ * @throws IOException
+ */
+ protected void setAlgorithmParameters(HttpServletResponse response, AlgorithmParameters params) throws IOException
+ {
+ if(params != null)
+ {
+ response.setHeader(HEADER_ALGORITHM_PARAMETERS, Base64.encodeBytes(params.getEncoded()));
+ }
+ }
+
+ /**
+ * Decode cipher algorithm parameters from the HTTP method
+ *
+ * @param method HttpMethod
+ * @return decoded algorithm parameters
+ * @throws IOException
+ */
+ protected AlgorithmParameters decodeAlgorithmParameters(HttpMethod method) throws IOException
+ {
+ Header header = method.getResponseHeader(HEADER_ALGORITHM_PARAMETERS);
+ if(header != null)
+ {
+ byte[] algorithmParams = Base64.decode(header.getValue());
+ AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams);
+ return algorithmParameters;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * Decode cipher algorithm parameters from the HTTP method
+ *
+ * @param req
+ * @return decoded algorithm parameters
+ * @throws IOException
+ */
+ protected AlgorithmParameters decodeAlgorithmParameters(HttpServletRequest req) throws IOException
+ {
+ String header = req.getHeader(HEADER_ALGORITHM_PARAMETERS);
+ if(header != null)
+ {
+ byte[] algorithmParams = Base64.decode(header);
+ AlgorithmParameters algorithmParameters = encryptor.decodeAlgorithmParameters(algorithmParams);
+ return algorithmParameters;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decryptResponseBody(HttpMethod method) throws IOException
+ {
+ // TODO fileoutputstream if content is especially large?
+ InputStream body = method.getResponseBodyAsStream();
+ if(body != null)
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ FileCopyUtils.copy(body, out);
+
+ AlgorithmParameters params = decodeAlgorithmParameters(method);
+ if(params != null)
+ {
+ byte[] decrypted = encryptor.decrypt(KeyProvider.ALIAS_SOLR, params, out.toByteArray());
+ return decrypted;
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Unable to decrypt response body, missing encryption algorithm parameters");
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decryptBody(HttpServletRequest req) throws IOException
+ {
+ if(req.getMethod().equals("POST"))
+ {
+ InputStream bodyStream = req.getInputStream();
+ if(bodyStream != null)
+ {
+ // expect algorithParameters header
+ AlgorithmParameters p = decodeAlgorithmParameters(req);
+
+ // decrypt the body
+ InputStream in = encryptor.decrypt(KeyProvider.ALIAS_SOLR, p, bodyStream);
+ return IOUtils.toByteArray(in);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean authenticateResponse(HttpMethod method, String remoteIP, byte[] decryptedBody)
+ {
+ try
+ {
+ byte[] expectedMAC = getResponseMac(method);
+ Long timestamp = getResponseTimestamp(method);
+ if(timestamp == null)
+ {
+ return false;
+ }
+ remoteIP = IPUtils.getRealIPAddress(remoteIP);
+ return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), remoteIP));
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException("Unable to authenticate HTTP response", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean authenticate(HttpServletRequest req, byte[] decryptedBody)
+ {
+ try
+ {
+ byte[] expectedMAC = getMac(req);
+ Long timestamp = getTimestamp(req);
+ if(timestamp == null)
+ {
+ return false;
+ }
+ String ipAddress = IPUtils.getRealIPAddress(req.getRemoteAddr());
+ return authenticate(expectedMAC, new MACInput(decryptedBody, timestamp.longValue(), ipAddress));
+ }
+ catch(Exception e)
+ {
+ throw new AlfrescoRuntimeException("Unable to authenticate HTTP request", e);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setRequestAuthentication(HttpMethod method, byte[] message) throws IOException
+ {
+ long requestTimestamp = System.currentTimeMillis();
+
+ // add MAC header
+ byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR, new MACInput(message, requestTimestamp, getLocalIPAddress()));
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP request " + method.getPath());
+ logger.debug("Setting timestamp " + requestTimestamp + " on HTTP request " + method.getPath());
+ }
+
+ setRequestMac(method, mac);
+
+ // prevent replays
+ setRequestTimestamp(method, requestTimestamp);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setResponseAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
+ byte[] responseBody, AlgorithmParameters params) throws IOException
+ {
+ long responseTimestamp = System.currentTimeMillis();
+ byte[] mac = macUtils.generateMAC(KeyProvider.ALIAS_SOLR,
+ new MACInput(responseBody, responseTimestamp, getLocalIPAddress()));
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Setting MAC " + Arrays.toString(mac) + " on HTTP response to request " + httpRequest.getRequestURI());
+ logger.debug("Setting timestamp " + responseTimestamp + " on HTTP response to request " + httpRequest.getRequestURI());
+ }
+
+ setAlgorithmParameters(httpResponse, params);
+ setMac(httpResponse, mac);
+
+ // prevent replays
+ setTimestamp(httpResponse, responseTimestamp);
+ }
+
+ protected boolean authenticate(byte[] expectedMAC, MACInput macInput)
+ {
+ // check the MAC and, if valid, check that the timestamp is under the threshold and that the remote IP is
+ // the expected IP
+ boolean authorized = macUtils.validateMAC(KeyProvider.ALIAS_SOLR, expectedMAC, macInput) &&
+ validateTimestamp(macInput.getTimestamp());
+ return authorized;
+ }
+
+ protected boolean validateTimestamp(long timestamp)
+ {
+ long currentTime = System.currentTimeMillis();
+ return((currentTime - timestamp) < messageTimeout);
+ }
+
+}
diff --git a/src/main/java/org/alfresco/encryption/DefaultEncryptor.java b/src/main/java/org/alfresco/encryption/DefaultEncryptor.java
new file mode 100644
index 0000000000..630fa3d56b
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/DefaultEncryptor.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.PropertyCheck;
+
+/**
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class DefaultEncryptor extends AbstractEncryptor
+{
+ private boolean cacheCiphers = true;
+ private final ThreadLocal> threadCipher;
+
+ /**
+ * Default constructor for IOC
+ */
+ public DefaultEncryptor()
+ {
+ threadCipher = new ThreadLocal>();
+ }
+
+ /**
+ * Convenience constructor for tests
+ */
+ /* package */ DefaultEncryptor(KeyProvider keyProvider, String cipherAlgorithm, String cipherProvider)
+ {
+ this();
+ setKeyProvider(keyProvider);
+ setCipherAlgorithm(cipherAlgorithm);
+ setCipherProvider(cipherProvider);
+ }
+
+ public void init()
+ {
+ super.init();
+ PropertyCheck.mandatory(this, "cipherAlgorithm", cipherAlgorithm);
+ }
+
+ public void setCacheCiphers(boolean cacheCiphers)
+ {
+ this.cacheCiphers = cacheCiphers;
+ }
+
+ protected Cipher createCipher(int mode, String algorithm, String provider, Key key, AlgorithmParameters params)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException
+ {
+ Cipher cipher = null;
+
+ if (cipherProvider == null)
+ {
+ cipher = Cipher.getInstance(algorithm);
+ }
+ else
+ {
+ cipher = Cipher.getInstance(algorithm, provider);
+ }
+ cipher.init(mode, key, params);
+
+ return cipher;
+ }
+
+ protected Cipher getCachedCipher(String keyAlias, int mode, AlgorithmParameters params, Key key)
+ throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidAlgorithmParameterException
+ {
+ CachedCipher cipherInfo = null;
+ Cipher cipher = null;
+
+ Map ciphers = threadCipher.get();
+ if(ciphers == null)
+ {
+ ciphers = new HashMap(5);
+ threadCipher.set(ciphers);
+ }
+ cipherInfo = ciphers.get(new CipherKey(keyAlias, mode));
+ if(cipherInfo == null)
+ {
+ cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params);
+ ciphers.put(new CipherKey(keyAlias, mode), new CachedCipher(cipher, key));
+
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Cipher constructed: alias=" + keyAlias + "; mode=" + mode + ": " + cipher);
+ }
+ }
+ else
+ {
+ // the key has changed, re-construct the cipher
+ if(cipherInfo.getKey() != key)
+ {
+ // key has changed, rendering the cached cipher out of date. Re-create the cipher with
+ // the new key.
+ cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params);
+ ciphers.put(new CipherKey(keyAlias, mode), new CachedCipher(cipher, key));
+ }
+ else
+ {
+ cipher = cipherInfo.getCipher();
+ }
+ }
+
+ return cipher;
+ }
+
+ @Override
+ public Cipher getCipher(String keyAlias, AlgorithmParameters params, int mode)
+ {
+ Cipher cipher = null;
+
+ // Get the encryption key
+ Key key = keyProvider.getKey(keyAlias);
+ if(key == null)
+ {
+ // No encryption possible
+ return null;
+ }
+
+ try
+ {
+ if(cacheCiphers)
+ {
+ cipher = getCachedCipher(keyAlias, mode, params, key);
+ }
+ else
+ {
+ cipher = createCipher(mode, cipherAlgorithm, cipherProvider, key, params);
+ }
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException(
+ "Failed to construct cipher: alias=" + keyAlias + "; mode=" + mode,
+ e);
+ }
+
+ return cipher;
+ }
+
+ public boolean keyAvailable(String keyAlias)
+ {
+ return keyProvider.getKey(keyAlias) != null;
+ }
+
+ private static class CipherKey
+ {
+ private String keyAlias;
+ private int mode;
+
+ public CipherKey(String keyAlias, int mode)
+ {
+ super();
+ this.keyAlias = keyAlias;
+ this.mode = mode;
+ }
+
+ public String getKeyAlias()
+ {
+ return keyAlias;
+ }
+
+ public int getMode()
+ {
+ return mode;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((keyAlias == null) ? 0 : keyAlias.hashCode());
+ result = prime * result + mode;
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if(this == obj)
+ {
+ return true;
+ }
+
+ if(!(obj instanceof CipherKey))
+ {
+ return false;
+ }
+
+ CipherKey other = (CipherKey)obj;
+ if(keyAlias == null)
+ {
+ if (other.keyAlias != null)
+ {
+ return false;
+ }
+ }
+ else if(!keyAlias.equals(other.keyAlias))
+ {
+ return false;
+ }
+
+ if(mode != other.mode)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ /*
+ * Stores a cipher and the key used to construct it.
+ */
+ private static class CachedCipher
+ {
+ private Key key;
+ private Cipher cipher;
+
+ public CachedCipher(Cipher cipher, Key key)
+ {
+ super();
+ this.cipher = cipher;
+ this.key = key;
+ }
+
+ public Cipher getCipher()
+ {
+ return cipher;
+ }
+
+ public Key getKey()
+ {
+ return key;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java b/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java
new file mode 100644
index 0000000000..7bf33ca8b4
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/DefaultFallbackEncryptor.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+import java.security.InvalidKeyException;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.Pair;
+
+/**
+ * The fallback encryptor provides a fallback mechanism for decryption, first using the default
+ * encryption keys and, if they fail (perhaps because they have been changed), falling back
+ * to a backup set of keys.
+ *
+ * Note that encryption will be performed only using the default encryption keys.
+ *
+ * @since 4.0
+ */
+public class DefaultFallbackEncryptor implements FallbackEncryptor
+{
+ private Encryptor fallback;
+ private Encryptor main;
+
+ public DefaultFallbackEncryptor()
+ {
+ }
+
+ public DefaultFallbackEncryptor(Encryptor main, Encryptor fallback)
+ {
+ this();
+ this.main = main;
+ this.fallback = fallback;
+ }
+
+ public void setFallback(Encryptor fallback)
+ {
+ this.fallback = fallback;
+ }
+
+ public void setMain(Encryptor main)
+ {
+ this.main = main;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pair encrypt(String keyAlias,
+ AlgorithmParameters params, byte[] input)
+ {
+ // Note: encrypt supported only for main encryptor
+ Pair ret = main.encrypt(keyAlias, params, input);
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public byte[] decrypt(String keyAlias, AlgorithmParameters params,
+ byte[] input)
+ {
+ byte[] ret;
+
+ // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated),
+ // fall back to fallback encryptor.
+ try
+ {
+ ret = main.decrypt(keyAlias, params, input);
+ }
+ catch(Throwable e)
+ {
+ ret = fallback.decrypt(keyAlias, params, input);
+ }
+
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream decrypt(String keyAlias, AlgorithmParameters params,
+ InputStream in)
+ {
+ InputStream ret;
+
+ // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated),
+ // fall back to fallback encryptor.
+ try
+ {
+ ret = main.decrypt(keyAlias, params, in);
+ }
+ catch(Throwable e)
+ {
+ ret = fallback.decrypt(keyAlias, params, in);
+ }
+
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Pair encryptObject(String keyAlias,
+ AlgorithmParameters params, Object input)
+ {
+ // Note: encrypt supported only for main encryptor
+ Pair ret = main.encryptObject(keyAlias, params, input);
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Object decryptObject(String keyAlias, AlgorithmParameters params,
+ byte[] input)
+ {
+ Object ret;
+
+ // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated),
+ // fall back to fallback encryptor.
+ try
+ {
+ ret = main.decryptObject(keyAlias, params, input);
+ }
+ catch(Throwable e)
+ {
+ ret = fallback.decryptObject(keyAlias, params, input);
+ }
+
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Serializable sealObject(String keyAlias, AlgorithmParameters params,
+ Serializable input)
+ {
+ // Note: encrypt supported only for main encryptor
+ Serializable ret = main.sealObject(keyAlias, params, input);
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Serializable unsealObject(String keyAlias, Serializable input)
+ throws InvalidKeyException
+ {
+ Serializable ret;
+
+ // for decryption, try the main encryptor. If that fails (possibly as a result of the keys being updated),
+ // fall back to fallback encryptor.
+ try
+ {
+ ret = main.unsealObject(keyAlias, input);
+ }
+ catch(Throwable e)
+ {
+ ret = fallback.unsealObject(keyAlias, input);
+ }
+
+ return ret;
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AlgorithmParameters decodeAlgorithmParameters(byte[] encoded)
+ {
+ AlgorithmParameters ret;
+
+ try
+ {
+ ret = main.decodeAlgorithmParameters(encoded);
+ }
+ catch(AlfrescoRuntimeException e)
+ {
+ ret = fallback.decodeAlgorithmParameters(encoded);
+ }
+
+ return ret;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean keyAvailable(String keyAlias)
+ {
+ return main.keyAvailable(keyAlias);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean backupKeyAvailable(String keyAlias)
+ {
+ return fallback.keyAvailable(keyAlias);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java b/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java
new file mode 100644
index 0000000000..7096da7b18
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/EncryptingOutputStream.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2005-2010 Alfresco Software, Ltd. All rights reserved.
+ *
+ * License rights for this program may be obtained from Alfresco Software, Ltd.
+ * pursuant to a written agreement and any use of this program without such an
+ * agreement is prohibited.
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An output stream that encrypts data to another output stream. A lightweight yet secure hybrid encryption scheme is
+ * used. A random symmetric key is generated and encrypted using the receiver's public key. The supplied data is then
+ * encrypted using the symmetric key and sent to the underlying stream on a streaming basis. An HMAC checksum is also
+ * computed on an ongoing basis and appended to the output when the stream is closed. This class can be used in
+ * conjunction with {@link DecryptingInputStream} to transport data securely.
+ */
+public class EncryptingOutputStream extends OutputStream
+{
+ /** The wrapped stream. */
+ private final OutputStream wrapped;
+
+ /** The output cipher. */
+ private final Cipher outputCipher;
+
+ /** The MAC generator. */
+ private final Mac mac;
+
+ /** Internal buffer for MAC computation. */
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
+
+ /** A DataOutputStream on top of our interal buffer. */
+ private final DataOutputStream dataStr = new DataOutputStream(this.buffer);
+
+ /**
+ * Constructs an EncryptingOutputStream using default symmetric encryption parameters.
+ *
+ * @param wrapped
+ * outputstream to store the encrypted data
+ * @param receiverKey
+ * the receiver's public key for encrypting the symmetric key
+ * @param rand
+ * a secure source of randomness
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ */
+ public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final SecureRandom rand)
+ throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+ IllegalBlockSizeException, BadPaddingException
+ {
+ this(wrapped, receiverKey, "AES", rand, 128, "CBC", "PKCS5PADDING");
+ }
+
+ /**
+ * Constructs an EncryptingOutputStream.
+ *
+ * @param wrapped
+ * outputstream to store the encrypted data
+ * @param receiverKey
+ * the receiver's public key for encrypting the symmetric key
+ * @param algorithm
+ * symmetric encryption algorithm (e.g. "AES")
+ * @param rand
+ * a secure source of randomness
+ * @param strength
+ * the key size in bits (e.g. 128)
+ * @param mode
+ * encryption mode (e.g. "CBC")
+ * @param padding
+ * padding scheme (e.g. "PKCS5PADDING")
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ * @throws NoSuchAlgorithmException
+ * the no such algorithm exception
+ * @throws NoSuchPaddingException
+ * the no such padding exception
+ * @throws InvalidKeyException
+ * the invalid key exception
+ * @throws BadPaddingException
+ * the bad padding exception
+ * @throws IllegalBlockSizeException
+ * the illegal block size exception
+ */
+ public EncryptingOutputStream(final OutputStream wrapped, final PublicKey receiverKey, final String algorithm,
+ final SecureRandom rand, final int strength, final String mode, final String padding) throws IOException,
+ NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
+ BadPaddingException
+ {
+ // Initialise
+ this.wrapped = wrapped;
+
+ // Generate a random symmetric key
+ final KeyGenerator keyGen = KeyGenerator.getInstance(algorithm);
+ keyGen.init(strength, rand);
+ final Key symKey = keyGen.generateKey();
+
+ // Instantiate Symmetric cipher for encryption.
+ this.outputCipher = Cipher.getInstance(algorithm + "/" + mode + "/" + padding);
+ this.outputCipher.init(Cipher.ENCRYPT_MODE, symKey, rand);
+
+ // Set up HMAC
+ this.mac = Mac.getInstance("HMACSHA1");
+ final byte[] macKeyBytes = new byte[20];
+ rand.nextBytes(macKeyBytes);
+ final Key macKey = new SecretKeySpec(macKeyBytes, "HMACSHA1");
+ this.mac.init(macKey);
+
+ // Set up RSA to encrypt symmetric key
+ final Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA1ANDMGF1PADDING");
+ rsa.init(Cipher.ENCRYPT_MODE, receiverKey, rand);
+
+ // Write the header
+
+ // Write out an RSA-encrypted block for the key of the cipher.
+ writeBlock(rsa.doFinal(symKey.getEncoded()));
+
+ // Write out RSA-encrypted Initialisation Vector block
+ writeBlock(rsa.doFinal(this.outputCipher.getIV()));
+
+ // Write out key for HMAC.
+ writeBlock(this.outputCipher.doFinal(macKey.getEncoded()));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.OutputStream#write(int)
+ */
+ @Override
+ public void write(final int b) throws IOException
+ {
+ write(new byte[]
+ {
+ (byte) b
+ }, 0, 1);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.OutputStream#write(byte[])
+ */
+ @Override
+ public void write(final byte b[]) throws IOException
+ {
+ write(b, 0, b.length);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.OutputStream#write(byte[], int, int)
+ */
+ @Override
+ public void write(final byte b[], final int off, final int len) throws IOException
+ {
+ if (b == null)
+ {
+ throw new NullPointerException();
+ }
+ else if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0)
+ {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0)
+ {
+ return;
+ }
+ final byte[] out = this.outputCipher.update(b, off, len); // Encrypt data.
+ if (out != null && out.length > 0)
+ {
+ writeBlock(out);
+ }
+ }
+
+ /**
+ * Writes a block of data, preceded by its length, and adds it to the HMAC checksum.
+ *
+ * @param out
+ * the data to be written.
+ * @throws IOException
+ * Signals that an I/O exception has occurred.
+ */
+ private void writeBlock(final byte[] out) throws IOException
+ {
+ this.dataStr.writeInt(out.length); // Write length.
+ this.dataStr.write(out); // Write encrypted data.
+ this.dataStr.flush();
+ final byte[] block = this.buffer.toByteArray();
+ this.buffer.reset();
+ this.mac.update(block);
+ this.wrapped.write(block);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.OutputStream#flush()
+ */
+ @Override
+ public void flush() throws IOException
+ {
+ this.wrapped.flush();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.io.OutputStream#close()
+ */
+ @Override
+ public void close() throws IOException
+ {
+ try
+ {
+ // Write the last block
+ writeBlock(this.outputCipher.doFinal());
+ }
+ catch (final GeneralSecurityException e)
+ {
+ throw new RuntimeException(e);
+ }
+ // Write the MAC code
+ writeBlock(this.mac.doFinal());
+ this.wrapped.close();
+ this.dataStr.close();
+ }
+
+}
diff --git a/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java b/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java
new file mode 100644
index 0000000000..bc58421067
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/EncryptionKeysRegistry.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Stores registered encryption keys.
+ *
+ * @since 4.0
+ *
+ */
+public interface EncryptionKeysRegistry
+{
+ public static enum KEY_STATUS
+ {
+ OK, CHANGED, MISSING;
+ };
+
+ /**
+ * Is the key with alias 'keyAlias' registered?
+ * @param keyAlias String
+ * @return boolean
+ */
+ public boolean isKeyRegistered(String keyAlias);
+
+ /**
+ * Register the key.
+ *
+ * @param keyAlias String
+ * @param key Key
+ */
+ public void registerKey(String keyAlias, Key key);
+
+ /**
+ * Unregister the key.
+ *
+ * @param keyAlias String
+ */
+ public void unregisterKey(String keyAlias);
+
+ /**
+ * Check the validity of the key against the registry.
+ *
+ * @param keyAlias String
+ * @param key Key
+ * @return KEY_STATUS
+ */
+ public KEY_STATUS checkKey(String keyAlias, Key key);
+
+ /**
+ * Remove the set of keys from the registry.
+ *
+ * @param keys Set
+ */
+ public void removeRegisteredKeys(Set keys);
+
+ /**
+ * Return those keys in the set that have been registered.
+ *
+ * @param keys Set
+ * @return List
+ */
+ public List getRegisteredKeys(Set keys);
+}
diff --git a/src/main/java/org/alfresco/encryption/EncryptionUtils.java b/src/main/java/org/alfresco/encryption/EncryptionUtils.java
new file mode 100644
index 0000000000..d8211b3c45
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/EncryptionUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.IOException;
+import java.security.AlgorithmParameters;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.httpclient.HttpMethod;
+
+/**
+ * Various encryption utility methods.
+ *
+ * @since 4.0
+ */
+public interface EncryptionUtils
+{
+ /**
+ * Decrypt the response body of the http method
+ *
+ * @param method
+ * @return decrypted response body
+ * @throws IOException
+ */
+ public byte[] decryptResponseBody(HttpMethod method) throws IOException;
+
+ /**
+ * Decrypt the body of the http request
+ *
+ * @param req
+ * @return decrypted response body
+ * @throws IOException
+ */
+ public byte[] decryptBody(HttpServletRequest req) throws IOException;
+
+ /**
+ * Authenticate the http method response: validate the MAC, check that the remote IP is
+ * as expected and that the timestamp is recent.
+ *
+ * @param method
+ * @param remoteIP
+ * @param decryptedBody
+ * @return true if the method reponse is authentic, false otherwise
+ */
+ public boolean authenticateResponse(HttpMethod method, String remoteIP, byte[] decryptedBody);
+
+ /**
+ * Authenticate the http request: validate the MAC, check that the remote IP is
+ * as expected and that the timestamp is recent.
+ *
+ * @param req
+ * @param decryptedBody
+ * @return true if the method request is authentic, false otherwise
+ */
+ public boolean authenticate(HttpServletRequest req, byte[] decryptedBody);
+
+ /**
+ * Encrypt the http method request body
+ *
+ * @param method
+ * @param message
+ * @throws IOException
+ */
+ public void setRequestAuthentication(HttpMethod method, byte[] message) throws IOException;
+
+ /**
+ * Sets authentication headers on the HTTP response.
+ *
+ * @param httpRequest
+ * @param httpResponse
+ * @param responseBody
+ * @param params
+ * @throws IOException
+ */
+ public void setResponseAuthentication(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
+ byte[] responseBody, AlgorithmParameters params) throws IOException;
+
+ /**
+ * Set the algorithm parameters header on the method request
+ *
+ * @param method
+ * @param params
+ * @throws IOException
+ */
+ public void setRequestAlgorithmParameters(HttpMethod method, AlgorithmParameters params) throws IOException;
+}
diff --git a/src/main/java/org/alfresco/encryption/Encryptor.java b/src/main/java/org/alfresco/encryption/Encryptor.java
new file mode 100644
index 0000000000..dc58db43e3
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/Encryptor.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.AlgorithmParameters;
+import java.security.InvalidKeyException;
+
+import org.alfresco.util.Pair;
+
+/**
+ * Interface providing methods to encrypt and decrypt data.
+ *
+ * @since 4.0
+ */
+public interface Encryptor
+{
+ /**
+ * Encrypt some bytes
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to encrypt
+ * @return the encrypted data and parameters used
+ */
+ Pair encrypt(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Decrypt some bytes
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to decrypt
+ * @return the unencrypted data
+ */
+ byte[] decrypt(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Decrypt an input stream
+ *
+ * @param keyAlias the encryption key alias
+ * @param in the data to decrypt
+ * @return the unencrypted data
+ */
+ InputStream decrypt(String keyAlias, AlgorithmParameters params, InputStream in);
+
+ /**
+ * Encrypt an object
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to write to bytes
+ * @return the encrypted data and parameters used
+ */
+ Pair encryptObject(String keyAlias, AlgorithmParameters params, Object input);
+
+ /**
+ * Decrypt data as an object
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the data to decrypt
+ * @return the unencrypted data deserialized
+ */
+ Object decryptObject(String keyAlias, AlgorithmParameters params, byte[] input);
+
+ /**
+ * Convenience method to seal on object up cryptographically.
+ *
+ * Note that the original object may be returned directly if there is no key associated with
+ * the alias.
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to encrypt and seal
+ * @return the sealed object that can be decrypted with the original key
+ */
+ Serializable sealObject(String keyAlias, AlgorithmParameters params, Serializable input);
+
+ /**
+ * Convenience method to unseal on object sealed up cryptographically.
+ *
+ * Note that the algorithm parameters not provided on the assumption that a symmetric key
+ * algorithm is in use - only the key is required for unsealing.
+ *
+ * Note that the original object may be returned directly if there is no key associated with
+ * the alias or if the input object is not a SealedObject
.
+ *
+ * @param keyAlias the encryption key alias
+ * @param input the object to decrypt and unseal
+ * @return the original unsealed object that was encrypted with the original key
+ * @throws IllegalStateException if the key alias is not valid and the input is a
+ * SealedObject
+ */
+ Serializable unsealObject(String keyAlias, Serializable input) throws InvalidKeyException;
+
+ /**
+ * Decodes encoded cipher algorithm parameters
+ *
+ * @param encoded the encoded cipher algorithm parameters
+ * @return the decoded cipher algorithmParameters
+ */
+ AlgorithmParameters decodeAlgorithmParameters(byte[] encoded);
+
+ boolean keyAvailable(String keyAlias);
+}
diff --git a/src/main/java/org/alfresco/encryption/FallbackEncryptor.java b/src/main/java/org/alfresco/encryption/FallbackEncryptor.java
new file mode 100644
index 0000000000..a5428fcadd
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/FallbackEncryptor.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+/**
+ * A fallback encryptor provides a fallback mechanism for decryption, first using the default
+ * encryption keys and, if they fail (perhaps because they have been changed), falling back
+ * to a backup set of keys.
+ *
+ * Note that encryption will be performed only using the default encryption keys.
+ *
+ * @since 4.0
+ */
+public interface FallbackEncryptor extends Encryptor
+{
+ /**
+ * Is the backup key available in order to fall back to?
+ *
+ * @return boolean
+ */
+ boolean backupKeyAvailable(String keyAlias);
+}
diff --git a/src/main/java/org/alfresco/encryption/GenerateSecretKey.java b/src/main/java/org/alfresco/encryption/GenerateSecretKey.java
new file mode 100644
index 0000000000..8e10773505
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/GenerateSecretKey.java
@@ -0,0 +1,47 @@
+package org.alfresco.encryption;
+
+import java.security.SecureRandom;
+
+import javax.crypto.spec.DESedeKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ *
+ * Generate a secret key for use by the repository.
+ *
+ * @since 4.0
+ *
+ */
+public class GenerateSecretKey
+{
+ public byte[] generateKeyData()
+ {
+ try
+ {
+ SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+ random.setSeed(System.currentTimeMillis());
+ byte bytes[] = new byte[DESedeKeySpec.DES_EDE_KEY_LEN];
+ random.nextBytes(bytes);
+ return bytes;
+ }
+ catch(Exception e)
+ {
+ throw new RuntimeException("Unable to generate secret key", e);
+ }
+ }
+
+ public static void main(String args[])
+ {
+ try
+ {
+ GenerateSecretKey gen = new GenerateSecretKey();
+ byte[] bytes = gen.generateKeyData();
+ System.out.print(Base64.encodeBase64String(bytes));
+ }
+ catch(Throwable e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java b/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java
new file mode 100644
index 0000000000..4048f9c0bc
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/InvalidKeystoreException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class InvalidKeystoreException extends Exception
+{
+ private static final long serialVersionUID = -1324791685965572313L;
+
+ public InvalidKeystoreException(String message)
+ {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/KeyMap.java b/src/main/java/org/alfresco/encryption/KeyMap.java
new file mode 100644
index 0000000000..74e9e2cec7
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeyMap.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A simple map of key aliases to keys. Each key has an associated timestamp indicating
+ * when it was last loaded from the keystore on disk.
+ *
+ * @since 4.0
+ *
+ */
+public class KeyMap
+{
+ private Map keys;
+
+ public KeyMap()
+ {
+ this.keys = new HashMap(5);
+ }
+
+ public KeyMap(Map keys)
+ {
+ super();
+ this.keys = keys;
+ }
+
+ public int numKeys()
+ {
+ return keys.size();
+ }
+
+ public Set getKeyAliases()
+ {
+ return keys.keySet();
+ }
+
+ // always returns an instance; if null will return a CachedKey.NULL
+ public CachedKey getCachedKey(String keyAlias)
+ {
+ CachedKey cachedKey = keys.get(keyAlias);
+ return (cachedKey != null ? cachedKey : CachedKey.NULL);
+ }
+
+ public Key getKey(String keyAlias)
+ {
+ return getCachedKey(keyAlias).getKey();
+ }
+
+ public void setKey(String keyAlias, Key key)
+ {
+ keys.put(keyAlias, new CachedKey(key));
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/KeyProvider.java b/src/main/java/org/alfresco/encryption/KeyProvider.java
new file mode 100644
index 0000000000..a24a398953
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeyProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+
+/**
+ * A key provider returns the secret keys for different use cases.
+ *
+ * @since 4.0
+ */
+public interface KeyProvider
+{
+ // TODO: Allow the aliases to be configured i.e. include an alias mapper
+ /**
+ * Constant representing the keystore alias for keys to encrypt/decrypt node metadata
+ */
+ public static final String ALIAS_METADATA = "metadata";
+
+ /**
+ * Constant representing the keystore alias for keys to encrypt/decrypt SOLR transfer data
+ */
+ public static final String ALIAS_SOLR = "solr";
+
+ /**
+ * Get an encryption key if available.
+ *
+ * @param keyAlias the key alias
+ * @return the encryption key and a timestamp of when it was last changed
+ */
+ public Key getKey(String keyAlias);
+}
diff --git a/src/main/java/org/alfresco/encryption/KeyResourceLoader.java b/src/main/java/org/alfresco/encryption/KeyResourceLoader.java
new file mode 100644
index 0000000000..f32325ed80
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeyResourceLoader.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Manages key resources (key store and key store passwords)
+ *
+ * @since 4.0
+ *
+ */
+public interface KeyResourceLoader
+{
+ /**
+ * Loads and returns an InputStream of the key store at the configured location.
+ * If the file cannot be found this method returns null.
+ *
+ * @return InputStream
+ * @throws FileNotFoundException
+ */
+ public InputStream getKeyStore(String keyStoreLocation) throws FileNotFoundException;
+
+ /**
+ * Loads key metadata from the configured passwords file location.
+ *
+ * Note that the passwords are not cached locally.
+ * If the file cannot be found this method returns null.
+ *
+ * @return Properties
+ * @throws IOException
+ */
+ public Properties loadKeyMetaData(String keyMetaDataFileLocation) throws IOException, FileNotFoundException;
+}
diff --git a/src/main/java/org/alfresco/encryption/KeyStoreParameters.java b/src/main/java/org/alfresco/encryption/KeyStoreParameters.java
new file mode 100644
index 0000000000..5b8ebe4504
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeyStoreParameters.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import org.alfresco.util.PropertyCheck;
+
+/**
+ * Stores Java keystore initialisation parameters.
+ *
+ * @since 4.0
+ *
+ */
+public class KeyStoreParameters
+{
+ private String name;
+ private String type;
+ private String provider;
+ private String keyMetaDataFileLocation;
+ private String location;
+
+ public KeyStoreParameters()
+ {
+ }
+
+ public KeyStoreParameters(String name, String type, String keyStoreProvider,
+ String keyMetaDataFileLocation, String location)
+ {
+ super();
+ this.name = name;
+ this.type = type;
+ this.provider = keyStoreProvider;
+ this.keyMetaDataFileLocation = keyMetaDataFileLocation;
+ this.location = location;
+ }
+
+ public void init()
+ {
+ if (!PropertyCheck.isValidPropertyString(getLocation()))
+ {
+ setLocation(null);
+ }
+ if (!PropertyCheck.isValidPropertyString(getProvider()))
+ {
+ setProvider(null);
+ }
+ if (!PropertyCheck.isValidPropertyString(getType()))
+ {
+ setType(null);
+ }
+ if (!PropertyCheck.isValidPropertyString(getKeyMetaDataFileLocation()))
+ {
+ setKeyMetaDataFileLocation(null);
+ }
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String getType()
+ {
+ return type;
+ }
+
+ public String getProvider()
+ {
+ return provider;
+ }
+
+ public String getKeyMetaDataFileLocation()
+ {
+ return keyMetaDataFileLocation;
+ }
+
+ public String getLocation()
+ {
+ return location;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public void setType(String type)
+ {
+ this.type = type;
+ }
+
+ public void setProvider(String provider)
+ {
+ this.provider = provider;
+ }
+
+ public void setKeyMetaDataFileLocation(String keyMetaDataFileLocation)
+ {
+ this.keyMetaDataFileLocation = keyMetaDataFileLocation;
+ }
+
+ public void setLocation(String location)
+ {
+ this.location = location;
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/KeysReport.java b/src/main/java/org/alfresco/encryption/KeysReport.java
new file mode 100644
index 0000000000..49f10fce81
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeysReport.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.util.List;
+
+/**
+ * A report on which keys have changed and which keys have not changed.
+ *
+ * @since 4.0
+ *
+ */
+public class KeysReport
+{
+ private List keysChanged;
+ private List keysUnchanged;
+
+ public KeysReport(List keysChanged, List keysUnchanged)
+ {
+ super();
+ this.keysChanged = keysChanged;
+ this.keysUnchanged = keysUnchanged;
+ }
+
+ public List getKeysChanged()
+ {
+ return keysChanged;
+ }
+
+ public List getKeysUnchanged()
+ {
+ return keysUnchanged;
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java b/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java
new file mode 100644
index 0000000000..dcc2a5c960
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/KeystoreKeyProvider.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.security.Key;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ *
+ * Provides system-wide secret keys for symmetric database encryption from a key store
+ * in the filesystem. Just wraps a key store.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class KeystoreKeyProvider extends AbstractKeyProvider
+{
+ private static final Log logger = LogFactory.getLog(KeystoreKeyProvider.class);
+
+ private AlfrescoKeyStore keyStore;
+ private boolean useBackupKeys = false;
+
+ /**
+ * Constructs the provider with required defaults
+ */
+ public KeystoreKeyProvider()
+ {
+ }
+
+ public KeystoreKeyProvider(KeyStoreParameters keyStoreParameters, KeyResourceLoader keyResourceLoader)
+ {
+ this();
+ this.keyStore = new AlfrescoKeyStoreImpl(keyStoreParameters, keyResourceLoader);
+ init();
+ }
+
+ public void setUseBackupKeys(boolean useBackupKeys)
+ {
+ this.useBackupKeys = useBackupKeys;
+ }
+
+ /**
+ *
+ * @param keyStore
+ */
+ public KeystoreKeyProvider(AlfrescoKeyStore keyStore)
+ {
+ this();
+ this.keyStore = keyStore;
+ init();
+ }
+
+ public void setKeyStore(AlfrescoKeyStore keyStore)
+ {
+ this.keyStore = keyStore;
+ }
+
+ public void init()
+ {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Key getKey(String keyAlias)
+ {
+ if(useBackupKeys)
+ {
+ return keyStore.getBackupKey(keyAlias);
+ }
+ else
+ {
+ return keyStore.getKey(keyAlias);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/MACUtils.java b/src/main/java/org/alfresco/encryption/MACUtils.java
new file mode 100644
index 0000000000..e7f7eaad6b
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/MACUtils.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Key;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.crypto.Mac;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Provides support for generating and checking MACs (Message Authentication Codes) using Alfresco's
+ * secret keys.
+ *
+ * @since 4.0
+ *
+ */
+public class MACUtils
+{
+ private static Log logger = LogFactory.getLog(Encryptor.class);
+ private static byte SEPARATOR = 0;
+
+ private final ThreadLocal threadMac;
+
+ private KeyProvider keyProvider;
+ private String macAlgorithm;
+
+ /**
+ * Default constructor for IOC
+ */
+ public MACUtils()
+ {
+ threadMac = new ThreadLocal();
+ }
+
+ public void setKeyProvider(KeyProvider keyProvider)
+ {
+ this.keyProvider = keyProvider;
+ }
+
+ public void setMacAlgorithm(String macAlgorithm)
+ {
+ this.macAlgorithm = macAlgorithm;
+ }
+
+ protected Mac getMac(String keyAlias) throws Exception
+ {
+ Mac mac = threadMac.get();
+ if(mac == null)
+ {
+ mac = Mac.getInstance(macAlgorithm);
+
+ threadMac.set(mac);
+ }
+ Key key = keyProvider.getKey(keyAlias);
+ if(key == null)
+ {
+ throw new AlfrescoRuntimeException("Unexpected null key for key alias " + keyAlias);
+ }
+ mac.init(key);
+ return mac;
+ }
+
+ protected byte[] longToByteArray(long l) throws IOException
+ {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(bos);
+ dos.writeLong(l);
+ dos.flush();
+ return bos.toByteArray();
+ }
+
+ public byte[] generateMAC(String keyAlias, MACInput macInput)
+ {
+ try
+ {
+ InputStream fullMessage = macInput.getMACInput();
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Generating MAC for " + macInput + "...");
+ }
+
+ Mac mac = getMac(keyAlias);
+
+ byte[] buf = new byte[1024];
+ int len;
+ while((len = fullMessage.read(buf, 0, 1024)) != -1)
+ {
+ mac.update(buf, 0, len);
+ }
+ byte[] newMAC = mac.doFinal();
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("...done. MAC is " + Arrays.toString(newMAC));
+ }
+
+ return newMAC;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to generate MAC", e);
+ }
+ }
+
+ /**
+ * Compares the expectedMAC against the MAC generated from
+ * Assumes message has been decrypted
+ * @param keyAlias String
+ * @param expectedMAC byte[]
+ * @param macInput MACInput
+ * @return boolean
+ */
+ public boolean validateMAC(String keyAlias, byte[] expectedMAC, MACInput macInput)
+ {
+ try
+ {
+ byte[] mac = generateMAC(keyAlias, macInput);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("Validating expected MAC " + Arrays.toString(expectedMAC) + " against mac " + Arrays.toString(mac) + " for MAC input " + macInput + "...");
+ }
+
+ boolean areEqual = Arrays.equals(expectedMAC, mac);
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug(areEqual ? "...MAC validation succeeded." : "...MAC validation failed.");
+ }
+
+ return areEqual;
+ }
+ catch (Exception e)
+ {
+ throw new AlfrescoRuntimeException("Failed to validate MAC", e);
+ }
+ }
+
+ /**
+ * Represents the information to be fed into the MAC generator
+ *
+ * @since 4.0
+ *
+ */
+ public static class MACInput
+ {
+ // The message, may be null
+ private InputStream message;
+ private long timestamp;
+ private String ipAddress;
+
+ public MACInput(byte[] message, long timestamp, String ipAddress)
+ {
+ this.message = (message != null ? new ByteArrayInputStream(message) : null);
+ this.timestamp = timestamp;
+ this.ipAddress = ipAddress;
+ }
+
+ public InputStream getMessage()
+ {
+ return message;
+ }
+
+ public long getTimestamp()
+ {
+ return timestamp;
+ }
+
+ public String getIpAddress()
+ {
+ return ipAddress;
+ }
+
+ public InputStream getMACInput() throws IOException
+ {
+ List inputStreams = new ArrayList();
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ DataOutputStream out = new DataOutputStream(bytes);
+ out.writeUTF(ipAddress);
+ out.writeByte(SEPARATOR);
+ out.writeLong(timestamp);
+ inputStreams.add(new ByteArrayInputStream(bytes.toByteArray()));
+
+ if(message != null)
+ {
+ inputStreams.add(message);
+ }
+
+ return new MessageInputStream(inputStreams);
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder("MACInput[");
+ sb.append("timestamp: ").append(getTimestamp());
+ sb.append("ipAddress: ").append(getIpAddress());
+ return sb.toString();
+ }
+ }
+
+ private static class MessageInputStream extends InputStream
+ {
+ private List input;
+ private InputStream activeInputStream;
+ private int currentStream = 0;
+
+ public MessageInputStream(List input)
+ {
+ this.input = input;
+ this.currentStream = 0;
+ this.activeInputStream = input.get(currentStream);
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ IOException firstIOException = null;
+
+ for(InputStream in : input)
+ {
+ try
+ {
+ in.close();
+ }
+ catch(IOException e)
+ {
+ if(firstIOException == null)
+ {
+ firstIOException = e;
+ }
+ }
+ }
+
+ if(firstIOException != null)
+ {
+ throw firstIOException;
+ }
+
+ }
+
+ @Override
+ public int read() throws IOException
+ {
+ int i = activeInputStream.read();
+ if(i == -1)
+ {
+ currentStream++;
+ if(currentStream >= input.size())
+ {
+ return -1;
+ }
+ else
+ {
+ activeInputStream = input.get(currentStream);
+ i = activeInputStream.read();
+ }
+ }
+
+ return i;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/MissingKeyException.java b/src/main/java/org/alfresco/encryption/MissingKeyException.java
new file mode 100644
index 0000000000..ed55eebb78
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/MissingKeyException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class MissingKeyException extends Exception
+{
+ private static final long serialVersionUID = -7843412242954504581L;
+
+ private String keyAlias;
+ private String keyStoreLocation;
+
+ public MissingKeyException(String message)
+ {
+ super(message);
+ }
+
+ public MissingKeyException(String keyAlias, String keyStoreLocation)
+ {
+ // TODO i18n
+ super("Key " + keyAlias + " is missing from keystore " + keyStoreLocation);
+ }
+
+ public String getKeyAlias()
+ {
+ return keyAlias;
+ }
+
+ public String getKeyStoreLocation()
+ {
+ return keyStoreLocation;
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java b/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java
new file mode 100644
index 0000000000..cf4b538b9c
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/SpringKeyResourceLoader.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.Resource;
+import org.springframework.util.ResourceUtils;
+
+/**
+ * Loads key resources (key store and key store passwords) from the Spring classpath.
+ *
+ * @since 4.0
+ *
+ */
+public class SpringKeyResourceLoader implements KeyResourceLoader, ApplicationContextAware
+{
+ /**
+ * The application context might not be available, in which case the usual URL
+ * loading is used.
+ */
+ private ApplicationContext applicationContext;
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
+ {
+ this.applicationContext = applicationContext;
+ }
+
+ /**
+ * Helper method to switch between application context resource loading or
+ * simpler current classloader resource loading.
+ */
+ private InputStream getSafeInputStream(String location)
+ {
+ try
+ {
+ final InputStream is;
+ if (applicationContext != null)
+ {
+ Resource resource = applicationContext.getResource(location);
+ if (resource.exists())
+ {
+ is = new BufferedInputStream(resource.getInputStream());
+ }
+ else
+ {
+ // Fall back to conventional loading
+ File file = ResourceUtils.getFile(location);
+ if (file.exists())
+ {
+ is = new BufferedInputStream(new FileInputStream(file));
+ }
+ else
+ {
+ is = null;
+ }
+ }
+ }
+ else
+ {
+ // Load conventionally (i.e. we are in a unit test)
+ File file = ResourceUtils.getFile(location);
+ if (file.exists())
+ {
+ is = new BufferedInputStream(new FileInputStream(file));
+ }
+ else
+ {
+ is = null;
+ }
+ }
+
+ return is;
+ }
+ catch (IOException e)
+ {
+ return null;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public InputStream getKeyStore(String keyStoreLocation)
+ {
+ if (keyStoreLocation == null)
+ {
+ return null;
+ }
+ return getSafeInputStream(keyStoreLocation);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Properties loadKeyMetaData(String keyMetaDataFileLocation) throws IOException
+ {
+ if (keyMetaDataFileLocation == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ InputStream is = getSafeInputStream(keyMetaDataFileLocation);
+ if (is == null)
+ {
+ return null;
+ }
+ else
+ {
+ try
+ {
+ Properties p = new Properties();
+ p.load(is);
+ return p;
+ }
+ finally
+ {
+ try { is.close(); } catch (Throwable e) {}
+ }
+ }
+ }
+ catch(FileNotFoundException e)
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java b/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java
new file mode 100644
index 0000000000..f2259dc10e
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/ssl/AuthSSLInitializationError.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption.ssl;
+
+/**
+ *
+ * Signals fatal error in initialization of {@link AuthSSLProtocolSocketFactory}.
+ *
+ *
+ *
+ * Adapted from code here: http://svn.apache.org/viewvc/httpcomponents/oac.hc3x/trunk/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLX509TrustManager.java?revision=608014&view=co
+ *
+ *
+ * @since 4.0
+ */
+public class AuthSSLInitializationError extends Error
+{
+ private static final long serialVersionUID = 8135341334029823112L;
+
+ /**
+ * Creates a new AuthSSLInitializationError.
+ */
+ public AuthSSLInitializationError()
+ {
+ super();
+ }
+
+ /**
+ * Creates a new AuthSSLInitializationError with the specified message.
+ *
+ * @param message error message
+ */
+ public AuthSSLInitializationError(String message)
+ {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java b/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java
new file mode 100644
index 0000000000..d3a08e6b3f
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/ssl/AuthSSLProtocolSocketFactory.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption.ssl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+
+import org.alfresco.encryption.AlfrescoKeyStore;
+import org.alfresco.encryption.KeyResourceLoader;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.httpclient.ConnectTimeoutException;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ *
+ * Mutual Authentication against an Alfresco repository.
+ *
+ * AuthSSLProtocolSocketFactory can be used to validate the identity of the HTTPS
+ * server against a list of trusted certificates and to authenticate to the HTTPS
+ * server using a private key.
+ *
+ *
+ *
+ * Adapted from code here: http://svn.apache.org/viewvc/httpcomponents/oac.hc3x/trunk/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLX509TrustManager.java?revision=608014&view=co
+ *
+ *
+ *
+ * AuthSSLProtocolSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore truststore} file containing one or several trusted certificates.
+ * The client secure socket will reject the connection during the SSL session handshake
+ * if the target HTTPS server attempts to authenticate itself with a non-trusted
+ * certificate.
+ *
+ *
+ *
+ * AuthSSLProtocolSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore keystore} file containg a private key/public certificate pair.
+ * The client secure socket will use the private key to authenticate itself to the target
+ * HTTPS server during the SSL session handshake if requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented by the client
+ * in order to establish client's authenticity
+ *
+ *
+ *
+ * @since 4.0
+ */
+public class AuthSSLProtocolSocketFactory implements SecureProtocolSocketFactory
+{
+ /** Log object for this class. */
+ private static final Log logger = LogFactory.getLog(AuthSSLProtocolSocketFactory.class);
+
+ private SSLContext sslcontext = null;
+
+ private AlfrescoKeyStore keyStore = null;
+ private AlfrescoKeyStore trustStore = null;
+
+ /**
+ * Constructor for AuthSSLProtocolSocketFactory. Either a keystore or truststore file
+ * must be given. Otherwise SSL context initialization error will result.
+ *
+ * @param sslKeyStore SSL parameters to use.
+ * @param keyResourceLoader loads key resources from an arbitrary source e.g. classpath
+ */
+ public AuthSSLProtocolSocketFactory(AlfrescoKeyStore sslKeyStore, AlfrescoKeyStore sslTrustStore, KeyResourceLoader keyResourceLoader)
+ {
+ super();
+ this.keyStore = sslKeyStore;
+ this.trustStore = sslTrustStore;
+ }
+
+ private SSLContext createSSLContext()
+ {
+ KeyManager[] keymanagers = keyStore.createKeyManagers();;
+ TrustManager[] trustmanagers = trustStore.createTrustManagers();
+
+ try
+ {
+ SSLContext sslcontext = SSLContext.getInstance("TLS");
+ sslcontext.init(keymanagers, trustmanagers, null);
+ return sslcontext;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create SSL context", e);
+ }
+ }
+
+ private SSLContext getSSLContext()
+ {
+ try
+ {
+ if(this.sslcontext == null)
+ {
+ this.sslcontext = createSSLContext();
+ }
+ return this.sslcontext;
+ }
+ catch(Throwable e)
+ {
+ throw new AlfrescoRuntimeException("Unable to create SSL context", e);
+ }
+ }
+
+ /**
+ * Attempts to get a new socket connection to the given host within the given time limit.
+ *
+ * To circumvent the limitations of older JREs that do not support connect timeout a
+ * controller thread is executed. The controller thread attempts to create a new socket
+ * within the given limit of time. If socket constructor does not return until the
+ * timeout expires, the controller terminates and throws an {@link ConnectTimeoutException}
+ *
+ *
+ * @param host the host name/IP
+ * @param port the port on the host
+ * @param localAddress the local host name/IP to bind the socket to
+ * @param localPort the port on the local machine
+ * @param params {@link HttpConnectionParams Http connection parameters}
+ *
+ * @return Socket a new socket
+ *
+ * @throws IOException if an I/O error occurs while creating the socket
+ * @throws UnknownHostException if the IP address of the host cannot be
+ * determined
+ */
+ public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort,
+ final HttpConnectionParams params) throws IOException, UnknownHostException, ConnectTimeoutException
+ {
+ SSLSocket sslSocket = null;
+
+ if(params == null)
+ {
+ throw new IllegalArgumentException("Parameters may not be null");
+ }
+ int timeout = params.getConnectionTimeout();
+ SocketFactory socketfactory = getSSLContext().getSocketFactory();
+ if(timeout == 0)
+ {
+ sslSocket = (SSLSocket)socketfactory.createSocket(host, port, localAddress, localPort);
+ }
+ else
+ {
+ sslSocket = (SSLSocket)socketfactory.createSocket();
+ SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
+ SocketAddress remoteaddr = new InetSocketAddress(host, port);
+ sslSocket.bind(localaddr);
+ sslSocket.connect(remoteaddr, timeout);
+ }
+
+ return sslSocket;
+ }
+
+ /**
+ * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
+ */
+ public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort)
+ throws IOException, UnknownHostException
+ {
+ SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
+ return sslSocket;
+ }
+
+ /**
+ * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
+ */
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException
+ {
+ SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(host, port);
+ return sslSocket;
+ }
+
+ /**
+ * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
+ */
+ public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
+ throws IOException, UnknownHostException
+ {
+ SSLSocket sslSocket = (SSLSocket)getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
+ return sslSocket;
+ }
+}
diff --git a/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java b/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java
new file mode 100644
index 0000000000..1252193e94
--- /dev/null
+++ b/src/main/java/org/alfresco/encryption/ssl/SSLEncryptionParameters.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.encryption.ssl;
+
+import org.alfresco.encryption.KeyStoreParameters;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class SSLEncryptionParameters
+{
+ private KeyStoreParameters keyStoreParameters;
+ private KeyStoreParameters trustStoreParameters;
+
+ /**
+ * Default constructor (for use by Spring)
+ */
+ public SSLEncryptionParameters()
+ {
+ super();
+ }
+
+ public SSLEncryptionParameters(KeyStoreParameters keyStoreParameters, KeyStoreParameters trustStoreParameters)
+ {
+ super();
+ this.keyStoreParameters = keyStoreParameters;
+ this.trustStoreParameters = trustStoreParameters;
+ }
+
+ public KeyStoreParameters getKeyStoreParameters()
+ {
+ return keyStoreParameters;
+ }
+
+ public KeyStoreParameters getTrustStoreParameters()
+ {
+ return trustStoreParameters;
+ }
+
+ public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters)
+ {
+ this.keyStoreParameters = keyStoreParameters;
+ }
+
+ public void setTrustStoreParameters(KeyStoreParameters trustStoreParameters)
+ {
+ this.trustStoreParameters = trustStoreParameters;
+ }
+}
diff --git a/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java b/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java
new file mode 100644
index 0000000000..af0f059b88
--- /dev/null
+++ b/src/main/java/org/alfresco/error/AlfrescoRuntimeException.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.error;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.springframework.extensions.surf.util.I18NUtil;
+
+import org.alfresco.api.AlfrescoPublicApi;
+
+/**
+ * I18n'ed runtime exception thrown by Alfresco code.
+ *
+ * @author gavinc
+ */
+@AlfrescoPublicApi
+public class AlfrescoRuntimeException extends RuntimeException
+{
+ /**
+ * Serial version UUID
+ */
+ private static final long serialVersionUID = 3787143176461219632L;
+
+ private static final String MESSAGE_DELIMITER = " ";
+
+ private String msgId;
+ private transient Object[] msgParams = null;
+
+ /**
+ * Helper factory method making use of variable argument numbers
+ */
+ public static AlfrescoRuntimeException create(String msgId, Object ...objects)
+ {
+ return new AlfrescoRuntimeException(msgId, objects);
+ }
+
+ /**
+ * Helper factory method making use of variable argument numbers
+ */
+ public static AlfrescoRuntimeException create(Throwable cause, String msgId, Object ...objects)
+ {
+ return new AlfrescoRuntimeException(msgId, objects, cause);
+ }
+
+ /**
+ * Utility to convert a general Throwable to a RuntimeException. No conversion is done if the
+ * throwable is already a RuntimeException .
+ *
+ * @see #create(Throwable, String, Object...)
+ */
+ public static RuntimeException makeRuntimeException(Throwable e, String msgId, Object ...objects)
+ {
+ if (e instanceof RuntimeException)
+ {
+ return (RuntimeException) e;
+ }
+ // Convert it
+ return AlfrescoRuntimeException.create(e, msgId, objects);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param msgId the message id
+ */
+ public AlfrescoRuntimeException(String msgId)
+ {
+ super(resolveMessage(msgId, null));
+ this.msgId = msgId;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param msgId the message id
+ * @param msgParams the message parameters
+ */
+ public AlfrescoRuntimeException(String msgId, Object[] msgParams)
+ {
+ super(resolveMessage(msgId, msgParams));
+ this.msgId = msgId;
+ this.msgParams = msgParams;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param msgId the message id
+ * @param cause the exception cause
+ */
+ public AlfrescoRuntimeException(String msgId, Throwable cause)
+ {
+ super(resolveMessage(msgId, null), cause);
+ this.msgId = msgId;
+ }
+
+ /**
+ * Constructor
+ *
+ * @param msgId the message id
+ * @param msgParams the message parameters
+ * @param cause the exception cause
+ */
+ public AlfrescoRuntimeException(String msgId, Object[] msgParams, Throwable cause)
+ {
+ super(resolveMessage(msgId, msgParams), cause);
+ this.msgId = msgId;
+ this.msgParams = msgParams;
+ }
+
+ /**
+ * @return the msgId
+ */
+ public String getMsgId()
+ {
+ return msgId;
+ }
+
+ /**
+ * @return the msgParams
+ */
+ public Object[] getMsgParams()
+ {
+ return msgParams;
+ }
+
+ /**
+ * @return the numericalId
+ */
+ public String getNumericalId()
+ {
+ return getMessage().split(MESSAGE_DELIMITER)[0];
+ }
+
+ /**
+ * Resolves the message id to the localised string.
+ *
+ * If a localised message can not be found then the message Id is
+ * returned.
+ *
+ * @param messageId the message Id
+ * @param params message parameters
+ * @return the localised message (or the message id if none found)
+ */
+ private static String resolveMessage(String messageId, Object[] params)
+ {
+ String message = I18NUtil.getMessage(messageId, params);
+ if (message == null)
+ {
+ // If a localized string cannot be found then return the messageId and the params
+ message = messageId;
+ if (params != null)
+ {
+ message += " - " + Arrays.toString(params);
+ }
+ }
+ return buildErrorLogNumber(message);
+ }
+
+ /**
+ * Generate an error log number - based on MMDDXXXX - where M is month,
+ * D is day and X is an atomic integer count.
+ *
+ * @param message Message to prepend the error log number to
+ *
+ * @return message with error log number prefix
+ */
+ private static String buildErrorLogNumber(String message)
+ {
+ // ensure message is not null
+ if (message == null)
+ {
+ message= "";
+ }
+
+ Date today = new Date();
+ StringBuilder buf = new StringBuilder(message.length() + 10);
+ padInt(buf, today.getMonth(), 2);
+ padInt(buf, today.getDate(), 2);
+ padInt(buf, errorCounter.getAndIncrement(), 4);
+ buf.append(MESSAGE_DELIMITER);
+ buf.append(message);
+ return buf.toString();
+ }
+
+ /**
+ * Helper to zero pad a number to specified length
+ */
+ private static void padInt(StringBuilder buffer, int value, int length)
+ {
+ String strValue = Integer.toString(value);
+ for (int i = length - strValue.length(); i > 0; i--)
+ {
+ buffer.append('0');
+ }
+ buffer.append(strValue);
+ }
+
+ private static AtomicInteger errorCounter = new AtomicInteger();
+
+ /**
+ * Get the root cause.
+ */
+ public Throwable getRootCause()
+ {
+ Throwable cause = this;
+ for (Throwable tmp = this; tmp != null ; tmp = cause.getCause())
+ {
+ cause = tmp;
+ }
+ return cause;
+ }
+}
diff --git a/src/main/java/org/alfresco/error/ExceptionStackUtil.java b/src/main/java/org/alfresco/error/ExceptionStackUtil.java
new file mode 100644
index 0000000000..8ff83c2452
--- /dev/null
+++ b/src/main/java/org/alfresco/error/ExceptionStackUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.error;
+
+/**
+ * Helper class to provide information about exception stacks.
+ *
+ * @author Derek Hulley
+ */
+public class ExceptionStackUtil
+{
+ /**
+ * Searches through the exception stack of the given throwable to find any instance
+ * of the possible cause. The top-level throwable will also be tested.
+ *
+ * @param throwable the exception condition to search
+ * @param possibleCauses the types of the exception conditions of interest
+ * @return Returns the first instance that matches one of the given
+ * possible types, or null if there is nothing in the stack
+ */
+ public static Throwable getCause(Throwable throwable, Class> ... possibleCauses)
+ {
+ while (throwable != null)
+ {
+ for (Class> possibleCauseClass : possibleCauses)
+ {
+ Class> throwableClass = throwable.getClass();
+ if (possibleCauseClass.isAssignableFrom(throwableClass))
+ {
+ // We have a match
+ return throwable;
+ }
+ }
+ // There was no match, so dig deeper
+ Throwable cause = throwable.getCause();
+ throwable = (throwable == cause) ? null : cause;
+ }
+ // Nothing found
+ return null;
+ }
+}
diff --git a/src/main/java/org/alfresco/error/StackTraceUtil.java b/src/main/java/org/alfresco/error/StackTraceUtil.java
new file mode 100644
index 0000000000..1291226afe
--- /dev/null
+++ b/src/main/java/org/alfresco/error/StackTraceUtil.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.error;
+
+
+/**
+ * Helper class around outputting stack traces.
+ *
+ * @author Derek Hulley
+ */
+public class StackTraceUtil
+{
+ /**
+ * Builds a message with the stack trace of the form:
+ *
+ * SOME MESSAGE:
+ * Started at:
+ * com.package...
+ * com.package...
+ * ...
+ *
+ *
+ * @param msg the initial error message
+ * @param stackTraceElements the stack trace elements
+ * @param sb the buffer to append to
+ * @param maxDepth the maximum number of trace elements to output. 0 or less means output all.
+ */
+ public static void buildStackTrace(
+ String msg,
+ StackTraceElement[] stackTraceElements,
+ StringBuilder sb,
+ int maxDepth)
+ {
+ String lineEnding = System.getProperty("line.separator", "\n");
+
+ sb.append(msg).append(" ").append(lineEnding)
+ .append(" Started at: ").append(lineEnding);
+ for (int i = 0; i < stackTraceElements.length; i++)
+ {
+ if (i > maxDepth && maxDepth > 0)
+ {
+ sb.append(" ...");
+ break;
+ }
+ sb.append(" ").append(stackTraceElements[i]);
+ if (i < stackTraceElements.length - 1)
+ {
+ sb.append(lineEnding);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/hibernate/DialectFactory.java b/src/main/java/org/alfresco/hibernate/DialectFactory.java
new file mode 100644
index 0000000000..eb143b8c9d
--- /dev/null
+++ b/src/main/java/org/alfresco/hibernate/DialectFactory.java
@@ -0,0 +1,177 @@
+/*
+ * 2011 - Alfresco Software, Ltd.
+ * This file was copied from org.hibernate.dialect.DialectFactory
+ */
+package org.alfresco.hibernate;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.hibernate.HibernateException;
+import org.hibernate.cfg.Environment;
+import org.hibernate.dialect.Dialect;
+import org.hibernate.util.ReflectHelper;
+
+/**
+ * A factory for generating Dialect instances.
+ *
+ * @author Steve Ebersole
+ * @author Alfresco
+ */
+public class DialectFactory {
+
+ /**
+ * Builds an appropriate Dialect instance.
+ *
+ * If a dialect is explicitly named in the incoming properties, it is used. Otherwise, the database name and version
+ * (obtained from connection metadata) are used to make the dertemination.
+ *
+ * An exception is thrown if a dialect was not explicitly set and the database name is not known.
+ *
+ * @param props The configuration properties.
+ * @param databaseName The name of the database product (obtained from metadata).
+ * @param databaseMajorVersion The major version of the database product (obtained from metadata).
+ *
+ * @return The appropriate dialect.
+ *
+ * @throws HibernateException No dialect specified and database name not known.
+ */
+ public static Dialect buildDialect(Properties props, String databaseName, int databaseMajorVersion)
+ throws HibernateException {
+ String dialectName = props.getProperty( Environment.DIALECT );
+ if ( dialectName == null || dialectName.length() == 0) {
+ return determineDialect( databaseName, databaseMajorVersion );
+ }
+ else {
+ // Push the dialect onto the system properties
+ System.setProperty(Environment.DIALECT, dialectName);
+ return buildDialect( dialectName );
+ }
+ }
+
+ /**
+ * Determine the appropriate Dialect to use given the database product name
+ * and major version.
+ *
+ * @param databaseName The name of the database product (obtained from metadata).
+ * @param databaseMajorVersion The major version of the database product (obtained from metadata).
+ *
+ * @return An appropriate dialect instance.
+ */
+ public static Dialect determineDialect(String databaseName, int databaseMajorVersion) {
+ if ( databaseName == null ) {
+ throw new HibernateException( "Hibernate Dialect must be explicitly set" );
+ }
+
+ DatabaseDialectMapper mapper = ( DatabaseDialectMapper ) MAPPERS.get( databaseName );
+ if ( mapper == null ) {
+ throw new HibernateException( "Hibernate Dialect must be explicitly set for database: " + databaseName );
+ }
+
+ String dialectName = mapper.getDialectClass( databaseMajorVersion );
+ // Push the dialect onto the system properties
+ System.setProperty(Environment.DIALECT, dialectName);
+ return buildDialect( dialectName );
+ }
+
+ /**
+ * Returns a dialect instance given the name of the class to use.
+ *
+ * @param dialectName The name of the dialect class.
+ *
+ * @return The dialect instance.
+ */
+ public static Dialect buildDialect(String dialectName) {
+ try {
+ return ( Dialect ) ReflectHelper.classForName( dialectName ).newInstance();
+ }
+ catch ( ClassNotFoundException cnfe ) {
+ throw new HibernateException( "Dialect class not found: " + dialectName );
+ }
+ catch ( Exception e ) {
+ throw new HibernateException( "Could not instantiate dialect class", e );
+ }
+ }
+
+ /**
+ * For a given database product name, instances of
+ * DatabaseDialectMapper know which Dialect to use for different versions.
+ */
+ public static interface DatabaseDialectMapper {
+ public String getDialectClass(int majorVersion);
+ }
+
+ /**
+ * A simple DatabaseDialectMapper for dialects which are independent
+ * of the underlying database product version.
+ */
+ public static class VersionInsensitiveMapper implements DatabaseDialectMapper {
+ private String dialectClassName;
+
+ public VersionInsensitiveMapper(String dialectClassName) {
+ this.dialectClassName = dialectClassName;
+ }
+
+ public String getDialectClass(int majorVersion) {
+ return dialectClassName;
+ }
+ }
+
+ // TODO : this is the stuff it'd be nice to move to a properties file or some other easily user-editable place
+ private static final Map MAPPERS = new HashMap();
+ static {
+ // detectors...
+ MAPPERS.put( "HSQL Database Engine", new VersionInsensitiveMapper( "org.hibernate.dialect.HSQLDialect" ) );
+ MAPPERS.put( "H2", new VersionInsensitiveMapper( "org.hibernate.dialect.H2Dialect" ) );
+ MAPPERS.put( "MySQL", new VersionInsensitiveMapper( "org.hibernate.dialect.MySQLDialect" ) );
+ MAPPERS.put( "PostgreSQL", new VersionInsensitiveMapper( "org.hibernate.dialect.PostgreSQLDialect" ) );
+ MAPPERS.put( "Apache Derby", new VersionInsensitiveMapper( "org.hibernate.dialect.DerbyDialect" ) );
+
+ MAPPERS.put( "Ingres", new VersionInsensitiveMapper( "org.hibernate.dialect.IngresDialect" ) );
+ MAPPERS.put( "ingres", new VersionInsensitiveMapper( "org.hibernate.dialect.IngresDialect" ) );
+ MAPPERS.put( "INGRES", new VersionInsensitiveMapper( "org.hibernate.dialect.IngresDialect" ) );
+
+ MAPPERS.put( "Microsoft SQL Server Database", new VersionInsensitiveMapper( "org.hibernate.dialect.SQLServerDialect" ) );
+ MAPPERS.put( "Microsoft SQL Server", new VersionInsensitiveMapper( "org.hibernate.dialect.SQLServerDialect" ) );
+ MAPPERS.put( "Sybase SQL Server", new VersionInsensitiveMapper( "org.hibernate.dialect.SybaseDialect" ) );
+ MAPPERS.put( "Adaptive Server Enterprise", new VersionInsensitiveMapper( "org.hibernate.dialect.SybaseDialect" ) );
+
+ MAPPERS.put( "Informix Dynamic Server", new VersionInsensitiveMapper( "org.hibernate.dialect.InformixDialect" ) );
+
+ MAPPERS.put( "DB2/NT", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/LINUX", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/6000", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/HPUX", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/SUN", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/LINUX390", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/AIX64", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/NT",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/NT64",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2 UDP",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/LINUX",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/LINUX390",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/LINUXZ64",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/400 SQL",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/6000",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2 UDB iSeries",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/AIX64",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/HPUX",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/HP64",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/SUN",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/SUN64",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/PTX",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/2",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+ MAPPERS.put( "DB2/LINUXX8664",new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ));
+
+ MAPPERS.put( "MySQL", new VersionInsensitiveMapper( "org.hibernate.dialect.MySQLInnoDBDialect" ) );
+ MAPPERS.put( "DB2/NT64", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "DB2/LINUX", new VersionInsensitiveMapper( "org.hibernate.dialect.DB2Dialect" ) );
+ MAPPERS.put( "Microsoft SQL Server Database", new VersionInsensitiveMapper( "org.alfresco.repo.domain.hibernate.dialect.AlfrescoSQLServerDialect" ) );
+ MAPPERS.put( "Microsoft SQL Server", new VersionInsensitiveMapper( "org.alfresco.repo.domain.hibernate.dialect.AlfrescoSQLServerDialect" ) );
+ MAPPERS.put( "Sybase SQL Server", new VersionInsensitiveMapper( "org.alfresco.repo.domain.hibernate.dialect.AlfrescoSybaseAnywhereDialect" ) );
+ MAPPERS.put( "Oracle", new VersionInsensitiveMapper( "org.alfresco.repo.domain.hibernate.dialect.AlfrescoOracle9Dialect" ) );
+ }
+}
diff --git a/src/main/java/org/alfresco/hibernate/DialectFactoryBean.java b/src/main/java/org/alfresco/hibernate/DialectFactoryBean.java
new file mode 100644
index 0000000000..75f6b5f25a
--- /dev/null
+++ b/src/main/java/org/alfresco/hibernate/DialectFactoryBean.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.hibernate;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.cfg.Environment;
+import org.hibernate.dialect.Dialect;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.orm.hibernate3.LocalSessionFactoryBean;
+
+/**
+ * Factory for the Hibernate dialect. Allows dialect detection logic to be centralized and the dialect to be injected
+ * where required as a singleton from the container.
+ *
+ * @author dward
+ */
+public class DialectFactoryBean implements FactoryBean
+{
+
+ /** The local session factory. */
+ private LocalSessionFactoryBean localSessionFactory;
+
+ /**
+ * Sets the local session factory.
+ *
+ * @param localSessionFactory
+ * the new local session factory
+ */
+ public void setLocalSessionFactory(LocalSessionFactoryBean localSessionFactory)
+ {
+ this.localSessionFactory = localSessionFactory;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public Dialect getObject() throws SQLException
+ {
+ Session session = ((SessionFactory) this.localSessionFactory.getObject()).openSession();
+ Configuration cfg = this.localSessionFactory.getConfiguration();
+ Connection con = null;
+ try
+ {
+ // make sure that we AUTO-COMMIT
+ con = session.connection();
+ con.setAutoCommit(true);
+ DatabaseMetaData meta = con.getMetaData();
+ Dialect dialect = DialectFactory.buildDialect(cfg.getProperties(), meta.getDatabaseProductName(), meta
+ .getDatabaseMajorVersion());
+ dialect = changeDialect(cfg, dialect);
+ return dialect;
+ }
+ finally
+ {
+ try
+ {
+ con.close();
+ }
+ catch (Exception e)
+ {
+ }
+ }
+ }
+
+ /**
+ * Substitute the dialect with an alternative, if possible.
+ *
+ * @param cfg
+ * the configuration
+ * @param dialect
+ * the dialect
+ * @return the dialect
+ */
+ private Dialect changeDialect(Configuration cfg, Dialect dialect)
+ {
+ String dialectName = cfg.getProperty(Environment.DIALECT);
+ if (dialectName == null || dialectName.length() == 0)
+ {
+ // Fix the dialect property to match the detected dialect
+ cfg.setProperty(Environment.DIALECT, dialect.getClass().getName());
+ }
+ return dialect;
+ // TODO: https://issues.alfresco.com/jira/browse/ETHREEOH-679
+ // else if (dialectName.equals(Oracle9Dialect.class.getName()))
+ // {
+ // String subst = AlfrescoOracle9Dialect.class.getName();
+ // LogUtil.warn(logger, WARN_DIALECT_SUBSTITUTING, dialectName, subst);
+ // cfg.setProperty(Environment.DIALECT, subst);
+ // }
+ // else if (dialectName.equals(MySQLDialect.class.getName()))
+ // {
+ // String subst = MySQLInnoDBDialect.class.getName();
+ // LogUtil.warn(logger, WARN_DIALECT_SUBSTITUTING, dialectName, subst);
+ // cfg.setProperty(Environment.DIALECT, subst);
+ // }
+ // else if (dialectName.equals(MySQL5Dialect.class.getName()))
+ // {
+ // String subst = MySQLInnoDBDialect.class.getName();
+ // LogUtil.warn(logger, WARN_DIALECT_SUBSTITUTING, dialectName, subst);
+ // cfg.setProperty(Environment.DIALECT, subst);
+ // }
+ }
+
+ @Override
+ public Class> getObjectType()
+ {
+ return Dialect.class;
+ }
+
+ @Override
+ public boolean isSingleton()
+ {
+ return true;
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java b/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java
new file mode 100644
index 0000000000..0c6e6b6406
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/AbstractHttpClient.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpConnectionManager;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.URI;
+import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.HeadMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public abstract class AbstractHttpClient implements AlfrescoHttpClient
+{
+ private static final Log logger = LogFactory.getLog(AlfrescoHttpClient.class);
+
+ public static final String ALFRESCO_DEFAULT_BASE_URL = "/alfresco";
+
+ public static final int DEFAULT_SAVEPOST_BUFFER = 4096;
+
+ // Remote Server access
+ protected HttpClient httpClient = null;
+
+ private String baseUrl = ALFRESCO_DEFAULT_BASE_URL;
+
+ public AbstractHttpClient(HttpClient httpClient)
+ {
+ this.httpClient = httpClient;
+ }
+
+ protected HttpClient getHttpClient()
+ {
+ return httpClient;
+ }
+
+ /**
+ * @return the baseUrl
+ */
+ public String getBaseUrl()
+ {
+ return baseUrl;
+ }
+
+ /**
+ * @param baseUrl the baseUrl to set
+ */
+ public void setBaseUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ }
+
+ private boolean isRedirect(HttpMethod method)
+ {
+ switch (method.getStatusCode()) {
+ case HttpStatus.SC_MOVED_TEMPORARILY:
+ case HttpStatus.SC_MOVED_PERMANENTLY:
+ case HttpStatus.SC_SEE_OTHER:
+ case HttpStatus.SC_TEMPORARY_REDIRECT:
+ if (method.getFollowRedirects()) {
+ return true;
+ } else {
+ return false;
+ }
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Send Request to the repository
+ */
+ protected HttpMethod sendRemoteRequest(Request req) throws AuthenticationException, IOException
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("");
+ logger.debug("* Request: " + req.getMethod() + " " + req.getFullUri() + (req.getBody() == null ? "" : "\n" + new String(req.getBody(), "UTF-8")));
+ }
+
+ HttpMethod method = createMethod(req);
+
+ // execute method
+ executeMethod(method);
+
+ // Deal with redirect
+ if(isRedirect(method))
+ {
+ Header locationHeader = method.getResponseHeader("location");
+ if (locationHeader != null)
+ {
+ String redirectLocation = locationHeader.getValue();
+ method.setURI(new URI(redirectLocation, true));
+ httpClient.executeMethod(method);
+ }
+ }
+
+ return method;
+ }
+
+ protected long executeMethod(HttpMethod method) throws HttpException, IOException
+ {
+ // execute method
+
+ long startTime = System.currentTimeMillis();
+
+ // TODO: Pool, and sent host configuration and state on execution
+ getHttpClient().executeMethod(method);
+
+ return System.currentTimeMillis() - startTime;
+ }
+
+ protected HttpMethod createMethod(Request req) throws IOException
+ {
+ StringBuilder url = new StringBuilder(128);
+ url.append(baseUrl);
+ url.append("/service/");
+ url.append(req.getFullUri());
+
+ // construct method
+ HttpMethod httpMethod = null;
+ String method = req.getMethod();
+ if(method.equalsIgnoreCase("GET"))
+ {
+ GetMethod get = new GetMethod(url.toString());
+ httpMethod = get;
+ httpMethod.setFollowRedirects(true);
+ }
+ else if(method.equalsIgnoreCase("POST"))
+ {
+ PostMethod post = new PostMethod(url.toString());
+ httpMethod = post;
+ ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(req.getBody(), req.getType());
+ if (req.getBody().length > DEFAULT_SAVEPOST_BUFFER)
+ {
+ post.getParams().setBooleanParameter(HttpMethodParams.USE_EXPECT_CONTINUE, true);
+ }
+ post.setRequestEntity(requestEntity);
+ // Note: not able to automatically follow redirects for POST, this is handled by sendRemoteRequest
+ }
+ else if(method.equalsIgnoreCase("HEAD"))
+ {
+ HeadMethod head = new HeadMethod(url.toString());
+ httpMethod = head;
+ httpMethod.setFollowRedirects(true);
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Http Method " + method + " not supported");
+ }
+
+ if (req.getHeaders() != null)
+ {
+ for (Map.Entry header : req.getHeaders().entrySet())
+ {
+ httpMethod.setRequestHeader(header.getKey(), header.getValue());
+ }
+ }
+
+ return httpMethod;
+ }
+
+ /* (non-Javadoc)
+ * @see org.alfresco.httpclient.AlfrescoHttpClient#close()
+ */
+ @Override
+ public void close()
+ {
+ if(httpClient != null)
+ {
+ HttpConnectionManager connectionManager = httpClient.getHttpConnectionManager();
+ if(connectionManager instanceof MultiThreadedHttpConnectionManager)
+ {
+ ((MultiThreadedHttpConnectionManager)connectionManager).shutdown();
+ }
+ }
+
+ }
+
+
+
+}
diff --git a/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java b/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java
new file mode 100644
index 0000000000..9629cff1ce
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/AlfrescoHttpClient.java
@@ -0,0 +1,30 @@
+package org.alfresco.httpclient;
+
+import java.io.IOException;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public interface AlfrescoHttpClient
+{
+ /**
+ * Send Request to the repository
+ */
+ public Response sendRequest(Request req) throws AuthenticationException, IOException;
+
+
+ /**
+ * Set the base url to alfresco
+ * - normally /alfresco
+ * @param baseUrl
+ */
+ public void setBaseUrl(String baseUrl);
+
+
+ /**
+ *
+ */
+ public void close();
+}
diff --git a/src/main/java/org/alfresco/httpclient/AuthenticationException.java b/src/main/java/org/alfresco/httpclient/AuthenticationException.java
new file mode 100644
index 0000000000..5f5b084803
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/AuthenticationException.java
@@ -0,0 +1,21 @@
+package org.alfresco.httpclient;
+
+import org.apache.commons.httpclient.HttpMethod;
+
+public class AuthenticationException extends Exception
+{
+ private static final long serialVersionUID = -407003742855571557L;
+
+ private HttpMethod method;
+
+ public AuthenticationException(HttpMethod method)
+ {
+ this.method = method;
+ }
+
+ public HttpMethod getMethod()
+ {
+ return method;
+ }
+
+}
diff --git a/src/main/java/org/alfresco/httpclient/GetRequest.java b/src/main/java/org/alfresco/httpclient/GetRequest.java
new file mode 100644
index 0000000000..1960850972
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/GetRequest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+/**
+ * HTTP GET Request
+ *
+ * @since 4.0
+ */
+public class GetRequest extends Request
+{
+ public GetRequest(String uri)
+ {
+ super("get", uri);
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/HeadRequest.java b/src/main/java/org/alfresco/httpclient/HeadRequest.java
new file mode 100644
index 0000000000..9b8499ffa2
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/HeadRequest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+/**
+ * HTTP HEAD request
+ *
+ * @since 4.0
+ */
+public class HeadRequest extends Request
+{
+ public HeadRequest(String uri)
+ {
+ super("head", uri);
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/HttpClientFactory.java b/src/main/java/org/alfresco/httpclient/HttpClientFactory.java
new file mode 100644
index 0000000000..1e4054f670
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/HttpClientFactory.java
@@ -0,0 +1,777 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.AlgorithmParameters;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.alfresco.encryption.AlfrescoKeyStore;
+import org.alfresco.encryption.AlfrescoKeyStoreImpl;
+import org.alfresco.encryption.EncryptionUtils;
+import org.alfresco.encryption.Encryptor;
+import org.alfresco.encryption.KeyProvider;
+import org.alfresco.encryption.KeyResourceLoader;
+import org.alfresco.encryption.KeyStoreParameters;
+import org.alfresco.encryption.ssl.AuthSSLProtocolSocketFactory;
+import org.alfresco.encryption.ssl.SSLEncryptionParameters;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.Pair;
+import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpHost;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.HttpVersion;
+import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
+import org.apache.commons.httpclient.SimpleHttpConnectionManager;
+import org.apache.commons.httpclient.URI;
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.httpclient.cookie.CookiePolicy;
+import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.params.DefaultHttpParams;
+import org.apache.commons.httpclient.params.DefaultHttpParamsFactory;
+import org.apache.commons.httpclient.params.HttpClientParams;
+import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
+import org.apache.commons.httpclient.params.HttpConnectionParams;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.httpclient.params.HttpParams;
+import org.apache.commons.httpclient.protocol.Protocol;
+import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
+import org.apache.commons.httpclient.util.DateUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A factory to create HttpClients and AlfrescoHttpClients based on the setting of the 'secureCommsType' property.
+ *
+ * @since 4.0
+ */
+public class HttpClientFactory
+{
+ public static enum SecureCommsType
+ {
+ HTTPS, NONE;
+
+ public static SecureCommsType getType(String type)
+ {
+ if(type.equalsIgnoreCase("https"))
+ {
+ return HTTPS;
+ }
+ else if(type.equalsIgnoreCase("none"))
+ {
+ return NONE;
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid communications type");
+ }
+ }
+ };
+
+ private static final Log logger = LogFactory.getLog(HttpClientFactory.class);
+
+ private SSLEncryptionParameters sslEncryptionParameters;
+ private KeyResourceLoader keyResourceLoader;
+ private SecureCommsType secureCommsType;
+
+ // for md5 http client (no longer used but kept for now)
+ private KeyStoreParameters keyStoreParameters;
+ private MD5EncryptionParameters encryptionParameters;
+
+ private String host;
+ private int port;
+ private int sslPort;
+
+ private AlfrescoKeyStore sslKeyStore;
+ private AlfrescoKeyStore sslTrustStore;
+ private ProtocolSocketFactory sslSocketFactory;
+
+ private int maxTotalConnections = 40;
+
+ private int maxHostConnections = 40;
+
+ private Integer socketTimeout = null;
+
+ private int connectionTimeout = 0;
+
+ public HttpClientFactory()
+ {
+ }
+
+ public HttpClientFactory(SecureCommsType secureCommsType, SSLEncryptionParameters sslEncryptionParameters,
+ KeyResourceLoader keyResourceLoader, KeyStoreParameters keyStoreParameters,
+ MD5EncryptionParameters encryptionParameters, String host, int port, int sslPort, int maxTotalConnections,
+ int maxHostConnections, int socketTimeout)
+ {
+ this.secureCommsType = secureCommsType;
+ this.sslEncryptionParameters = sslEncryptionParameters;
+ this.keyResourceLoader = keyResourceLoader;
+ this.keyStoreParameters = keyStoreParameters;
+ this.encryptionParameters = encryptionParameters;
+ this.host = host;
+ this.port = port;
+ this.sslPort = sslPort;
+ this.maxTotalConnections = maxTotalConnections;
+ this.maxHostConnections = maxHostConnections;
+ this.socketTimeout = socketTimeout;
+ init();
+ }
+
+ public void init()
+ {
+ this.sslKeyStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getKeyStoreParameters(), keyResourceLoader);
+ this.sslTrustStore = new AlfrescoKeyStoreImpl(sslEncryptionParameters.getTrustStoreParameters(), keyResourceLoader);
+ this.sslSocketFactory = new AuthSSLProtocolSocketFactory(sslKeyStore, sslTrustStore, keyResourceLoader);
+
+ // Setup the Apache httpclient library to use our concurrent HttpParams factory
+ DefaultHttpParams.setHttpParamsFactory(new NonBlockingHttpParamsFactory());
+ }
+
+ public void setHost(String host)
+ {
+ this.host = host;
+ }
+
+ public String getHost()
+ {
+ return host;
+ }
+
+ public void setPort(int port)
+ {
+ this.port = port;
+ }
+
+ public int getPort()
+ {
+ return port;
+ }
+
+ public void setSslPort(int sslPort)
+ {
+ this.sslPort = sslPort;
+ }
+
+ public boolean isSSL()
+ {
+ return secureCommsType == SecureCommsType.HTTPS;
+ }
+
+ public void setSecureCommsType(String type)
+ {
+ try
+ {
+ this.secureCommsType = SecureCommsType.getType(type);
+ }
+ catch(IllegalArgumentException e)
+ {
+ throw new AlfrescoRuntimeException("", e);
+ }
+ }
+
+ public void setSSLEncryptionParameters(SSLEncryptionParameters sslEncryptionParameters)
+ {
+ this.sslEncryptionParameters = sslEncryptionParameters;
+ }
+
+ public void setKeyStoreParameters(KeyStoreParameters keyStoreParameters)
+ {
+ this.keyStoreParameters = keyStoreParameters;
+ }
+
+ public void setEncryptionParameters(MD5EncryptionParameters encryptionParameters)
+ {
+ this.encryptionParameters = encryptionParameters;
+ }
+
+ public void setKeyResourceLoader(KeyResourceLoader keyResourceLoader)
+ {
+ this.keyResourceLoader = keyResourceLoader;
+ }
+
+ /**
+ * @return the maxTotalConnections
+ */
+ public int getMaxTotalConnections()
+ {
+ return maxTotalConnections;
+ }
+
+ /**
+ * @param maxTotalConnections the maxTotalConnections to set
+ */
+ public void setMaxTotalConnections(int maxTotalConnections)
+ {
+ this.maxTotalConnections = maxTotalConnections;
+ }
+
+ /**
+ * @return the maxHostConnections
+ */
+ public int getMaxHostConnections()
+ {
+ return maxHostConnections;
+ }
+
+ /**
+ * @param maxHostConnections the maxHostConnections to set
+ */
+ public void setMaxHostConnections(int maxHostConnections)
+ {
+ this.maxHostConnections = maxHostConnections;
+ }
+
+ /**
+ * Attempts to connect to a server will timeout after this period (millis).
+ * Default is zero (the timeout is not used).
+ *
+ * @param connectionTimeout time in millis.
+ */
+ public void setConnectionTimeout(int connectionTimeout)
+ {
+ this.connectionTimeout = connectionTimeout;
+ }
+
+ protected HttpClient constructHttpClient()
+ {
+ MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
+ HttpClient httpClient = new HttpClient(connectionManager);
+ HttpClientParams params = httpClient.getParams();
+ params.setBooleanParameter(HttpConnectionParams.TCP_NODELAY, true);
+ params.setBooleanParameter(HttpConnectionParams.STALE_CONNECTION_CHECK, true);
+ if (socketTimeout != null)
+ {
+ params.setSoTimeout(socketTimeout);
+ }
+ HttpConnectionManagerParams connectionManagerParams = httpClient.getHttpConnectionManager().getParams();
+ connectionManagerParams.setMaxTotalConnections(maxTotalConnections);
+ connectionManagerParams.setDefaultMaxConnectionsPerHost(maxHostConnections);
+ connectionManagerParams.setConnectionTimeout(connectionTimeout);
+
+ return httpClient;
+ }
+
+ protected HttpClient getHttpsClient()
+ {
+ return getHttpsClient(host, sslPort);
+ }
+
+ protected HttpClient getHttpsClient(String httpsHost, int httpsPort)
+ {
+ // Configure a custom SSL socket factory that will enforce mutual authentication
+ HttpClient httpClient = constructHttpClient();
+ HttpHostFactory hostFactory = new HttpHostFactory(new Protocol("https", sslSocketFactory, httpsPort));
+ httpClient.setHostConfiguration(new HostConfigurationWithHostFactory(hostFactory));
+ httpClient.getHostConfiguration().setHost(httpsHost, httpsPort, "https");
+ return httpClient;
+ }
+
+ protected HttpClient getDefaultHttpClient()
+ {
+ return getDefaultHttpClient(host, port);
+ }
+
+ protected HttpClient getDefaultHttpClient(String httpHost, int httpPort)
+ {
+ HttpClient httpClient = constructHttpClient();
+ httpClient.getHostConfiguration().setHost(httpHost, httpPort);
+ return httpClient;
+ }
+
+ protected AlfrescoHttpClient getAlfrescoHttpsClient()
+ {
+ AlfrescoHttpClient repoClient = new HttpsClient(getHttpsClient());
+ return repoClient;
+ }
+
+ protected AlfrescoHttpClient getAlfrescoHttpClient()
+ {
+ AlfrescoHttpClient repoClient = new DefaultHttpClient(getDefaultHttpClient());
+ return repoClient;
+ }
+
+ protected HttpClient getMD5HttpClient(String host, int port)
+ {
+ HttpClient httpClient = constructHttpClient();
+ httpClient.getHostConfiguration().setHost(host, port);
+ return httpClient;
+ }
+
+
+ public AlfrescoHttpClient getRepoClient(String host, int port)
+ {
+ AlfrescoHttpClient repoClient = null;
+
+ if(secureCommsType == SecureCommsType.HTTPS)
+ {
+ repoClient = getAlfrescoHttpsClient();
+ }
+ else if(secureCommsType == SecureCommsType.NONE)
+ {
+ repoClient = getAlfrescoHttpClient();
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'");
+ }
+
+ return repoClient;
+ }
+
+ public HttpClient getHttpClient()
+ {
+ HttpClient httpClient = null;
+
+ if(secureCommsType == SecureCommsType.HTTPS)
+ {
+ httpClient = getHttpsClient();
+ }
+ else if(secureCommsType == SecureCommsType.NONE)
+ {
+ httpClient = getDefaultHttpClient();
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'");
+ }
+
+ return httpClient;
+ }
+
+ public HttpClient getHttpClient(String host, int port)
+ {
+ HttpClient httpClient = null;
+
+ if(secureCommsType == SecureCommsType.HTTPS)
+ {
+ httpClient = getHttpsClient(host, port);
+ }
+ else if(secureCommsType == SecureCommsType.NONE)
+ {
+ httpClient = getDefaultHttpClient(host, port);
+ }
+ else
+ {
+ throw new AlfrescoRuntimeException("Invalid Solr secure communications type configured in alfresco.secureComms, should be 'ssl'or 'none'");
+ }
+
+ return httpClient;
+ }
+
+
+
+ /**
+ * A secure client connection to the repository.
+ *
+ * @since 4.0
+ *
+ */
+ class HttpsClient extends AbstractHttpClient
+ {
+ public HttpsClient(HttpClient httpClient)
+ {
+ super(httpClient);
+ }
+
+ /**
+ * Send Request to the repository
+ */
+ public Response sendRequest(Request req) throws AuthenticationException, IOException
+ {
+ HttpMethod method = super.sendRemoteRequest(req);
+ return new HttpMethodResponse(method);
+ }
+ }
+
+ /**
+ * Simple HTTP client to connect to the Alfresco server. Simply wraps a HttpClient.
+ *
+ * @since 4.0
+ */
+ class DefaultHttpClient extends AbstractHttpClient
+ {
+ public DefaultHttpClient(HttpClient httpClient)
+ {
+ super(httpClient);
+ }
+
+ /**
+ * Send Request to the repository
+ */
+ public Response sendRequest(Request req) throws AuthenticationException, IOException
+ {
+ HttpMethod method = super.sendRemoteRequest(req);
+ return new HttpMethodResponse(method);
+ }
+ }
+
+
+
+ static class SecureHttpMethodResponse extends HttpMethodResponse
+ {
+ protected HostConfiguration hostConfig;
+ protected EncryptionUtils encryptionUtils;
+ // Need to get as a byte array because we need to read the request twice, once for authentication
+ // and again by the web service.
+ protected byte[] decryptedBody;
+
+ public SecureHttpMethodResponse(HttpMethod method, HostConfiguration hostConfig,
+ EncryptionUtils encryptionUtils) throws AuthenticationException, IOException
+ {
+ super(method);
+ this.hostConfig = hostConfig;
+ this.encryptionUtils = encryptionUtils;
+
+ if(method.getStatusCode() == HttpStatus.SC_OK)
+ {
+ this.decryptedBody = encryptionUtils.decryptResponseBody(method);
+ // authenticate the response
+ if(!authenticate())
+ {
+ throw new AuthenticationException(method);
+ }
+ }
+ }
+
+ protected boolean authenticate() throws IOException
+ {
+ return encryptionUtils.authenticateResponse(method, hostConfig.getHost(), decryptedBody);
+ }
+
+ public InputStream getContentAsStream() throws IOException
+ {
+ if(decryptedBody != null)
+ {
+ return new ByteArrayInputStream(decryptedBody);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ private static class HttpHostFactory
+ {
+ private Map protocols;
+
+ public HttpHostFactory(Protocol httpsProtocol)
+ {
+ protocols = new HashMap(2);
+ protocols.put("https", httpsProtocol);
+ }
+
+ /** Get a host for the given parameters. This method need not be thread-safe. */
+ public HttpHost getHost(String host, int port, String scheme)
+ {
+ if(scheme == null)
+ {
+ scheme = "http";
+ }
+ Protocol protocol = protocols.get(scheme);
+ if(protocol == null)
+ {
+ protocol = Protocol.getProtocol("http");
+ if(protocol == null)
+ {
+ throw new IllegalArgumentException("Unrecognised scheme parameter");
+ }
+ }
+
+ return new HttpHost(host, port, protocol);
+ }
+ }
+
+ private static class HostConfigurationWithHostFactory extends HostConfiguration
+ {
+ private final HttpHostFactory factory;
+
+ public HostConfigurationWithHostFactory(HttpHostFactory factory)
+ {
+ this.factory = factory;
+ }
+
+ public synchronized void setHost(String host, int port, String scheme)
+ {
+ setHost(factory.getHost(host, port, scheme));
+ }
+
+ public synchronized void setHost(String host, int port)
+ {
+ setHost(factory.getHost(host, port, "http"));
+ }
+
+ @SuppressWarnings("unused")
+ public synchronized void setHost(URI uri)
+ {
+ try {
+ setHost(uri.getHost(), uri.getPort(), uri.getScheme());
+ } catch(URIException e) {
+ throw new IllegalArgumentException(e.toString());
+ }
+ }
+ }
+
+ /**
+ * An extension of the DefaultHttpParamsFactory that uses a RRW lock pattern rather than
+ * full synchronization around the parameter CRUD - to avoid locking on many reads.
+ *
+ * @author Kevin Roast
+ */
+ public static class NonBlockingHttpParamsFactory extends DefaultHttpParamsFactory
+ {
+ private volatile HttpParams httpParams;
+
+ /* (non-Javadoc)
+ * @see org.apache.commons.httpclient.params.DefaultHttpParamsFactory#getDefaultParams()
+ */
+ @Override
+ public HttpParams getDefaultParams()
+ {
+ if (httpParams == null)
+ {
+ synchronized (this)
+ {
+ if (httpParams == null)
+ {
+ httpParams = createParams();
+ }
+ }
+ }
+
+ return httpParams;
+ }
+
+ /**
+ * NOTE: This is a copy of the code in {@link DefaultHttpParamsFactory}
+ * Unfortunately this is required because although the factory pattern allows the
+ * override of the default param creation, it does not allow the class of the actual
+ * HttpParam implementation to be changed.
+ */
+ @Override
+ protected HttpParams createParams()
+ {
+ HttpClientParams params = new NonBlockingHttpParams(null);
+
+ params.setParameter(HttpMethodParams.USER_AGENT, "Spring Surf via Apache HttpClient/3.1");
+ params.setVersion(HttpVersion.HTTP_1_1);
+ params.setConnectionManagerClass(SimpleHttpConnectionManager.class);
+ params.setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
+ params.setHttpElementCharset("US-ASCII");
+ params.setContentCharset("ISO-8859-1");
+ params.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
+
+ List datePatterns = Arrays.asList(
+ new String[] {
+ DateUtil.PATTERN_RFC1123,
+ DateUtil.PATTERN_RFC1036,
+ DateUtil.PATTERN_ASCTIME,
+ "EEE, dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MMM-yyyy HH-mm-ss z",
+ "EEE, dd MMM yy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH:mm:ss z",
+ "EEE dd MMM yyyy HH:mm:ss z",
+ "EEE dd-MMM-yyyy HH-mm-ss z",
+ "EEE dd-MMM-yy HH:mm:ss z",
+ "EEE dd MMM yy HH:mm:ss z",
+ "EEE,dd-MMM-yy HH:mm:ss z",
+ "EEE,dd-MMM-yyyy HH:mm:ss z",
+ "EEE, dd-MM-yyyy HH:mm:ss z",
+ }
+ );
+ params.setParameter(HttpMethodParams.DATE_PATTERNS, datePatterns);
+
+ String agent = null;
+ try
+ {
+ agent = System.getProperty("httpclient.useragent");
+ }
+ catch (SecurityException ignore)
+ {
+ }
+ if (agent != null)
+ {
+ params.setParameter(HttpMethodParams.USER_AGENT, agent);
+ }
+
+ String preemptiveDefault = null;
+ try
+ {
+ preemptiveDefault = System.getProperty("httpclient.authentication.preemptive");
+ }
+ catch (SecurityException ignore)
+ {
+ }
+ if (preemptiveDefault != null)
+ {
+ preemptiveDefault = preemptiveDefault.trim().toLowerCase();
+ if (preemptiveDefault.equals("true"))
+ {
+ params.setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, Boolean.TRUE);
+ }
+ else if (preemptiveDefault.equals("false"))
+ {
+ params.setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, Boolean.FALSE);
+ }
+ }
+
+ String defaultCookiePolicy = null;
+ try
+ {
+ defaultCookiePolicy = System.getProperty("apache.commons.httpclient.cookiespec");
+ }
+ catch (SecurityException ignore)
+ {
+ }
+ if (defaultCookiePolicy != null)
+ {
+ if ("COMPATIBILITY".equalsIgnoreCase(defaultCookiePolicy))
+ {
+ params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
+ }
+ else if ("NETSCAPE_DRAFT".equalsIgnoreCase(defaultCookiePolicy))
+ {
+ params.setCookiePolicy(CookiePolicy.NETSCAPE);
+ }
+ else if ("RFC2109".equalsIgnoreCase(defaultCookiePolicy))
+ {
+ params.setCookiePolicy(CookiePolicy.RFC_2109);
+ }
+ }
+
+ return params;
+ }
+ }
+
+ /**
+ * @author Kevin Roast
+ */
+ public static class NonBlockingHttpParams extends HttpClientParams
+ {
+ private HashMap parameters = new HashMap(8);
+ private ReadWriteLock paramLock = new ReentrantReadWriteLock();
+
+ public NonBlockingHttpParams()
+ {
+ super();
+ }
+
+ public NonBlockingHttpParams(HttpParams defaults)
+ {
+ super(defaults);
+ }
+
+ @Override
+ public Object getParameter(final String name)
+ {
+ // See if the parameter has been explicitly defined
+ Object param = null;
+ paramLock.readLock().lock();
+ try
+ {
+ param = this.parameters.get(name);
+ }
+ finally
+ {
+ paramLock.readLock().unlock();
+ }
+ if (param == null)
+ {
+ // If not, see if defaults are available
+ HttpParams defaults = getDefaults();
+ if (defaults != null)
+ {
+ // Return default parameter value
+ param = defaults.getParameter(name);
+ }
+ }
+ return param;
+ }
+
+ @Override
+ public void setParameter(final String name, final Object value)
+ {
+ paramLock.writeLock().lock();
+ try
+ {
+ this.parameters.put(name, value);
+ }
+ finally
+ {
+ paramLock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public boolean isParameterSetLocally(final String name)
+ {
+ paramLock.readLock().lock();
+ try
+ {
+ return (this.parameters.get(name) != null);
+ }
+ finally
+ {
+ paramLock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public void clear()
+ {
+ paramLock.writeLock().lock();
+ try
+ {
+ this.parameters.clear();
+ }
+ finally
+ {
+ paramLock.writeLock().unlock();
+ }
+ }
+
+ @Override
+ public Object clone() throws CloneNotSupportedException
+ {
+ NonBlockingHttpParams clone = (NonBlockingHttpParams)super.clone();
+ paramLock.readLock().lock();
+ try
+ {
+ clone.parameters = (HashMap) this.parameters.clone();
+ }
+ finally
+ {
+ paramLock.readLock().unlock();
+ }
+ clone.setDefaults(getDefaults());
+ return clone;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java b/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java
new file mode 100644
index 0000000000..4bf7a9bbbe
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/HttpMethodResponse.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethod;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class HttpMethodResponse implements Response
+{
+ protected HttpMethod method;
+
+ public HttpMethodResponse(HttpMethod method) throws IOException
+ {
+ this.method = method;
+ }
+
+ public void release()
+ {
+ method.releaseConnection();
+ }
+
+ public InputStream getContentAsStream() throws IOException
+ {
+ return method.getResponseBodyAsStream();
+ }
+
+ public String getContentType()
+ {
+ return getHeader("Content-Type");
+ }
+
+ public String getHeader(String name)
+ {
+ Header header = method.getResponseHeader(name);
+ return (header != null) ? header.getValue() : null;
+ }
+
+ public int getStatus()
+ {
+ return method.getStatusCode();
+ }
+
+}
diff --git a/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java b/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java
new file mode 100644
index 0000000000..158b060ad7
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/MD5EncryptionParameters.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class MD5EncryptionParameters
+{
+ private String cipherAlgorithm;
+ private long messageTimeout;
+ private String macAlgorithm;
+
+ public MD5EncryptionParameters()
+ {
+
+ }
+
+ public MD5EncryptionParameters(String cipherAlgorithm,
+ Long messageTimeout, String macAlgorithm)
+ {
+ this.cipherAlgorithm = cipherAlgorithm;
+ this.messageTimeout = messageTimeout;
+ this.macAlgorithm = macAlgorithm;
+ }
+
+ public String getCipherAlgorithm()
+ {
+ return cipherAlgorithm;
+ }
+
+ public void setCipherAlgorithm(String cipherAlgorithm)
+ {
+ this.cipherAlgorithm = cipherAlgorithm;
+ }
+
+ public long getMessageTimeout()
+ {
+ return messageTimeout;
+ }
+
+ public String getMacAlgorithm()
+ {
+ return macAlgorithm;
+ }
+
+ public void setMessageTimeout(long messageTimeout)
+ {
+ this.messageTimeout = messageTimeout;
+ }
+
+ public void setMacAlgorithm(String macAlgorithm)
+ {
+ this.macAlgorithm = macAlgorithm;
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/PostRequest.java b/src/main/java/org/alfresco/httpclient/PostRequest.java
new file mode 100644
index 0000000000..08133a12ad
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/PostRequest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * HTTP POST Request
+ *
+ * @since 4.0
+ */
+public class PostRequest extends Request
+{
+ public PostRequest(String uri, String post, String contentType)
+ throws UnsupportedEncodingException
+ {
+ super("post", uri);
+ setBody(getEncoding() == null ? post.getBytes() : post.getBytes(getEncoding()));
+ setType(contentType);
+ }
+
+ public PostRequest(String uri, byte[] post, String contentType)
+ {
+ super("post", uri);
+ setBody(post);
+ setType(contentType);
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/Request.java b/src/main/java/org/alfresco/httpclient/Request.java
new file mode 100644
index 0000000000..a6328535c5
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/Request.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.util.Map;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public class Request
+{
+ private String method;
+ private String uri;
+ private Map args;
+ private Map headers;
+ private byte[] body;
+ private String encoding = "UTF-8";
+ private String contentType;
+
+ public Request(Request req)
+ {
+ this.method = req.method;
+ this.uri= req.uri;
+ this.args = req.args;
+ this.headers = req.headers;
+ this.body = req.body;
+ this.encoding = req.encoding;
+ this.contentType = req.contentType;
+ }
+
+ public Request(String method, String uri)
+ {
+ this.method = method;
+ this.uri = uri;
+ }
+
+ public String getMethod()
+ {
+ return method;
+ }
+
+ public String getUri()
+ {
+ return uri;
+ }
+
+ public String getFullUri()
+ {
+ // calculate full uri
+ String fullUri = uri == null ? "" : uri;
+ if (args != null && args.size() > 0)
+ {
+ char prefix = (uri.indexOf('?') == -1) ? '?' : '&';
+ for (Map.Entry arg : args.entrySet())
+ {
+ fullUri += prefix + arg.getKey() + "=" + (arg.getValue() == null ? "" : arg.getValue());
+ prefix = '&';
+ }
+ }
+
+ return fullUri;
+ }
+
+ public Request setArgs(Map args)
+ {
+ this.args = args;
+ return this;
+ }
+
+ public Map getArgs()
+ {
+ return args;
+ }
+
+ public Request setHeaders(Map headers)
+ {
+ this.headers = headers;
+ return this;
+ }
+
+ public Map getHeaders()
+ {
+ return headers;
+ }
+
+ public Request setBody(byte[] body)
+ {
+ this.body = body;
+ return this;
+ }
+
+ public byte[] getBody()
+ {
+ return body;
+ }
+
+ public Request setEncoding(String encoding)
+ {
+ this.encoding = encoding;
+ return this;
+ }
+
+ public String getEncoding()
+ {
+ return encoding;
+ }
+
+ public Request setType(String contentType)
+ {
+ this.contentType = contentType;
+ return this;
+ }
+
+ public String getType()
+ {
+ return contentType;
+ }
+}
diff --git a/src/main/java/org/alfresco/httpclient/Response.java b/src/main/java/org/alfresco/httpclient/Response.java
new file mode 100644
index 0000000000..59dbae23bf
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/Response.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.httpclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ *
+ * @since 4.0
+ *
+ */
+public interface Response
+{
+ public InputStream getContentAsStream() throws IOException;
+
+ public String getHeader(String name);
+
+ public String getContentType();
+
+ public int getStatus();
+
+// public Long getRequestDuration();
+
+ public void release();
+}
diff --git a/src/main/java/org/alfresco/httpclient/SecureHttpClient.java b/src/main/java/org/alfresco/httpclient/SecureHttpClient.java
new file mode 100644
index 0000000000..93c93bcd6b
--- /dev/null
+++ b/src/main/java/org/alfresco/httpclient/SecureHttpClient.java
@@ -0,0 +1,149 @@
+package org.alfresco.httpclient;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.AlgorithmParameters;
+
+import org.alfresco.encryption.EncryptionUtils;
+import org.alfresco.encryption.Encryptor;
+import org.alfresco.encryption.KeyProvider;
+import org.alfresco.encryption.KeyResourceLoader;
+import org.alfresco.util.Pair;
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Simple HTTP client to connect to the Alfresco server.
+ *
+ * @since 4.0
+ */
+public class SecureHttpClient //extends AbstractHttpClient
+{
+// private static final Log logger = LogFactory.getLog(SecureHttpClient.class);
+//
+// private Encryptor encryptor;
+// private EncryptionUtils encryptionUtils;
+// private EncryptionService encryptionService;
+// private EncryptionParameters encryptionParameters;
+//
+// /**
+// * For testing purposes.
+// *
+// * @param solrResourceLoader
+// * @param alfrescoHost
+// * @param alfrescoPort
+// * @param encryptionParameters
+// */
+// public SecureHttpClient(HttpClientFactory httpClientFactory, String host, int port, EncryptionService encryptionService)
+// {
+// super(httpClientFactory, host, port);
+// this.encryptionUtils = encryptionService.getEncryptionUtils();
+// this.encryptor = encryptionService.getEncryptor();
+// this.encryptionService = encryptionService;
+// this.encryptionParameters = encryptionService.getEncryptionParameters();
+// }
+//
+// public SecureHttpClient(HttpClientFactory httpClientFactory, KeyResourceLoader keyResourceLoader, String host, int port,
+// EncryptionParameters encryptionParameters)
+// {
+// super(httpClientFactory, host, port);
+// this.encryptionParameters = encryptionParameters;
+// this.encryptionService = new EncryptionService(alfrescoHost, alfrescoPort, keyResourceLoader, encryptionParameters);
+// this.encryptionUtils = encryptionService.getEncryptionUtils();
+// this.encryptor = encryptionService.getEncryptor();
+// }
+//
+// protected HttpMethod createMethod(Request req) throws IOException
+// {
+// byte[] message = null;
+// HttpMethod method = super.createMethod(req);
+//
+// if(req.getMethod().equalsIgnoreCase("POST"))
+// {
+// message = req.getBody();
+// // encrypt body
+// Pair encrypted = encryptor.encrypt(KeyProvider.ALIAS_SOLR, null, message);
+// encryptionUtils.setRequestAlgorithmParameters(method, encrypted.getSecond());
+//
+// ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(encrypted.getFirst(), "application/octet-stream");
+// ((PostMethod)method).setRequestEntity(requestEntity);
+// }
+//
+// encryptionUtils.setRequestAuthentication(method, message);
+//
+// return method;
+// }
+//
+// protected HttpMethod sendRemoteRequest(Request req) throws AuthenticationException, IOException
+// {
+// HttpMethod method = super.sendRemoteRequest(req);
+//
+// // check that the request returned with an ok status
+// if(method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED)
+// {
+// throw new AuthenticationException(method);
+// }
+//
+// return method;
+// }
+//
+// /**
+// * Send Request to the repository
+// */
+// public Response sendRequest(Request req) throws AuthenticationException, IOException
+// {
+// HttpMethod method = super.sendRemoteRequest(req);
+// return new SecureHttpMethodResponse(method, httpClient.getHostConfiguration(), encryptionUtils);
+// }
+//
+// public static class SecureHttpMethodResponse extends HttpMethodResponse
+// {
+// protected HostConfiguration hostConfig;
+// protected EncryptionUtils encryptionUtils;
+// // Need to get as a byte array because we need to read the request twice, once for authentication
+// // and again by the web service.
+// protected byte[] decryptedBody;
+//
+// public SecureHttpMethodResponse(HttpMethod method, HostConfiguration hostConfig,
+// EncryptionUtils encryptionUtils) throws AuthenticationException, IOException
+// {
+// super(method);
+// this.hostConfig = hostConfig;
+// this.encryptionUtils = encryptionUtils;
+//
+// if(method.getStatusCode() == HttpStatus.SC_OK)
+// {
+// this.decryptedBody = encryptionUtils.decryptResponseBody(method);
+// // authenticate the response
+// if(!authenticate())
+// {
+// throw new AuthenticationException(method);
+// }
+// }
+// }
+//
+// protected boolean authenticate() throws IOException
+// {
+// return encryptionUtils.authenticateResponse(method, hostConfig.getHost(), decryptedBody);
+// }
+//
+// public InputStream getContentAsStream() throws IOException
+// {
+// if(decryptedBody != null)
+// {
+// return new ByteArrayInputStream(decryptedBody);
+// }
+// else
+// {
+// return null;
+// }
+// }
+// }
+
+}
diff --git a/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java b/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java
new file mode 100644
index 0000000000..0939129cf3
--- /dev/null
+++ b/src/main/java/org/alfresco/i18n/ResourceBundleBootstrapComponent.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.i18n;
+
+import java.util.List;
+
+import org.springframework.extensions.surf.util.I18NUtil;
+
+/**
+ * Resource bundle bootstrap component.
+ *
+ * Provides a convenient way to make resource bundles available via Spring config.
+ *
+ * @author Roy Wetherall
+ */
+public class ResourceBundleBootstrapComponent
+{
+ /**
+ * Set the resource bundles to be registered. This should be a list of resource
+ * bundle base names whose content will be made available across the repository.
+ *
+ * @param resourceBundles the resource bundles
+ */
+ public void setResourceBundles(List resourceBundles)
+ {
+ for (String resourceBundle : resourceBundles)
+ {
+ I18NUtil.registerResourceBundle(resourceBundle);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/BatchingDAO.java b/src/main/java/org/alfresco/ibatis/BatchingDAO.java
new file mode 100644
index 0000000000..fc35dfce98
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/BatchingDAO.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+/**
+ * Interface for DAOs that offer batching. This should be provided as an optimization
+ * and DAO implementations that can't supply batching should just do nothing.
+ *
+ * @author Derek Hulley
+ * @since 3.2.1
+ */
+public interface BatchingDAO
+{
+ /**
+ * Start a batch of insert or update commands
+ *
+ * @throws RuntimeException wrapping a SQLException
+ */
+ void startBatch();
+ /**
+ * Write a batch of insert or update commands
+ *
+ * @throws RuntimeException wrapping a SQLException
+ */
+ void executeBatch();
+}
diff --git a/src/main/java/org/alfresco/ibatis/ByteArrayTypeHandler.java b/src/main/java/org/alfresco/ibatis/ByteArrayTypeHandler.java
new file mode 100644
index 0000000000..406363569e
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/ByteArrayTypeHandler.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.alfresco.ibatis.SerializableTypeHandler.DeserializationException;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.TypeHandler;
+
+/**
+ * MyBatis 3.x TypeHandler for _byte[] to BLOB types.
+ *
+ * @author sglover
+ * @since 5.0
+ */
+public class ByteArrayTypeHandler implements TypeHandler
+{
+ /**
+ * @throws DeserializationException if the object could not be deserialized
+ */
+ public Object getResult(ResultSet rs, String columnName) throws SQLException
+ {
+ byte[] ret = null;
+ try
+ {
+ byte[] bytes = rs.getBytes(columnName);
+ if(bytes != null && !rs.wasNull())
+ {
+ ret = bytes;
+ }
+ }
+ catch (Throwable e)
+ {
+ throw new DeserializationException(e);
+ }
+ return ret;
+ }
+
+ @Override
+ public Object getResult(ResultSet rs, int columnIndex) throws SQLException
+ {
+ byte[] ret = null;
+ try
+ {
+ byte[] bytes = rs.getBytes(columnIndex);
+ if(bytes != null && !rs.wasNull())
+ {
+ ret = bytes;
+ }
+ }
+ catch (Throwable e)
+ {
+ throw new DeserializationException(e);
+ }
+ return ret;
+ }
+
+ public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException
+ {
+ if (parameter == null)
+ {
+ ps.setNull(i, Types.BINARY);
+ }
+ else
+ {
+ try
+ {
+ ps.setBytes(i, (byte[])parameter);
+ }
+ catch (Throwable e)
+ {
+ throw new SerializationException(e);
+ }
+ }
+ }
+
+ public Object getResult(CallableStatement cs, int columnIndex) throws SQLException
+ {
+ throw new UnsupportedOperationException("Unsupported");
+ }
+
+ /**
+ * @return Returns the value given
+ */
+ public Object valueOf(String s)
+ {
+ return s;
+ }
+
+ /**
+ * Marker exception to allow deserialization issues to be dealt with by calling code.
+ * If this exception remains uncaught, it will be very difficult to find and rectify
+ * the data issue.
+ *
+ * @author sglover
+ * @since 5.0
+ */
+ public static class DeserializationException extends RuntimeException
+ {
+ private static final long serialVersionUID = 4673487701048985340L;
+
+ public DeserializationException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+
+ /**
+ * Marker exception to allow serialization issues to be dealt with by calling code.
+ * Unlike with {@link DeserializationException deserialization}, it is not important
+ * to handle this exception neatly.
+ *
+ * @author sglover
+ * @since 5.0
+ */
+ public static class SerializationException extends RuntimeException
+ {
+ private static final long serialVersionUID = 962957884262870228L;
+
+ public SerializationException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBean.java b/src/main/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBean.java
new file mode 100644
index 0000000000..fc056594eb
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBean.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.io.IOException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.resource.HierarchicalResourceLoader;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.executor.ErrorContext;
+import org.apache.ibatis.logging.Log;
+import org.apache.ibatis.logging.LogFactory;
+import org.apache.ibatis.mapping.DatabaseIdProvider;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.reflection.factory.ObjectFactory;
+import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.TransactionFactory;
+import org.apache.ibatis.type.TypeHandler;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.NestedIOException;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
+
+import static org.springframework.util.Assert.notNull;
+import static org.springframework.util.ObjectUtils.isEmpty;
+import static org.springframework.util.StringUtils.hasLength;
+import static org.springframework.util.StringUtils.tokenizeToStringArray;
+
+/**
+ * Extends the MyBatis-Spring support by allowing a choice of {@link org.springframework.core.io.ResourceLoader}. The
+ * {@link #setResourceLoader(HierarchicalResourceLoader) ResourceLoader} will be used to load the SqlMapConfig
+ * file and use a {@link HierarchicalXMLConfigBuilder} to read the individual MyBatis (3.x) resources.
+ *
+ * Pending a better way to extend/override, much of the implementation is a direct copy of the MyBatis-Spring
+ * {@link SqlSessionFactoryBean}; some of the protected methods do not have access to the object's state
+ * and can therefore not be overridden successfully.
+ *
+ * This is equivalent to HierarchicalSqlMapClientFactoryBean which extended iBatis (2.x).
+ * See also: IBATIS-589
+ * and:
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+//note: effectively replaces SqlSessionFactoryBean to use hierarchical resource loader
+public class HierarchicalSqlSessionFactoryBean extends SqlSessionFactoryBean
+{
+
+ private HierarchicalResourceLoader resourceLoader;
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private Resource configLocation;
+
+ private Resource[] mapperLocations;
+
+ private DataSource dataSource;
+
+ private TransactionFactory transactionFactory;
+
+ private Properties configurationProperties;
+
+ private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
+
+ private SqlSessionFactory sqlSessionFactory;
+
+ private String environment = SqlSessionFactoryBean.class.getSimpleName(); // EnvironmentAware requires spring 3.1
+
+ private boolean failFast;
+
+ private Interceptor[] plugins;
+
+ private TypeHandler>[] typeHandlers;
+
+ private String typeHandlersPackage;
+
+ private Class>[] typeAliases;
+
+ private String typeAliasesPackage;
+
+ private Class> typeAliasesSuperType;
+
+ private DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
+
+ private ObjectFactory objectFactory;
+
+ private ObjectWrapperFactory objectWrapperFactory;
+ /**
+ * Default constructor
+ */
+ public HierarchicalSqlSessionFactoryBean()
+ {
+ }
+
+ /**
+ * Set the resource loader to use. To use the #resource.dialect# placeholder, use the
+ * {@link HierarchicalResourceLoader}.
+ *
+ * @param resourceLoader the resource loader to use
+ */
+ public void setResourceLoader(HierarchicalResourceLoader resourceLoader)
+ {
+ this.resourceLoader = resourceLoader;
+ }
+
+ /**
+ * Sets the ObjectFactory.
+ *
+ * @since 1.1.2
+ * @param objectFactory ObjectFactory
+ */
+ public void setObjectFactory(ObjectFactory objectFactory) {
+ this.objectFactory = objectFactory;
+ }
+
+ /**
+ * Sets the ObjectWrapperFactory.
+ *
+ * @since 1.1.2
+ * @param objectWrapperFactory ObjectWrapperFactory
+ */
+ public void setObjectWrapperFactory(ObjectWrapperFactory objectWrapperFactory) {
+ this.objectWrapperFactory = objectWrapperFactory;
+ }
+
+ /**
+ * Sets the DatabaseIdProvider.
+ *
+ * @since 1.1.0
+ * @return DatabaseIdProvider
+ */
+ public DatabaseIdProvider getDatabaseIdProvider() {
+ return databaseIdProvider;
+ }
+
+ /**
+ * Gets the DatabaseIdProvider
+ *
+ * @since 1.1.0
+ * @param databaseIdProvider DatabaseIdProvider
+ */
+ public void setDatabaseIdProvider(DatabaseIdProvider databaseIdProvider) {
+ this.databaseIdProvider = databaseIdProvider;
+ }
+
+ /**
+ * Mybatis plugin list.
+ *
+ * @since 1.0.1
+ *
+ * @param plugins list of plugins
+ *
+ */
+ public void setPlugins(Interceptor[] plugins) {
+ this.plugins = plugins;
+ }
+
+ /**
+ * Packages to search for type aliases.
+ *
+ * @since 1.0.1
+ *
+ * @param typeAliasesPackage package to scan for domain objects
+ *
+ */
+ public void setTypeAliasesPackage(String typeAliasesPackage) {
+ this.typeAliasesPackage = typeAliasesPackage;
+ }
+
+ /**
+ * Super class which domain objects have to extend to have a type alias created.
+ * No effect if there is no package to scan configured.
+ *
+ * @since 1.1.2
+ *
+ * @param typeAliasesSuperType super class for domain objects
+ *
+ */
+ public void setTypeAliasesSuperType(Class> typeAliasesSuperType) {
+ this.typeAliasesSuperType = typeAliasesSuperType;
+ }
+
+ /**
+ * Packages to search for type handlers.
+ *
+ * @since 1.0.1
+ *
+ * @param typeHandlersPackage package to scan for type handlers
+ *
+ */
+ public void setTypeHandlersPackage(String typeHandlersPackage) {
+ this.typeHandlersPackage = typeHandlersPackage;
+ }
+
+ /**
+ * Set type handlers. They must be annotated with {@code MappedTypes} and optionally with {@code MappedJdbcTypes}
+ *
+ * @since 1.0.1
+ *
+ * @param typeHandlers Type handler list
+ */
+ public void setTypeHandlers(TypeHandler>[] typeHandlers) {
+ this.typeHandlers = typeHandlers;
+ }
+
+ /**
+ * List of type aliases to register. They can be annotated with {@code Alias}
+ *
+ * @since 1.0.1
+ *
+ * @param typeAliases Type aliases list
+ */
+ public void setTypeAliases(Class>[] typeAliases) {
+ this.typeAliases = typeAliases;
+ }
+
+ /**
+ * If true, a final check is done on Configuration to assure that all mapped
+ * statements are fully loaded and there is no one still pending to resolve
+ * includes. Defaults to false.
+ *
+ * @since 1.0.1
+ *
+ * @param failFast enable failFast
+ */
+ public void setFailFast(boolean failFast) {
+ this.failFast = failFast;
+ }
+
+ /**
+ * Set the location of the MyBatis {@code SqlSessionFactory} config file. A typical value is
+ * "WEB-INF/mybatis-configuration.xml".
+ */
+ public void setConfigLocation(Resource configLocation) {
+ this.configLocation = configLocation;
+ }
+
+ /**
+ * Set locations of MyBatis mapper files that are going to be merged into the {@code SqlSessionFactory}
+ * configuration at runtime.
+ *
+ * This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file.
+ * This property being based on Spring's resource abstraction also allows for specifying
+ * resource patterns here: e.g. "classpath*:sqlmap/*-mapper.xml".
+ */
+ public void setMapperLocations(Resource[] mapperLocations) {
+ this.mapperLocations = mapperLocations;
+ }
+
+ /**
+ * Set optional properties to be passed into the SqlSession configuration, as alternative to a
+ * {@code <properties>} tag in the configuration xml file. This will be used to
+ * resolve placeholders in the config file.
+ */
+ public void setConfigurationProperties(Properties sqlSessionFactoryProperties) {
+ this.configurationProperties = sqlSessionFactoryProperties;
+ }
+
+ /**
+ * Set the JDBC {@code DataSource} that this instance should manage transactions for. The {@code DataSource}
+ * should match the one used by the {@code SqlSessionFactory}: for example, you could specify the same
+ * JNDI DataSource for both.
+ *
+ * A transactional JDBC {@code Connection} for this {@code DataSource} will be provided to application code
+ * accessing this {@code DataSource} directly via {@code DataSourceUtils} or {@code DataSourceTransactionManager}.
+ *
+ * The {@code DataSource} specified here should be the target {@code DataSource} to manage transactions for, not
+ * a {@code TransactionAwareDataSourceProxy}. Only data access code may work with
+ * {@code TransactionAwareDataSourceProxy}, while the transaction manager needs to work on the
+ * underlying target {@code DataSource}. If there's nevertheless a {@code TransactionAwareDataSourceProxy}
+ * passed in, it will be unwrapped to extract its target {@code DataSource}.
+ *
+ */
+ public void setDataSource(DataSource dataSource) {
+ if (dataSource instanceof TransactionAwareDataSourceProxy) {
+ // If we got a TransactionAwareDataSourceProxy, we need to perform
+ // transactions for its underlying target DataSource, else data
+ // access code won't see properly exposed transactions (i.e.
+ // transactions for the target DataSource).
+ this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
+ } else {
+ this.dataSource = dataSource;
+ }
+ }
+
+ /**
+ * Sets the {@code SqlSessionFactoryBuilder} to use when creating the {@code SqlSessionFactory}.
+ *
+ * This is mainly meant for testing so that mock SqlSessionFactory classes can be injected. By
+ * default, {@code SqlSessionFactoryBuilder} creates {@code DefaultSqlSessionFactory} instances.
+ *
+ */
+ public void setSqlSessionFactoryBuilder(SqlSessionFactoryBuilder sqlSessionFactoryBuilder) {
+ this.sqlSessionFactoryBuilder = sqlSessionFactoryBuilder;
+ }
+
+ /**
+ * Set the MyBatis TransactionFactory to use. Default is {@code SpringManagedTransactionFactory}
+ *
+ * The default {@code SpringManagedTransactionFactory} should be appropriate for all cases:
+ * be it Spring transaction management, EJB CMT or plain JTA. If there is no active transaction,
+ * SqlSession operations will execute SQL statements non-transactionally.
+ *
+ * It is strongly recommended to use the default {@code TransactionFactory}. If not used, any
+ * attempt at getting an SqlSession through Spring's MyBatis framework will throw an exception if
+ * a transaction is active.
+ *
+ * @see SpringManagedTransactionFactory
+ * @param transactionFactory the MyBatis TransactionFactory
+ */
+ public void setTransactionFactory(TransactionFactory transactionFactory) {
+ this.transactionFactory = transactionFactory;
+ }
+
+ /**
+ * NOTE: This class overrides any {@code Environment} you have set in the MyBatis
+ * config file. This is used only as a placeholder name. The default value is
+ * {@code SqlSessionFactoryBean.class.getSimpleName()}.
+ *
+ * @param environment the environment name
+ */
+ public void setEnvironment(String environment) {
+ this.environment = environment;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+
+ PropertyCheck.mandatory(this, "resourceLoader", resourceLoader);
+
+ notNull(dataSource, "Property 'dataSource' is required");
+ notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
+
+ this.sqlSessionFactory = buildSqlSessionFactory();
+ }
+
+
+
+
+ /**
+ * Build a {@code SqlSessionFactory} instance.
+ *
+ * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a
+ * {@code SqlSessionFactory} instance based on an Reader.
+ *
+ * @return SqlSessionFactory
+ * @throws IOException if loading the config file failed
+ */
+ protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
+
+ Configuration configuration;
+
+ HierarchicalXMLConfigBuilder xmlConfigBuilder = null;
+ if (this.configLocation != null) {
+ try {
+ xmlConfigBuilder = new HierarchicalXMLConfigBuilder(resourceLoader, this.configLocation.getInputStream(), null, this.configurationProperties);
+ configuration = xmlConfigBuilder.getConfiguration();
+ } catch (Exception ex) {
+ throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
+ } finally {
+ ErrorContext.instance().reset();
+ }
+ } else {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
+ }
+ configuration = new Configuration();
+ configuration.setVariables(this.configurationProperties);
+ }
+
+ if (this.objectFactory != null) {
+ configuration.setObjectFactory(this.objectFactory);
+ }
+
+ if (this.objectWrapperFactory != null) {
+ configuration.setObjectWrapperFactory(this.objectWrapperFactory);
+ }
+
+ if (hasLength(this.typeAliasesPackage)) {
+ String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
+ ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
+ for (String packageToScan : typeAliasPackageArray) {
+ configuration.getTypeAliasRegistry().registerAliases(packageToScan,
+ typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Scanned package: '" + packageToScan + "' for aliases");
+ }
+ }
+ }
+
+ if (!isEmpty(this.typeAliases)) {
+ for (Class> typeAlias : this.typeAliases) {
+ configuration.getTypeAliasRegistry().registerAlias(typeAlias);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Registered type alias: '" + typeAlias + "'");
+ }
+ }
+ }
+
+ if (!isEmpty(this.plugins)) {
+ for (Interceptor plugin : this.plugins) {
+ configuration.addInterceptor(plugin);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Registered plugin: '" + plugin + "'");
+ }
+ }
+ }
+
+ if (hasLength(this.typeHandlersPackage)) {
+ String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
+ ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
+ for (String packageToScan : typeHandlersPackageArray) {
+ configuration.getTypeHandlerRegistry().register(packageToScan);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Scanned package: '" + packageToScan + "' for type handlers");
+ }
+ }
+ }
+
+ if (!isEmpty(this.typeHandlers)) {
+ for (TypeHandler> typeHandler : this.typeHandlers) {
+ configuration.getTypeHandlerRegistry().register(typeHandler);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Registered type handler: '" + typeHandler + "'");
+ }
+ }
+ }
+
+ if (xmlConfigBuilder != null) {
+ try {
+ xmlConfigBuilder.parse();
+
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Parsed configuration file: '" + this.configLocation + "'");
+ }
+ } catch (Exception ex) {
+ throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
+ } finally {
+ ErrorContext.instance().reset();
+ }
+ }
+
+ if (this.transactionFactory == null) {
+ this.transactionFactory = new SpringManagedTransactionFactory();
+ }
+
+ Environment environment = new Environment(this.environment, this.transactionFactory, this.dataSource);
+ configuration.setEnvironment(environment);
+
+ //Commented out to be able to use dummy dataSource in tests.
+ /*
+ if (this.databaseIdProvider != null) {
+ try {
+ configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
+ } catch (SQLException e) {
+ throw new NestedIOException("Failed getting a databaseId", e);
+ }
+ }
+ */
+
+ if (!isEmpty(this.mapperLocations)) {
+ for (Resource mapperLocation : this.mapperLocations) {
+ if (mapperLocation == null) {
+ continue;
+ }
+
+ try {
+ XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
+ configuration, mapperLocation.toString(), configuration.getSqlFragments());
+ xmlMapperBuilder.parse();
+ } catch (Exception e) {
+ throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
+ } finally {
+ ErrorContext.instance().reset();
+ }
+
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Parsed mapper file: '" + mapperLocation + "'");
+ }
+ }
+ } else {
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Property 'mapperLocations' was not specified, only MyBatis mapper files specified in the config xml were loaded");
+ }
+ }
+
+ return this.sqlSessionFactoryBuilder.build(configuration);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public SqlSessionFactory getObject() throws Exception {
+ if (this.sqlSessionFactory == null) {
+ afterPropertiesSet();
+ }
+
+ return this.sqlSessionFactory;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Class extends SqlSessionFactory> getObjectType() {
+ return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isSingleton() {
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void onApplicationEvent(ApplicationEvent event) {
+ if (failFast && event instanceof ContextRefreshedEvent) {
+ // fail-fast -> check all statements are completed
+ this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/HierarchicalXMLConfigBuilder.java b/src/main/java/org/alfresco/ibatis/HierarchicalXMLConfigBuilder.java
new file mode 100644
index 0000000000..89d1ecf3da
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/HierarchicalXMLConfigBuilder.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2005-2016 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+
+import org.alfresco.util.resource.HierarchicalResourceLoader;
+import org.apache.ibatis.builder.BaseBuilder;
+import org.apache.ibatis.builder.BuilderException;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
+import org.apache.ibatis.datasource.DataSourceFactory;
+import org.apache.ibatis.executor.ErrorContext;
+import org.apache.ibatis.executor.loader.ProxyFactory;
+import org.apache.ibatis.io.Resources;
+import org.apache.ibatis.mapping.DatabaseIdProvider;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.parsing.XNode;
+import org.apache.ibatis.parsing.XPathParser;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.reflection.DefaultReflectorFactory;
+import org.apache.ibatis.reflection.MetaClass;
+import org.apache.ibatis.reflection.ReflectorFactory;
+import org.apache.ibatis.reflection.factory.ObjectFactory;
+import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
+import org.apache.ibatis.session.AutoMappingBehavior;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.LocalCacheScope;
+import org.apache.ibatis.transaction.TransactionFactory;
+import org.apache.ibatis.type.JdbcType;
+import org.springframework.core.io.Resource;
+
+
+/**
+ * Extends the MyBatis XMLConfigBuilder to allow the selection of a {@link org.springframework.core.io.ResourceLoader}
+ * that will be used to load the resources specified in the mapper 's resource .
+ *
+ * By using the resource.dialect placeholder with hierarchical resource loading,
+ * different resource files can be picked up for different dialects. This reduces duplication
+ * when supporting multiple database configurations.
+ *
+ * <configuration>
+ * <mappers>
+ * <mapper resource="org/x/y/#resource.dialect#/View1.xml"/>
+ * <mapper resource="org/x/y/#resource.dialect#/View2.xml"/>
+ * </mappers>
+ * </configuration>
+ *
+ *
+ * Much of the implementation is a direct copy of the MyBatis {@link org.apache.ibatis.builder.xml.XMLConfigBuilder}; some
+ * of the protected methods do not have access to the object's state and can therefore
+ * not be overridden successfully: IBATIS-589
+
+ * Pending a better way to extend/override, much of the implementation is a direct copy of the MyBatis
+ * {@link org.mybatis.spring.SqlSessionFactoryBean}; some of the protected methods do not have access to the object's state
+ * and can therefore not be overridden successfully.
+ *
+ * This is equivalent to HierarchicalSqlMapConfigParser which extended iBatis (2.x).
+ * See also: IBATIS-589
+ * and:
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+// note: effectively extends XMLConfigBuilder to use hierarchical resource loader
+public class HierarchicalXMLConfigBuilder extends BaseBuilder
+{
+ private boolean parsed;
+ private XPathParser parser;
+ private String environment;
+ private ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
+
+ // EXTENDED
+ final private HierarchicalResourceLoader resourceLoader;
+
+ public HierarchicalXMLConfigBuilder(HierarchicalResourceLoader resourceLoader, InputStream inputStream, String environment, Properties props)
+ {
+ super(new Configuration());
+
+ // EXTENDED
+ this.resourceLoader = resourceLoader;
+
+ ErrorContext.instance().resource("SQL Mapper Configuration");
+ this.configuration.setVariables(props);
+ this.parsed = false;
+ this.environment = environment;
+ this.parser = new XPathParser(inputStream, true, props, new XMLMapperEntityResolver());
+ }
+
+ public Configuration parse() {
+ if (parsed) {
+ throw new BuilderException("Each XMLConfigBuilder can only be used once.");
+ }
+ parsed = true;
+ parseConfiguration(parser.evalNode("/configuration"));
+ return configuration;
+ }
+
+ private void parseConfiguration(XNode root) {
+ try {
+ //issue #117 read properties first
+ propertiesElement(root.evalNode("properties"));
+ typeAliasesElement(root.evalNode("typeAliases"));
+ pluginElement(root.evalNode("plugins"));
+ objectFactoryElement(root.evalNode("objectFactory"));
+ objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
+ reflectionFactoryElement(root.evalNode("reflectionFactory"));
+ settingsElement(root.evalNode("settings"));
+ // read it after objectFactory and objectWrapperFactory issue #631
+ environmentsElement(root.evalNode("environments"));
+ databaseIdProviderElement(root.evalNode("databaseIdProvider"));
+ typeHandlerElement(root.evalNode("typeHandlers"));
+ mapperElement(root.evalNode("mappers"));
+ } catch (Exception e) {
+ throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
+ }
+ }
+
+ private void typeAliasesElement(XNode parent) {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ String typeAliasPackage = child.getStringAttribute("name");
+ configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
+ } else {
+ String alias = child.getStringAttribute("alias");
+ String type = child.getStringAttribute("type");
+ try {
+ Class> clazz = Resources.classForName(type);
+ if (alias == null) {
+ typeAliasRegistry.registerAlias(clazz);
+ } else {
+ typeAliasRegistry.registerAlias(alias, clazz);
+ }
+ } catch (ClassNotFoundException e) {
+ throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
+ }
+ }
+ }
+ }
+ }
+
+ private void pluginElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ String interceptor = child.getStringAttribute("interceptor");
+ Properties properties = child.getChildrenAsProperties();
+ Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
+ interceptorInstance.setProperties(properties);
+ configuration.addInterceptor(interceptorInstance);
+ }
+ }
+ }
+
+ private void objectFactoryElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties properties = context.getChildrenAsProperties();
+ ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
+ factory.setProperties(properties);
+ configuration.setObjectFactory(factory);
+ }
+ }
+
+ private void objectWrapperFactoryElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
+ configuration.setObjectWrapperFactory(factory);
+ }
+ }
+
+ private void reflectionFactoryElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance();
+ configuration.setReflectorFactory(factory);
+ }
+ }
+
+ private void propertiesElement(XNode context) throws Exception {
+ if (context != null) {
+ Properties defaults = context.getChildrenAsProperties();
+ String resource = context.getStringAttribute("resource");
+ String url = context.getStringAttribute("url");
+ if (resource != null && url != null) {
+ throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
+ }
+ if (resource != null) {
+ defaults.putAll(Resources.getResourceAsProperties(resource));
+ } else if (url != null) {
+ defaults.putAll(Resources.getUrlAsProperties(url));
+ }
+ Properties vars = configuration.getVariables();
+ if (vars != null) {
+ defaults.putAll(vars);
+ }
+ parser.setVariables(defaults);
+ configuration.setVariables(defaults);
+ }
+ }
+
+ private void settingsElement(XNode context) throws Exception {
+ if (context != null) {
+ Properties props = context.getChildrenAsProperties();
+ // Check that all settings are known to the configuration class
+ MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
+ for (Object key : props.keySet()) {
+ if (!metaConfig.hasSetter(String.valueOf(key))) {
+ throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
+ }
+ }
+ configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
+ configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
+ configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
+ configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
+ configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), true));
+ configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
+ configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
+ configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
+ configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
+ configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
+ configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
+ configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
+ configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
+ configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
+ configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
+ configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
+ configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
+ configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
+ configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
+ configuration.setLogPrefix(props.getProperty("logPrefix"));
+ configuration.setLogImpl(resolveClass(props.getProperty("logImpl")));
+ }
+ }
+
+ private void environmentsElement(XNode context) throws Exception {
+ if (context != null) {
+ if (environment == null) {
+ environment = context.getStringAttribute("default");
+ }
+ for (XNode child : context.getChildren()) {
+ String id = child.getStringAttribute("id");
+ if (isSpecifiedEnvironment(id)) {
+ TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
+ DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
+ DataSource dataSource = dsFactory.getDataSource();
+ Environment.Builder environmentBuilder = new Environment.Builder(id)
+ .transactionFactory(txFactory)
+ .dataSource(dataSource);
+ configuration.setEnvironment(environmentBuilder.build());
+ }
+ }
+ }
+ }
+
+ private void databaseIdProviderElement(XNode context) throws Exception {
+ DatabaseIdProvider databaseIdProvider = null;
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties properties = context.getChildrenAsProperties();
+ databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
+ databaseIdProvider.setProperties(properties);
+ }
+ Environment environment = configuration.getEnvironment();
+ if (environment != null && databaseIdProvider != null) {
+ String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
+ configuration.setDatabaseId(databaseId);
+ }
+ }
+
+ private TransactionFactory transactionManagerElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
+ factory.setProperties(props);
+ return factory;
+ }
+ throw new BuilderException("Environment declaration requires a TransactionFactory.");
+ }
+
+ private DataSourceFactory dataSourceElement(XNode context) throws Exception {
+ if (context != null) {
+ String type = context.getStringAttribute("type");
+ Properties props = context.getChildrenAsProperties();
+ DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
+ factory.setProperties(props);
+ return factory;
+ }
+ throw new BuilderException("Environment declaration requires a DataSourceFactory.");
+ }
+
+ private void typeHandlerElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ String typeHandlerPackage = child.getStringAttribute("name");
+ typeHandlerRegistry.register(typeHandlerPackage);
+ } else {
+ String javaTypeName = child.getStringAttribute("javaType");
+ String jdbcTypeName = child.getStringAttribute("jdbcType");
+ String handlerTypeName = child.getStringAttribute("handler");
+ Class> javaTypeClass = resolveClass(javaTypeName);
+ JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
+ Class> typeHandlerClass = resolveClass(handlerTypeName);
+ if (javaTypeClass != null) {
+ if (jdbcType == null) {
+ typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
+ } else {
+ typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
+ }
+ } else {
+ typeHandlerRegistry.register(typeHandlerClass);
+ }
+ }
+ }
+ }
+ }
+
+ private void mapperElement(XNode parent) throws Exception {
+ if (parent != null) {
+ for (XNode child : parent.getChildren()) {
+ if ("package".equals(child.getName())) {
+ String mapperPackage = child.getStringAttribute("name");
+ configuration.addMappers(mapperPackage);
+ } else {
+ String resource = child.getStringAttribute("resource");
+ String url = child.getStringAttribute("url");
+ String mapperClass = child.getStringAttribute("class");
+ if (resource != null && url == null && mapperClass == null) {
+ ErrorContext.instance().resource(resource);
+
+ // // EXTENDED
+ // inputStream = Resources.getResourceAsStream(resource);
+ InputStream inputStream = null;
+ Resource res = resourceLoader.getResource(resource);
+ if (res != null && res.exists())
+ {
+ inputStream = res.getInputStream();
+ }
+ else {
+ throw new BuilderException("Failed to get resource: "+resource);
+ }
+
+ //InputStream inputStream = Resources.getResourceAsStream(resource);
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
+ mapperParser.parse();
+ } else if (resource == null && url != null && mapperClass == null) {
+ ErrorContext.instance().resource(url);
+ InputStream inputStream = Resources.getUrlAsStream(url);
+ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
+ mapperParser.parse();
+ } else if (resource == null && url == null && mapperClass != null) {
+ Class> mapperInterface = Resources.classForName(mapperClass);
+ configuration.addMapper(mapperInterface);
+ } else {
+ throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
+ }
+ }
+ }
+ }
+ }
+
+ private boolean isSpecifiedEnvironment(String id) {
+ if (environment == null) {
+ throw new BuilderException("No environment specified.");
+ } else if (id == null) {
+ throw new BuilderException("Environment requires an id attribute.");
+ } else if (environment.equals(id)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/IdsEntity.java b/src/main/java/org/alfresco/ibatis/IdsEntity.java
new file mode 100644
index 0000000000..d8f4f09e2c
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/IdsEntity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2005-2013 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.util.List;
+
+/**
+ * Entity bean to carry ID-style information
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+public class IdsEntity
+{
+ private Long idOne;
+ private Long idTwo;
+ private Long idThree;
+ private Long idFour;
+ private List ids;
+ public Long getIdOne()
+ {
+ return idOne;
+ }
+ public void setIdOne(Long id)
+ {
+ this.idOne = id;
+ }
+ public Long getIdTwo()
+ {
+ return idTwo;
+ }
+ public void setIdTwo(Long id)
+ {
+ this.idTwo = id;
+ }
+ public Long getIdThree()
+ {
+ return idThree;
+ }
+ public void setIdThree(Long idThree)
+ {
+ this.idThree = idThree;
+ }
+ public Long getIdFour()
+ {
+ return idFour;
+ }
+ public void setIdFour(Long idFour)
+ {
+ this.idFour = idFour;
+ }
+ public List getIds()
+ {
+ return ids;
+ }
+ public void setIds(List ids)
+ {
+ this.ids = ids;
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/RetryingCallbackHelper.java b/src/main/java/org/alfresco/ibatis/RetryingCallbackHelper.java
new file mode 100644
index 0000000000..6b92b5bf6c
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/RetryingCallbackHelper.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A helper that runs a unit of work, transparently retrying the unit of work if
+ * an error occurs.
+ *
+ * Defaults:
+ *
+ * maxRetries: 5
+ * retryWaitMs: 10
+ *
+ *
+ * @author Derek Hulley
+ * @since 3.4
+ */
+public class RetryingCallbackHelper
+{
+ private static final Log logger = LogFactory.getLog(RetryingCallbackHelper.class);
+
+ /** The maximum number of retries. -1 for infinity. */
+ private int maxRetries;
+ /** How much time to wait with each retry. */
+ private int retryWaitMs;
+
+ /**
+ * Callback interface
+ * @author Derek Hulley
+ */
+ public interface RetryingCallback
+ {
+ /**
+ * Perform a unit of work.
+ *
+ * @return Return the result of the unit of work
+ * @throws Throwable This can be anything and will guarantee either a retry or a rollback
+ */
+ public Result execute() throws Throwable;
+ };
+
+ /**
+ * Default constructor.
+ */
+ public RetryingCallbackHelper()
+ {
+ this.maxRetries = 5;
+ this.retryWaitMs = 10;
+ }
+
+ /**
+ * Set the maximimum number of retries. -1 for infinity.
+ */
+ public void setMaxRetries(int maxRetries)
+ {
+ this.maxRetries = maxRetries;
+ }
+
+ public void setRetryWaitMs(int retryWaitMs)
+ {
+ this.retryWaitMs = retryWaitMs;
+ }
+
+ /**
+ * Execute a callback until it succeeds, fails or until a maximum number of retries have
+ * been attempted.
+ *
+ * @param callback The callback containing the unit of work.
+ * @return Returns the result of the unit of work.
+ * @throws RuntimeException all checked exceptions are converted
+ */
+ public R doWithRetry(RetryingCallback callback)
+ {
+ // Track the last exception caught, so that we can throw it if we run out of retries.
+ RuntimeException lastException = null;
+ for (int count = 0; count == 0 || count < maxRetries; count++)
+ {
+ try
+ {
+ // Do the work.
+ R result = callback.execute();
+ if (logger.isDebugEnabled())
+ {
+ if (count != 0)
+ {
+ logger.debug("\n" +
+ "Retrying work succeeded: \n" +
+ " Thread: " + Thread.currentThread().getName() + "\n" +
+ " Iteration: " + count);
+ }
+ }
+ return result;
+ }
+ catch (Throwable e)
+ {
+ lastException = (e instanceof RuntimeException) ?
+ (RuntimeException) e :
+ new AlfrescoRuntimeException("Exception in Transaction.", e);
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("\n" +
+ "Retrying work failed: \n" +
+ " Thread: " + Thread.currentThread().getName() + "\n" +
+ " Iteration: " + count + "\n" +
+ " Exception follows:",
+ e);
+ }
+ else if (logger.isInfoEnabled())
+ {
+ String msg = String.format(
+ "Retrying %s: count %2d; wait: %3dms; msg: \"%s\"; exception: (%s)",
+ Thread.currentThread().getName(),
+ count, retryWaitMs,
+ e.getMessage(),
+ e.getClass().getName());
+ logger.info(msg);
+ }
+ try
+ {
+ Thread.sleep(retryWaitMs);
+ }
+ catch (InterruptedException ie)
+ {
+ // Do nothing.
+ }
+ // Try again
+ continue;
+ }
+ }
+ // We've worn out our welcome and retried the maximum number of times.
+ // So, fail.
+ throw lastException;
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/RollupResultHandler.java b/src/main/java/org/alfresco/ibatis/RollupResultHandler.java
new file mode 100644
index 0000000000..4d2419e46a
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/RollupResultHandler.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.ibatis.executor.result.DefaultResultContext;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.ResultContext;
+import org.apache.ibatis.session.ResultHandler;
+import org.mybatis.spring.SqlSessionTemplate;
+
+/**
+ * A {@link ResultHandler} that collapses multiple rows based on a set of properties.
+ *
+ * This class is derived from earlier RollupRowHandler used to workaround the groupBy and nested ResultMap
+ * behaviour in iBatis (2.3.4.726) IBATIS-503 .
+ *
+ * The set of properties given act as a unique key. When the unique key changes , the collection
+ * values from the nested ResultMap are coalesced and the given {@link ResultHandler} is called. It is
+ * possible to embed several instances of this handler for deeply-nested ResultMap declarations.
+ *
+ * Use this instance as a regular {@link ResultHandler}, but with one big exception: call {@link #processLastResults()}
+ * after executing the SQL statement. Remove the groupBy attribute from the iBatis ResultMap
+ * declaration.
+ *
+ * Example iBatis 2.x (TODO migrate example to MyBatis 3.x):
+ *
+ <resultMap id="result_AuditQueryAllValues"
+ extends="alfresco.audit.result_AuditQueryNoValues"
+ class="AuditQueryResult">
+ <result property="auditValues" resultMap="alfresco.propval.result_PropertyIdSearchRow"/>
+ </resultMap>
+ *
+ * Example usage:
+ *
+ RowHandler rowHandler = new RowHandler()
+ {
+ public void handleRow(Object valueObject)
+ {
+ // DO SOMETHING
+ }
+ };
+ RollupRowHandler rollupRowHandler = new RollupRowHandler(
+ new String[] {"auditEntryId"},
+ "auditValues",
+ rowHandler,
+ maxResults);
+
+ if (maxResults > 0)
+ {
+ // Calculate the maximum results required
+ int sqlMaxResults = (maxResults > 0 ? ((maxResults+1) * 20) : Integer.MAX_VALUE);
+
+ List rows = template.queryForList(SELECT_ENTRIES_WITH_VALUES, params, 0, sqlMaxResults);
+ for (AuditQueryResult row : rows)
+ {
+ rollupRowHandler.handleRow(row);
+ }
+ // Don't process last result:
+ // rollupRowHandler.processLastResults();
+ // The last result may be incomplete
+ }
+ else
+ {
+ template.queryWithRowHandler(SELECT_ENTRIES_WITH_VALUES, params, rollupRowHandler);
+ rollupRowHandler.processLastResults();
+ }
+ *
+ *
+ * This class is not thread-safe; use a new instance for each use.
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+public class RollupResultHandler implements ResultHandler
+{
+ private final String[] keyProperties;
+ private final String collectionProperty;
+ private final ResultHandler resultHandler;
+ private final int maxResults;
+
+ private Object[] lastKeyValues;
+ private List rawResults;
+ private int resultCount;
+
+ private Configuration configuration;
+
+ /**
+ * @param keyProperties the properties that make up the unique key
+ * @param collectionProperty the property mapped using a nested ResultMap
+ * @param resultHandler the result handler that will receive the rolled-up results
+ */
+ public RollupResultHandler(Configuration configuration, String[] keyProperties, String collectionProperty, ResultHandler resultHandler)
+ {
+ this(configuration, keyProperties, collectionProperty, resultHandler, Integer.MAX_VALUE);
+ }
+
+ /**
+ * @param keyProperties the properties that make up the unique key
+ * @param collectionProperty the property mapped using a nested ResultMap
+ * @param resultHandler the result handler that will receive the rolled-up results
+ * @param maxResults the maximum number of results to retrieve (-1 for no limit).
+ * Make sure that the query result limit is large enough to produce this
+ * at least this number of results
+ */
+ public RollupResultHandler(Configuration configuration, String[] keyProperties, String collectionProperty, ResultHandler resultHandler, int maxResults)
+ {
+ if (keyProperties == null || keyProperties.length == 0)
+ {
+ throw new IllegalArgumentException("RollupRowHandler can only be used with at least one key property.");
+ }
+ if (collectionProperty == null)
+ {
+ throw new IllegalArgumentException("RollupRowHandler must have a collection property.");
+ }
+ this.configuration = configuration;
+ this.keyProperties = keyProperties;
+ this.collectionProperty = collectionProperty;
+ this.resultHandler = resultHandler;
+ this.maxResults = maxResults;
+ this.rawResults = new ArrayList(100);
+ }
+
+ public void handleResult(ResultContext context)
+ {
+ // Shortcut if we have processed enough results
+ if (maxResults > 0 && resultCount >= maxResults)
+ {
+ return;
+ }
+
+ Object valueObject = context.getResultObject();
+ MetaObject probe = configuration.newMetaObject(valueObject);
+
+ // Check if the key has changed
+ if (lastKeyValues == null)
+ {
+ lastKeyValues = getKeyValues(probe);
+ resultCount = 0;
+ }
+ // Check if it has changed
+ Object[] currentKeyValues = getKeyValues(probe);
+ if (!Arrays.deepEquals(lastKeyValues, currentKeyValues))
+ {
+ // Key has changed, so handle the results
+ Object resultObject = coalesceResults(configuration, rawResults, collectionProperty);
+ if (resultObject != null)
+ {
+ DefaultResultContext resultContext = new DefaultResultContext();
+ resultContext.nextResultObject(resultObject);
+
+ resultHandler.handleResult(resultContext);
+ resultCount++;
+ }
+ rawResults.clear();
+ lastKeyValues = currentKeyValues;
+ }
+ // Add the new value to the results for next time
+ rawResults.add(valueObject);
+ // Done
+ }
+
+ /**
+ * Client code must call this method once the query returns so that the final results
+ * can be passed to the inner RowHandler. If a query is limited by size, then it is
+ * possible that the unprocessed results represent an incomplete final object; in this case
+ * it would be best to ignore the last results. If the query is complete (i.e. all results
+ * are returned) then this method should be called.
+ *
+ * If you want X results and each result is made up of N rows (on average), then set the query
+ * limit to:
+ * L = X * (N+1)
+ * and don't call this method.
+ */
+ public void processLastResults()
+ {
+ // Shortcut if we have processed enough results
+ if (maxResults > 0 && resultCount >= maxResults)
+ {
+ return;
+ }
+ // Handle any outstanding results
+ Object resultObject = coalesceResults(configuration, rawResults, collectionProperty);
+ if (resultObject != null)
+ {
+ DefaultResultContext resultContext = new DefaultResultContext();
+ resultContext.nextResultObject(resultObject);
+
+ resultHandler.handleResult(resultContext);
+ resultCount++;
+ rawResults.clear(); // Stop it from being used again
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Object coalesceResults(Configuration configuration, List valueObjects, String collectionProperty)
+ {
+ // Take the first result as the base value
+ Object resultObject = null;
+ MetaObject probe = null;
+ Collection collection = null;
+ for (Object object : valueObjects)
+ {
+ if (collection == null)
+ {
+ resultObject = object;
+ probe = configuration.newMetaObject(resultObject);
+ collection = (Collection) probe.getValue(collectionProperty);
+ }
+ else
+ {
+ Collection> addedValues = (Collection) probe.getValue(collectionProperty);
+ collection.addAll(addedValues);
+ }
+ }
+ // Done
+ return resultObject;
+ }
+
+ /**
+ * @return Returns the values for the {@link RollupResultHandler#keyProperties}
+ */
+ private Object[] getKeyValues(MetaObject probe)
+ {
+ Object[] keyValues = new Object[keyProperties.length];
+ for (int i = 0; i < keyProperties.length; i++)
+ {
+ keyValues[i] = probe.getValue(keyProperties[i]);
+ }
+ return keyValues;
+ }
+}
diff --git a/src/main/java/org/alfresco/ibatis/SerializableTypeHandler.java b/src/main/java/org/alfresco/ibatis/SerializableTypeHandler.java
new file mode 100644
index 0000000000..094264cae2
--- /dev/null
+++ b/src/main/java/org/alfresco/ibatis/SerializableTypeHandler.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.ibatis;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.TypeHandler;
+
+/**
+ * MyBatis 3.x TypeHandler for java.io.Serializable to BLOB types.
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+public class SerializableTypeHandler implements TypeHandler
+{
+ public static final int DEFAULT_SERIALIZABLE_TYPE = Types.LONGVARBINARY;
+ private static volatile int serializableType = DEFAULT_SERIALIZABLE_TYPE;
+
+ /**
+ * @see Types
+ */
+ public static void setSerializableType(int serializableType)
+ {
+ SerializableTypeHandler.serializableType = serializableType;
+ }
+
+ /**
+ * @return Returns the SQL type to use for serializable columns
+ */
+ public static int getSerializableType()
+ {
+ return serializableType;
+ }
+
+ /**
+ * @throws DeserializationException if the object could not be deserialized
+ */
+ public Object getResult(ResultSet rs, String columnName) throws SQLException
+ {
+ final Serializable ret;
+ try
+ {
+ InputStream is = rs.getBinaryStream(columnName);
+ if (is == null || rs.wasNull())
+ {
+ return null;
+ }
+ // Get the stream and deserialize
+ ObjectInputStream ois = new ObjectInputStream(is);
+ Object obj = ois.readObject();
+ // Success
+ ret = (Serializable) obj;
+ }
+ catch (Throwable e)
+ {
+ throw new DeserializationException(e);
+ }
+ return ret;
+ }
+
+ @Override
+ public Object getResult(ResultSet rs, int columnIndex) throws SQLException
+ {
+ final Serializable ret;
+ try
+ {
+ InputStream is = rs.getBinaryStream(columnIndex);
+ if (is == null || rs.wasNull())
+ {
+ return null;
+ }
+ // Get the stream and deserialize
+ ObjectInputStream ois = new ObjectInputStream(is);
+ Object obj = ois.readObject();
+ // Success
+ ret = (Serializable) obj;
+ }
+ catch (Throwable e)
+ {
+ throw new DeserializationException(e);
+ }
+ return ret;
+ }
+
+ public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException
+ {
+ if (parameter == null)
+ {
+ ps.setNull(i, SerializableTypeHandler.serializableType);
+ }
+ else
+ {
+ try
+ {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(parameter);
+ byte[] bytes = baos.toByteArray();
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ ps.setBinaryStream(i, bais, bytes.length);
+ }
+ catch (Throwable e)
+ {
+ throw new SerializationException(e);
+ }
+ }
+ }
+
+ public Object getResult(CallableStatement cs, int columnIndex) throws SQLException
+ {
+ throw new UnsupportedOperationException("Unsupported");
+ }
+
+ /**
+ * @return Returns the value given
+ */
+ public Object valueOf(String s)
+ {
+ return s;
+ }
+
+ /**
+ * Marker exception to allow deserialization issues to be dealt with by calling code.
+ * If this exception remains uncaught, it will be very difficult to find and rectify
+ * the data issue.
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+ public static class DeserializationException extends RuntimeException
+ {
+ private static final long serialVersionUID = 4673487701048985340L;
+
+ public DeserializationException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+
+ /**
+ * Marker exception to allow serialization issues to be dealt with by calling code.
+ * Unlike with {@link DeserializationException deserialization}, it is not important
+ * to handle this exception neatly.
+ *
+ * @author Derek Hulley
+ * @since 3.2
+ */
+ public static class SerializationException extends RuntimeException
+ {
+ private static final long serialVersionUID = 962957884262870228L;
+
+ public SerializationException(Throwable cause)
+ {
+ super(cause);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/processor/Processor.java b/src/main/java/org/alfresco/processor/Processor.java
new file mode 100644
index 0000000000..52c7e61c1a
--- /dev/null
+++ b/src/main/java/org/alfresco/processor/Processor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.processor;
+
+/**
+ * Interface for Proccessor classes - such as Template or Scripting Processors.
+ *
+ * @author Roy Wetherall
+ */
+public interface Processor
+{
+ /**
+ * Get the name of the processor
+ *
+ * @return the name of the processor
+ */
+ public String getName();
+
+ /**
+ * The file extension that the processor is associated with, null if none.
+ *
+ * @return the extension
+ */
+ public String getExtension();
+
+ /**
+ * Registers a processor extension with the processor
+ *
+ * @param processorExtension the process extension
+ */
+ public void registerProcessorExtension(ProcessorExtension processorExtension);
+}
\ No newline at end of file
diff --git a/src/main/java/org/alfresco/processor/ProcessorExtension.java b/src/main/java/org/alfresco/processor/ProcessorExtension.java
new file mode 100644
index 0000000000..c9dbb97e07
--- /dev/null
+++ b/src/main/java/org/alfresco/processor/ProcessorExtension.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.processor;
+
+/**
+ * Interface to represent a server side script implementation
+ *
+ * @author Roy Wetherall
+ */
+public interface ProcessorExtension
+{
+ /**
+ * Returns the name of the extension
+ *
+ * @return the name of the extension
+ */
+ String getExtensionName();
+}
diff --git a/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java b/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java
new file mode 100644
index 0000000000..832ee3a350
--- /dev/null
+++ b/src/main/java/org/alfresco/query/AbstractCachingCannedQueryFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.List;
+
+/**
+ * Caching support extension for {@link CannedQueryFactory} implementations.
+ *
+ * Depending on the parameters provided, this class may choose to pick up existing results
+ * and re-use them for later page requests; the client will not have knowledge of the
+ * shortcuts.
+ *
+ * TODO: This is work-in-progress
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractCachingCannedQueryFactory extends AbstractCannedQueryFactory
+{
+ /**
+ * Base implementation that provides a caching facade around the query.
+ *
+ * @return a decoraded facade query that will cache query results for later paging requests
+ */
+ @Override
+ public final CannedQuery getCannedQuery(CannedQueryParameters parameters)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Derived classes must implement this method to provide the raw query that supports the given
+ * parameters. All requests must be serviced without any further caching in order to prevent
+ * duplicate caching.
+ *
+ * @param parameters the query parameters as given by the client
+ * @return the query that will generate the results
+ */
+ protected abstract CannedQuery getCannedQueryImpl(CannedQueryParameters parameters);
+
+ private class CannedQueryCacheFacade extends AbstractCannedQuery
+ {
+ private final AbstractCannedQuery delegate;
+
+ private CannedQueryCacheFacade(CannedQueryParameters params, AbstractCannedQuery delegate)
+ {
+ super(params);
+ this.delegate = delegate;
+ }
+
+ @Override
+ protected List queryAndFilter(CannedQueryParameters parameters)
+ {
+ // Copy the parameters and remove all references to paging.
+ // The underlying query will return full or filtered results (possibly also sorted)
+ // but will not apply page limitations
+
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/query/AbstractCannedQuery.java b/src/main/java/org/alfresco/query/AbstractCannedQuery.java
new file mode 100644
index 0000000000..a8effe90fc
--- /dev/null
+++ b/src/main/java/org/alfresco/query/AbstractCannedQuery.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.GUID;
+import org.alfresco.util.Pair;
+import org.alfresco.util.ParameterCheck;
+
+/**
+ * Basic support for canned query implementations.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractCannedQuery implements CannedQuery
+{
+ private final CannedQueryParameters parameters;
+ private final String queryExecutionId;
+ private CannedQueryResults results;
+
+ /**
+ * Construct the canned query given the original parameters applied.
+ *
+ * A random GUID query execution ID will be generated.
+ *
+ * @param parameters the original query parameters
+ */
+ protected AbstractCannedQuery(CannedQueryParameters parameters)
+ {
+ ParameterCheck.mandatory("parameters", parameters);
+ this.parameters = parameters;
+ this.queryExecutionId = GUID.generate();
+ }
+
+ @Override
+ public CannedQueryParameters getParameters()
+ {
+ return parameters;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "AbstractCannedQuery [parameters=" + parameters + ", class=" + this.getClass() + "]";
+ }
+
+ @Override
+ public synchronized final CannedQueryResults execute()
+ {
+ // Check that we are not requerying
+ if (results != null)
+ {
+ throw new IllegalStateException(
+ "This query instance has already by used." +
+ " It can only be used to query once.");
+ }
+
+ // Get the raw query results
+ List rawResults = queryAndFilter(parameters);
+ if (rawResults == null)
+ {
+ throw new AlfrescoRuntimeException("Execution returned 'null' results");
+ }
+
+ // Apply sorting
+ if (isApplyPostQuerySorting())
+ {
+ rawResults = applyPostQuerySorting(rawResults, parameters.getSortDetails());
+ }
+
+ // Apply permissions
+ if (isApplyPostQueryPermissions())
+ {
+ // Work out the number of results required
+ int requestedCount = parameters.getResultsRequired();
+ rawResults = applyPostQueryPermissions(rawResults, requestedCount);
+ }
+
+ // Get total count
+ final Pair totalCount = getTotalResultCount(rawResults);
+
+ // Apply paging
+ CannedQueryPageDetails pagingDetails = parameters.getPageDetails();
+ List> pages = Collections.singletonList(rawResults);
+ if (isApplyPostQueryPaging())
+ {
+ pages = applyPostQueryPaging(rawResults, pagingDetails);
+ }
+
+ // Construct results object
+ final List> finalPages = pages;
+
+ // Has more items beyond requested pages ? ... ie. at least one more page (with at least one result)
+ final boolean hasMoreItems = (rawResults.size() > pagingDetails.getResultsRequiredForPaging());
+
+ results = new CannedQueryResults()
+ {
+ @Override
+ public CannedQuery getOriginatingQuery()
+ {
+ return AbstractCannedQuery.this;
+ }
+
+ @Override
+ public String getQueryExecutionId()
+ {
+ return queryExecutionId;
+ }
+
+ @Override
+ public Pair getTotalResultCount()
+ {
+ if (parameters.getTotalResultCountMax() > 0)
+ {
+ return totalCount;
+ }
+ else
+ {
+ throw new IllegalStateException("Total results were not requested in parameters.");
+ }
+ }
+
+ @Override
+ public int getPagedResultCount()
+ {
+ int finalPagedCount = 0;
+ for (List page : finalPages)
+ {
+ finalPagedCount += page.size();
+ }
+ return finalPagedCount;
+ }
+
+ @Override
+ public int getPageCount()
+ {
+ return finalPages.size();
+ }
+
+ @Override
+ public R getSingleResult()
+ {
+ if (finalPages.size() != 1 && finalPages.get(0).size() != 1)
+ {
+ throw new IllegalStateException("There must be exactly one page of one result available.");
+ }
+ return finalPages.get(0).get(0);
+ }
+
+ @Override
+ public List getPage()
+ {
+ if (finalPages.size() != 1)
+ {
+ throw new IllegalStateException("There must be exactly one page of results available.");
+ }
+ return finalPages.get(0);
+ }
+
+ @Override
+ public List> getPages()
+ {
+ return finalPages;
+ }
+
+ @Override
+ public boolean hasMoreItems()
+ {
+ return hasMoreItems;
+ }
+ };
+ return results;
+ }
+
+ /**
+ * Implement the basic query, returning either filtered or all results.
+ *
+ * The implementation may optimally select, filter, sort and apply permissions.
+ * If not, however, the subsequent post-query methods
+ * ({@link #applyPostQuerySorting(List, CannedQuerySortDetails)},
+ * {@link #applyPostQueryPermissions(List, int)} and
+ * {@link #applyPostQueryPaging(List, CannedQueryPageDetails)}) can
+ * be used to trim the results as required.
+ *
+ * @param parameters the full parameters to be used for execution
+ */
+ protected abstract List queryAndFilter(CannedQueryParameters parameters);
+
+ /**
+ * Override to get post-query calls to do sorting.
+ *
+ * @return true to get a post-query call to sort (default false )
+ */
+ protected boolean isApplyPostQuerySorting()
+ {
+ return false;
+ }
+
+ /**
+ * Called before {@link #applyPostQueryPermissions(List, int)} to allow the results to be sorted prior to permission checks.
+ * Note that the query implementation may optimally sort results during retrieval, in which case this method does not need to be implemented.
+ *
+ * @param results the results to sort
+ * @param sortDetails details of the sorting requirements
+ * @return the results according to the new sort order
+ */
+ protected List applyPostQuerySorting(List results, CannedQuerySortDetails sortDetails)
+ {
+ throw new UnsupportedOperationException("Override this method if post-query sorting is required.");
+ }
+
+ /**
+ * Override to get post-query calls to apply permission filters.
+ *
+ * @return true to get a post-query call to apply permissions (default false )
+ */
+ protected boolean isApplyPostQueryPermissions()
+ {
+ return false;
+ }
+
+ /**
+ * Called after the query to filter out results based on permissions.
+ * Note that the query implementation may optimally only select results
+ * based on available privileges, in which case this method does not need to be implemented.
+ *
+ * Permission evaluations should continue until the requested number of results are retrieved
+ * or all available results have been examined.
+ *
+ * @param results the results to apply permissions to
+ * @param requestedCount the minimum number of results to pass the permission checks
+ * in order to fully satisfy the paging requirements
+ * @return the remaining results (as a single "page") after permissions have been applied
+ */
+ protected List applyPostQueryPermissions(List results, int requestedCount)
+ {
+ throw new UnsupportedOperationException("Override this method if post-query filtering is required.");
+ }
+
+ /**
+ * Get the total number of available results after querying, filtering, sorting and permission checking.
+ *
+ * The default implementation assumes that the given results are the final total possible.
+ *
+ * @param results the results after filtering and sorting, but before paging
+ * @return pair representing (a) the total number of results and
+ * (b) the estimated (or actual) number of maximum results
+ * possible for this query.
+ *
+ * @see CannedQueryParameters#getTotalResultCountMax()
+ */
+ protected Pair getTotalResultCount(List results)
+ {
+ Integer size = results.size();
+ return new Pair(size, size);
+ }
+
+ /**
+ * Override to get post-query calls to do pull out paged results.
+ *
+ * @return true to get a post-query call to page (default true )
+ */
+ protected boolean isApplyPostQueryPaging()
+ {
+ return true;
+ }
+
+ /**
+ * Called after the {@link #applyPostQuerySorting(List, CannedQuerySortDetails) sorting phase} to pull out results specific
+ * to the required pages. Note that the query implementation may optimally
+ * create page-specific results, in which case this method does not need to be implemented.
+ *
+ * The base implementation assumes that results are not paged and that the current results
+ * are all the available results i.e. that paging still needs to be applied.
+ *
+ * @param results full results (all or excess pages)
+ * @param pageDetails details of the paging requirements
+ * @return the specific page of results as per the query parameters
+ */
+ protected List> applyPostQueryPaging(List results, CannedQueryPageDetails pageDetails)
+ {
+ int skipResults = pageDetails.getSkipResults();
+ int pageSize = pageDetails.getPageSize();
+ int pageCount = pageDetails.getPageCount();
+ int pageNumber = pageDetails.getPageNumber();
+
+ int availableResults = results.size();
+ int totalResults = pageSize * pageCount;
+ int firstResult = skipResults + ((pageNumber-1) * pageSize); // first of window
+
+ List> pages = new ArrayList>(pageCount);
+
+ // First some shortcuts
+ if (skipResults == 0 && pageSize > availableResults)
+ {
+ return Collections.singletonList(results); // Requesting more results in one page than are available
+ }
+ else if (firstResult > availableResults)
+ {
+ return pages; // Start of first page is after all results
+ }
+
+ // Build results
+ Iterator iterator = results.listIterator(firstResult);
+ int countTotal = 0;
+ List page = new ArrayList(Math.min(results.size(), pageSize)); // Prevent memory blow-out
+ pages.add(page);
+ while (iterator.hasNext() && countTotal < totalResults)
+ {
+ if (page.size() == pageSize)
+ {
+ // Create a page and add it to the results
+ page = new ArrayList(pageSize);
+ pages.add(page);
+ }
+ R next = iterator.next();
+ page.add(next);
+
+ countTotal++;
+ }
+
+ // Done
+ return pages;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java b/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java
new file mode 100644
index 0000000000..86efc92e93
--- /dev/null
+++ b/src/main/java/org/alfresco/query/AbstractCannedQueryFactory.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import org.alfresco.util.GUID;
+import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.registry.NamedObjectRegistry;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Basic services for {@link CannedQueryFactory} implementations.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public abstract class AbstractCannedQueryFactory implements CannedQueryFactory, InitializingBean, BeanNameAware
+{
+ private String name;
+ @SuppressWarnings("rawtypes")
+ private NamedObjectRegistry registry;
+
+ /**
+ * Set the name with which to {@link #setRegistry(NamedObjectRegistry) register}
+ * @param name the name of the bean
+ */
+ public void setBeanName(String name)
+ {
+ this.name = name;
+ }
+
+ /**
+ * Set the registry with which to register
+ */
+ @SuppressWarnings("rawtypes")
+ public void setRegistry(NamedObjectRegistry registry)
+ {
+ this.registry = registry;
+ }
+
+ /**
+ * Registers the instance
+ */
+ public void afterPropertiesSet() throws Exception
+ {
+ PropertyCheck.mandatory(this, "name", name);
+ PropertyCheck.mandatory(this, "registry", registry);
+
+ registry.register(name, this);
+ }
+
+ /**
+ * Helper method to construct a unique query execution ID based on the
+ * instance of the factory and the parameters provided.
+ *
+ * @param parameters the query parameters
+ * @return a unique query instance ID
+ */
+ protected String getQueryExecutionId(CannedQueryParameters parameters)
+ {
+ // Create a GUID
+ String uuid = name + "-" + GUID.generate();
+ return uuid;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CannedQuery getCannedQuery(Object parameterBean, int skipResults, int pageSize, String queryExecutionId)
+ {
+ return getCannedQuery(new CannedQueryParameters(parameterBean, skipResults, pageSize, queryExecutionId));
+ }
+}
diff --git a/src/main/java/org/alfresco/query/CannedQuery.java b/src/main/java/org/alfresco/query/CannedQuery.java
new file mode 100644
index 0000000000..f6b7b55cd5
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQuery.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Interface for named query implementations. These are queries that encapsulate varying
+ * degrees of functionality, but ultimately provide support for paging results.
+ *
+ * Note that each instance of the query is stateful and cannot be reused.
+ *
+ * @param the query result type
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public interface CannedQuery
+{
+ /**
+ * Get the original parameters used to generate the query.
+ *
+ * @return the parameters used to obtain the named query.
+ */
+ CannedQueryParameters getParameters();
+
+ /**
+ * Execute the named query, which was provided to support the
+ * {@link #getParameters() parameters} originally provided.
+ *
+ * Note: This method can only be used once ; to requery, get a new
+ * instance from the {@link CannedQueryFactory factory}.
+ *
+ * @return the query results
+ *
+ * @throws IllegalStateException on second and subsequent calls to this method
+ */
+ CannedQueryResults execute();
+}
diff --git a/src/main/java/org/alfresco/query/CannedQueryException.java b/src/main/java/org/alfresco/query/CannedQueryException.java
new file mode 100644
index 0000000000..1950c169fd
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQueryException.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+
+/**
+ * Exception generated by failures to execute canned queries.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class CannedQueryException extends AlfrescoRuntimeException
+{
+ private static final long serialVersionUID = -4985399145374964458L;
+
+ /**
+ * @param msg the message
+ */
+ public CannedQueryException(String msg)
+ {
+ super(msg);
+ }
+
+ /**
+ * @param msg the message
+ * @param cause the exception cause
+ */
+ public CannedQueryException(String msg, Throwable cause)
+ {
+ super(msg, cause);
+ }
+
+ /**
+ * @param msgId the message id
+ * @param msgParams the message parameters
+ */
+ public CannedQueryException(String msgId, Object[] msgParams)
+ {
+ super(msgId, msgParams);
+ }
+
+ /**
+ * @param msgId the message id
+ * @param msgParams the message parameters
+ * @param cause the exception cause
+ */
+ public CannedQueryException(String msgId, Object[] msgParams, Throwable cause)
+ {
+ super(msgId, msgParams, cause);
+ }
+}
diff --git a/src/main/java/org/alfresco/query/CannedQueryFactory.java b/src/main/java/org/alfresco/query/CannedQueryFactory.java
new file mode 100644
index 0000000000..56e7716026
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQueryFactory.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Interface for factory implementations for producing instances of {@link CannedQuery}
+ * based on all the query parameters.
+ *
+ * @param the query result type
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+public interface CannedQueryFactory
+{
+ /**
+ * Retrieve an instance of a {@link CannedQuery} based on the full range of
+ * available parameters.
+ *
+ * @param parameters the full query parameters
+ * @return an implementation that will execute the query
+ */
+ CannedQuery getCannedQuery(CannedQueryParameters parameters);
+
+ /**
+ * Retrieve an instance of a {@link CannedQuery} based on limited parameters.
+ *
+ * @param parameterBean the values that the query will be based on or null
+ * if not relevant to the query
+ * @param skipResults results to skip before page
+ * @param pageSize the size of page - ie. max items (if skipResults = 0)
+ * @param queryExecutionId ID of a previously-executed query to be used during follow-up
+ * page requests - null if not available
+ * @return an implementation that will execute the query
+ */
+ CannedQuery getCannedQuery(Object parameterBean, int skipResults, int pageSize, String queryExecutionId);
+}
diff --git a/src/main/java/org/alfresco/query/CannedQueryPageDetails.java b/src/main/java/org/alfresco/query/CannedQueryPageDetails.java
new file mode 100644
index 0000000000..56520b3a5e
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQueryPageDetails.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2005-2013 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Details for canned queries supporting paged results.
+ *
+ * Results are {@link #skipResults skipped}, chopped into pages of
+ * {@link #pageSize appropriate size} before the {@link #pageCount start page}
+ * and {@link #pageNumber number} are returned.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class CannedQueryPageDetails
+{
+ public static final int DEFAULT_SKIP_RESULTS = 0;
+ public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
+ public static final int DEFAULT_PAGE_NUMBER = 1;
+ public static final int DEFAULT_PAGE_COUNT = 1;
+
+ private final int skipResults;
+ private final int pageSize;
+ private final int pageNumber;
+ private final int pageCount;
+
+ /**
+ * Construct with defaults
+ *
+ * skipResults: {@link #DEFAULT_SKIP_RESULTS}
+ * pageSize: {@link #DEFAULT_PAGE_SIZE}
+ * pageNumber: {@link #DEFAULT_PAGE_NUMBER}
+ * pageCount: {@link #DEFAULT_PAGE_COUNT}
+ *
+ */
+ public CannedQueryPageDetails()
+ {
+ this(DEFAULT_SKIP_RESULTS, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_COUNT);
+ }
+
+ /**
+ * Construct with defaults
+ *
+ * pageNumber: {@link #DEFAULT_PAGE_NUMBER}
+ * pageCount: {@link #DEFAULT_PAGE_COUNT}
+ *
+ * @param skipResults results to skip before page one
+ * (default {@link #DEFAULT_SKIP_RESULTS} )
+ * @param pageSize the size of each page
+ * (default {@link #DEFAULT_PAGE_SIZE} )
+ */
+ public CannedQueryPageDetails(int skipResults, int pageSize)
+ {
+ this (skipResults, pageSize, DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_COUNT);
+ }
+
+ /**
+ * @param skipResults results to skip before page one
+ * (default {@link #DEFAULT_SKIP_RESULTS} )
+ * @param pageSize the size of each page
+ * (default {@link #DEFAULT_PAGE_SIZE} )
+ * @param pageNumber the first page number to return
+ * (default {@link #DEFAULT_PAGE_NUMBER} )
+ * @param pageCount the number of pages to return
+ * (default {@link #DEFAULT_PAGE_COUNT} )
+ */
+ public CannedQueryPageDetails(int skipResults, int pageSize, int pageNumber, int pageCount)
+ {
+ this.skipResults = skipResults;
+ this.pageSize = pageSize;
+ this.pageNumber = pageNumber;
+ this.pageCount = pageCount;
+
+ // Do some checks
+ if (skipResults < 0)
+ {
+ throw new IllegalArgumentException("Cannot skip fewer than 0 results.");
+ }
+ if (pageSize < 1)
+ {
+ throw new IllegalArgumentException("pageSize must be greater than zero.");
+ }
+ if (pageNumber < 1)
+ {
+ throw new IllegalArgumentException("pageNumber must be greater than zero.");
+ }
+ if (pageCount < 1)
+ {
+ throw new IllegalArgumentException("pageCount must be greater than zero.");
+ }
+ }
+
+ /**
+ * Helper constructor to transform a paging request into the Canned Query form.
+ *
+ * @param pagingRequest the paging details
+ */
+ public CannedQueryPageDetails(PagingRequest pagingRequest)
+ {
+ this(pagingRequest.getSkipCount(), pagingRequest.getMaxItems());
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("NamedQueryPageDetails ")
+ .append("[skipResults=").append(skipResults)
+ .append(", pageSize=").append(pageSize)
+ .append(", pageCount=").append(pageCount)
+ .append(", pageNumber=").append(pageNumber)
+ .append("]");
+ return sb.toString();
+ }
+
+ /**
+ * Get the number of query results to skip before applying further page parameters
+ * @return results to skip before page one
+ */
+ public int getSkipResults()
+ {
+ return skipResults;
+ }
+
+ /**
+ * Get the size of each page
+ * @return the size of each page
+ */
+ public int getPageSize()
+ {
+ return pageSize;
+ }
+
+ /**
+ * Get the first page number to return
+ * @return the first page number to return
+ */
+ public int getPageNumber()
+ {
+ return pageNumber;
+ }
+
+ /**
+ * Get the total number of pages to return
+ * @return the number of pages to return
+ */
+ public int getPageCount()
+ {
+ return pageCount;
+ }
+
+ /**
+ * Calculate the number of results that would be required to satisy this paging request.
+ * Note that the skip size can significantly increase this number even if the page sizes
+ * are small.
+ *
+ * @return the number of results required for proper paging
+ */
+ public int getResultsRequiredForPaging()
+ {
+ int tmp = skipResults + pageCount * pageSize;
+ if(tmp < 0)
+ {
+ // overflow
+ return Integer.MAX_VALUE;
+ }
+ else
+ {
+ return tmp;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/query/CannedQueryParameters.java b/src/main/java/org/alfresco/query/CannedQueryParameters.java
new file mode 100644
index 0000000000..7554ac2862
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQueryParameters.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Parameters defining the {@link CannedQuery named query} to execute.
+ *
+ * The implementations of the underlying queries may be vastly different
+ * depending on seemingly-minor variations in the parameters; only set the
+ * parameters that are required.
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class CannedQueryParameters
+{
+ public static final int DEFAULT_TOTAL_COUNT_MAX = 0; // default 0 => don't request total count
+
+ private final Object parameterBean;
+ private final CannedQueryPageDetails pageDetails;
+ private final CannedQuerySortDetails sortDetails;
+ private final int totalResultCountMax;
+ private final String queryExecutionId;
+
+ /**
+ *
+ * pageDetails : null
+ * sortDetails : null
+ * totalResultCountMax : 0
+ * queryExecutionId : null
+ *
+ *
+ */
+ public CannedQueryParameters(Object parameterBean)
+ {
+ this (parameterBean, null, null, DEFAULT_TOTAL_COUNT_MAX, null);
+ }
+
+ /**
+ * Defaults:
+ *
+ * pageDetails.pageNumber : 1
+ * pageDetails.pageCount : 1
+ * totalResultCountMax : 0
+ *
+ *
+ */
+ public CannedQueryParameters(
+ Object parameterBean,
+ int skipResults,
+ int pageSize,
+ String queryExecutionId)
+ {
+ this (
+ parameterBean,
+ new CannedQueryPageDetails(skipResults, pageSize, CannedQueryPageDetails.DEFAULT_PAGE_NUMBER, CannedQueryPageDetails.DEFAULT_PAGE_COUNT),
+ null,
+ DEFAULT_TOTAL_COUNT_MAX,
+ queryExecutionId);
+ }
+
+ /**
+ * Defaults:
+ *
+ * totalResultCountMax : 0
+ * queryExecutionId : null
+ *
+ *
+ */
+ public CannedQueryParameters(
+ Object parameterBean,
+ CannedQueryPageDetails pageDetails,
+ CannedQuerySortDetails sortDetails)
+ {
+ this (parameterBean, pageDetails, sortDetails, DEFAULT_TOTAL_COUNT_MAX, null);
+ }
+
+ /**
+ * Construct all the parameters for executing a named query, using values from the
+ * {@link PagingRequest}.
+ *
+ * @param parameterBean the values that the query will be based on or null
+ * if not relevant to the query
+ * @param sortDetails the type of sorting to be applied or null for none
+ * @param pagingRequest the type of paging to be applied or null for none
+ */
+ public CannedQueryParameters(
+ Object parameterBean,
+ CannedQuerySortDetails sortDetails,
+ PagingRequest pagingRequest)
+ {
+ this (
+ parameterBean,
+ pagingRequest == null ? null : new CannedQueryPageDetails(pagingRequest),
+ sortDetails,
+ pagingRequest == null ? 0 : pagingRequest.getRequestTotalCountMax(),
+ pagingRequest == null ? null : pagingRequest.getQueryExecutionId());
+ }
+
+ /**
+ * Construct all the parameters for executing a named query. Note that the allowable values
+ * for the arguments depend on the specific query being executed.
+ *
+ * @param parameterBean the values that the query will be based on or null
+ * if not relevant to the query
+ * @param pageDetails the type of paging to be applied or null for none
+ * @param sortDetails the type of sorting to be applied or null for none
+ * @param totalResultCountMax greater than zero if the query should not only return the required rows
+ * but should also return the total number of possible rows up to
+ * the given maximum.
+ * @param queryExecutionId ID of a previously-executed query to be used during follow-up
+ * page requests - null if not available
+ */
+ @SuppressWarnings("unchecked")
+ public CannedQueryParameters(
+ Object parameterBean,
+ CannedQueryPageDetails pageDetails,
+ CannedQuerySortDetails sortDetails,
+ int totalResultCountMax,
+ String queryExecutionId)
+ {
+ if (totalResultCountMax < 0)
+ {
+ throw new IllegalArgumentException("totalResultCountMax cannot be negative.");
+ }
+
+ this.parameterBean = parameterBean;
+ this.pageDetails = pageDetails == null ? new CannedQueryPageDetails() : pageDetails;
+ this.sortDetails = sortDetails == null ? new CannedQuerySortDetails() : sortDetails;
+ this.totalResultCountMax = totalResultCountMax;
+ this.queryExecutionId = queryExecutionId;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("NamedQueryParameters ")
+ .append("[parameterBean=").append(parameterBean)
+ .append(", pageDetails=").append(pageDetails)
+ .append(", sortDetails=").append(sortDetails)
+ .append(", requestTotalResultCountMax=").append(totalResultCountMax)
+ .append(", queryExecutionId=").append(queryExecutionId)
+ .append("]");
+ return sb.toString();
+ }
+
+ public String getQueryExecutionId()
+ {
+ return queryExecutionId;
+ }
+
+ /**
+ * @return the sort details (never null )
+ */
+ public CannedQuerySortDetails getSortDetails()
+ {
+ return sortDetails;
+ }
+
+ /**
+ * @return the query paging details (never null )
+ */
+ public CannedQueryPageDetails getPageDetails()
+ {
+ return pageDetails;
+ }
+
+ /**
+ * @return if > 0 then the query should not only return the required rows but should
+ * also return the total count (number of possible rows) up to the given max
+ * if 0 then query does not need to return the total count
+ */
+ public int getTotalResultCountMax()
+ {
+ return totalResultCountMax;
+ }
+
+ /**
+ * Helper method to get the total number of query results that need to be obtained in order
+ * to satisfy the {@link #getPageDetails() paging requirements}, the
+ * maximum result count ... and an extra to provide
+ * 'hasMore' functionality.
+ *
+ * @return the minimum number of results required before pages can be created
+ */
+ public int getResultsRequired()
+ {
+ int resultsForPaging = pageDetails.getResultsRequiredForPaging();
+ if (resultsForPaging < Integer.MAX_VALUE) // Add one for 'hasMore'
+ {
+ resultsForPaging++;
+ }
+ int maxRequired = Math.max(totalResultCountMax, resultsForPaging);
+ return maxRequired;
+ }
+
+ /**
+ * @return parameterBean the values that the query will be based on or null
+ * if not relevant to the query
+ */
+ public Object getParameterBean()
+ {
+ return parameterBean;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/CannedQueryResults.java b/src/main/java/org/alfresco/query/CannedQueryResults.java
new file mode 100644
index 0000000000..1dda96309e
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQueryResults.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.List;
+
+/**
+ * Interface for results returned by {@link CannedQuery canned queries}.
+ *
+ * @author Derek Hulley, janv
+ * @since 4.0
+ */
+public interface CannedQueryResults extends PagingResults
+{
+ /**
+ * Get the instance of the query that generated these results.
+ *
+ * @return the query that generated these results.
+ */
+ CannedQuery getOriginatingQuery();
+
+ /**
+ * Get the total number of results available within the pages of this result.
+ * The count excludes results chopped out by the paging process i.e. it is only
+ * the count of results physically obtainable through this instance.
+ *
+ * @return number of results available in the pages
+ */
+ int getPagedResultCount();
+
+ /**
+ * Get the number of pages available
+ *
+ * @return the number of pages available
+ */
+ int getPageCount();
+
+ /**
+ * Get a single result if there is only one result expected.
+ *
+ * @return a single result
+ * @throws IllegalStateException if the query returned more than one result
+ */
+ R getSingleResult();
+
+ /**
+ * Get the paged results
+ *
+ * @return a list of paged results
+ */
+ List> getPages();
+}
diff --git a/src/main/java/org/alfresco/query/CannedQuerySortDetails.java b/src/main/java/org/alfresco/query/CannedQuerySortDetails.java
new file mode 100644
index 0000000000..134c4a176f
--- /dev/null
+++ b/src/main/java/org/alfresco/query/CannedQuerySortDetails.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.util.Pair;
+
+/**
+ * Details for canned queries supporting sorted results
+ *
+ * @author Derek Hulley
+ * @since 4.0
+ */
+public class CannedQuerySortDetails
+{
+ /**
+ * Sort ordering for the sort pairs.
+ * @author Derek Hulley
+ * @since 4.0
+ */
+ public static enum SortOrder
+ {
+ ASCENDING,
+ DESCENDING
+ }
+
+ private final List> sortPairs;
+
+ /**
+ * Construct the sort details with a variable number of sort pairs.
+ *
+ * Sorting is done by:
+ * key: the key type to sort on
+ * sortOrder: the ordering of values associated with the key
+ *
+ * @param sortPairs the sort pairs, which will be applied in order
+ */
+ public CannedQuerySortDetails(Pair extends Object, SortOrder> ... sortPairs)
+ {
+ this.sortPairs = Collections.unmodifiableList(Arrays.asList(sortPairs));
+ }
+
+ /**
+ * Construct the sort details from a list of sort pairs.
+ *
+ * Sorting is done by:
+ * key: the key type to sort on
+ * sortOrder: the ordering of values associated with the key
+ *
+ * @param sortPairs the sort pairs, which will be applied in order
+ */
+ public CannedQuerySortDetails(List> sortPairs)
+ {
+ this.sortPairs = Collections.unmodifiableList(sortPairs);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CannedQuerySortDetails [sortPairs=" + sortPairs + "]";
+ }
+
+ /**
+ * Get the sort definitions. The instance will become unmodifiable after this has been called.
+ */
+ public List> getSortPairs()
+ {
+ return Collections.unmodifiableList(sortPairs);
+ }
+}
diff --git a/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java b/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java
new file mode 100644
index 0000000000..547b419a7f
--- /dev/null
+++ b/src/main/java/org/alfresco/query/EmptyCannedQueryResults.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.util.Pair;
+
+/**
+ * An always empty {@link CannedQueryResults}, used when you know
+ * you can short circuit a query when no results are found.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class EmptyCannedQueryResults extends EmptyPagingResults implements CannedQueryResults
+{
+ private CannedQuery query;
+
+ public EmptyCannedQueryResults(CannedQuery query)
+ {
+ this.query = query;
+ }
+
+ @Override
+ public CannedQuery getOriginatingQuery() {
+ return query;
+ }
+
+ @Override
+ public int getPageCount() {
+ return 0;
+ }
+
+ @Override
+ public int getPagedResultCount() {
+ return 0;
+ }
+
+ @Override
+ public List> getPages() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public R getSingleResult() {
+ return null;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/EmptyPagingResults.java b/src/main/java/org/alfresco/query/EmptyPagingResults.java
new file mode 100644
index 0000000000..995605adf5
--- /dev/null
+++ b/src/main/java/org/alfresco/query/EmptyPagingResults.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.util.Pair;
+
+/**
+ * An always empty {@link PagingResults}, used when you know
+ * you can short circuit a query when no results are found.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class EmptyPagingResults implements PagingResults
+{
+ /**
+ * Returns an empty page
+ */
+ public List getPage()
+ {
+ return Collections.emptyList();
+ }
+
+ /**
+ * No more items remain
+ */
+ public boolean hasMoreItems()
+ {
+ return false;
+ }
+
+ /**
+ * There are no results
+ */
+ public Pair getTotalResultCount()
+ {
+ return new Pair(0,0);
+ }
+
+ /**
+ * There is no unique query ID, as no query was done
+ */
+ public String getQueryExecutionId()
+ {
+ return null;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/ListBackedPagingResults.java b/src/main/java/org/alfresco/query/ListBackedPagingResults.java
new file mode 100644
index 0000000000..a2833cc67d
--- /dev/null
+++ b/src/main/java/org/alfresco/query/ListBackedPagingResults.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.util.Pair;
+
+/**
+ * Wraps a list of items as a {@link PagingResults}, used typically when
+ * migrating from a full listing system to a paged one.
+ *
+ * @author Nick Burch
+ * @since Odin
+ */
+public class ListBackedPagingResults implements PagingResults
+{
+ private List results;
+ private int size;
+ private boolean hasMore;
+
+ public ListBackedPagingResults(List list)
+ {
+ this.results = Collections.unmodifiableList(list);
+
+ // No more items remain, the page is everything
+ size = list.size();
+ hasMore = false;
+ }
+ public ListBackedPagingResults(List list, PagingRequest paging)
+ {
+ // Excerpt
+ int start = paging.getSkipCount();
+ int end = Math.min(list.size(), start + paging.getMaxItems());
+ if (paging.getMaxItems() == 0)
+ {
+ end = list.size();
+ }
+
+ this.results = Collections.unmodifiableList(
+ list.subList(start, end));
+ this.size = list.size();
+ this.hasMore = ! (list.size() == end);
+ }
+
+ /**
+ * Returns the whole set of results as one page
+ */
+ public List getPage()
+ {
+ return results;
+ }
+
+ public boolean hasMoreItems()
+ {
+ return hasMore;
+ }
+
+ /**
+ * We know exactly how many results there are
+ */
+ public Pair getTotalResultCount()
+ {
+ return new Pair(size, size);
+ }
+
+ /**
+ * There is no unique query ID, as no query was done
+ */
+ public String getQueryExecutionId()
+ {
+ return null;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/PageDetails.java b/src/main/java/org/alfresco/query/PageDetails.java
new file mode 100644
index 0000000000..c26fec0634
--- /dev/null
+++ b/src/main/java/org/alfresco/query/PageDetails.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2005-2012 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Stores paging details based on a PagingRequest.
+ *
+ * @author steveglover
+ *
+ */
+public class PageDetails
+{
+ private boolean hasMoreItems = false;
+ private int pageSize;
+ private int skipCount;
+ private int maxItems;
+ private int end;
+
+ public PageDetails(int pageSize, boolean hasMoreItems, int skipCount, int maxItems, int end)
+ {
+ super();
+ this.hasMoreItems = hasMoreItems;
+ this.pageSize = pageSize;
+ this.skipCount = skipCount;
+ this.maxItems = maxItems;
+ this.end = end;
+ }
+
+ public int getSkipCount()
+ {
+ return skipCount;
+ }
+
+ public int getMaxItems()
+ {
+ return maxItems;
+ }
+
+ public int getEnd()
+ {
+ return end;
+ }
+
+ public boolean hasMoreItems()
+ {
+ return hasMoreItems;
+ }
+
+ public int getPageSize()
+ {
+ return pageSize;
+ }
+
+ public static PageDetails getPageDetails(PagingRequest pagingRequest, int totalSize)
+ {
+ int skipCount = pagingRequest.getSkipCount();
+ int maxItems = pagingRequest.getMaxItems();
+ int end = skipCount + maxItems;
+ int pageSize = -1;
+ if(end < 0 || end > totalSize)
+ {
+ // overflow or greater than the total
+ end = totalSize;
+ pageSize = end - skipCount;
+ }
+ else
+ {
+ pageSize = maxItems;
+ }
+ if(pageSize < 0)
+ {
+ pageSize = 0;
+ }
+ boolean hasMoreItems = end < totalSize;
+ return new PageDetails(pageSize, hasMoreItems, skipCount, maxItems, end);
+ }
+}
diff --git a/src/main/java/org/alfresco/query/PagingRequest.java b/src/main/java/org/alfresco/query/PagingRequest.java
new file mode 100644
index 0000000000..a35639e45f
--- /dev/null
+++ b/src/main/java/org/alfresco/query/PagingRequest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2005-2013 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import org.alfresco.api.AlfrescoPublicApi;
+
+/**
+ * Simple wrapper for single page request (with optional request for total count up to a given max)
+ *
+ * @author janv
+ * @since 4.0
+ */
+@AlfrescoPublicApi
+public class PagingRequest
+{
+ private int skipCount = CannedQueryPageDetails.DEFAULT_SKIP_RESULTS;
+ private int maxItems;
+
+ private int requestTotalCountMax = 0; // request total count up to a given max (0 => do not request total count)
+ private String queryExecutionId;
+
+ /**
+ * Construct a page request
+ *
+ * @param maxItems the maximum number of items per page
+ */
+ public PagingRequest(int maxItems)
+ {
+ this.maxItems = maxItems;
+ }
+
+ /**
+ * Construct a page request
+ *
+ * @param maxItems the maximum number of items per page
+ * @param skipCount the number of items to skip before the first page
+ */
+ public PagingRequest(int skipCount, int maxItems)
+ {
+ this.skipCount = skipCount;
+ this.maxItems = maxItems;
+ }
+
+ /**
+ * Construct a page request
+ *
+ * @param maxItems the maximum number of items per page
+ * @param queryExecutionId a query execution ID associated with ealier paged requests
+ */
+ public PagingRequest(int maxItems, String queryExecutionId)
+ {
+ setMaxItems(maxItems);
+ this.queryExecutionId = queryExecutionId;
+ }
+
+ /**
+ * Construct a page request
+ *
+ * @param skipCount the number of items to skip before the first page
+ * @param maxItems the maximum number of items per page
+ * @param queryExecutionId a query execution ID associated with ealier paged requests
+ */
+ public PagingRequest(int skipCount, int maxItems, String queryExecutionId)
+ {
+ setSkipCount(skipCount);
+ setMaxItems(maxItems);
+ this.queryExecutionId = queryExecutionId;
+ }
+
+ /**
+ * Results to skip before retrieving the page. Usually a multiple of page size (ie. page size * num pages to skip).
+ * Default is 0.
+ *
+ * @return the number of results to skip before the page
+ */
+ public int getSkipCount()
+ {
+ return skipCount;
+ }
+
+ /**
+ * Change the skip count. Must be called before the paging query is run.
+ */
+ protected void setSkipCount(int skipCount)
+ {
+ this.skipCount = (skipCount < 0 ? CannedQueryPageDetails.DEFAULT_SKIP_RESULTS : skipCount);
+ }
+
+ /**
+ * Size of the page - if skip count is 0 then return up to max items.
+ *
+ * @return the maximum size of the page
+ */
+ public int getMaxItems()
+ {
+ return maxItems;
+ }
+
+ /**
+ * Change the size of the page. Must be called before the paging query is run.
+ */
+ protected void setMaxItems(int maxItems)
+ {
+ this.maxItems = (maxItems < 0 ? CannedQueryPageDetails.DEFAULT_PAGE_SIZE : maxItems);
+ }
+
+ /**
+ * Get requested total count (up to a given maximum).
+ */
+ public int getRequestTotalCountMax()
+ {
+ return requestTotalCountMax;
+ }
+
+ /**
+ * Set request total count (up to a given maximum). Default is 0 => do not request total count (which allows possible query optimisation).
+ *
+ * @param requestTotalCountMax
+ */
+ public void setRequestTotalCountMax(int requestTotalCountMax)
+ {
+ this.requestTotalCountMax = requestTotalCountMax;
+ }
+
+ /**
+ * Get a unique ID associated with these query results. This must be available before and
+ * after execution i.e. it must depend on the type of query and the query parameters
+ * rather than the execution results. Client has the option to pass this back as a hint when
+ * paging.
+ *
+ * @return a unique ID associated with the query execution results
+ */
+ public String getQueryExecutionId()
+ {
+ return queryExecutionId;
+ }
+
+ /**
+ * Change the unique query ID for the results. Must be called before the paging query is run.
+ */
+ protected void setQueryExecutionId(String queryExecutionId)
+ {
+ this.queryExecutionId = queryExecutionId;
+ }
+}
diff --git a/src/main/java/org/alfresco/query/PagingResults.java b/src/main/java/org/alfresco/query/PagingResults.java
new file mode 100644
index 0000000000..8cfb2e87b8
--- /dev/null
+++ b/src/main/java/org/alfresco/query/PagingResults.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+import java.util.List;
+
+import org.alfresco.api.AlfrescoPublicApi;
+import org.alfresco.util.Pair;
+
+/**
+ * Marker interface for single page of results
+ *
+ * @author janv
+ * @since 4.0
+ */
+@AlfrescoPublicApi
+public interface PagingResults
+{
+ /**
+ * Get the page of results.
+ *
+ * @return the results - possibly empty but never null
+ */
+ public List getPage();
+
+ /**
+ * True if more items on next page.
+ *
+ * Note: could also return true if page was cutoff/trimmed for some reason
+ * (eg. due to permission checks of large page of requested max items)
+ *
+ * @return true if more items (eg. on next page)
+ * - true => at least one more page (or incomplete page - if cutoff)
+ * - false => last page (or incomplete page - if cutoff)
+ */
+ public boolean hasMoreItems();
+
+ /**
+ * Get the total result count assuming no paging applied. This value will only be available if
+ * the query supports it and the client requested it. By default, it is not requested.
+ *
+ * Returns result as an approx "range" pair
+ *
+ * null (or lower is null): unknown total count (or not requested by the client).
+ * lower = upper : total count should be accurate
+ * lower < upper : total count is an approximation ("about") - somewhere in the given range (inclusive)
+ * upper is null : total count is "more than" lower (upper is unknown)
+ *
+ *
+ * @return Returns the total results as a range (all results, including the paged results returned)
+ */
+ public Pair getTotalResultCount();
+
+ /**
+ * Get a unique ID associated with these query results. This must be available before and
+ * after execution i.e. it must depend on the type of query and the query parameters
+ * rather than the execution results. Client has the option to pass this back as a hint when
+ * paging.
+ *
+ * @return a unique ID associated with the query execution results
+ */
+ public String getQueryExecutionId();
+}
diff --git a/src/main/java/org/alfresco/query/PermissionedResults.java b/src/main/java/org/alfresco/query/PermissionedResults.java
new file mode 100644
index 0000000000..b31613890c
--- /dev/null
+++ b/src/main/java/org/alfresco/query/PermissionedResults.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2005-2011 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.query;
+
+/**
+ * Marker interface to show that permissions have already been applied to the results (and possibly cutoff)
+ *
+ * @author janv
+ * @since 4.0
+ */
+public interface PermissionedResults
+{
+ /**
+ * @return true - if permissions have been applied to the results
+ */
+ public boolean permissionsApplied();
+
+ /**
+ * @return true - if permission checks caused results to be cutoff (either due to max count or max time)
+ */
+ public boolean hasMoreItems();
+}
diff --git a/src/main/java/org/alfresco/scripts/ScriptException.java b/src/main/java/org/alfresco/scripts/ScriptException.java
new file mode 100644
index 0000000000..6fb5b6ac04
--- /dev/null
+++ b/src/main/java/org/alfresco/scripts/ScriptException.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.scripts;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+
+/**
+ * @author Kevin Roast
+ */
+public class ScriptException extends AlfrescoRuntimeException
+{
+ private static final long serialVersionUID = 1739480648583299623L;
+
+ /**
+ * @param msgId String
+ */
+ public ScriptException(String msgId)
+ {
+ super(msgId);
+ }
+
+ /**
+ * @param msgId String
+ * @param cause Throwable
+ */
+ public ScriptException(String msgId, Throwable cause)
+ {
+ super(msgId, cause);
+ }
+
+ /**
+ * @param msgId String
+ * @param params Object[]
+ */
+ public ScriptException(String msgId, Object[] params)
+ {
+ super(msgId, params);
+ }
+
+ /**
+ * @param msgId String
+ * @param msgParams Object[]
+ * @param cause Throwable
+ */
+ public ScriptException(String msgId, Object[] msgParams, Throwable cause)
+ {
+ super(msgId, msgParams, cause);
+ }
+}
diff --git a/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java b/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java
new file mode 100644
index 0000000000..06be2ef1f0
--- /dev/null
+++ b/src/main/java/org/alfresco/scripts/ScriptResourceHelper.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.scripts;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * @author Kevin Roast
+ */
+public class ScriptResourceHelper
+{
+ private static final String SCRIPT_ROOT = "_root";
+ private static final String IMPORT_PREFIX = "
+ * Multiple includes of the same resource are dealt with correctly and nested includes of scripts
+ * is fully supported.
+ *
+ * Note that for performance reasons the script import directive syntax and placement in the file
+ * is very strict. The import lines must always be first in the file - even before any comments.
+ * Immediately that the script service detects a non-import line it will assume the rest of the
+ * file is executable script and no longer attempt to search for any further import directives. Therefore
+ * all imports should be at the top of the script, one following the other, in the correct syntax and
+ * with no comments present - the only separators valid between import directives is white space.
+ *
+ * @param script The script content to resolve imports in
+ *
+ * @return a valid script with all nested includes resolved into a single script instance
+ */
+ public static String resolveScriptImports(String script, ScriptResourceLoader loader, Log logger)
+ {
+ // use a linked hashmap to preserve order of includes - the key in the collection is used
+ // to resolve multiple includes of the same scripts and therefore cyclic includes also
+ Map scriptlets = new LinkedHashMap(8, 1.0f);
+
+ // perform a recursive resolve of all script imports
+ recurseScriptImports(SCRIPT_ROOT, script, loader, scriptlets, logger);
+
+ if (scriptlets.size() == 1)
+ {
+ // quick exit for single script with no includes
+ if (logger.isTraceEnabled())
+ logger.trace("Script content resolved to:\r\n" + script);
+
+ return script;
+ }
+ else
+ {
+ // calculate total size of buffer required for the script and all includes
+ int length = 0;
+ for (String scriptlet : scriptlets.values())
+ {
+ length += scriptlet.length();
+ }
+ // append the scripts together to make a single script
+ StringBuilder result = new StringBuilder(length);
+ for (String scriptlet : scriptlets.values())
+ {
+ result.append(scriptlet);
+ }
+
+ if (logger.isTraceEnabled())
+ logger.trace("Script content resolved to:\r\n" + result.toString());
+
+ return result.toString();
+ }
+ }
+
+ /**
+ * Recursively resolve imports in the specified scripts, adding the imports to the
+ * specific list of scriplets to combine later.
+ *
+ * @param location Script location - used to ensure duplicates are not added
+ * @param script The script to recursively resolve imports for
+ * @param scripts The collection of scriplets to execute with imports resolved and removed
+ */
+ private static void recurseScriptImports(
+ String location, String script, ScriptResourceLoader loader, Map scripts, Log logger)
+ {
+ int index = 0;
+ // skip any initial whitespace
+ for (; index')
+ {
+ // found end of import line - so we have a resource path
+ String resource = script.substring(resourceStart, index);
+
+ if (logger.isDebugEnabled())
+ logger.debug("Found script resource import: " + resource);
+
+ if (scripts.containsKey(resource) == false)
+ {
+ // load the script resource (and parse any recursive includes...)
+ String includedScript = loader.loadScriptResource(resource);
+ if (includedScript != null)
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Succesfully located script '" + resource + "'");
+ recurseScriptImports(resource, includedScript, loader, scripts, logger);
+ }
+ }
+ else
+ {
+ if (logger.isDebugEnabled())
+ logger.debug("Note: already imported resource: " + resource);
+ }
+
+ // continue scanning this script for additional includes
+ // skip the last two characters of the import directive
+ for (index += 2; index");
+ }
+ else
+ {
+ throw new ScriptException(
+ "Malformed 'import' line - must be first in file, no comments and strictly of the form:" +
+ "\r\n");
+ }
+ }
+ else
+ {
+ // no (further) includes found - include the original script content
+ if (logger.isDebugEnabled())
+ logger.debug("Imports resolved, adding resource '" + location);
+ if (logger.isTraceEnabled())
+ logger.trace(script);
+ scripts.put(location, script);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java b/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java
new file mode 100644
index 0000000000..dfa4683794
--- /dev/null
+++ b/src/main/java/org/alfresco/scripts/ScriptResourceLoader.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.scripts;
+
+/**
+ * @author Kevin Roast
+ */
+public interface ScriptResourceLoader
+{
+ public String loadScriptResource(String resource);
+}
diff --git a/src/main/java/org/alfresco/util/AbstractTriggerBean.java b/src/main/java/org/alfresco/util/AbstractTriggerBean.java
new file mode 100644
index 0000000000..079374693e
--- /dev/null
+++ b/src/main/java/org/alfresco/util/AbstractTriggerBean.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.util;
+
+import org.alfresco.api.AlfrescoPublicApi;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.util.bean.BooleanBean;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.JobDetail;
+import org.quartz.Scheduler;
+import org.quartz.Trigger;
+import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.scheduling.quartz.JobDetailAwareTrigger;
+
+/**
+ * A utility bean to wrap sceduling a job with a scheduler.
+ *
+ * @author Andy Hind
+ */
+@AlfrescoPublicApi
+public abstract class AbstractTriggerBean implements InitializingBean, JobDetailAwareTrigger, BeanNameAware, DisposableBean
+{
+
+ protected static Log logger = LogFactory.getLog(AbstractTriggerBean.class);
+
+ private JobDetail jobDetail;
+
+ private Scheduler scheduler;
+
+ private String beanName;
+
+ private Trigger trigger;
+
+ private boolean enabled = true;
+
+ public AbstractTriggerBean()
+ {
+ super();
+ }
+
+ /**
+ * Get the definition of the job to run.
+ */
+ public JobDetail getJobDetail()
+ {
+ return jobDetail;
+ }
+
+ /**
+ * Set the definition of the job to run.
+ *
+ * @param jobDetail
+ */
+ public void setJobDetail(JobDetail jobDetail)
+ {
+ this.jobDetail = jobDetail;
+ }
+
+ /**
+ * Get the scheduler with which the job and trigger are scheduled.
+ *
+ * @return The scheduler
+ */
+ public Scheduler getScheduler()
+ {
+ return scheduler;
+ }
+
+ /**
+ * Set the scheduler.
+ *
+ * @param scheduler
+ */
+ public void setScheduler(Scheduler scheduler)
+ {
+ this.scheduler = scheduler;
+ }
+
+ /**
+ * Set the scheduler
+ */
+ public void afterPropertiesSet() throws Exception
+ {
+ // Check properties are set
+ if (jobDetail == null)
+ {
+ throw new AlfrescoRuntimeException("Job detail has not been set");
+ }
+ if (scheduler == null)
+ {
+ logger.warn("Job " + getBeanName() + " is not active");
+ }
+ else if (!enabled)
+ {
+ logger.warn("Job " + getBeanName() + " is not enabled");
+ }
+ else
+ {
+ logger.info("Job " + getBeanName() + " is active and enabled");
+ // Register the job with the scheduler
+ this.trigger = getTrigger();
+ if (this.trigger == null)
+ {
+ logger.error("Job " + getBeanName() + " is not active (invalid trigger)");
+ }
+ else
+ {
+ logger.info("Job " + getBeanName() + " is active");
+
+ String jobName = jobDetail.getKey().getName();
+ String groupName = jobDetail.getKey().getGroup();
+
+ if(scheduler.getJobDetail(jobName, groupName) != null)
+ {
+ // Job is already defined delete it
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("job already registered with scheduler jobName:" + jobName);
+ }
+ scheduler.deleteJob(jobName, groupName);
+ }
+
+ if(logger.isDebugEnabled())
+ {
+ logger.debug("schedule job:" + jobDetail + " using " + this.trigger
+ + " startTime: " + this.trigger.getStartTime());
+ }
+ scheduler.scheduleJob(jobDetail, this.trigger);
+ }
+ }
+ }
+
+ /**
+ * Ensures that the job is unscheduled with the context is shut down.
+ */
+ public void destroy() throws Exception
+ {
+ if (this.trigger != null)
+ {
+ if (!this.scheduler.isShutdown())
+ {
+ scheduler.unscheduleJob(this.trigger.getName(), this.trigger.getGroup());
+ }
+ this.trigger = null;
+ }
+ }
+
+ /**
+ * Abstract method for implementations to build their trigger.
+ *
+ * @return The trigger
+ * @throws Exception
+ */
+ public abstract Trigger getTrigger() throws Exception;
+
+ /**
+ * Get the bean name as this trigger is created
+ */
+ public void setBeanName(String name)
+ {
+ this.beanName = name;
+ }
+
+ /**
+ * Get the bean/trigger name.
+ *
+ * @return The name of the bean
+ */
+ public String getBeanName()
+ {
+ return beanName;
+ }
+
+
+ public boolean isEnabled()
+ {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ this.enabled = enabled;
+ }
+
+ public void setEnabledFromBean(BooleanBean enabled)
+ {
+ this.enabled = enabled.isTrue();
+ }
+}
diff --git a/src/main/java/org/alfresco/util/ArgumentHelper.java b/src/main/java/org/alfresco/util/ArgumentHelper.java
new file mode 100644
index 0000000000..fff0df53ec
--- /dev/null
+++ b/src/main/java/org/alfresco/util/ArgumentHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class to assist in extracting program arguments.
+ *
+ * @author Derek Hulley
+ * @since V2.1-A
+ */
+public class ArgumentHelper
+{
+ private String usage;
+ private Map args;
+
+ public static Map ripArgs(String ... args)
+ {
+ Map argsMap = new HashMap(5);
+ for (String arg : args)
+ {
+ int index = arg.indexOf('=');
+ if (!arg.startsWith("--") || index < 0 || index == arg.length() - 1)
+ {
+ // Ignore it
+ continue;
+ }
+ String name = arg.substring(2, index);
+ String value = arg.substring(index + 1, arg.length());
+ argsMap.put(name, value);
+ }
+ return argsMap;
+ }
+
+ public ArgumentHelper(String usage, String[] args)
+ {
+ this.usage = usage;
+ this.args = ArgumentHelper.ripArgs(args);
+ }
+
+ /**
+ * @throws IllegalArgumentException if the argument doesn't match the requirements.
+ */
+ public String getStringValue(String arg, boolean mandatory, boolean nonEmpty)
+ {
+ String value = args.get(arg);
+ if (value == null && mandatory)
+ {
+ throw new IllegalArgumentException("Argument '" + arg + "' is required.");
+ }
+ else if (value != null && value.length() == 0 && nonEmpty)
+ {
+ throw new IllegalArgumentException("Argument '" + arg + "' may not be empty.");
+ }
+ return value;
+ }
+
+ /**
+ * @return Returns the value assigned or the minimum value if the parameter was not present
+ * @throws IllegalArgumentException if the argument doesn't match the requirements.
+ */
+ public int getIntegerValue(String arg, boolean mandatory, int minValue, int maxValue)
+ {
+ String valueStr = args.get(arg);
+ if (valueStr == null)
+ {
+ if (mandatory)
+ {
+ throw new IllegalArgumentException("Argument '" + arg + "' is required.");
+ }
+ else
+ {
+ return minValue;
+ }
+ }
+ // Now convert
+ try
+ {
+ int value = Integer.parseInt(valueStr);
+ if (value < minValue || value > maxValue)
+ {
+ throw new IllegalArgumentException("Argument '" + arg + "' must be in range " + minValue + " to " + maxValue + ".");
+ }
+ return value;
+ }
+ catch (NumberFormatException e)
+ {
+ throw new IllegalArgumentException("Argument '" + arg + "' must be a valid integer.");
+ }
+ }
+
+ public void printUsage()
+ {
+ System.out.println(usage);
+ }
+}
diff --git a/src/main/java/org/alfresco/util/BridgeTable.java b/src/main/java/org/alfresco/util/BridgeTable.java
new file mode 100644
index 0000000000..04b582293f
--- /dev/null
+++ b/src/main/java/org/alfresco/util/BridgeTable.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2005-2012 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.util;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Generic bridge table support with optional reference counting to allow multiple membership for an object via several
+ * relationships.
+ *
+ * @author Andy
+ */
+public class BridgeTable
+{
+ HashMap>> descendants = new HashMap>>();
+
+ HashMap>> ancestors = new HashMap>>();
+
+ ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
+
+ public void addLink(T parent, T child)
+ {
+ readWriteLock.writeLock().lock();
+ try
+ {
+ addDescendants(parent, child);
+ addAncestors(parent, child);
+ }
+ finally
+ {
+ readWriteLock.writeLock().unlock();
+ }
+
+ }
+
+ public void addLink(Pair link)
+ {
+ addLink(link.getFirst(), link.getSecond());
+ }
+
+ public void addLinks(Collection> links)
+ {
+ for (Pair link : links)
+ {
+ addLink(link);
+ }
+ }
+
+ public void removeLink(T parent, T child)
+ {
+ readWriteLock.writeLock().lock();
+ try
+ {
+ removeDescendants(parent, child);
+ removeAncestors(parent, child);
+ }
+ finally
+ {
+ readWriteLock.writeLock().unlock();
+ }
+
+ }
+
+ public void removeLink(Pair link)
+ {
+ removeLink(link.getFirst(), link.getSecond());
+ }
+
+ public void removeLinks(Collection> links)
+ {
+ for (Pair link : links)
+ {
+ removeLink(link);
+ }
+ }
+
+ public HashSet getDescendants(T node)
+ {
+ return getDescendants(node, 1, Integer.MAX_VALUE);
+ }
+
+ public HashSet getDescendants(T node, int position)
+ {
+ return getDescendants(node, position, position);
+ }
+
+ public HashSet getDescendants(T node, int start, int end)
+ {
+ HashSet answer = new HashSet();
+ HashMap> found = descendants.get(node);
+ if (found != null)
+ {
+ for (Integer key : found.keySet())
+ {
+ if ((key.intValue() >= start) && (key.intValue() <= end))
+ {
+ HashMap asd = found.get(key);
+ answer.addAll(asd.keySet());
+ }
+ }
+ }
+ return answer;
+ }
+
+ public HashSet getAncestors(T node)
+ {
+ return getAncestors(node, 1, Integer.MAX_VALUE);
+ }
+
+ public HashSet getAncestors(T node, int position)
+ {
+ return getAncestors(node, position, position);
+ }
+
+ public HashSet getAncestors(T node, int start, int end)
+ {
+ HashSet answer = new HashSet();
+ HashMap> found = ancestors.get(node);
+ if (found != null)
+ {
+ for (Integer key : found.keySet())
+ {
+ if ((key.intValue() >= start) && (key.intValue() <= end))
+ {
+ HashMap asd = found.get(key);
+ answer.addAll(asd.keySet());
+ }
+ }
+ }
+ return answer;
+ }
+
+ /**
+ * @param parent T
+ * @param child T
+ */
+ private void addDescendants(T parent, T child)
+ {
+ HashMap> parentsDescendants = descendants.get(parent);
+ if (parentsDescendants == null)
+ {
+ parentsDescendants = new HashMap>();
+ descendants.put(parent, parentsDescendants);
+ }
+
+ HashMap> childDescendantsToAdd = descendants.get(child);
+
+ // add all the childs children to the parents descendants
+
+ add(childDescendantsToAdd, Integer.valueOf(0), parentsDescendants, child);
+
+ // add childs descendants to all parents ancestors at the correct depth
+
+ HashMap> ancestorsToFixUp = ancestors.get(parent);
+
+ if (ancestorsToFixUp != null)
+ {
+ for (Integer ancestorPosition : ancestorsToFixUp.keySet())
+ {
+ HashMap ancestorsToFixUpAtPosition = ancestorsToFixUp.get(ancestorPosition);
+ for (T ancestorToFixUpAtPosition : ancestorsToFixUpAtPosition.keySet())
+ {
+ HashMap> ancestorDescendants = descendants.get(ancestorToFixUpAtPosition);
+ add(childDescendantsToAdd, ancestorPosition, ancestorDescendants, child);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param parent T
+ * @param child T
+ */
+ private void removeDescendants(T parent, T child)
+ {
+ HashMap> parentsDescendants = descendants.get(parent);
+ if (parentsDescendants == null)
+ {
+ return;
+ }
+
+ HashMap> childDescendantsToRemove = descendants.get(child);
+
+ // add all the childs children to the parents descendants
+
+ remove(childDescendantsToRemove, Integer.valueOf(0), parentsDescendants, child);
+
+ // add childs descendants to all parents ancestors at the correct depth
+
+ HashMap> ancestorsToFixUp = ancestors.get(parent);
+
+ if (ancestorsToFixUp != null)
+ {
+ for (Integer ancestorPosition : ancestorsToFixUp.keySet())
+ {
+ HashMap ancestorsToFixUpAtPosition = ancestorsToFixUp.get(ancestorPosition);
+ for (T ancestorToFixUpAtPosition : ancestorsToFixUpAtPosition.keySet())
+ {
+ HashMap> ancestorDescendants = descendants.get(ancestorToFixUpAtPosition);
+ remove(childDescendantsToRemove, ancestorPosition, ancestorDescendants, child);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param parent T
+ * @param child T
+ */
+ private void removeAncestors(T parent, T child)
+ {
+ HashMap> childsAncestors = ancestors.get(child);
+ if (childsAncestors == null)
+ {
+ return;
+ }
+
+ HashMap> parentAncestorsToRemove = ancestors.get(parent);
+
+ // add all the childs children to the parents descendants
+
+ remove(parentAncestorsToRemove, Integer.valueOf(0), childsAncestors, parent);
+
+ // add childs descendants to all parents ancestors at the correct depth
+
+ HashMap> decendantsToFixUp = descendants.get(child);
+
+ if (decendantsToFixUp != null)
+ {
+ for (Integer descendantPosition : decendantsToFixUp.keySet())
+ {
+ HashMap decendantsToFixUpAtPosition = decendantsToFixUp.get(descendantPosition);
+ for (T descendantToFixUpAtPosition : decendantsToFixUpAtPosition.keySet())
+ {
+ HashMap> descendantAncestors = ancestors.get(descendantToFixUpAtPosition);
+ remove(parentAncestorsToRemove, descendantPosition, descendantAncestors, parent);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param toAdd HashMap>
+ * @param position Integer
+ * @param target HashMap>
+ * @param node T
+ */
+ private void add(HashMap> toAdd, Integer position, HashMap> target, T node)
+ {
+ // add direct child
+ Integer directKey = Integer.valueOf(position.intValue() + 1);
+ HashMap direct = target.get(directKey);
+ if (direct == null)
+ {
+ direct = new HashMap();
+ target.put(directKey, direct);
+ }
+ Counter counter = direct.get(node);
+ if (counter == null)
+ {
+ counter = new Counter();
+ direct.put(node, counter);
+ }
+ counter.increment();
+
+ if (toAdd != null)
+ {
+ for (Integer depth : toAdd.keySet())
+ {
+ Integer newKey = Integer.valueOf(position.intValue() + depth.intValue() + 1);
+ HashMap toAddAtDepth = toAdd.get(depth);
+ HashMap targetAtDepthPlusOne = target.get(newKey);
+ if (targetAtDepthPlusOne == null)
+ {
+ targetAtDepthPlusOne = new HashMap();
+ target.put(newKey, targetAtDepthPlusOne);
+ }
+
+ for (T key : toAddAtDepth.keySet())
+ {
+ Counter counterToAdd = toAddAtDepth.get(key);
+ Counter counterToAddTo = targetAtDepthPlusOne.get(key);
+ if (counterToAddTo == null)
+ {
+ counterToAddTo = new Counter();
+ targetAtDepthPlusOne.put(key, counterToAddTo);
+ }
+ counterToAddTo.add(counterToAdd);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param toRemove HashMap>
+ * @param position Integer
+ * @param target HashMap>
+ * @param node T
+ */
+ private void remove(HashMap> toRemove, Integer position, HashMap> target, T node)
+ {
+ // remove direct child
+ Integer directKey = Integer.valueOf(position.intValue() + 1);
+ HashMap direct = target.get(directKey);
+ if (direct != null)
+ {
+ Counter counter = direct.get(node);
+ if (counter != null)
+ {
+ counter.decrement();
+ if (counter.getCount() == 0)
+ {
+ direct.remove(node);
+ }
+ }
+
+ }
+
+ if (toRemove != null)
+ {
+ for (Integer depth : toRemove.keySet())
+ {
+ Integer newKey = Integer.valueOf(position.intValue() + depth.intValue() + 1);
+ HashMap toRemoveAtDepth = toRemove.get(depth);
+ HashMap targetAtDepthPlusOne = target.get(newKey);
+ if (targetAtDepthPlusOne != null)
+ {
+ for (T key : toRemoveAtDepth.keySet())
+ {
+ Counter counterToRemove = toRemoveAtDepth.get(key);
+ Counter counterToRemoveFrom = targetAtDepthPlusOne.get(key);
+ if (counterToRemoveFrom != null)
+ {
+ counterToRemoveFrom.remove(counterToRemove);
+ if (counterToRemoveFrom.getCount() == 0)
+ {
+ targetAtDepthPlusOne.remove(key);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param parent T
+ * @param child T
+ */
+ private void addAncestors(T parent, T child)
+ {
+ HashMap> childsAncestors = ancestors.get(child);
+ if (childsAncestors == null)
+ {
+ childsAncestors = new HashMap>();
+ ancestors.put(child, childsAncestors);
+ }
+
+ HashMap> parentAncestorsToAdd = ancestors.get(parent);
+
+ // add all the childs children to the parents descendants
+
+ add(parentAncestorsToAdd, Integer.valueOf(0), childsAncestors, parent);
+
+ // add childs descendants to all parents ancestors at the correct depth
+
+ HashMap> descenantsToFixUp = descendants.get(child);
+
+ if (descenantsToFixUp != null)
+ {
+ for (Integer descendantPosition : descenantsToFixUp.keySet())
+ {
+ HashMap descenantsToFixUpAtPosition = descenantsToFixUp.get(descendantPosition);
+ for (T descenantToFixUpAtPosition : descenantsToFixUpAtPosition.keySet())
+ {
+ HashMap> descendatAncestors = ancestors.get(descenantToFixUpAtPosition);
+ add(parentAncestorsToAdd, descendantPosition, descendatAncestors, parent);
+ }
+ }
+ }
+ }
+
+ public int size()
+ {
+ return ancestors.size();
+ }
+
+ private static class Counter
+ {
+ int count = 0;
+
+ void increment()
+ {
+ count++;
+ }
+
+ void decrement()
+ {
+ count--;
+ }
+
+ int getCount()
+ {
+ return count;
+ }
+
+ void add(Counter other)
+ {
+ count += other.count;
+ }
+
+ void remove(Counter other)
+ {
+ count -= other.count;
+ }
+ }
+
+ /**
+ * @return Set
+ */
+ public Set keySet()
+ {
+ return ancestors.keySet();
+ }
+}
diff --git a/src/main/java/org/alfresco/util/CachingDateFormat.java b/src/main/java/org/alfresco/util/CachingDateFormat.java
new file mode 100644
index 0000000000..263c20c045
--- /dev/null
+++ b/src/main/java/org/alfresco/util/CachingDateFormat.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2005-2010 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.util;
+
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.DateTimeFormatterBuilder;
+import org.joda.time.format.ISODateTimeFormat;
+import org.springframework.extensions.surf.exception.PlatformRuntimeException;
+
+/**
+ * Provides thread safe means of obtaining a cached date formatter.
+ *
+ * The cached string-date mappings are stored in a WeakHashMap .
+ *
+ * @see java.text.DateFormat#setLenient(boolean)
+ *
+ * @author Derek Hulley
+ */
+public class CachingDateFormat extends SimpleDateFormat
+{
+ private static final long serialVersionUID = 3258415049197565235L;
+
+ /**
yyyy-MM-dd'T'HH:mm:ss */
+ public static final String FORMAT_FULL_GENERIC = "yyyy-MM-dd'T'HH:mm:ss";
+
+ /** yyyy-MM-dd'T'HH:mm:ss */
+ public static final String FORMAT_CMIS_SQL = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+
+ public static final String FORMAT_SOLR = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
+
+ public static final StringAndResolution[] LENIENT_FORMATS;
+
+
+ static
+ {
+ ArrayList list = new ArrayList ();
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ssZ", Calendar.SECOND));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm:ss", Calendar.SECOND));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mmZ", Calendar.MINUTE));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH:mm", Calendar.MINUTE));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HHZ", Calendar.HOUR_OF_DAY));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'HH", Calendar.HOUR_OF_DAY));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'Z", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution("yyyy-MM-dd'T'", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution("yyyy-MM-ddZ", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution("yyyy-MM-dd", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution("yyyy-MMZ", Calendar.MONTH));
+ list.add( new StringAndResolution("yyyy-MM", Calendar.MONTH));
+ // year would duplicate :-) and eat stuff
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSSZ", Calendar.MILLISECOND));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss.SSS", Calendar.MILLISECOND));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ssZ", Calendar.SECOND));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm:ss", Calendar.SECOND));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mmZ", Calendar.MINUTE));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH:mm", Calendar.MINUTE));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HHZ", Calendar.HOUR_OF_DAY));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'HH", Calendar.HOUR_OF_DAY));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'Z",Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution( "yyyy-MMM-dd'T'",Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution( "yyyy-MMM-ddZ", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution( "yyyy-MMM-dd", Calendar.DAY_OF_MONTH));
+ list.add( new StringAndResolution( "yyyy-MMMZ", Calendar.MONTH));
+ list.add( new StringAndResolution( "yyyy-MMM", Calendar.MONTH));
+ list.add( new StringAndResolution("yyyyZ", Calendar.YEAR));
+ list.add( new StringAndResolution("yyyy", Calendar.YEAR));
+
+
+
+ LENIENT_FORMATS = list.toArray(new StringAndResolution[]{});
+ }
+
+ /** yyyy-MM-dd */
+ public static final String FORMAT_DATE_GENERIC = "yyyy-MM-dd";
+
+ /** HH:mm:ss */
+ public static final String FORMAT_TIME_GENERIC = "HH:mm:ss";
+
+ private static ThreadLocal s_localDateFormat = new ThreadLocal();
+
+ private static ThreadLocal