diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..ca086f3f5d --- /dev/null +++ b/pom.xml @@ -0,0 +1,233 @@ + + 4.0.0 + + + org.alfresco + alfresco-super-pom + 6 + + alfresco-core + 6.5-SNAPSHOT + Alfresco Core + Alfresco core libraries and utils + + + scm:svn:https://svn.alfresco.com/repos/alfresco-open-mirror/services/alfresco-core/trunk/ + scm:svn:https://svn.alfresco.com/repos/alfresco-enterprise/services/alfresco-core/trunk/ + + + + + alfresco-internal + https://artifacts.alfresco.com/nexus/content/repositories/releases + + + alfresco-internal-snapshots + https://artifacts.alfresco.com/nexus/content/repositories/snapshots + + + + + 3.2.16.RELEASE + 6.8 + + + + + commons-codec + commons-codec + 1.10 + + + commons-collections + commons-collections + 3.2.2 + + + commons-httpclient + commons-httpclient + 3.1-HTTPCLIENT-1265 + + + commons-logging + commons-logging + 1.2 + + + commons-io + commons-io + 2.4 + + + org.apache.commons + commons-math3 + 3.6.1 + + + org.hibernate + hibernate + 3.2.6-alf-20131023 + + + org.safehaus.jug + jug + 2.0.0 + asl + + + org.mybatis + mybatis + 3.3.0 + + + org.mybatis + mybatis-spring + 1.2.5 + + + org.mybatis + mybatis + + + + + log4j + log4j + 1.2.17 + + + org.json + json + 20160212 + + + org.springframework + spring-orm + ${dependency.spring.version} + + + org.springframework + spring-context + ${dependency.spring.version} + + + org.springframework + spring-context-support + ${dependency.spring.version} + + + org.quartz-scheduler + quartz + 1.8.3-alfresco-patched + + + org.alfresco.surf + spring-surf-core-configservice + ${dependency.surf.version} + + + com.sun.xml.bind + jaxb-xjc + 2.2.7 + + + com.sun.xml.bind + jaxb-impl + 2.2.7 + + + dom4j + dom4j + 1.6.1 + + + org.codehaus.guessencoding + guessencoding + 1.4 + + + javax.transaction + jta + 1.0.1b + + + joda-time + joda-time + 2.9.3 + + + org.apache.httpcomponents + httpclient + 4.5.2 + + + + + javax.servlet + servlet-api + 2.5 + provided + + + + + org.slf4j + slf4j-log4j12 + 1.7.21 + test + + + org.springframework + spring-test + ${dependency.spring.version} + test + + + junit + junit + 4.12 + test + + + org.mockito + mockito-all + 1.10.19 + test + + + commons-dbcp + commons-dbcp + 1.4-DBCP330 + test + + + + + + + + maven-release-plugin + + true + @{project.version} + + + + + + + + maven-jar-plugin + 2.6 + + + + test-jar + + + + + + + + diff --git a/src/main/java/org/alfresco/api/AlfrescoPublicApi.java b/src/main/java/org/alfresco/api/AlfrescoPublicApi.java new file mode 100644 index 0000000000..c3f66179a8 --- /dev/null +++ b/src/main/java/org/alfresco/api/AlfrescoPublicApi.java @@ -0,0 +1,41 @@ +/* + * 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.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to denote a class or method as part + * of the public API. When a class or method is so designated then + * we will not change it within a release in a way that would make + * it no longer backwardly compatible with an earlier version within + * the release. + * + * @author Greg Melahn + */ +@Target( {ElementType.TYPE,ElementType.METHOD} ) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AlfrescoPublicApi +{ +} diff --git a/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java b/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java new file mode 100644 index 0000000000..b3d1ff7622 --- /dev/null +++ b/src/main/java/org/alfresco/config/AlfrescoPropertiesPersister.java @@ -0,0 +1,77 @@ +/* + * 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.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.util.Enumeration; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.StringUtils; + +/** + * Simple extension to the{@link DefaultPropertiesPersister} to strip trailing whitespace + * from incoming properties. + * + * @author shane frensley + * @see org.springframework.util.DefaultPropertiesPersister + */ +public class AlfrescoPropertiesPersister extends DefaultPropertiesPersister +{ + + private static Log logger = LogFactory.getLog(AlfrescoPropertiesPersister.class); + + @Override + public void load(Properties props, InputStream is) throws IOException + { + super.load(props, is); + strip(props); + } + + @Override + public void load(Properties props, Reader reader) throws IOException + { + super.load(props, reader); + strip(props); + } + + public void loadFromXml(Properties props, InputStream is) throws IOException + { + super.loadFromXml(props, is); + strip(props); + } + + private void strip(Properties props) + { + for (Enumeration keys = props.keys(); keys.hasMoreElements();) + { + String key = (String) keys.nextElement(); + String val = StringUtils.trimTrailingWhitespace(props.getProperty(key)); + if (logger.isTraceEnabled()) + { + logger.trace("Trimmed trailing whitespace for property " + key + " = " + val); + } + props.setProperty(key, val); + } + } +} diff --git a/src/main/java/org/alfresco/config/JndiObjectFactoryBean.java b/src/main/java/org/alfresco/config/JndiObjectFactoryBean.java new file mode 100644 index 0000000000..9a2a90205d --- /dev/null +++ b/src/main/java/org/alfresco/config/JndiObjectFactoryBean.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.config; + +import java.sql.Connection; + +import javax.naming.NamingException; +import javax.sql.DataSource; + +/** + * An extended version of JndiObjectFactoryBean that actually tests a JNDI data source before falling back to its + * default object. Allows continued backward compatibility with old-style datasource configuration. + * + * @author dward + */ +public class JndiObjectFactoryBean extends org.springframework.jndi.JndiObjectFactoryBean +{ + + @Override + protected Object lookup() throws NamingException + { + Object candidate = super.lookup(); + if (candidate instanceof DataSource) + { + Connection con = null; + try + { + con = ((DataSource) candidate).getConnection(); + } + catch (Exception e) + { + NamingException e1 = new NamingException("Unable to get connection from " + getJndiName()); + e1.setRootCause(e); + throw e1; + } + finally + { + try + { + if (con != null) + { + con.close(); + } + } + catch (Exception e) + { + } + } + } + return candidate; + } +} diff --git a/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java b/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java new file mode 100644 index 0000000000..70453690e0 --- /dev/null +++ b/src/main/java/org/alfresco/config/JndiPropertiesFactoryBean.java @@ -0,0 +1,60 @@ +/* + * 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.Properties; + +import javax.naming.NamingException; + +import org.springframework.jndi.JndiTemplate; + +/** + * An extended {@link SystemPropertiesFactoryBean} that allows properties to be set through JNDI entries in + * java:comp/env/properties/*. The precedence given to system properties is still as per the superclass. + * + * @author dward + */ +public class JndiPropertiesFactoryBean extends SystemPropertiesFactoryBean +{ + private JndiTemplate jndiTemplate = new JndiTemplate(); + + @Override + protected void resolveMergedProperty(String propertyName, Properties props) + { + try + { + Object value = this.jndiTemplate.lookup("java:comp/env/properties/" + propertyName); + if (value != null) + { + String stringValue = value.toString(); + if (stringValue.length() > 0) + { + // Unfortunately, JBoss 4 wrongly expects every env-entry declared in web.xml to have an + // env-entry-value (even though these are meant to be decided on deployment!). So we treat the empty + // string as null. + props.setProperty(propertyName, stringValue); + } + } + } + catch (NamingException e) + { + // Fall back to merged value in props + } + } +} diff --git a/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.java b/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.java new file mode 100644 index 0000000000..b775ad0110 --- /dev/null +++ b/src/main/java/org/alfresco/config/JndiPropertyPlaceholderConfigurer.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.config; + +import java.util.Properties; + +import javax.naming.NamingException; + +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.jndi.JndiTemplate; + +/** + * An extended {@link PropertyPlaceholderConfigurer} that allows properties to be set through JNDI entries in + * java:comp/env/properties/*. The precedence given to system properties is still as per the superclass. + * + * @author dward + */ +public class JndiPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer +{ + private JndiTemplate jndiTemplate = new JndiTemplate(); + + @Override + protected String resolvePlaceholder(String placeholder, Properties props) + { + String result = null; + try + { + Object value = this.jndiTemplate.lookup("java:comp/env/properties/" + placeholder); + if (value != null) + { + result = value.toString(); + } + } + catch (NamingException e) + { + } + // Unfortunately, JBoss 4 wrongly expects every env-entry declared in web.xml to have an env-entry-value (even + // though these are meant to be decided on deployment!). So we treat the empty string as null. + return result == null || result.length() == 0 ? super.resolvePlaceholder(placeholder, props) : result; + } +} diff --git a/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.java b/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.java new file mode 100644 index 0000000000..b7d3f33720 --- /dev/null +++ b/src/main/java/org/alfresco/config/NonBlockingLazyInitTargetSource.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.config; + +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; +import org.springframework.beans.BeansException; + +/** + * A non-blocking version of LazyInitTargetSource. + * + * @author dward + */ +public class NonBlockingLazyInitTargetSource extends AbstractBeanFactoryBasedTargetSource +{ + + private static final long serialVersionUID = 4509578245779492037L; + private Object target; + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + public Object getTarget() throws BeansException + { + this.lock.readLock().lock(); + try + { + if (this.target != null) + { + return this.target; + } + } + finally + { + this.lock.readLock().unlock(); + } + this.lock.writeLock().lock(); + try + { + if (this.target == null) + { + this.target = getBeanFactory().getBean(getTargetBeanName()); + } + return this.target; + } + finally + { + this.lock.writeLock().unlock(); + } + } +} diff --git a/src/main/java/org/alfresco/config/PathMatchingHelper.java b/src/main/java/org/alfresco/config/PathMatchingHelper.java new file mode 100644 index 0000000000..d0f1c09ab6 --- /dev/null +++ b/src/main/java/org/alfresco/config/PathMatchingHelper.java @@ -0,0 +1,69 @@ +/* + * 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.io.IOException; +import java.net.URL; +import java.util.Set; + +import org.springframework.core.io.Resource; +import org.springframework.util.PathMatcher; + +/** + * An interface for plug ins to JBossEnabledResourcePatternResolver that avoids direct dependencies on + * application server specifics. + * + * @author dward + */ +public interface PathMatchingHelper +{ + /** + * Indicates whether this helper is capable of searching the given URL (i.e. its protocol is supported). + * + * @param rootURL + * the root url to be searched + * @return true if this helper is capable of searching the given URL + */ + public boolean canHandle(URL rootURL); + + /** + * Gets the resource at the given URL. + * + * @param url URL + * @return the resource at the given URL + * @throws IOException + * for any error + */ + public Resource getResource(URL url) throws IOException; + + /** + * Gets the set of resources under the given URL whose path matches the given sub pattern. + * + * @param matcher + * the matcher + * @param rootURL + * the root URL to be searched + * @param subPattern + * the ant-style pattern to match + * @return the set of matching resources + * @throws IOException + * for any error + */ + public Set getResources(PathMatcher matcher, URL rootURL, String subPattern) throws IOException; +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java b/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java new file mode 100644 index 0000000000..f9ebb16658 --- /dev/null +++ b/src/main/java/org/alfresco/config/SystemPropertiesFactoryBean.java @@ -0,0 +1,141 @@ +/* + * 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.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.beans.factory.config.PropertiesFactoryBean; +import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; +import org.springframework.core.Constants; + +/** + * Like the parent PropertiesFactoryBean, but overrides or augments the resulting property set with values + * from VM system properties. As with the Spring {@link PropertyPlaceholderConfigurer} the following modes are + * supported: + *
    + *
  • SYSTEM_PROPERTIES_MODE_NEVER: Don't use system properties at all.
  • + *
  • SYSTEM_PROPERTIES_MODE_FALLBACK: Fallback to a system property only for undefined properties.
  • + *
  • SYSTEM_PROPERTIES_MODE_OVERRIDE: (DEFAULT)Use a system property if it is available.
  • + *
+ * Note that system properties will only be included in the property set if defaults for the property have already been + * defined using {@link #setProperties(Properties)} or {@link #setLocations(org.springframework.core.io.Resource[])} or + * their names have been included explicitly in the set passed to {@link #setSystemProperties(Set)}. + * + * @author Derek Hulley + */ +public class SystemPropertiesFactoryBean extends PropertiesFactoryBean +{ + private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class); + + private int systemPropertiesMode = PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE; + private Set systemProperties = Collections.emptySet(); + + /** + * Set the system property mode by the name of the corresponding constant, e.g. "SYSTEM_PROPERTIES_MODE_OVERRIDE". + * + * @param constantName + * name of the constant + * @throws java.lang.IllegalArgumentException + * if an invalid constant was specified + * @see #setSystemPropertiesMode + */ + public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException + { + this.systemPropertiesMode = SystemPropertiesFactoryBean.constants.asNumber(constantName).intValue(); + } + + /** + * Set how to check system properties. + * + * @see PropertyPlaceholderConfigurer#setSystemPropertiesMode(int) + */ + public void setSystemPropertiesMode(int systemPropertiesMode) + { + this.systemPropertiesMode = systemPropertiesMode; + } + + /** + * Set the names of the properties that can be considered for overriding. + * + * @param systemProperties + * a set of properties that can be fetched from the system properties + */ + public void setSystemProperties(Set systemProperties) + { + this.systemProperties = systemProperties; + } + + @SuppressWarnings("unchecked") + @Override + protected Properties mergeProperties() throws IOException + { + // First do the default merge + Properties props = super.mergeProperties(); + + // Now resolve all the merged properties + if (this.systemPropertiesMode == PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_NEVER) + { + // If we are in never mode, we don't refer to system properties at all + for (String systemProperty : (Set) (Set) props.keySet()) + { + resolveMergedProperty(systemProperty, props); + } + } + else + { + // Otherwise, we allow unset properties to drift through from the systemProperties set and potentially set + // ones to be overriden by system properties + Set propNames = new HashSet((Set) (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 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: + *

+ * + * @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 ... 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 s_localDateOnlyFormat = new ThreadLocal(); + + private static ThreadLocal s_localTimeOnlyFormat = new ThreadLocal(); + + private static ThreadLocal s_localCmisSqlDatetime = new ThreadLocal(); + + private static ThreadLocal s_localSolrDatetime = new ThreadLocal(); + + private static ThreadLocal s_lenientParsers = new ThreadLocal(); + + transient private Map cacheDates = new WeakHashMap(89); + + private CachingDateFormat(String format) + { + super(format); + } + + public String toString() + { + return this.toPattern(); + } + + /** + * @param length + * the type of date format, e.g. {@link CachingDateFormat#LONG } + * @param locale + * the Locale that will be used to determine the + * date pattern + * + * @see #getDateFormat(String, boolean) + * @see CachingDateFormat#SHORT + * @see CachingDateFormat#MEDIUM + * @see CachingDateFormat#LONG + * @see CachingDateFormat#FULL + */ + public static SimpleDateFormat getDateFormat(int length, Locale locale, boolean lenient) + { + SimpleDateFormat dateFormat = (SimpleDateFormat) CachingDateFormat.getDateInstance(length, locale); + // extract the format string + String pattern = dateFormat.toPattern(); + // we have a pattern to use + return getDateFormat(pattern, lenient); + } + + /** + * @param dateLength + * the type of date format, e.g. {@link CachingDateFormat#LONG } + * @param timeLength + * the type of time format, e.g. {@link CachingDateFormat#LONG } + * @param locale + * the Locale that will be used to determine the + * date pattern + * + * @see #getDateFormat(String, boolean) + * @see CachingDateFormat#SHORT + * @see CachingDateFormat#MEDIUM + * @see CachingDateFormat#LONG + * @see CachingDateFormat#FULL + */ + public static SimpleDateFormat getDateTimeFormat(int dateLength, int timeLength, Locale locale, boolean lenient) + { + SimpleDateFormat dateFormat = (SimpleDateFormat) CachingDateFormat.getDateTimeInstance(dateLength, timeLength, locale); + // extract the format string + String pattern = dateFormat.toPattern(); + // we have a pattern to use + return getDateFormat(pattern, lenient); + } + + /** + * @param pattern + * the conversion pattern to use + * @param lenient + * true to allow the parser to extract the date in conceivable + * manner + * @return Returns a conversion-cacheing formatter for the given pattern, + * but the instance itself is not cached + */ + public static SimpleDateFormat getDateFormat(String pattern, boolean lenient) + { + // create an alfrescoDateFormat for cacheing purposes + SimpleDateFormat dateFormat = new CachingDateFormat(pattern); + // set leniency + dateFormat.setLenient(lenient); + // done + return dateFormat; + } + + /** + * @return Returns a thread-safe formatter for the generic date/time format + * + * @see #FORMAT_FULL_GENERIC + */ + public static SimpleDateFormat getDateFormat() + { + if (s_localDateFormat.get() != null) + { + return s_localDateFormat.get(); + } + + CachingDateFormat formatter = new CachingDateFormat(FORMAT_FULL_GENERIC); + // it must be strict + formatter.setLenient(false); + // put this into the threadlocal object + s_localDateFormat.set(formatter); + // done + return s_localDateFormat.get(); + } + + /** + * @return Returns a thread-safe formatter for the cmis sql datetime format + */ + public static SimpleDateFormat getCmisSqlDatetimeFormat() + { + if (s_localCmisSqlDatetime.get() != null) + { + return s_localCmisSqlDatetime.get(); + } + + CachingDateFormat formatter = new CachingDateFormat(FORMAT_CMIS_SQL); + // it must be strict + formatter.setLenient(false); + // put this into the threadlocal object + s_localCmisSqlDatetime.set(formatter); + // done + return s_localCmisSqlDatetime.get(); + } + + /** + * @return Returns a thread-safe formatter for the cmis sql datetime format + */ + public static SimpleDateFormat getSolrDatetimeFormat() + { + if (s_localSolrDatetime.get() != null) + { + return s_localSolrDatetime.get(); + } + + CachingDateFormat formatter = new CachingDateFormat(FORMAT_SOLR); + // it must be strict + formatter.setLenient(false); + // put this into the threadlocal object + s_localSolrDatetime.set(formatter); + // done + return s_localSolrDatetime.get(); + } + + /** + * @return Returns a thread-safe formatter for the generic date format + * + * @see #FORMAT_DATE_GENERIC + */ + public static SimpleDateFormat getDateOnlyFormat() + { + if (s_localDateOnlyFormat.get() != null) + { + return s_localDateOnlyFormat.get(); + } + + CachingDateFormat formatter = new CachingDateFormat(FORMAT_DATE_GENERIC); + // it must be strict + formatter.setLenient(false); + // put this into the threadlocal object + s_localDateOnlyFormat.set(formatter); + // done + return s_localDateOnlyFormat.get(); + } + + /** + * @return Returns a thread-safe formatter for the generic time format + * + * @see #FORMAT_TIME_GENERIC + */ + public static SimpleDateFormat getTimeOnlyFormat() + { + if (s_localTimeOnlyFormat.get() != null) + { + return s_localTimeOnlyFormat.get(); + } + + CachingDateFormat formatter = new CachingDateFormat(FORMAT_TIME_GENERIC); + // it must be strict + formatter.setLenient(false); + // put this into the threadlocal object + s_localTimeOnlyFormat.set(formatter); + // done + return s_localTimeOnlyFormat.get(); + } + + /** + * Parses and caches date strings. + * + * @see java.text.DateFormat#parse(java.lang.String, + * java.text.ParsePosition) + */ + public Date parse(String text, ParsePosition pos) + { + Date cached = cacheDates.get(text); + if (cached == null) + { + Date date = super.parse(text, pos); + if ((date != null) && (pos.getIndex() == text.length())) + { + cacheDates.put(text, date); + Date clonedDate = (Date) date.clone(); + return clonedDate; + } + else + { + return date; + } + } + else + { + pos.setIndex(text.length()); + Date clonedDate = (Date) cached.clone(); + return clonedDate; + } + } + + public static Pair lenientParse(String text, int minimumResolution) throws ParseException + { + DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); + try + { + Date parsed = fmt.parseDateTime(text).toDate(); + return new Pair(parsed, Calendar.MILLISECOND); + } + catch(IllegalArgumentException e) + { + + } + + SimpleDateFormatAndResolution[] formatters = getLenientFormatters(); + for(SimpleDateFormatAndResolution formatter : formatters) + { + if(formatter.resolution >= minimumResolution) + { + ParsePosition pp = new ParsePosition(0); + Date parsed = formatter.simpleDateFormat.parse(text, pp); + if ((pp.getIndex() < text.length()) || (parsed == null)) + { + continue; + } + return new Pair(parsed, formatter.resolution); + } + } + + throw new ParseException("Unknown date format", 0); + + + } + + public static SimpleDateFormatAndResolution[] getLenientFormatters() + { + if (s_lenientParsers.get() != null) + { + return s_lenientParsers.get(); + } + + int i = 0; + SimpleDateFormatAndResolution[] formatters = new SimpleDateFormatAndResolution[LENIENT_FORMATS.length]; + for(StringAndResolution format : LENIENT_FORMATS) + { + CachingDateFormat formatter = new CachingDateFormat(format.string); + // it must be strict + formatter.setLenient(false); + formatters[i++] = new SimpleDateFormatAndResolution(formatter, format.resolution); + } + + // put this into the threadlocal object + s_lenientParsers.set(formatters); + // done + return s_lenientParsers.get(); + } + + public static class StringAndResolution + { + String string; + int resolution; + + /** + * @return the resolution + */ + public int getResolution() + { + return resolution; + } + + /** + * @param resolution the resolution to set + */ + public void setResolution(int resolution) + { + this.resolution = resolution; + } + + StringAndResolution(String string, int resolution) + { + this.string = string; + this.resolution = resolution; + } + } + + public static class SimpleDateFormatAndResolution + { + SimpleDateFormat simpleDateFormat; + int resolution; + + SimpleDateFormatAndResolution(SimpleDateFormat simpleDateFormat, int resolution) + { + this.simpleDateFormat = simpleDateFormat; + this.resolution = resolution; + } + + /** + * @return the simpleDateFormat + */ + public SimpleDateFormat getSimpleDateFormat() + { + return simpleDateFormat; + } + + /** + * @return the resolution + */ + public int getResolution() + { + return resolution; + } + + } +} diff --git a/src/main/java/org/alfresco/util/Content.java b/src/main/java/org/alfresco/util/Content.java new file mode 100644 index 0000000000..d507d9c747 --- /dev/null +++ b/src/main/java/org/alfresco/util/Content.java @@ -0,0 +1,75 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.Reader; + + +/** + * Content + * + * @author dcaruana + */ +public interface Content +{ + /** + * Gets content as a string + * + * @return content as a string + * @throws IOException + */ + public String getContent() throws IOException; + + /** + * Gets the content mimetype + * + * @return mimetype + */ + public String getMimetype(); + + /** + * Gets the content encoding + * + * @return encoding + */ + public String getEncoding(); + + /** + * Gets the content length (in bytes) + * + * @return length + */ + public long getSize(); + + /** + * Gets the content input stream + * + * @return input stream + */ + public InputStream getInputStream(); + + /** + * Gets the content reader (which is sensitive to encoding) + * + * @return Reader + */ + public Reader getReader() throws IOException; +} diff --git a/src/main/java/org/alfresco/util/Convert.java b/src/main/java/org/alfresco/util/Convert.java new file mode 100644 index 0000000000..5074ff9691 --- /dev/null +++ b/src/main/java/org/alfresco/util/Convert.java @@ -0,0 +1,809 @@ +/* + * 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.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.alfresco.encoding.CharactersetFinder; +import org.alfresco.encoding.GuessEncodingCharsetFinder; +import org.alfresco.util.exec.RuntimeExec; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; + +/** + * Utility to convert text files. + *

+ * Check the usage options with the --help option. + *

+ * Here are some examples of how to use the main method: + *

    + *
  • + * --help
    + * Produce the help output. + *
  • + *
  • + * --dry-run --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" --ignore="(.svn|classes)" "w:\"
    + * Find all source (.java, .xml, .jsp and .properties) files in directory "w:\".
    + * List files and show which would change when converting to CR-LF (Windows) line endings.
    + * Where auto-detection of the file is ambiguous, assume UTF-8. + *
  • + *
  • + * --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" --ignore="(.svn|classes)" "w:\"
    + * Find all source (.java, .xml, .jsp and .properties) files in directory "w:\". Recurse into subdirectories.
    + * Convert files, where necessary, to have CR-LF (Windows) line endings.
    + * Where auto-detection of the file encoding is ambiguous, assume UTF-8.
    + * Backups (.bak) files will be created. + *
  • + *
  • + * --svn-update --no-backup --encoding=UTF-8 --line-ending=WINDOWS --match="(.java|.xml|.jsp|.properties)$" "w:\"
    + * Issue a 'svn status' command on directory "w:\" and match the regular expressions given to find files.
    + * Convert files, where necessary, to have CR-LF (Windows) line endings.
    + * Where auto-detection of the file encoding is ambiguous, assume UTF-8. Write out as UTF-8.
    + * No backups files will be created. + *
  • + *
+ * + * @author Derek Hulley + */ +public class Convert +{ + private static final String OPTION_HELP = "--help"; + private static final String OPTION_SVN_STATUS = "--svn-status"; + private static final String OPTION_MATCH = "--match="; + private static final String OPTION_IGNORE = "--ignore="; + private static final String OPTION_ENCODING= "--encoding="; + private static final String OPTION_LINE_ENDING = "--line-ending="; + private static final String OPTION_REPLACE_TABS= "--replace-tabs="; + private static final String OPTION_NO_RECURSE = "--no-recurse"; + private static final String OPTION_NO_BACKUP = "--no-backup"; + private static final String OPTION_DRY_RUN = "--dry-run"; + private static final String OPTION_VERBOSE = "--verbose"; + private static final String OPTION_QUIET = "--quiet"; + + private static final Set OPTIONS = new HashSet(13); + + static + { + OPTIONS.add(OPTION_HELP); + OPTIONS.add(OPTION_SVN_STATUS); + OPTIONS.add(OPTION_MATCH); + OPTIONS.add(OPTION_IGNORE); + OPTIONS.add(OPTION_ENCODING); + OPTIONS.add(OPTION_LINE_ENDING); + OPTIONS.add(OPTION_REPLACE_TABS); + OPTIONS.add(OPTION_NO_RECURSE); + OPTIONS.add(OPTION_NO_BACKUP); + OPTIONS.add(OPTION_DRY_RUN); + OPTIONS.add(OPTION_VERBOSE); + OPTIONS.add(OPTION_QUIET); + } + + /** + * @see GuessEncodingCharsetFinder + */ + private static final CharactersetFinder CHARACTER_ENCODING_FINDER = new GuessEncodingCharsetFinder(); + + private File startDir = null; + + private boolean svnStatus = false; + private boolean dryRun = false; + private Pattern matchPattern = null; + private Pattern ignorePattern = null; + private Charset charset = null; + private String lineEnding = null; + private Integer replaceTabs = null; + private boolean noRecurse = false; + private boolean noBackup = false; + private boolean verbose = false; + private boolean quiet = false; + + public static void main(String[] args) + { + if (args.length < 1) + { + printUsage(); + } + // Convert args to a list + List argList = new ArrayList(args.length); + List argListFixed = Arrays.asList(args); + argList.addAll(argListFixed); + // Extract all the options + Map optionValues = extractOptions(argList); + + // Check for help request + if (optionValues.containsKey(OPTION_HELP)) + { + printUsage(); + System.exit(0); + } + + // Check + if (argList.size() != 1) + { + printUsage(); + System.exit(1); + } + + // Get the directory to start in + File startDir = new File(argList.get(0)); + if (!startDir.exists() || !startDir.isDirectory()) + { + System.err.println("Convert: "); + System.err.println(" Unable to find directory: " + startDir); + System.err.flush(); + printUsage(); + System.exit(1); + } + + Convert convert = new Convert(optionValues, startDir); + convert.convert(); + } + + /** + * Private constructor for use by the main method. + */ + private Convert(Map optionValues, File startDir) + { + this.startDir = startDir; + + svnStatus = optionValues.containsKey(OPTION_SVN_STATUS); + dryRun = optionValues.containsKey(OPTION_DRY_RUN); + String match = optionValues.get(OPTION_MATCH); + String ignore = optionValues.get(OPTION_IGNORE); + String encoding = optionValues.get(OPTION_ENCODING); + lineEnding = optionValues.get(OPTION_LINE_ENDING); + noRecurse = optionValues.containsKey(OPTION_NO_RECURSE); + noBackup = optionValues.containsKey(OPTION_NO_BACKUP); + verbose = optionValues.containsKey(OPTION_VERBOSE); + quiet = optionValues.containsKey(OPTION_QUIET); + + // Check that the tab replacement count is correct + String replaceTabsStr = optionValues.get(OPTION_REPLACE_TABS); + if (replaceTabsStr != null) + { + try + { + replaceTabs = Integer.parseInt(replaceTabsStr); + } + catch (NumberFormatException e) + { + System.err.println("Convert: "); + System.err.println(" Unable to determine how many spaces to replace tabs with: " + replaceTabsStr); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + // Check the match regex expressions + if (match == null) + { + match = ".*"; + } + try + { + matchPattern = Pattern.compile(match); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unable to parse regular expression: " + match); + System.err.flush(); + printUsage(); + System.exit(1); + } + // Check the match regex expressions + if (ignore != null) + { + try + { + ignorePattern = Pattern.compile(ignore); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unable to parse regular expression: " + ignore); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + // Check the encoding + if (encoding != null) + { + try + { + charset = Charset.forName(encoding); + } + catch (Throwable e) + { + System.err.println("Convert: "); + System.err.println(" Unknown encoding: " + encoding); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + // Check line ending + if (lineEnding != null && !lineEnding.equals("WINDOWS") && !lineEnding.equals("UNIX")) + { + System.err.println("Convert: "); + System.err.println(" Line endings can be either WINDOWS or UNIX: " + lineEnding); + System.err.flush(); + printUsage(); + System.exit(1); + } + + // Check quiet/verbose match + if (verbose && quiet) + { + System.err.println("Convert: "); + System.err.println(" Cannot output in verbose and quiet mode."); + System.err.flush(); + printUsage(); + System.exit(1); + } + } + + private void convert() + { + try + { + if (!quiet) + { + System.out.print("Converting files matching " + matchPattern); + System.out.print(ignorePattern == null ? "" : " but not " + ignorePattern); + System.out.println(dryRun ? " [DRY RUN]" : ""); + } + if (!svnStatus) + { + // Do a recursive pattern match + convertDir(startDir); + } + else + { + // Use SVN + convertSvn(startDir); + } + } + catch (Throwable e) + { + e.printStackTrace(); + System.err.flush(); + printUsage(); + System.exit(1); + } + finally + { + System.out.flush(); + } + } + + private void convertSvn(File currentDir) throws Throwable + { + RuntimeExec exec = new RuntimeExec(); + exec.setCommand(new String[] {"svn", "status", currentDir.toString()}); + ExecutionResult result = exec.execute(); + if (!result.getSuccess()) + { + System.out.println("svn status command failed:" + exec); + } + // Get the output + String dump = result.getStdOut(); + BufferedReader reader = null; + try + { + reader = new BufferedReader(new StringReader(dump)); + while (true) + { + String line = reader.readLine(); + if (line == null) + { + break; + } + // Only lines that start with "A" or "M" + if (!line.startsWith("A") && !line.startsWith("M")) + { + continue; + } + String filename = line.substring(7).trim(); + if (filename.length() < 1) + { + continue; + } + File file = new File(filename); + if (!file.exists()) + { + continue; + } + // We found one + convertFile(file); + } + } + finally + { + if (reader != null) + { + try { reader.close(); } catch (Throwable e) {} + } + } + } + + /** + * Recursive method to do the conversion work. + */ + private void convertDir(File currentDir) throws Throwable + { + // Get all children of the folder + File[] childFiles = currentDir.listFiles(); + for (File childFile : childFiles) + { + if (childFile.isDirectory()) + { + if (noRecurse) + { + // Don't enter the directory + continue; + } + // Recurse + convertDir(childFile); + } + else + { + convertFile(childFile); + } + } + } + + private void convertFile(File file) throws Throwable + { + // We have a file, but does the pattern match + String filePath = file.getAbsolutePath(); + if (matchPattern.matcher(filePath).find()) + { + // It matches, but must we ignore it? + if (ignorePattern != null && ignorePattern.matcher(filePath).find()) + { + // It is ignorable + return; + } + } + else + { + // It missed the primary positive match + return; + } + + // Ignore folders + if (file.isDirectory()) + { + return; + } + + if (file.length() > (1024 * 1024)) // 1MB. TODO: Make an option + { + System.out.println(" (Too big)"); + } + File backupFile = null; + try + { + // Read the source file into memory + byte[] fileBytes = readFileIntoMemory(file); + // Calculate the MD5 for the file + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(fileBytes); + byte[] fileMd5 = md5.digest(); + // Guess the charset now + Charset fileCharset = guessCharset(fileBytes, charset); + + byte[] convertedBytes = fileBytes; + byte[] sourceBytes = fileBytes; + byte[] convertedMd5 = fileMd5; + + // Convert the tabs + if (replaceTabs != null) + { + sourceBytes = convertTabs(sourceBytes, fileCharset, replaceTabs); + } + // Convert the charset + if (charset != null) + { + // TODO + // sourceBytes = convert ... + } + // Convert the line endings + if (lineEnding != null) + { + convertedBytes = convertLineEndings(sourceBytes, fileCharset, lineEnding); + } + boolean changed = false; + if (convertedBytes == fileBytes) + { + // Nothing done + } + else + { + // Recalculate the converted MD5 + md5 = MessageDigest.getInstance("MD5"); + md5.update(convertedBytes); + convertedMd5 = md5.digest(); + // Now compare + changed = !Arrays.equals(fileMd5, convertedMd5); + } + // Make a backup of the file if it changed + if (changed) + { + if (!noBackup && !dryRun) + { + String backupFilename = file.getAbsolutePath() + ".bak"; + File backupFilePre = new File(backupFilename); + // Write the original file contents to the backup file + writeMemoryIntoFile(fileBytes, backupFilePre); + // That being successful, we can now reference it + backupFile = backupFilePre; + } + if (!quiet) + { + System.out.println(" " + file + " "); + } + // Only write to the file if this is not a dry run + if (!dryRun) + { + // Now write the converted buffer to the original file + writeMemoryIntoFile(convertedBytes, file); + } + } + else + { + if (verbose) + { + System.out.println(" " + file + " "); + } + } + } + catch (Throwable e) + { + if (backupFile != null) + { + try + { + file.delete(); + backupFile.renameTo(file); + } + catch (Throwable ee) + { + System.err.println("Failed to restore backup file: " + backupFile); + ee.printStackTrace(); + } + } + throw e; + } + finally + { + if (!quiet || verbose) + { + System.out.flush(); + } + } + } + + /** + * Brute force guessing by doing charset conversions.
+ */ + private static Charset guessCharset(byte[] bytes, Charset charset) throws Exception + { + Charset guessedCharset = CHARACTER_ENCODING_FINDER.detectCharset(bytes); + if (guessedCharset == null) + { + return charset; + } + else + { + return guessedCharset; + } + } + + private static byte[] convertTabs(byte[] bytes, Charset charset, int replaceTabs) throws Exception + { + // The tab character + char tab = '\t'; + char space = ' '; + + // The output + StringBuilder sb = new StringBuilder(bytes.length); + + String charsetName = charset.name(); + // Using the charset, convert to a string + String str = new String(bytes, charsetName); + char[] chars = str.toCharArray(); + for (char c : chars) + { + if (c == tab) + { + // Replace the tab + for (int i = 0; i < replaceTabs; i++) + { + sb.append(space); + } + } + else + { + sb.append(c); + } + } + // Done + return sb.toString().getBytes(charsetName); + } + + private static final String EOF_CHECK = "--EOF-CHECK--"; + private static byte[] convertLineEndings(byte[] bytes, Charset charset, String lineEnding) throws Exception + { + String charsetName = charset.name(); + // Using the charset, convert to a string + BufferedReader reader = null; + StringBuilder sb = new StringBuilder(bytes.length); + try + { + String str = new String(bytes, charsetName); + str = str + EOF_CHECK; + reader = new BufferedReader(new StringReader(str)); + String line = reader.readLine(); + while (line != null) + { + // Ignore the newline check + boolean addLine = true; + if (line.equals(EOF_CHECK)) + { + break; + } + else if (line.endsWith(EOF_CHECK)) + { + int index = line.indexOf(EOF_CHECK); + line = line.substring(0, index); + addLine = false; + } + // Write the line back out + sb.append(line); + if (!addLine) + { + // No newline + } + else if (lineEnding.equalsIgnoreCase("UNIX")) + { + sb.append("\n"); + } + else + { + sb.append("\r\n"); + } + line = reader.readLine(); + } + } + finally + { + if (reader != null) + { + try { reader.close(); } catch (Throwable e) {} + } + } + // Done + return sb.toString().getBytes(charsetName); + } + + private static byte[] readFileIntoMemory(File file) throws Exception + { + InputStream is = null; + OutputStream os = null; + try + { + is = new BufferedInputStream(new FileInputStream(file)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); + os = new BufferedOutputStream(baos); + byte[] buffer = new byte[1024]; + while (true) + { + int count = is.read(buffer); + if (count < 0) + { + break; + } + os.write(buffer, 0, count); + } + os.flush(); + byte[] memory = baos.toByteArray(); + return memory; + } + finally + { + if (is != null) + { + try { is.close(); } catch (Throwable e) {} + } + if (os != null) + { + try { os.close(); } catch (Throwable e) {} + } + } + } + + private static void writeMemoryIntoFile(byte[] bytes, File file) throws Exception + { + InputStream is = null; + OutputStream os = null; + try + { + is = new ByteArrayInputStream(bytes); + os = new BufferedOutputStream(new FileOutputStream(file)); + byte[] buffer = new byte[1024]; + while (true) + { + int count = is.read(buffer); + if (count < 0) + { + break; + } + os.write(buffer, 0, count); + } + os.flush(); + } + finally + { + if (is != null) + { + try { is.close(); } catch (Throwable e) {} + } + if (os != null) + { + try { os.close(); } catch (Throwable e) {} + } + } + } + + /** + * Extract all the options from the list of arguments. + * @param args the program arguments. This list will be modified. + * @return Returns a map of arguments and their values. Where the arguments have + * no values, an empty string is returned. + */ + private static Map extractOptions(List args) + { + Map optionValues = new HashMap(13); + // Iterate until we find a non-option + Iterator iterator = args.iterator(); + while (iterator.hasNext()) + { + String arg = iterator.next(); + boolean foundOption = false; + for (String option : OPTIONS) + { + if (!arg.startsWith(option)) + { + // It is a non-option + continue; + } + foundOption = true; + // We can remove the argument + iterator.remove(); + // Check if the option needs a value + if (option.endsWith("=")) + { + // Extract the option value + int index = arg.indexOf("="); + if (index == arg.length() - 1) + { + // There is nothing there, so we don't keep a value + } + else + { + String value = arg.substring(index + 1); + optionValues.put(option, value); + } + } + else + { + // Add the value to the map + String value = ""; + optionValues.put(option, value); + } + } + if (!foundOption) + { + // It is not an option + break; + } + } + // Done + return optionValues; + } + + public static void printUsage() + { + StringBuilder sb = new StringBuilder(1024); + sb.append("Usage: \n") + .append(" Convert [options] directory \n") + .append(" \n") + .append(" options: \n") + .append(" --help \n") + .append(" Print this help. \n") + .append(" --svn-status \n") + .append(" Execute a 'svn status' command against the directory and use the output for the file list. \n") + .append(" --match=?: \n") + .append(" A regular expression that all filenames must match. \n") + .append(" This argument can be escaped with double quotes, ie.g \"[a-zA-z0-9 ]\". \n") + .append(" The regular expression will be applied to the full path of the file. \n") + .append(" Name seperators will be '/' on Unix and ''\\'' on Windows systems. \n") + .append(" The default is \"--match=.*\", or match all files. \n") + .append(" --ignore=?: \n") + .append(" A regular expression that all filenames must not match. \n") + .append(" This argument can be escaped with double quotes, ie.g \"[a-zA-z0-9 ]\". \n") + .append(" The regular expression will be applied to the full path of the file. \n") + .append(" Name seperators will be '/' on Unix and ''\\'' on Windows systems. \n") + .append(" This option is not present by default. \n") + .append(" --encoding=? \n") + .append(" If not specified, the encoding of the files is left unchanged. \n") + .append(" Typical values would be UTF-8, UTF-16 or any java-recognized encoding string. \n") + .append(" --line-ending=? \n") + .append(" This can either be WINDOWS or UNIX. \n") + .append(" If not set, the line ending style is left unchanged. \n") + .append(" --replace-tabs=? \n") + .append(" Specify the number of spaces to insert in place of a tab. \n") + .append(" --no-recurse \n") + .append(" Do not recurse into subdirectories. \n") + .append(" --no-backup \n") + .append(" The default is to make a backup of all files prior to modification. \n") + .append(" With this option, no backups are made. \n") + .append(" --dry-run \n") + .append(" Do not modify or backup any files. \n") + .append(" No filesystem modifications are made. \n") + .append(" --verbose \n") + .append(" Dump all files checked to std.out. \n") + .append(" --quiet \n") + .append(" Don't dump anything to std.out. \n") + .append(" directory: \n") + .append(" The directory to start searching in. \n") + .append(" If the directory has spaces in it, then escape it with double quotes, e.g. \"C:\\Program Files\" \n") + .append(" \n") + .append("Details of the modifications being made are written to std.out. \n") + .append("Errors are written to std.err. \n") + .append("When used without any options, this program will behave like a FIND. \n"); + System.out.println(sb); + System.out.flush(); + } +} diff --git a/src/main/java/org/alfresco/util/CronTriggerBean.java b/src/main/java/org/alfresco/util/CronTriggerBean.java new file mode 100644 index 0000000000..25fe78c78c --- /dev/null +++ b/src/main/java/org/alfresco/util/CronTriggerBean.java @@ -0,0 +1,134 @@ +/* + * 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 java.util.Date; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.quartz.CronTrigger; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.Trigger; + +/** + * A utility bean to wrap scheduling a cron job with a given scheduler. + * + * @author Andy Hind + */ +@AlfrescoPublicApi +public class CronTriggerBean extends AbstractTriggerBean +{ + private static final long MILLISECONDS_PER_MINUTE = 60L * 1000L; + + /* + * Milliseconds delay before the job will be triggered. + */ + private long startDelay = 0; + + /* + * The cron expression to trigger execution. + */ + String cronExpression; + + /** + * Default constructor + * + */ + public CronTriggerBean() + { + super(); + } + + /** + * Get the cron expression that determines when this job is run. + * + * @return The cron expression + */ + public String getCronExpression() + { + return cronExpression; + } + + /** + * Set the cron expression that determines when this job is run. + * + * @param cronExpression + */ + public void setCronExpression(String cronExpression) + { + this.cronExpression = cronExpression; + } + + /** + * Build the cron trigger + * + * @return The trigger + * @throws Exception + */ + public Trigger getTrigger() throws Exception + { + Trigger trigger = new CronTrigger(getBeanName(), Scheduler.DEFAULT_GROUP, getCronExpression()); + if (this.startDelay > 0) + { + trigger.setStartTime(new Date(System.currentTimeMillis() + this.startDelay)); + } + JobDetail jd = super.getJobDetail(); + if (jd != null) + { + String jobName = super.getJobDetail().getKey().getName(); + if (jobName != null && !jobName.isEmpty()) + { + trigger.setJobName(jobName); + } + String jobGroup = super.getJobDetail().getKey().getGroup(); + if (jobGroup != null && !jobGroup.isEmpty()) + { + trigger.setJobGroup(jobGroup); + } + } + return trigger; + } + + public long getStartDelay() + { + return startDelay; + } + + public void setStartDelay(long startDelay) + { + this.startDelay = startDelay; + } + + public void setStartDelayMinutes(long startDelayMinutes) + { + this.startDelay = startDelayMinutes * MILLISECONDS_PER_MINUTE; + } + + + public void afterPropertiesSet() throws Exception + { + if ((cronExpression == null) || (cronExpression.trim().length() == 0)) + { + throw new AlfrescoRuntimeException( + "The cron expression has not been set, is zero length, or is all white space"); + } + super.afterPropertiesSet(); + } +} diff --git a/src/main/java/org/alfresco/util/Debug.java b/src/main/java/org/alfresco/util/Debug.java new file mode 100644 index 0000000000..0d3ddf834b --- /dev/null +++ b/src/main/java/org/alfresco/util/Debug.java @@ -0,0 +1,122 @@ +/* + * 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.net.URL; + +/** + * Class containing debugging utility methods + * + * @author gavinc + */ +public class Debug +{ + /** + * Returns the location of the file that will be loaded for the given class name + * + * @param className The class to load + * @return The location of the file that will be loaded + * @throws ClassNotFoundException + */ + public static String whichClass(String className) throws ClassNotFoundException + { + String path = className; + + // prepare the resource path + if (path.startsWith("/") == false) + { + path = "/" + path; + } + path = path.replace('.', '/'); + path = path + ".class"; + + // get the location + URL url = Debug.class.getResource(path); + if (url == null) + { + throw new ClassNotFoundException(className); + } + + // format the result + String location = url.toExternalForm(); + if (location.startsWith("jar")) + { + location = location.substring(10, location.lastIndexOf("!")); + } + else if (location.startsWith("file:")) + { + location = location.substring(6); + } + + return location; + } + + /** + * Returns the class loader that will load the given class name + * + * @param className The class to load + * @return The class loader the class will be loaded in + * @throws ClassNotFoundException + */ + public static String whichClassLoader(String className) throws ClassNotFoundException + { + String result = "Could not determine class loader for " + className; + + Class clazz = Class.forName(className); + ClassLoader loader = clazz.getClassLoader(); + + if (loader != null) + { + result = clazz.getClassLoader().toString(); + } + + return result; + } + + /** + * Returns the class loader hierarchy that will load the given class name + * + * @param className The class to load + * @return The hierarchy of class loaders used to load the class + * @throws ClassNotFoundException + */ + public static String whichClassLoaderHierarchy(String className) throws ClassNotFoundException + { + StringBuffer buffer = new StringBuffer(); + Class clazz = Class.forName(className); + ClassLoader loader = clazz.getClassLoader(); + if (loader != null) + { + buffer.append(loader.toString()); + + ClassLoader parent = loader.getParent(); + while (parent != null) + { + buffer.append("\n-> ").append(parent.toString()); + parent = parent.getParent(); + } + } + else + { + buffer.append("Could not determine class loader for " + className); + } + + return buffer.toString(); + } +} diff --git a/src/main/java/org/alfresco/util/Deleter.java b/src/main/java/org/alfresco/util/Deleter.java new file mode 100644 index 0000000000..04778260b1 --- /dev/null +++ b/src/main/java/org/alfresco/util/Deleter.java @@ -0,0 +1,123 @@ +/* + * 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.io.File; +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility to delete a file or directory recursively. + * @author britt + */ +public class Deleter +{ + private static final Log log = LogFactory.getLog(Deleter.class); + + /** + * Delete by path. + * @param path + */ + public static void Delete(String path) + { + File toDelete = new File(path); + Delete(toDelete); + } + + /** + * Delete by File. + * @param toDelete + */ + public static void Delete(File toDelete) + { + if (toDelete.isDirectory()) + { + File[] listing = toDelete.listFiles(); + for (File file : listing) + { + Delete(file); + } + } + toDelete.delete(); + } + + + /** + * Recursively deletes the parents of the specified file stopping when rootDir is reached. + * The file itself must have been deleted before calling this method - since only empty + * directories can be deleted. + *

+ * For example: deleteEmptyParents(new File("/tmp/a/b/c/d.txt"), "/tmp/a") + *

+ * Will delete directories c and b assuming that they are both empty. It will leave /tmp/a even if it is + * empty as this is the rootDir + * + * @param file The path of the file whose parent directories should be deleted. + * @param rootDir Top level directory where deletion should stop. Must be the canonical path + * to ensure correct comparisons. + */ + public static void deleteEmptyParents(File file, String rootDir) + { + File parent = file.getParentFile(); + boolean deleted = false; + do + { + try + { + if (parent.isDirectory() && !parent.getCanonicalPath().equals(rootDir)) + { + // Only an empty directory will successfully be deleted. + deleted = parent.delete(); + } + } + catch (IOException error) + { + log.error("Unable to construct canonical path for " + parent.getAbsolutePath()); + break; + } + + parent = parent.getParentFile(); + } + while(deleted); + } + + /** + * Same behaviour as for {@link Deleter#deleteEmptyParents(File, String)} but with the + * rootDir parameter specified as a {@link java.io.File} object. + * + * @see Deleter#deleteEmptyParents(File, String) + * @param file + * @param rootDir + */ + public static void deleteEmptyParents(File file, File rootDir) + { + try + { + deleteEmptyParents(file, rootDir.getCanonicalPath()); + } + catch (IOException e) + { + String msg = "Unable to convert rootDir to canonical form [rootDir=" + rootDir + "]"; + throw new RuntimeException(msg, e); + } + } +} diff --git a/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java b/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java new file mode 100644 index 0000000000..91a5a513c6 --- /dev/null +++ b/src/main/java/org/alfresco/util/DynamicallySizedThreadPoolExecutor.java @@ -0,0 +1,156 @@ +/* + * 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.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This is an instance of {@link java.util.concurrent.ThreadPoolExecutor} which + * behaves how one would expect it to, even when faced with an unlimited + * queue. Unlike the default {@link java.util.concurrent.ThreadPoolExecutor}, it + * will add new Threads up to {@link #setMaximumPoolSize(int) maximumPoolSize} + * when there is lots of pending work, rather than only when the queue is full + * (which it often never will be, especially for unlimited queues) + * + * @author Nick Burch + */ +public class DynamicallySizedThreadPoolExecutor extends ThreadPoolExecutor +{ + private static Log logger = LogFactory.getLog(DynamicallySizedThreadPoolExecutor.class); + + private final ReentrantLock lock = new ReentrantLock(); + private int realCorePoolSize; + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, RejectedExecutionHandler handler) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue, ThreadFactory threadFactory) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory); + this.realCorePoolSize = corePoolSize; + } + + public DynamicallySizedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, + BlockingQueue workQueue) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + this.realCorePoolSize = corePoolSize; + } + + @Override + public void setCorePoolSize(int corePoolSize) + { + this.realCorePoolSize = corePoolSize; + super.setCorePoolSize(corePoolSize); + } + + @Override + public void execute(Runnable command) + { + // Do we want to add another thread? + int threadCount = getPoolSize(); + if(logger.isDebugEnabled()) + { + logger.debug("Current pool size is " + threadCount + ", real core=" + realCorePoolSize + + ", current core=" + getCorePoolSize() + ", max=" + getMaximumPoolSize()); + } + + if(threadCount < getMaximumPoolSize()) + { + // We're not yet at the full thread count + + // Does the queue size warrant adding one? + // (If there are more than the maximum pool size of jobs pending, + // it's time to add another thread) + int queueSize = getQueue().size() + 1;// New job not yet added + if(queueSize >= getMaximumPoolSize()) + { + lock.lock(); + int currentCoreSize = getCorePoolSize(); + if(currentCoreSize < getMaximumPoolSize()) + { + super.setCorePoolSize(currentCoreSize+1); + + if(logger.isInfoEnabled()) + { + logger.info("Increased pool size to " + getCorePoolSize() + " from " + + currentCoreSize + " due to queue size of " + queueSize); + } + } + lock.unlock(); + } + } + + // Now run the actual work + super.execute(command); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) + { + // If the queue is looking empty, allow the pool to + // get rid of idle threads when it wants to + int threadCount = getPoolSize(); + if(threadCount == getMaximumPoolSize() && threadCount > realCorePoolSize) + { + int queueSize = getQueue().size(); + int currentCoreSize = getCorePoolSize(); + if(queueSize < 2 && currentCoreSize > realCorePoolSize) + { + // Almost out of work, allow the pool to reduce threads when + // required. Double checks the sizing inside a lock to avoid + // race conditions taking us below the real core size. + lock.lock(); + currentCoreSize = getCorePoolSize(); + if(currentCoreSize > realCorePoolSize) + { + super.setCorePoolSize(currentCoreSize-1); + + if(logger.isInfoEnabled()) + { + logger.info("Decreased pool size to " + getCorePoolSize() + " from " + + currentCoreSize + " (real core size is " + realCorePoolSize + + ") due to queue size of " + queueSize); + } + } + lock.unlock(); + } + } + } +} diff --git a/src/main/java/org/alfresco/util/EqualsHelper.java b/src/main/java/org/alfresco/util/EqualsHelper.java new file mode 100644 index 0000000000..7b7c4cc8a1 --- /dev/null +++ b/src/main/java/org/alfresco/util/EqualsHelper.java @@ -0,0 +1,261 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class providing helper methods for various types of equals functionality + * + * @author Derek Hulley + */ +@AlfrescoPublicApi +public class EqualsHelper +{ + /** + * Performs an equality check left.equals(right) after checking for null values + * + * @param left the Object appearing in the left side of an equals statement + * @param right the Object appearing in the right side of an equals statement + * @return Return true or false even if one or both of the objects are null + */ + public static boolean nullSafeEquals(Object left, Object right) + { + return (left == right) || (left != null && right != null && left.equals(right)); + } + + /** + * Performs an case-sensitive or case-insensitive equality check after checking for null values + * @param ignoreCase true to ignore case + */ + public static boolean nullSafeEquals(String left, String right, boolean ignoreCase) + { + if (ignoreCase) + { + return (left == right) || (left != null && right != null && left.equalsIgnoreCase(right)); + } + else + { + return (left == right) || (left != null && right != null && left.equals(right)); + } + } + + private static final int BUFFER_SIZE = 1024; + /** + * Performs a byte-level comparison between two streams. + * + * @param left the left stream. This is closed at the end of the operation. + * @param right an right stream. This is closed at the end of the operation. + * @return Returns true if the streams are identical to the last byte + */ + public static boolean binaryStreamEquals(InputStream left, InputStream right) throws IOException + { + try + { + if (left == right) + { + // The same stream! This is pretty pointless, but they are equal, nevertheless. + return true; + } + + byte[] leftBuffer = new byte[BUFFER_SIZE]; + byte[] rightBuffer = new byte[BUFFER_SIZE]; + while (true) + { + int leftReadCount = left.read(leftBuffer); + int rightReadCount = right.read(rightBuffer); + if (leftReadCount != rightReadCount) + { + // One stream ended before the other + return false; + } + else if (leftReadCount == -1) + { + // Both streams ended without any differences found + return true; + } + for (int i = 0; i < leftReadCount; i++) + { + if (leftBuffer[i] != rightBuffer[i]) + { + // We found a byte difference + return false; + } + } + } + // The only exits with 'return' statements, so there is no need for any code here + } + finally + { + try { left.close(); } catch (Throwable e) {} + try { right.close(); } catch (Throwable e) {} + } + } + + /** + * Compare two maps and generate a difference report between the actual and expected values. + * This method is particularly useful during unit tests as the result (if not null) + * can be appended to a failure message. + * + * @param actual the map in hand + * @param expected the map expected + * @return Returns a difference report or null if there were no + * differences. The message starts with a new line and is neatly + * formatted. + */ + public static String getMapDifferenceReport(Map actual, Map expected) + { + Map copyResult = new HashMap(actual); + + boolean failure = false; + + StringBuilder sb = new StringBuilder(1024); + sb.append("\nValues that don't match the expected values: "); + for (Map.Entry entry : expected.entrySet()) + { + Object key = entry.getKey(); + Object expectedValue = entry.getValue(); + Object resultValue = actual.get(key); + if (!EqualsHelper.nullSafeEquals(resultValue, expectedValue)) + { + sb.append("\n") + .append(" Key: ").append(key).append("\n") + .append(" Result: ").append(resultValue).append("\n") + .append(" Expected: ").append(expectedValue); + failure = true; + } + copyResult.remove(key); + } + sb.append("\nValues that are present but should not be: "); + for (Map.Entry entry : copyResult.entrySet()) + { + Object key = entry.getKey(); + Object resultValue = entry.getValue(); + sb.append("\n") + .append(" Key: ").append(key).append("\n") + .append(" Result: ").append(resultValue); + failure = true; + } + if (failure) + { + return sb.toString(); + } + else + { + return null; + } + } + + /** + * Enumeration for results returned by {@link EqualsHelper#getMapComparison(Map, Map) map comparisons}. + * + * @author Derek Hulley + * @since 3.3 + */ + public static enum MapValueComparison + { + /** The key was only present in the left map */ + LEFT_ONLY, + /** The key was only present in the right map */ + RIGHT_ONLY, + /** The key was present in both maps and the values were equal */ + EQUAL, + /** The key was present in both maps but not equal */ + NOT_EQUAL + } + + /** + * Compare two maps. + *

+ * The return codes that accompany the keys are: + *

    + *
  • {@link MapValueComparison#LEFT_ONLY}
  • + *
  • {@link MapValueComparison#RIGHT_ONLY}
  • + *
  • {@link MapValueComparison#EQUAL}
  • + *
  • {@link MapValueComparison#NOT_EQUAL}
  • + *
+ * + * @param the map key type + * @param the map value type + * @param left the left side of the comparison + * @param right the right side of the comparison + * @return Returns a map whose keys are a union of the two maps' keys, along with + * the value comparison result + */ + public static Map getMapComparison(Map left, Map right) + { + Set keys = new HashSet(left.size() + right.size()); + keys.addAll(left.keySet()); + keys.addAll(right.keySet()); + + Map diff = new HashMap(left.size() + right.size()); + + // Iterate over the keys and do the comparisons + for (K key : keys) + { + boolean leftHasKey = left.containsKey(key); + boolean rightHasKey = right.containsKey(key); + V leftValue = left.get(key); + V rightValue = right.get(key); + if (leftHasKey) + { + if (!rightHasKey) + { + diff.put(key, MapValueComparison.LEFT_ONLY); + } + else if (EqualsHelper.nullSafeEquals(leftValue, rightValue)) + { + diff.put(key, MapValueComparison.EQUAL); + } + else + { + diff.put(key, MapValueComparison.NOT_EQUAL); + } + } + else if (rightHasKey) + { + if (!leftHasKey) + { + diff.put(key, MapValueComparison.RIGHT_ONLY); + } + else if (EqualsHelper.nullSafeEquals(leftValue, rightValue)) + { + diff.put(key, MapValueComparison.EQUAL); + } + else + { + diff.put(key, MapValueComparison.NOT_EQUAL); + } + } + else + { + // How is it here? + } + } + + return diff; + } +} diff --git a/src/main/java/org/alfresco/util/ExpiringValueCache.java b/src/main/java/org/alfresco/util/ExpiringValueCache.java new file mode 100644 index 0000000000..d888628748 --- /dev/null +++ b/src/main/java/org/alfresco/util/ExpiringValueCache.java @@ -0,0 +1,93 @@ +/* + * 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.io.Serializable; + +/** + * Simple cache of a single Object value. + *

+ * The object placed in the cache will automatically be discarded after a timeout value. + * + * @author Kevin Roast + */ +public class ExpiringValueCache implements Serializable +{ + private static final long serialVersionUID = 1036233352030777619L; + + // default is to discard cached object after 1 minute + private final static long TIMEOUT_DEFAULT = 1000L*60L; + + private long timeout = TIMEOUT_DEFAULT; + private long snapshot = 0; + private T value; + + /** + * Default constructor. + * + * Uses the default timeout of 1 minute. + */ + public ExpiringValueCache() + { + } + + /** + * Constructor + * + * @param timeout Timeout in milliseconds before cached value is discarded + */ + public ExpiringValueCache(long timeout) + { + this.timeout = timeout; + } + + /** + * Put a value into the cache. The item will be return from the associated get() method + * until the timeout expires then null will be returned. + * + * @param value The object to store in the cache + */ + public void put(T value) + { + this.value = value; + this.snapshot = System.currentTimeMillis(); + } + + /** + * Get the cached object. The set item will be returned until it expires, then null will be returned. + * + * @return cached object or null if not set or expired. + */ + public T get() + { + if (snapshot + timeout < System.currentTimeMillis()) + { + this.value = null; + } + return this.value; + } + + /** + * Clear the cache value + */ + public void clear() + { + this.value = null; + } +} diff --git a/src/main/java/org/alfresco/util/FileFilterMode.java b/src/main/java/org/alfresco/util/FileFilterMode.java new file mode 100644 index 0000000000..3d7e488f91 --- /dev/null +++ b/src/main/java/org/alfresco/util/FileFilterMode.java @@ -0,0 +1,115 @@ +package org.alfresco.util; + +public class FileFilterMode +{ + /** + * Clients for which specific hiding/visibility behaviour may be requested. + * Do not remove or change the order of + */ + public static enum Client + { + cifs, imap, webdav, nfs, script, webclient, ftp, cmis, admin; + + /** + * @deprecated Use {@link Client#valueOf(String)} + */ + @Deprecated + public static Client getClient(String clientStr) + { + if(clientStr.equals("cifs")) + { + return cifs; + } + else if(clientStr.equals("imap")) + { + return imap; + } + else if(clientStr.equals("webdav")) + { + return webdav; + } + else if(clientStr.equals("nfs")) + { + return nfs; + } + else if(clientStr.equals("ftp")) + { + return ftp; + } + else if(clientStr.equals("script")) + { + return script; + } + else if(clientStr.equals("webclient")) + { + return webclient; + } + else if(clientStr.equals("cmis")) + { + return cmis; + } + else if(clientStr.equals("admin")) + { + return admin; + } + else + { + throw new IllegalArgumentException(); + } + } + }; + + public static enum Mode + { + BASIC, ENHANCED; + }; + + private static ThreadLocal client = new ThreadLocal() + { + protected Client initialValue() { + return null; + } + }; + + public static void clearClient() + { + client.set(null); + } + + public static Client setClient(Client newClient) + { + Client oldClient = client.get(); + client.set(newClient); + + return oldClient; + } + + public static Mode getMode() + { + Client client = getClient(); + if(client == null) + { + return Mode.BASIC; + } + else + { + switch(client) + { + case cifs : + case nfs : + case ftp : + case webdav : + case cmis : + case admin : + return Mode.ENHANCED; + default: + return Mode.BASIC; + } + } + } + + public static Client getClient() + { + return client.get(); + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/GUID.java b/src/main/java/org/alfresco/util/GUID.java new file mode 100644 index 0000000000..227e928a5e --- /dev/null +++ b/src/main/java/org/alfresco/util/GUID.java @@ -0,0 +1,174 @@ +/* + * 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.security.SecureRandom; +import java.util.Random; + +import org.safehaus.uuid.UUIDGenerator; +import org.alfresco.api.AlfrescoPublicApi; + +/** + * A wrapper class to serve up GUIDs + * + * @author kevinr + */ +@AlfrescoPublicApi +public final class GUID +{ + /** + * Private Constructor for GUID. + */ + private GUID() + { + } + +// protected static final char[] s_values = +// { +// '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', +// 'f' +// }; + + private static final SecureRandom[] SECURE_RANDOM_POOL = initSecureRandomArray(); + private static final int SECURE_RANDOM_POOL_MAX_ITEMS = 256; + private static final Random RANDOM = new Random(); + + + private static SecureRandom[] initSecureRandomArray() + { + SecureRandom[] array = new SecureRandom[SECURE_RANDOM_POOL_MAX_ITEMS]; + for (int i = 0; i < SECURE_RANDOM_POOL_MAX_ITEMS; i++) + { + array[i] = new SecureRandom(); + } + return array; + } + + /** + * Generates and returns a new GUID as a string based on a SecureRandom pool in other to avoid + * thread blocking in concurrent calls. + * + * @return String GUID + */ + public static String generate() + { + int randomInt = RANDOM.nextInt(SECURE_RANDOM_POOL_MAX_ITEMS); + return UUIDGenerator.getInstance().generateRandomBasedUUID(SECURE_RANDOM_POOL[randomInt]).toString(); + } + +// == Not sure if we need this functionality again (derekh) == +// +// /** +// * Convert a string with a guid inside into a byte[16] array +// * +// * @param str - the guid +// * @return - byte[16] containing the GUID +// * @throws InvalidGuidFormatException +// */ +// public static byte[] parseFromString(String str) throws InvalidGuidFormatException +// { +// byte[] data = new byte[16]; +// int dataPos = 0; +// +// byte bVal; +// int value = 0; +// int pos = 0; +// +// for(int i = 0; i < str.length(); i++) +// { +// char thisChar = str.charAt(i); +// +// int idx = 0; +// +// if(thisChar >= '0' && thisChar <= '9') +// { +// idx = thisChar - '0'; +// pos++; +// } +// else if(thisChar >= 'a' && thisChar <= 'f') +// { +// idx = thisChar - 'a' + 10; +// pos++; +// } +// else if(thisChar >= 'a' && thisChar <= 'f') +// { +// idx = thisChar - 'A' + 10; +// pos++; +// } +// else if(thisChar == '-' || thisChar == '{' || thisChar == '}') +// { +// // Doesn't matter +// } +// else +// { +// throw new InvalidGuidFormatException(); +// } +// +// try +// { +// if(pos == 1) +// value = idx; +// else if(pos == 2) +// { +// value = (value * 16) + idx; +// +// byte b = (byte) value; +// data[dataPos++] = b; +// +// pos = 0; +// } +// } +// catch(RuntimeException e) +// { +// // May occur if we go off the end of the data index +// throw new InvalidGuidFormatException(); +// } +// } +// +// return data; +// } +// +// /** +// * Convert a byte[16] containing a guid to a string representation +// * +// * @param data - the data +// * @return - the string +// */ +// public static String convertToString(byte[] data) +// { +// char[] output = new char[36]; +// int cPos = 0; +// +// for(int i = 0; i < 16; i++) +// { +// int v = data[i]; +// +// int lowVal = v & 0x000F; +// int hiVal = (v & 0x00F0) >> 4; +// +// output[cPos++] = s_values[hiVal]; +// output[cPos++] = s_values[lowVal]; +// +// if(cPos == 8 || cPos == 13 || cPos == 18 || cPos == 23) +// output[cPos++] = '-'; +// } +// +// return new String(output); +// } +} diff --git a/src/main/java/org/alfresco/util/IPUtils.java b/src/main/java/org/alfresco/util/IPUtils.java new file mode 100644 index 0000000000..1ddb9c928e --- /dev/null +++ b/src/main/java/org/alfresco/util/IPUtils.java @@ -0,0 +1,26 @@ +package org.alfresco.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class IPUtils +{ + /** + * Returns the "real" IP address represented by ipAddress. If ipAddress is a loopback + * address it is converted into the host's underlying IP address + * + * @param ipAddress String + * @return String + * @throws UnknownHostException + */ + public static String getRealIPAddress(String ipAddress) throws UnknownHostException + { + if(ipAddress.equals("localhost") || ipAddress.equals("127.0.0.1")) + { + // make sure we are using a "real" IP address + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } + + return ipAddress; + } +} diff --git a/src/main/java/org/alfresco/util/ISO8601DateFormat.java b/src/main/java/org/alfresco/util/ISO8601DateFormat.java new file mode 100644 index 0000000000..ca0a460d7e --- /dev/null +++ b/src/main/java/org/alfresco/util/ISO8601DateFormat.java @@ -0,0 +1,346 @@ +/* + * 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.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.IllegalInstantException; +import org.joda.time.LocalDate; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + + +/** + * Formatting support for ISO 8601 dates + *

+ *    sYYYY-MM-DDThh:mm:ss.sssTZD
+ * 
+ * where: + *
    + *
  • sYYYY Four-digit year with optional leading positive (+) or negative (-) sign. + * A negative sign indicates a year BCE. The absence of a sign or the presence of a + * positive sign indicates a year CE (for example, -0055 would indicate the year 55 BCE, + * while +1969 and 1969 indicate the year 1969 CE).
  • + *
  • MM Two-digit month (01 = January, etc.)
  • + *
  • DD Two-digit day of month (01 through 31)
  • + *
  • hh Two digits of hour (00 through 23)
  • + *
  • mm Two digits of minute (00 through 59)
  • + *
  • ss.sss Seconds, to three decimal places (00.000 through 59.999)
  • + *
  • TZD Time zone designator (either Z for Zulu, i.e. UTC, or +hh:mm or -hh:mm, i.e. an offset from UTC)
  • + *
+ */ +@AlfrescoPublicApi +public class ISO8601DateFormat +{ + private static ThreadLocal> calendarThreadLocal = new ThreadLocal>(); + /** + * Get a calendar object from cache. + * @return calendar object from cache or newly created (if cache is empty) + */ + public static Calendar getCalendar() + { + if (calendarThreadLocal.get() == null) + { + calendarThreadLocal.set(new HashMap()); + } + + Calendar calendar = calendarThreadLocal.get().get(TimeZone.getDefault()); + if (calendar == null) + { + calendar = new GregorianCalendar(); + calendarThreadLocal.get().put(TimeZone.getDefault(), calendar); + } + + return calendar; + } + + /** + * Format date into ISO format (UCT0 / Zulu) + * + * @param isoDate the date to format + * @return the ISO Zulu timezone formatted string + */ + public static String format(Date isoDate) + { + Calendar calendar = getCalendar(); + calendar.setTime(isoDate); + + // MNT-9790 + // org.joda.time.DateTime.DateTime take away some minutes from date before 1848 year at formatting. + // This behavior connected with acceptance of time zones based + // on the Greenwich meridian (it was in Great Britain, year 1848). + if (calendar.get(Calendar.YEAR) > 1847) + { + DateTime dt = new DateTime(isoDate, DateTimeZone.UTC); + return dt.toString(); + } + else + { + int val = 0; + StringBuilder formatted = new StringBuilder(28); + formatted.append(calendar.get(Calendar.YEAR)); + formatted.append('-'); + val = calendar.get(Calendar.MONTH) + 1; + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('-'); + val = calendar.get(Calendar.DAY_OF_MONTH); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('T'); + val = calendar.get(Calendar.HOUR_OF_DAY); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append(':'); + val = calendar.get(Calendar.MINUTE); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append(':'); + val = calendar.get(Calendar.SECOND); + formatted.append(val < 10 ? ("0" + val) : val); + formatted.append('.'); + val = calendar.get(Calendar.MILLISECOND); + if (val < 10) + { + formatted.append(val < 10 ? ("00" + val) : val); + } + else if (val >= 10 && val < 100) + { + formatted.append(val < 10 ? ("0" + val) : val); + } + else + { + formatted.append(val); + } + + TimeZone tz = calendar.getTimeZone(); + int offset = tz.getOffset(calendar.getTimeInMillis()); + if (offset != 0) + { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + formatted.append(offset < 0 ? '-' : '+'); + formatted.append(hours < 10 ? ("0" + hours) : hours); + formatted.append(':'); + formatted.append(minutes < 10 ? ("0" + minutes) : minutes); + } + else + { + formatted.append('Z'); + } + + return formatted.toString(); + } + } + + /** + * Normalise isoDate time to Zulu(UTC0) time-zone, removing any UTC offset. + * @param isoDate + * @return the ISO Zulu timezone formatted string + * e.g 2011-02-04T17:13:14.000+01:00 -> 2011-02-04T16:13:14.000Z + */ + public static String formatToZulu(String isoDate) + { + try + { + DateTime dt = new DateTime(isoDate, DateTimeZone.UTC); + return dt.toString(); + } catch (IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + /** + * Parse date from ISO formatted string. + * The ISO8601 date must include TimeZone offset information + * + * @param isoDate ISO string to parse + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parse(String isoDate) + { + return parseInternal(isoDate, null); + } + + /** + * Parse date from ISO formatted string, with an + * explicit timezone specified + * + * @param isoDate ISO string to parse + * @param timezone The TimeZone the date is in + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parse(String isoDate, TimeZone timezone) + { + return parseInternal(isoDate, timezone); + } + + /** + * Parse date from ISO formatted string, either in the specified + * TimeZone, or with TimeZone information taken from the date + * + * @param isoDate ISO string to parse + * @param timezone The time zone, null means default time zone + * @return the date + * @throws AlfrescoRuntimeException if the parse failed + */ + public static Date parseInternal(String isoDate, TimeZone timezone) + { + try + { + // null time-zone defaults to the local time-zone + DateTimeZone dtz = DateTimeZone.forTimeZone(timezone); + try + { + DateTime dateTime = new DateTime(isoDate, dtz); + Date date = dateTime.toDate(); + return date; + } + catch (IllegalInstantException ie) + { + // The exception is thrown when a DateTime was created with a date-time inside the DST gap - a time that did not exist. + // Parse the date ignoring the time. + DateTimeFormatter parser = ISODateTimeFormat.dateTimeParser(); + LocalDate ldate = new LocalDate(parser.parseLocalDate(isoDate), dtz); + // Default to the first valid date-time of the day, not always 00:00 (because of DST). + DateTime dateT = ldate.toDateTimeAtStartOfDay(dtz); + Date date = dateT.toDate(); + return date; + } + } + catch (IllegalArgumentException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + /** + * Checks whether or not the given ISO8601-formatted date-string contains a time-component + * instead of only the actual date. + * + * @param isoDate + * @return true, if time is present. + */ + public static boolean isTimeComponentDefined(String isoDate) + { + boolean defined = false; + + if(isoDate != null && isoDate.length() > 11) + { + // Find occurrence of T (sYYYY-MM-DDT..), sign is optional + int expectedLocation = 10; + if(isoDate.charAt(0) == '-' || isoDate.charAt(0) == '+') { + // Sign is included before year + expectedLocation++; + } + + defined = isoDate.length() >= expectedLocation && isoDate.charAt(expectedLocation) == 'T'; + } + + return defined; + } + + /** + * Parses the given ISO8601-formatted date-string, not taking into account the time-component. + * The time-information for the will be reset to zero. + * + * @param isoDate the day (formatted sYYYY-MM-DD) or a full date (sYYYY-MM-DDThh:mm:ss.sssTZD) + * @param timezone the timezone to use + * @return the parsed date + * + * @throws AlfrescoRuntimeException if the parsing failed. + */ + public static Date parseDayOnly(String isoDate, TimeZone timezone) + { + try + { + if(isoDate != null && isoDate.length() >= 10) + { + int offset = 0; + + // Sign can be included before year + boolean bc = false; + if(isoDate.charAt(0) == '-') + { + bc = true; + offset++; + } + else if(isoDate.charAt(0) == '+') + { + offset++; + } + + // Extract year + int year = Integer.parseInt(isoDate.substring(offset, offset += 4)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // Extract month + int month = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + if (isoDate.charAt(offset) != '-') + { + throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset)); + } + + // Extract day + int day = Integer.parseInt(isoDate.substring(offset += 1, offset += 2)); + + Calendar calendar = new GregorianCalendar(timezone); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + if(bc) + { + calendar.set(Calendar.ERA, GregorianCalendar.BC); + } + + return calendar.getTime(); + } + else + { + throw new AlfrescoRuntimeException("String passed is too short " + isoDate); + } + } + catch(IndexOutOfBoundsException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + catch(NumberFormatException e) + { + throw new AlfrescoRuntimeException("Failed to parse date " + isoDate, e); + } + } + + +} diff --git a/src/main/java/org/alfresco/util/InputStreamContent.java b/src/main/java/org/alfresco/util/InputStreamContent.java new file mode 100644 index 0000000000..7e497bf7b5 --- /dev/null +++ b/src/main/java/org/alfresco/util/InputStreamContent.java @@ -0,0 +1,120 @@ +/* + * 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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; + +import org.springframework.util.FileCopyUtils; + + +/** + * Input Stream based Content + */ +public class InputStreamContent implements Content, Serializable +{ + private static final long serialVersionUID = -7729633986840536282L; + + private InputStream stream; + private String mimetype; + private String encoding; + + /** cached result - to ensure we only read it once */ + private String content; + + + /** + * Constructor + * + * @param stream content input stream + * @param mimetype content mimetype + */ + public InputStreamContent(InputStream stream, String mimetype, String encoding) + { + this.stream = stream; + this.mimetype = mimetype; + this.encoding = encoding; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getContent() + */ + public String getContent() + throws IOException + { + // ensure we only try to read the content once - as this method may be called several times + // but the inputstream can only be processed a single time + if (this.content == null) + { + ByteArrayOutputStream os = new ByteArrayOutputStream(1024); + FileCopyUtils.copy(stream, os); // both streams are closed + byte[] bytes = os.toByteArray(); + // get the encoding for the string + String encoding = getEncoding(); + // create the string from the byte[] using encoding if necessary + this.content = (encoding == null) ? new String(bytes) : new String(bytes, encoding); + } + return this.content; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getInputStream() + */ + public InputStream getInputStream() + { + return stream; + } + + + public Reader getReader() + throws IOException + { + return (encoding == null) ? new InputStreamReader(stream) : new InputStreamReader(stream, encoding); + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getSize() + */ + public long getSize() + { + return -1; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getMimetype() + */ + public String getMimetype() + { + return mimetype; + } + + /* (non-Javadoc) + * @see org.alfresco.util.Content#getEncoding() + */ + public String getEncoding() + { + return encoding; + } + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/JMXUtils.java b/src/main/java/org/alfresco/util/JMXUtils.java new file mode 100644 index 0000000000..e64de9c850 --- /dev/null +++ b/src/main/java/org/alfresco/util/JMXUtils.java @@ -0,0 +1,63 @@ +/* + * 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.util; + +import java.util.Date; + +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; + +public class JMXUtils +{ + public static OpenType getOpenType(Object o) + { + if(o instanceof Long) + { + return SimpleType.LONG; + } + else if(o instanceof String) + { + return SimpleType.STRING; + } + else if(o instanceof Date) + { + return SimpleType.DATE; + } + else if(o instanceof Integer) + { + return SimpleType.INTEGER; + } + else if(o instanceof Boolean) + { + return SimpleType.BOOLEAN; + } + else if(o instanceof Double) + { + return SimpleType.DOUBLE; + } + else if(o instanceof Float) + { + return SimpleType.FLOAT; + } + else + { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/org/alfresco/util/LockHelper.java b/src/main/java/org/alfresco/util/LockHelper.java new file mode 100644 index 0000000000..5d64241a92 --- /dev/null +++ b/src/main/java/org/alfresco/util/LockHelper.java @@ -0,0 +1,72 @@ +/* + * 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 java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * Helper to make trying for read-write locks simpler + * + * @author Derek Hulley + * @since 4.1.7 + */ +public class LockHelper +{ + /** + * Exception generated when a lock try is unsuccessful + * + * @author Derek Hulley + * @since 4.1.7 + */ + public static class LockTryException extends RuntimeException + { + private static final long serialVersionUID = -3629889029591630609L; + + public LockTryException(String msg) + { + super(msg); + } + } + + /** + * Try to get a lock in the given number of milliseconds or get an exception + * + * @param lock the lock to try + * @param timeoutMs the number of milliseconds to try + * @param useCase {@link String} value which specifies description of use case when lock is needed + * @throws LockTryException the exception if the time is exceeded or the thread is interrupted + */ + public static void tryLock(Lock lock, long timeoutMs, String useCase) throws LockTryException + { + boolean gotLock = false; + try + { + gotLock = lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) + { + // Handled + } + if (!gotLock) + { + throw new LockTryException("Failed to get lock " + lock.getClass().getSimpleName() + " for " + useCase + " in " + timeoutMs + "ms."); + } + } +} diff --git a/src/main/java/org/alfresco/util/LogAdapter.java b/src/main/java/org/alfresco/util/LogAdapter.java new file mode 100644 index 0000000000..ae27069ddd --- /dev/null +++ b/src/main/java/org/alfresco/util/LogAdapter.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 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.util; + +import org.apache.commons.logging.Log; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class to adapt a {@link Log} class. + * + * @since 4.2 + * + * @author Alan Davis + */ +@AlfrescoPublicApi +public abstract class LogAdapter implements Log +{ + final protected Log log; + + /** + * Constructor of an optional wrapped {@link Log}. + * @param log + */ + protected LogAdapter(Log log) + { + this.log = log; + } + + @Override + public void trace(Object arg0) + { + trace(arg0, null); + } + + @Override + public void trace(Object arg0, Throwable arg1) + { + if (log != null) + { + log.trace(arg0, arg1); + } + } + + @Override + public void debug(Object arg0) + { + debug(arg0, null); + } + + @Override + public void debug(Object arg0, Throwable arg1) + { + if (log != null) + { + log.debug(arg0, arg1); + } + } + + @Override + public void info(Object arg0) + { + info(arg0, null); + } + + @Override + public void info(Object arg0, Throwable arg1) + { + if (log != null) + { + log.info(arg0, arg1); + } + } + + @Override + public void warn(Object arg0) + { + warn(arg0, null); + } + + @Override + public void warn(Object arg0, Throwable arg1) + { + if (log != null) + { + log.warn(arg0, arg1); + } + } + + @Override + public void error(Object arg0) + { + error(arg0, null); + } + + @Override + public void error(Object arg0, Throwable arg1) + { + if (log != null) + { + log.error(arg0, arg1); + } + } + + @Override + public void fatal(Object arg0) + { + fatal(arg0, null); + } + + @Override + public void fatal(Object arg0, Throwable arg1) + { + if (log != null) + { + log.fatal(arg0, arg1); + } + } + + @Override + public boolean isTraceEnabled() + { + return log != null && log.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() + { + return log != null && log.isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled() + { + return log != null && log.isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled() + { + return log != null && log.isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() + { + return log != null && log.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() + { + return log != null && log.isFatalEnabled(); + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/LogTee.java b/src/main/java/org/alfresco/util/LogTee.java new file mode 100644 index 0000000000..512f95a5b8 --- /dev/null +++ b/src/main/java/org/alfresco/util/LogTee.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 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.util; + +import org.apache.commons.logging.Log; + +/** + * Utility class to split or 'tee' two {@link Log} classes. + * + * @since 4.2 + * + * @author Alan Davis + */ +public class LogTee extends LogAdapter +{ + protected Log log2; + + public LogTee(Log log1, Log log2) + { + super(log1); + this.log2 = log2; + } + + @Override + public void trace(Object arg0, Throwable arg1) + { + log.trace(arg0, arg1); + log2.trace(arg0, arg1); + } + + @Override + public void debug(Object arg0, Throwable arg1) + { + log.debug(arg0, arg1); + log2.debug(arg0, arg1); + } + + @Override + public void info(Object arg0, Throwable arg1) + { + log.info(arg0, arg1); + log2.info(arg0, arg1); + } + + @Override + public void warn(Object arg0, Throwable arg1) + { + log.warn(arg0, arg1); + log2.warn(arg0, arg1); + } + + @Override + public void error(Object arg0, Throwable arg1) + { + log.error(arg0, arg1); + log2.error(arg0, arg1); + } + + @Override + public void fatal(Object arg0, Throwable arg1) + { + log.fatal(arg0, arg1); + log2.fatal(arg0, arg1); + } + + @Override + public boolean isTraceEnabled() + { + return log.isTraceEnabled() || log2.isTraceEnabled(); + } + + @Override + public boolean isDebugEnabled() + { + return log.isDebugEnabled() || log2.isDebugEnabled(); + } + + @Override + public boolean isInfoEnabled() + { + return log.isInfoEnabled() || log2.isInfoEnabled(); + } + + @Override + public boolean isWarnEnabled() + { + return log.isWarnEnabled() || log2.isWarnEnabled(); + } + + @Override + public boolean isErrorEnabled() + { + return log.isErrorEnabled() || log2.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() + { + return log.isFatalEnabled() || log2.isFatalEnabled(); + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/LogUtil.java b/src/main/java/org/alfresco/util/LogUtil.java new file mode 100644 index 0000000000..515a2938f9 --- /dev/null +++ b/src/main/java/org/alfresco/util/LogUtil.java @@ -0,0 +1,102 @@ +/* + * 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 org.springframework.extensions.surf.util.I18NUtil; +import org.apache.commons.logging.Log; + +/** + * Utility class to assist with I18N of log messages. + *

+ * Calls to this class should still be wrapped with the appropriate log level checks: + *

+ * if (logger.isDebugEnabled())
+ * {
+ *     LogUtil.debug(logger, MSG_EXECUTING_STATEMENT, sql);
+ * }
+ * 
+ * + * @see org.springframework.extensions.surf.util.I18NUtil + * @since 2.1 + * + * @author Derek Hulley + */ +public class LogUtil +{ + /** + * Log an I18Nized message to DEBUG. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void debug(Log logger, String messageKey, Object ... args) + { + logger.debug(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to INFO. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void info(Log logger, String messageKey, Object ... args) + { + logger.info(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to WARN. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void warn(Log logger, String messageKey, Object ... args) + { + logger.warn(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to ERROR. + * + * @param logger the logger to use + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void error(Log logger, String messageKey, Object ... args) + { + logger.error(I18NUtil.getMessage(messageKey, args)); + } + + /** + * Log an I18Nized message to ERROR with a given source error. + * + * @param logger the logger to use + * @param e the exception cause of the issue + * @param messageKey the message key + * @param args the required message arguments + */ + public static final void error(Log logger, Throwable e, String messageKey, Object ... args) + { + logger.error(I18NUtil.getMessage(messageKey, args), e); + } +} diff --git a/src/main/java/org/alfresco/util/MD5.java b/src/main/java/org/alfresco/util/MD5.java new file mode 100644 index 0000000000..aee2761744 --- /dev/null +++ b/src/main/java/org/alfresco/util/MD5.java @@ -0,0 +1,116 @@ +/* + * 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.security.*; + +/** +* The MD5 utility class computes the MD5 digest (aka: "hash") of a block +* of data; an MD5 digest is a 32-char ASCII string. +* +* The synchronized/static function "Digest" is useful for situations where +* lock contention in the application is not expected to be an issue. +* +* The unsynchronized/non-static method "digest" is useful in a +* multi-threaded program that wanted to avoid locking by creating +* an MD5 object for exclusive use by a single thread. +* +* +*
+*  EXAMPLE 1:  Static usage
+*
+*      import org..alfresco.util.MD5;
+*      String x = MD5.Digest("hello".getBytes());
+*
+*
+*  EXAMPLE 2:  Per-thread non-static usage
+*
+*      import org..alfresco.util.MD5;
+*      MD5 md5 = new MD5();
+*      ...
+*      String x = md5.digest("hello".getBytes());
+*
+* 
+*/ +public class MD5 +{ + private static final byte[] ToHex_ = + { '0','1','2','3','4','5','6','7', + '8','9','a','b','c','d','e','f' + }; + + private MessageDigest md5_ = null; + + static private MessageDigest Md5_; + static + { + try { Md5_ = MessageDigest.getInstance("MD5");} // MD5 is supported + catch ( NoSuchAlgorithmException e ) {}; // safe to swallow + }; + + /** + * Constructor for use with the unsynchronized/non-static method + * "digest" method. Note that the "digest" function is not + * thread-safe, so if you want to use it, every thread must create + * its own MD5 instance. If you don't want to bother & are willing + * to deal with the potential for lock contention, use the synchronized + * static "Digest" function instead of creating an instance via this + * constructor. + */ + public MD5() + { + try { md5_ = MessageDigest.getInstance("MD5");} // MD5 is supported + catch ( NoSuchAlgorithmException e ) {}; // safe to swallow + } + + /** + * Thread-safe static digest (hashing) function. + * + * If you want to avoid lock contention, create an instance of MD5 + * per-thead, anc call the unsynchronized method 'digest' instead. + */ + public static synchronized String Digest(byte[] dataToHash) + { + Md5_.update(dataToHash, 0, dataToHash.length); + return HexStringFromBytes( Md5_.digest() ); + } + + /** + * Non-threadsafe MD5 digest (hashing) function + */ + public String digest(byte[] dataToHash) + { + md5_.update(dataToHash, 0, dataToHash.length); + return HexStringFromBytes( md5_.digest() ); + } + + private static String HexStringFromBytes(byte[] b) + { + byte [] hex_bytes = new byte[ b.length * 2 ]; + int i=0,j=0; + + for (i=0; i < b.length; i++) + { + hex_bytes[j] = ToHex_[ ( b[i] & 0x000000F0 ) >> 4 ] ; + hex_bytes[j+1] = ToHex_[ b[i] & 0x0000000F ]; + j+=2; + } + return new String( hex_bytes ); + } +} diff --git a/src/main/java/org/alfresco/util/MaxSizeMap.java b/src/main/java/org/alfresco/util/MaxSizeMap.java new file mode 100644 index 0000000000..bc402bca39 --- /dev/null +++ b/src/main/java/org/alfresco/util/MaxSizeMap.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.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Map that ejects the last recently accessed or inserted element(s) to keep the size to a specified maximum. + * + * @param + * Key + * @param + * Value + */ +public class MaxSizeMap extends LinkedHashMap +{ + private static final long serialVersionUID = 3753219027867262507L; + + private final int maxSize; + + /** + * @param maxSize maximum size of the map. + * @param accessOrder true for access-order, false for insertion-order. + */ + public MaxSizeMap(int maxSize, boolean accessOrder) + { + super(maxSize * 2, 0.75f, accessOrder); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) + { + return super.size() > this.maxSize; + } +} diff --git a/src/main/java/org/alfresco/util/OneToManyBiMap.java b/src/main/java/org/alfresco/util/OneToManyBiMap.java new file mode 100644 index 0000000000..5d49ec19ca --- /dev/null +++ b/src/main/java/org/alfresco/util/OneToManyBiMap.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * An extension of org.alfresco.util.OneToManyMap that stores the + * inverse mapping from a value to its key. + * + * @author Nick Smith + */ +public interface OneToManyBiMap extends OneToManyMap +{ + + /** + * Returns the key, if any, for the specified value. If the + * specified value does not exist within the map then this method returns + * null. + * + * @param value + * @return The key to the specified value or null. + */ + public abstract K getKey(V value); + + /** + * Removes the specified value from the OneToManyBiMap. If this was the only value associated with the key to this value, then the key is also removed. + * + * @param value The value to be removed. + * @return The key that is associated with the value to be removed. + */ + public abstract K removeValue(V value); + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/OneToManyHashBiMap.java b/src/main/java/org/alfresco/util/OneToManyHashBiMap.java new file mode 100644 index 0000000000..57dd0937dd --- /dev/null +++ b/src/main/java/org/alfresco/util/OneToManyHashBiMap.java @@ -0,0 +1,168 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author Nick Smith + */ +public class OneToManyHashBiMap implements Map>, OneToManyBiMap +{ + // The 'forward' map. + private OneToManyHashMap map = new OneToManyHashMap(); + + // The inverse map. + private Map inverse = new HashMap(); + + public void clear() + { + map.clear(); + inverse.clear(); + } + + public boolean containsKey(Object key) + { + return map.containsKey(key); + } + + public boolean containsValue(Object value) + { + return map.containsValue(value); + } + + public boolean containsSingleValue(V value) + { + return inverse.containsKey(value); + } + + public Set>> entrySet() + { + return map.entrySet(); + } + + public Set> entries() + { + return map.entries(); + } + + public Set get(Object key) + { + return map.get(key); + } + + /* + * @see org.alfresco.util.OneToManyBiMap#getKey(V) + */ + public K getKey(V value) + { + return inverse.get(value); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public Set keySet() + { + return map.keySet(); + } + + public Set put(K key, Set values) + { + map.put(key, values); + for (V value : values) + { + inverse.put(value, key); + } + return null; + } + + public V putSingleValue(K key, V value) + { + inverse.put(value, key); + return map.putSingleValue(key, value); + } + + public void putAll(Map> m) + { + map.putAll(m); + for (Entry> entry : m.entrySet()) + { + K key = entry.getKey(); + for (V value : entry.getValue()) + { + inverse.put(value, key); + } + } + } + + public void putAllSingleValues(Map m) + { + map.putAllSingleValues(m); + for (Entry entry : m.entrySet()) + { + inverse.put(entry.getValue(), entry.getKey()); + } + } + + public Set remove(Object key) + { + Set values = map.remove(key); + for (V value : values) + { + inverse.remove(value); + } + return values; + } + + /* + * @see org.alfresco.util.OneToManyBiMap#removeValue(V) + */ + public K removeValue(V value) + { + K key = inverse.remove(value); + Set values = map.get(key); + values.remove(value); + if (values.size() == 0) map.remove(key); + return key; + } + + public int size() + { + return map.size(); + } + + public Collection> values() + { + return map.values(); + } + + public Collection flatValues() + { + return Collections.unmodifiableCollection(inverse.keySet()); + } + +} diff --git a/src/main/java/org/alfresco/util/OneToManyHashMap.java b/src/main/java/org/alfresco/util/OneToManyHashMap.java new file mode 100644 index 0000000000..da13105ca1 --- /dev/null +++ b/src/main/java/org/alfresco/util/OneToManyHashMap.java @@ -0,0 +1,190 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +/** + * @author Nick Smith + */ +public class OneToManyHashMap implements Map>, OneToManyMap +{ + //Delegate map. + private final Map> map = new HashMap>(); + + public void clear() + { + map.clear(); + } + + public boolean containsKey(Object key) + { + return map.containsKey(key); + } + + public boolean containsValue(Object value) + { + return map.containsValue(value); + } + + /* + * @see org.alfresco.util.OneToManyMap#containsSingleValue(V) + */ + public boolean containsSingleValue(V value) + { + Collection> values = map.values(); + for (Set set : values) + { + if (set.contains(value)) return true; + + } + return false; + } + + public Set>> entrySet() + { + return map.entrySet(); + } + + /* + * @see org.alfresco.util.OneToManyMap#entries() + */ + public Set> entries() + { + Set> entries = new HashSet>(); + for (Entry> entry : map.entrySet()) + { + final K key = entry.getKey(); + final Set values = entry.getValue(); + for (final V value : values) + { + entries.add(new Entry() + { + + public K getKey() + { + return key; + } + + public V getValue() + { + return value; + } + + // Not Thread-safe! + public V setValue(V newValue) + { + throw new UnsupportedOperationException( + "Cannot modify the entries returned by " + + OneToManyHashMap.class.getName() + ".entries()!"); + } + }); + } + } + return entries; + } + + public Set get(Object key) + { + Set set = map.get(key); + if (set == null) set = new HashSet(); + return Collections.unmodifiableSet(set); + } + + public boolean isEmpty() + { + return map.isEmpty(); + } + + public Set keySet() + { + return map.keySet(); + } + + public Set put(K key, Set value) + { + return map.put(key, value); + } + + /* + * @see org.alfresco.util.OneToManyMap#putSingleValue(K, V) + */ + public V putSingleValue(K key, V value) + { + Set values = map.get(key); + if (values == null) + { + values = new HashSet(); + map.put(key, values); + } + values.add(value); + return value; + } + + public void putAll(Map> m) + { + map.putAll(m); + } + + /* + * @see org.alfresco.util.OneToManyMap#putAllSingleValues(java.util.Map) + */ + public void putAllSingleValues(Map m) + { + for (Entry entry : m.entrySet()) + { + putSingleValue(entry.getKey(), entry.getValue()); + } + } + + public Set remove(Object key) + { + return map.remove(key); + } + + public int size() + { + return map.size(); + } + + public Collection> values() + { + return map.values(); + } + + /* + * @see org.alfresco.util.OneToManyMap#flatValues() + */ + public Collection flatValues() + { + LinkedList flatValues = new LinkedList(); + for (Set values : map.values()) + { + flatValues.addAll(values); + } + return flatValues; + } +} diff --git a/src/main/java/org/alfresco/util/OneToManyMap.java b/src/main/java/org/alfresco/util/OneToManyMap.java new file mode 100644 index 0000000000..e678f2c43b --- /dev/null +++ b/src/main/java/org/alfresco/util/OneToManyMap.java @@ -0,0 +1,96 @@ +/* + * 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.Collection; +import java.util.Map; +import java.util.Set; + +/** + * An extension of java.util.Map that represents a mapping + * from a key to a set of values. In addition to the standard + * java.util.Map methods this interface also provides several useful + * methods for directly accessing the values rather than having to access values + * via a java.util.Set + * + * @author Nick Smith + */ +public interface OneToManyMap extends Map> +{ + /** + * This method returns true if any of the value sets in the + * OneToManyMap contains an equivalent object to the value + * parameter, where equivalence is determined using the + * equals(Object) method. + * + * @param value The value being searched for. + * @return Returns true if any of the value sets contains a + * matching value, otherwise returns false + */ + public abstract boolean containsSingleValue(V value); + + /** + * This method is similar to the java.util.Map.entrySet() + * method, however the entries returned map from a key to a value, rather + * than from a key(K) to a value(V) rather than + * froma key(K) to a set of values(Set<V>).
+ * Note that the entries returned by this method do not support the method + * java.util.Map.Entry.setValue(V). + * + * @return The + * Set<Entry<K, V>> representing all the key-value pairs in the ManyToOneMap. + */ + public abstract Set> entries(); + + /** + * This method is similar to the method java.util.Map.put(K, V) + * , however it allows the user to add a single value to the map rather than + * adding a java.util.Set containing one or more values. If the + * specified key already has a set of values associated with it then the new + * value is added to this set. Otherwise a new set is created and the new + * value is added to that. + * + * @param key + * @param value + * @return returns the newly added value. + */ + public abstract V putSingleValue(K key, V value); + + /** + * This method is similar to java.utilMap.putAll(Map m), + * however the map specified is from keys to values instead of keys to sets + * of values. + * + * @param m A map containing the key-value mappings to be added to the + * ManyToOneMap. + */ + public abstract void putAllSingleValues(Map m); + + /** + * Returns a Collection of all the values in the map. Unlike + * values() the values are in a single flattened + * Collection<V> rather than a + * Collection<Set<V>>. + * + * @return All the values in the map as a flattened Collection. + */ + public abstract Collection flatValues(); + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/PackageMarker.java b/src/main/java/org/alfresco/util/PackageMarker.java new file mode 100644 index 0000000000..c851c7947b --- /dev/null +++ b/src/main/java/org/alfresco/util/PackageMarker.java @@ -0,0 +1,22 @@ +package org.alfresco.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This empty marker annotation is added to make sure .class files are actually generated + * for package-info.java files. This allow to speed up incremental compilation time, + * so that each build tool will be able to properly detect differences between sources and + * .class compiled files. + * + * See https://jira.codehaus.org/browse/MCOMPILER-205?focusedCommentId=326795&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-326795 + * for more details + * + * NOTE: This annotation should be added in each package-info.java file + * @author Gabriele Columbro + * + */ +@Retention(RetentionPolicy.SOURCE) +public @interface PackageMarker { + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/Pair.java b/src/main/java/org/alfresco/util/Pair.java new file mode 100644 index 0000000000..60e5344692 --- /dev/null +++ b/src/main/java/org/alfresco/util/Pair.java @@ -0,0 +1,142 @@ +/* + * 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.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.io.ObjectInputStream.GetField; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Utility class for containing two things that aren't like each other + */ +@AlfrescoPublicApi +public final class Pair implements Serializable +{ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static final Pair NULL_PAIR = new Pair(null, null); + + @SuppressWarnings("unchecked") + public static final Pair nullPair() + { + return NULL_PAIR; + } + + private static final long serialVersionUID = -7406248421185630612L; + + /** + * The first member of the pair. + */ + private F first; + + /** + * The second member of the pair. + */ + private S second; + + /** + * Make a new one. + * + * @param first The first member. + * @param second The second member. + */ + public Pair(F first, S second) + { + this.first = first; + this.second = second; + } + + /** + * Get the first member of the tuple. + * @return The first member. + */ + public final F getFirst() + { + return first; + } + + /** + * Get the second member of the tuple. + * @return The second member. + */ + public final S getSecond() + { + return second; + } + + public final void setFirst(F first) + { + this.first = first; + } + + public final void setSecond(S second) + { + this.second = second; + } + + @Override + public boolean equals(Object other) + { + if (this == other) + { + return true; + } + if (other == null || !(other instanceof Pair)) + { + return false; + } + Pair o = (Pair)other; + return EqualsHelper.nullSafeEquals(this.first, o.first) && + EqualsHelper.nullSafeEquals(this.second, o.second); + } + + @Override + public int hashCode() + { + return (first == null ? 0 : first.hashCode()) + (second == null ? 0 : second.hashCode()); + } + + @Override + public String toString() + { + return "(" + first + ", " + second + ")"; + } + + /** + * Ensure that previously-serialized instances don't fail due to the member name change. + */ + @SuppressWarnings("unchecked") + private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException + { + GetField fields = is.readFields(); + if (fields.defaulted("first")) + { + // This is a pre-V3.3 + this.first = (F) fields.get("fFirst", null); + this.second = (S) fields.get("fSecond", null); + } + else + { + this.first = (F) fields.get("first", null); + this.second = (S) fields.get("second", null); + } + } +} diff --git a/src/main/java/org/alfresco/util/ParameterCheck.java b/src/main/java/org/alfresco/util/ParameterCheck.java new file mode 100644 index 0000000000..efc932e0b7 --- /dev/null +++ b/src/main/java/org/alfresco/util/ParameterCheck.java @@ -0,0 +1,76 @@ +/* + * 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.Collection; + +/** + * Utility class to perform various common parameter checks + * + * @author gavinc + */ +public final class ParameterCheck +{ + /** + * Checks that the parameter with the given name has content i.e. it is not + * null + * + * @param strParamName Name of parameter to check + * @param object Value of the parameter to check + */ + public static final void mandatory(final String strParamName, final Object object) + { + // check that the object is not null + if (object == null) + { + throw new IllegalArgumentException(strParamName + " is a mandatory parameter"); + } + } + + /** + * Checks that the string parameter with the given name has content i.e. it + * is not null and not zero length + * + * @param strParamName Name of parameter to check + * @param strParamValue Value of the parameter to check + */ + public static final void mandatoryString(final String strParamName, final String strParamValue) + { + // check that the given string value has content + if (strParamValue == null || strParamValue.length() == 0) + { + throw new IllegalArgumentException(strParamName + " is a mandatory parameter"); + } + } + + /** + * Checks that the collection parameter contains at least one item. + * + * @param strParamName Name of parameter to check + * @param coll collection to check + */ + public static final void mandatoryCollection(final String strParamName, final Collection coll) + { + if (coll == null || coll.size() == 0) + { + throw new IllegalArgumentException(strParamName + " collection must contain at least one item"); + } + } + +} diff --git a/src/main/java/org/alfresco/util/PathMapper.java b/src/main/java/org/alfresco/util/PathMapper.java new file mode 100644 index 0000000000..3b537ebcc4 --- /dev/null +++ b/src/main/java/org/alfresco/util/PathMapper.java @@ -0,0 +1,327 @@ +/* + * 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.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A component that maps source data paths to target data paths. + *

+ * This class caches results and is thread-safe. + * + * @author Derek Hulley + * @since 3.2 + */ +public class PathMapper +{ + private static final Log logger = LogFactory.getLog(PathMapper.class); + + private final ReentrantReadWriteLock.ReadLock readLock; + private final ReentrantReadWriteLock.WriteLock writeLock; + + private boolean locked; + /** + * Used to lookup path translations + */ + private final Map> pathMaps; + /** + * Cached fine-grained path translations (derived data) + */ + private final Map> derivedPathMaps; + private final Map> derivedPathMapsPartial; + + /** + * Default constructor + */ + public PathMapper() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + + pathMaps = new HashMap>(37); + derivedPathMaps = new HashMap>(127); + derivedPathMapsPartial = new HashMap>(127); + } + + /** + * Locks the instance against further modifications. + */ + public void lock() + { + writeLock.lock(); + try + { + locked = true; + } + finally + { + writeLock.unlock(); + } + } + + public void clear() + { + writeLock.lock(); + try + { + if (locked) + { + throw new IllegalStateException("The PathMapper has been locked against further changes"); + } + pathMaps.clear(); + derivedPathMaps.clear(); + derivedPathMapsPartial.clear(); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Add a path mapping. + * + * @param sourcePath the source path + * @param targetPath the target path + */ + public void addPathMap(String sourcePath, String targetPath) + { + writeLock.lock(); + try + { + if (locked) + { + throw new IllegalStateException("The PathMapper has been locked against further changes"); + } + derivedPathMaps.clear(); + derivedPathMapsPartial.clear(); + Set targetPaths = pathMaps.get(sourcePath); + if (targetPaths == null) + { + targetPaths = new HashSet(5); + pathMaps.put(sourcePath, targetPaths); + } + targetPaths.add(targetPath); + } + finally + { + writeLock.unlock(); + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Added path map: " + sourcePath + " --> " + targetPath); + } + } + + /** + * Gets the remapped paths for the given source path, excluding any derivative + * paths i.e. does exact path matching only. + * + * @param sourcePath the source path + * @return Returns the target paths (never null) + */ + public Set getMappedPaths(String sourcePath) + { + readLock.lock(); + try + { + Set targetPaths = derivedPathMaps.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + } + finally + { + readLock.unlock(); + } + // We didn't find anything, so update the cache + writeLock.lock(); + try + { + return updateMappedPaths(sourcePath); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Gets the remapped paths for the given source path, including any derivative + * paths i.e. does partial path matching. + * + * @param sourcePath the source path + * @return Returns the target paths (never null) + */ + public Set getMappedPathsWithPartialMatch(String sourcePath) + { + readLock.lock(); + try + { + Set targetPaths = derivedPathMapsPartial.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + } + finally + { + readLock.unlock(); + } + // We didn't find anything, so update the cache + writeLock.lock(); + try + { + return updateMappedPathsPartial(sourcePath); + } + finally + { + writeLock.unlock(); + } + } + + public boolean isEmpty() + { + readLock.lock(); + try + { + return pathMaps.isEmpty(); + } + finally + { + readLock.unlock(); + } + } + + private Set updateMappedPaths(String sourcePath) + { + // Do a double-check + Set targetPaths = derivedPathMaps.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + targetPaths = new HashSet(17); + derivedPathMaps.put(sourcePath, targetPaths); + // Now remap it and build the target values + for (Map.Entry> entry : pathMaps.entrySet()) + { + String mapSourcePath = entry.getKey(); + Set mapTargetPaths = entry.getValue(); + // If the map source matches the source, then it's simple + if (mapSourcePath.equals(sourcePath)) + { + targetPaths.addAll(mapTargetPaths); + continue; + } + // It is not an exact match, so check if it starts with the source + int index = sourcePath.indexOf(mapSourcePath); + if (index != 0) + { + // It doesn't match the start, so ignore it + continue; + } + // Replace the beginning with the mapped targets + for (String mapTargetPath : mapTargetPaths) + { + if (mapTargetPath.equals(mapSourcePath)) + { + // Direct mapping, so shortcut + targetPaths.add(sourcePath); + } + else + { + String newPath = (mapTargetPath + sourcePath.substring(mapSourcePath.length())); + targetPaths.add(newPath); + } + } + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Cached path mapping: \n" + + " Source: " + sourcePath + "\n" + + " Targets: " + targetPaths); + } + return targetPaths; + } + + private Set updateMappedPathsPartial(String sourcePath) + { + // Do a double-check + Set targetPaths = derivedPathMapsPartial.get(sourcePath); + if (targetPaths != null) + { + return targetPaths; + } + targetPaths = new HashSet(17); + derivedPathMapsPartial.put(sourcePath, targetPaths); + // Now remap it and build the target values + for (Map.Entry> entry : pathMaps.entrySet()) + { + String mapSourcePath = entry.getKey(); + Set mapTargetPaths = entry.getValue(); + // It is not an exact match, so check if it starts with the source + int index = mapSourcePath.indexOf(sourcePath); + if (index != 0) + { + // It doesn't match the start, so ignore it + continue; + } + // Record the partial matches + targetPaths.addAll(mapTargetPaths); + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug( + "Cached path mapping (partial): \n" + + " Source: " + sourcePath + "\n" + + " Targets: " + targetPaths); + } + return targetPaths; + } + + public Map convertMap(Map valueMap) + { + Map resultMap = new HashMap(valueMap.size() * 2 + 1); + for (Map.Entry entry : valueMap.entrySet()) + { + String path = entry.getKey(); + V value = entry.getValue(); + Set mappedPaths = getMappedPaths(path); + for (String mappedPath : mappedPaths) + { + resultMap.put(mappedPath, value); + } + } + return resultMap; + } +} diff --git a/src/main/java/org/alfresco/util/PatternFilter.java b/src/main/java/org/alfresco/util/PatternFilter.java new file mode 100644 index 0000000000..1ec0a05c43 --- /dev/null +++ b/src/main/java/org/alfresco/util/PatternFilter.java @@ -0,0 +1,69 @@ +/* + * 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.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Matches a path against a set of regular expression filters + * + */ +public class PatternFilter +{ + private List patterns; + + /** + * A list of regular expressions that represent patterns of files. + * + * @param regexps list of regular expressions + * + * @see String#matches(java.lang.String) + */ + public void setPatterns(List regexps) + { + this.patterns = new ArrayList(regexps.size()); + for(String regexp : regexps) + { + this.patterns.add(Pattern.compile(regexp)); + } + } + + public boolean isFiltered(String path) + { + // check against all the regular expressions + boolean matched = false; + + for (Pattern regexp : patterns) + { + if(!regexp.matcher(path).matches()) + { + // it is not a match - try next one + continue; + } + else + { + matched = true; + break; + } + } + + return matched; + } +} diff --git a/src/main/java/org/alfresco/util/PropertyCheck.java b/src/main/java/org/alfresco/util/PropertyCheck.java new file mode 100644 index 0000000000..37c4158a0b --- /dev/null +++ b/src/main/java/org/alfresco/util/PropertyCheck.java @@ -0,0 +1,105 @@ +/* + * 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 org.alfresco.error.AlfrescoRuntimeException; + +/** + * Helper class for for use when checking properties. This class uses + * I18N for its messages. + * + * @author Derek Hulley + */ +public class PropertyCheck +{ + public static final String ERR_PROPERTY_NOT_SET = "system.err.property_not_set"; + + /** + * Checks that the property with the given name is not null. + * + * @param target the object on which the property must have been set + * @param propertyName the name of the property + * @param value of the property value + */ + public static void mandatory(Object target, String propertyName, Object value) + { + if (value == null) + { + throw new AlfrescoRuntimeException( + ERR_PROPERTY_NOT_SET, + new Object[] {propertyName, target, target.getClass()}); + } + } + + /** + * Checks that the given string is not: + *

    + *
  • null
  • + *
  • empty
  • + *
  • a placeholder of form '${...}'
  • + *
+ * + * @param value the value to check + * @return true if the checks all pass + */ + public static boolean isValidPropertyString(String value) + { + if (value == null || value.length() == 0) + { + return false; + } + if (value.startsWith("${") && value.endsWith("}")) + { + return false; + } + else + { + return true; + } + } + + /** + * Dig out the property name from a placeholder-style property of form + * ${prop.name}, which will yield prop.name. If the placeholders + * are not there, the value is returned directly. null values are + * not allowed, but empty strings are. + * + * @param value The property with or without property placeholders + * @return Returns the core property without the property placeholders + * ${ and }. + * @throws IllegalArgumentException if the value is null + */ + public static String getPropertyName(String value) + { + if (value == null) + { + throw new IllegalArgumentException("'value' is a required argument."); + } + if (!value.startsWith("${")) + { + return value; + } + if (!value.endsWith("}")) + { + return value; + } + int strLen = value.length(); + return value.substring(2, strLen - 1); + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java b/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java new file mode 100644 index 0000000000..1b9a9928da --- /dev/null +++ b/src/main/java/org/alfresco/util/ReadWriteLockExecuter.java @@ -0,0 +1,118 @@ +/* + * 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.concurrent.locks.ReentrantReadWriteLock; + +/** + * Utility object that wraps read and write methods within the context of a + * {@link ReentrantReadWriteLock}. The callback's methods are best-suited + * to fetching values from a cache or protecting members that need lazy + * initialization. + *

+ * Client code should construct an instance of this class for each resource + * (or set of resources) that need to be protected. + * + * @author Derek Hulley + * @since 3.4 + */ +public abstract class ReadWriteLockExecuter +{ + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + /** + * Default constructor + */ + public ReadWriteLockExecuter() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + } + + /** + * Execute the read-only part of the work. + * + * @return Returns a value of interest or null if + * the {@link #getWithWriteLock()} method must be + * called + * @throws Throwable all checked exceptions are wrapped in a RuntimeException + */ + protected abstract T getWithReadLock() throws Throwable; + + /** + * Execute the write part of the work. + *

+ * NOTE: It is important to perform a double-check on the resource + * before assuming it is not null; there is a window between the {@link #getWithReadLock()} + * and the {@link #getWithWriteLock()} during which another thread may have populated + * the resource of interest. + * + * @return Returns the value of interest of null + * @throws Throwable all checked exceptions are wrapped in a RuntimeException + */ + protected abstract T getWithWriteLock() throws Throwable; + + public T execute() + { + T ret = null; + readLock.lock(); + try + { + ret = this.getWithReadLock(); + // We do the null check here so that less time is spent outside of the lock + if (ret != null) + { + return ret; + } + } + catch (RuntimeException e) + { + throw e; + } + catch (Throwable e) + { + throw new RuntimeException("Exception during 'getWithReadLock'", e); + } + finally + { + readLock.unlock(); + } + // If we got here, then we didn't get a result and need to go for the write lock + writeLock.lock(); + try + { + // The return value is not of interest to us + return this.getWithWriteLock(); + } + catch (RuntimeException e) + { + throw e; + } + catch (Throwable e) + { + throw new RuntimeException("Exception during 'getWithWriteLock'", e); + } + finally + { + writeLock.unlock(); + } + } +} diff --git a/src/main/java/org/alfresco/util/ReflectionHelper.java b/src/main/java/org/alfresco/util/ReflectionHelper.java new file mode 100644 index 0000000000..6d06ea326b --- /dev/null +++ b/src/main/java/org/alfresco/util/ReflectionHelper.java @@ -0,0 +1,192 @@ +/* + * 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.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Static Helper methods for instantiating objects from reflection. + * + * @author muzquiano + */ +public class ReflectionHelper +{ + private static Log logger = LogFactory.getLog(ReflectionHelper.class); + + private ReflectionHelper() + { + } + + /** + * Constructs a new object for the given class name. + * The construction takes no arguments. + * + * If an exception occurs during construction, null is returned. + * + * All exceptions are written to the Log instance for this class. + * + * @param className String + * @return Object + */ + public static Object newObject(String className) + { + Object o = null; + + try + { + Class clazz = Class.forName(className); + o = clazz.newInstance(); + } + catch (ClassNotFoundException cnfe) + { + logger.debug(cnfe); + } + catch (InstantiationException ie) + { + logger.debug(ie); + } + catch (IllegalAccessException iae) + { + logger.debug(iae); + } + return o; + } + + /** + * Constructs a new object for the given class name and with the given + * arguments. The arguments must be specified in terms of their Class[] + * types and their Object[] values. + * + * Example: + * + * String s = newObject("java.lang.String", new Class[] { String.class}, + * new String[] { "test"}); + * + * is equivalent to: + * + * String s = new String("test"); + * + * If an exception occurs during construction, null is returned. + * + * All exceptions are written to the Log instance for this class. + + * @param className String + * @param argTypes Class[] + * @param args Object[] + * @return Object + */ + public static Object newObject(String className, Class[] argTypes, Object[] args) + { + /** + * We have some mercy here - if they called and did not pass in any + * arguments, then we will call through to the pure newObject() method. + */ + if (args == null || args.length == 0) + { + return newObject(className); + } + + /** + * Try to build the object + * + * If an exception occurs, we log it and return null. + */ + Object o = null; + try + { + // base class + Class clazz = Class.forName(className); + + Constructor c = clazz.getDeclaredConstructor(argTypes); + o = c.newInstance(args); + } + catch (ClassNotFoundException cnfe) + { + logger.debug(cnfe); + } + catch (InstantiationException ie) + { + logger.debug(ie); + } + catch (IllegalAccessException iae) + { + logger.debug(iae); + } + catch (NoSuchMethodException nsme) + { + logger.debug(nsme); + } + catch (InvocationTargetException ite) + { + logger.debug(ite); + } + return o; + } + + /** + * Invokes a method on the given object by passing the given arguments + * into the method. + * + * @param obj Object + * @param method String + * @param argTypes Class[] + * @param args Object[] + * @return Object + */ + public static Object invoke(Object obj, String method, Class[] argTypes, Object[] args) + { + if (obj == null || method == null) + { + throw new IllegalArgumentException("Object and Method must be supplied."); + } + + /** + * Try to invoke the method. + * + * If the method is unable to be invoked, we log and return null. + */ + try + { + Method m = obj.getClass().getMethod(method, argTypes); + if(m != null) + { + return m.invoke(obj, args); + } + } + catch(NoSuchMethodException nsme) + { + logger.debug(nsme); + } + catch(IllegalAccessException iae) + { + logger.debug(iae); + } + catch(InvocationTargetException ite) + { + logger.debug(ite); + } + + return null; + } +} diff --git a/src/main/java/org/alfresco/util/SchedulerStarterBean.java b/src/main/java/org/alfresco/util/SchedulerStarterBean.java new file mode 100644 index 0000000000..d236e15fe6 --- /dev/null +++ b/src/main/java/org/alfresco/util/SchedulerStarterBean.java @@ -0,0 +1,66 @@ +/* + * 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 org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.context.ApplicationEvent; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; + +public class SchedulerStarterBean extends AbstractLifecycleBean +{ + protected final static Log log = LogFactory.getLog(SchedulerStarterBean.class); + + private Scheduler scheduler; + + @Override + protected void onBootstrap(ApplicationEvent event) + { + try + { + log.info("Scheduler started"); + scheduler.start(); + } + catch (SchedulerException e) + { + throw new AlfrescoRuntimeException("Scheduler failed to start", e); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // Nothing required + // This is done by the SchedulerFactoryBean.destroy() - DisposableBean + } + + public Scheduler getScheduler() + { + return scheduler; + } + + public void setScheduler(Scheduler scheduler) + { + this.scheduler = scheduler; + } + +} diff --git a/src/main/java/org/alfresco/util/SerializationUtils.java b/src/main/java/org/alfresco/util/SerializationUtils.java new file mode 100644 index 0000000000..0b58e62d74 --- /dev/null +++ b/src/main/java/org/alfresco/util/SerializationUtils.java @@ -0,0 +1,276 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * NOTE: This utility class is a copy of org.apache.commons.lang3.SerializationUtils + * + * Please see http://issues.alfresco.com/jira/browse/ALF-5044 for why this is done. + * + * @author Apache Software Foundation + * @author Daniel L. Rall + * @author Jeff Varszegi + * @author Gary Gregory + * + *

+ * Assists with the serialization process and performs additional functionality + * based on serialization. + *

+ *

+ *

    + *
  • Deep clone using serialization + *
  • Serialize managing finally and IOException + *
  • Deserialize managing finally and IOException + *
+ * + *

+ * This class throws exceptions for invalid null inputs. Each + * method documents its behaviour in more detail. + *

+ * + *

+ * #ThreadSafe# + *

+ * + */ +public class SerializationUtils +{ + + /** + *

+ * SerializationUtils instances should NOT be constructed in standard + * programming. Instead, the class should be used as + * SerializationUtils.clone(object). + *

+ * + *

+ * This constructor is public to permit tools that require a JavaBean + * instance to operate. + *

+ */ + public SerializationUtils() + { + super(); + } + + // Clone + // ----------------------------------------------------------------------- + /** + *

+ * Deep clone an Object using serialization. + *

+ * + *

+ * This is many times slower than writing clone methods by hand on all + * objects in your object graph. However, for complex object graphs, or for + * those that don't support deep cloning this can be a simple alternative + * implementation. Of course all the objects must be + * Serializable. + *

+ * + * @param object + * the Serializable object to clone + * @return the cloned object + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static T clone(T object) + { + /* + * when we serialize and deserialize an object, it is reasonable to + * assume the deserialized object is of the same type as the original + * serialized object + */ + @SuppressWarnings("unchecked") + final T result = (T) deserialize(serialize(object)); + return result; + } + + // Serialize + // ----------------------------------------------------------------------- + /** + *

+ * Serializes an Object to the specified stream. + *

+ * + *

+ * The stream will be closed once the object is written. This avoids the + * need for a finally clause, and maybe also exception handling, in the + * application code. + *

+ * + *

+ * The stream passed in is not buffered internally within this method. This + * is the responsibility of your application if desired. + *

+ * + * @param obj + * the object to serialize to bytes, may be null + * @param outputStream + * the stream to write to, must not be null + * @throws IllegalArgumentException + * if outputStream is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static void serialize(Serializable obj, OutputStream outputStream) + { + if (outputStream == null) + { + throw new IllegalArgumentException("The OutputStream must not be null"); + } + ObjectOutputStream out = null; + try + { + // stream closed in the finally + out = new ObjectOutputStream(outputStream); + out.writeObject(obj); + + } catch (IOException ex) + { + throw new AlfrescoRuntimeException("Failed to serialize", ex); + } finally + { + try + { + if (out != null) + { + out.close(); + } + } catch (IOException ex) + { + // ignore close exception + } + } + } + + /** + *

+ * Serializes an Object to a byte array for + * storage/serialization. + *

+ * + * @param obj + * the object to serialize to bytes + * @return a byte[] with the converted Serializable + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static byte[] serialize(Serializable obj) + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(512); + serialize(obj, baos); + return baos.toByteArray(); + } + + // Deserialize + // ----------------------------------------------------------------------- + /** + *

+ * Deserializes an Object from the specified stream. + *

+ * + *

+ * The stream will be closed once the object is written. This avoids the + * need for a finally clause, and maybe also exception handling, in the + * application code. + *

+ * + *

+ * The stream passed in is not buffered internally within this method. This + * is the responsibility of your application if desired. + *

+ * + * @param inputStream + * the serialized object input stream, must not be null + * @return the deserialized object + * @throws IllegalArgumentException + * if inputStream is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static Object deserialize(InputStream inputStream) + { + if (inputStream == null) + { + throw new IllegalArgumentException("The InputStream must not be null"); + } + ObjectInputStream in = null; + try + { + // stream closed in the finally + in = new ObjectInputStream(inputStream); + return in.readObject(); + + } catch (ClassNotFoundException ex) + { + throw new AlfrescoRuntimeException("Failed to deserialize", ex); + } catch (IOException ex) + { + throw new AlfrescoRuntimeException("Failed to deserialize", ex); + } finally + { + try + { + if (in != null) + { + in.close(); + } + } catch (IOException ex) + { + // ignore close exception + } + } + } + + /** + *

+ * Deserializes a single Object from an array of bytes. + *

+ * + * @param objectData + * the serialized object, must not be null + * @return the deserialized object + * @throws IllegalArgumentException + * if objectData is null + * @throws AlfrescoRuntimeException + * (runtime) if the serialization fails + */ + public static Object deserialize(byte[] objectData) + { + if (objectData == null) + { + throw new IllegalArgumentException("The byte[] must not be null"); + } + ByteArrayInputStream bais = new ByteArrayInputStream(objectData); + return deserialize(bais); + } + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/TempFileProvider.java b/src/main/java/org/alfresco/util/TempFileProvider.java new file mode 100644 index 0000000000..678515de40 --- /dev/null +++ b/src/main/java/org/alfresco/util/TempFileProvider.java @@ -0,0 +1,506 @@ +/* + * 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.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * A helper class that provides temporary files, providing a common point to clean + * them up. + * + *

+ * The contents of ALFRESCO_TEMP_FILE_DIR [%java.io.tmpdir%/Alfresco] are managed by this + * class. Temporary files and directories are cleaned by TempFileCleanerJob so that + * after a delay [default 1 hour] the contents of the alfresco temp dir, + * both files and directories are removed. + * + *

+ * Some temporary files may need to live longer than 1 hour. The temp file provider allows special sub folders which + * are cleaned less frequently. By default, files in the long life folders will remain for 24 hours + * unless cleaned by the application code earlier. + * + *

+ * The other contents of %java.io.tmpdir% are not touched by the cleaner job. + * + *

TempFileCleanerJob Job Data: protectHours, number of hours to keep temporary files, default 1 hour. + * + * @author derekh + * @author mrogers + */ +@AlfrescoPublicApi +public class TempFileProvider +{ + private static final int BUFFER_SIZE = 40 * 1024; + + /** + * subdirectory in the temp directory where Alfresco temporary files will go + */ + public static final String ALFRESCO_TEMP_FILE_DIR = "Alfresco"; + + /** + * The prefix for the long life temporary files. + */ + public static final String ALFRESCO_LONG_LIFE_FILE_DIR = "longLife"; + + /** the system property key giving us the location of the temp directory */ + public static final String SYSTEM_KEY_TEMP_DIR = "java.io.tmpdir"; + + private static final Log logger = LogFactory.getLog(TempFileProvider.class); + + private static int MAX_RETRIES = 3; + + /** + * Static class only + */ + private TempFileProvider() + { + } + + /** + * Get the Java Temp dir e.g. java.io.tempdir + * + * @return Returns the system temporary directory i.e. isDir == true + */ + public static File getSystemTempDir() + { + String systemTempDirPath = System.getProperty(SYSTEM_KEY_TEMP_DIR); + if (systemTempDirPath == null) + { + throw new AlfrescoRuntimeException("System property not available: " + SYSTEM_KEY_TEMP_DIR); + } + File systemTempDir = new File(systemTempDirPath); + if (logger.isDebugEnabled()) + { + logger.debug("Created system temporary directory: " + systemTempDir); + } + return systemTempDir; + } + + /** + * Get the Alfresco temp dir, by defaut %java.io.tempdir%/Alfresco. + * Will create the temp dir on the fly if it does not already exist. + * + * @return Returns a temporary directory, i.e. isDir == true + */ + public static File getTempDir() + { + return getTempDir(ALFRESCO_TEMP_FILE_DIR); + } + + /** + * Get the specified temp dir, %java.io.tempdir%/dirName. + * Will create the temp dir on the fly if it does not already exist. + * + * @param dirName the name of sub-directory in %java.io.tempdir% + * + * @return Returns a temporary directory, i.e. isDir == true + */ + public static File getTempDir(String dirName) + { + File systemTempDir = getSystemTempDir(); + // append the Alfresco directory + File tempDir = new File(systemTempDir, dirName); + // ensure that the temp directory exists + if (tempDir.exists()) + { + // nothing to do + } + else + { + // not there yet + if (!tempDir.mkdirs()) + { + // We didn't create it but perhaps it was made by some other thread + if (!tempDir.exists()) + { + // It's definitely not there + throw new AlfrescoRuntimeException("Failed to create temp directory: " + tempDir); + } + } + else + { + // This thread created it + if (logger.isDebugEnabled()) + { + logger.debug("Created temp directory: " + tempDir); + } + } + } + // done + return tempDir; + } + + /** + * creates a longer living temp dir. Files within the longer living + * temp dir will not be garbage collected as soon as "normal" temporary files. + * By default long life temp files will live for for 24 hours rather than 1 hour. + *

+ * Code using the longer life temporary files should be careful to clean up since + * abuse of this feature may result in out of memory/disk space errors. + * @param key can be blank in which case the system will generate a folder to be used by all processes + * or can be used to create a unique temporary folder name for a particular process. At the end of the process + * the client can simply delete the entire temporary folder. + * @return the long life temporary directory + */ + public static File getLongLifeTempDir(String key) + { + /** + * Long life temporary directories have a prefix at the start of the + * folder name. + */ + String folderName = ALFRESCO_LONG_LIFE_FILE_DIR + "_" + key; + + File tempDir = getTempDir(); + + // append the Alfresco directory + File longLifeDir = new File(tempDir, folderName); + // ensure that the temp directory exists + + if (longLifeDir.exists()) + { + if (logger.isDebugEnabled()) + { + logger.debug("Already exists: " + longLifeDir); + } + // nothing to do + return longLifeDir; + } + else + { + /** + * We need to create a temporary directory + * + * We may have a race condition here if more than one thread attempts to create + * the temp dir. + * + * mkdirs can't be synchronized + * See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4742723 + */ + for(int retry = 0; retry < MAX_RETRIES; retry++) + { + boolean created = longLifeDir.mkdirs(); + + if (created) + { + // Yes we created the temp dir + if (logger.isDebugEnabled()) + { + logger.debug("Created long life temp directory: " + longLifeDir); + } + return longLifeDir; + } + else + { + if(longLifeDir.exists()) + { + // created by another thread, but that's O.K. + if (logger.isDebugEnabled()) + { + logger.debug("Another thread created long life temp directory: " + longLifeDir); + } + return longLifeDir; + } + } + } + } + throw new AlfrescoRuntimeException("Failed to create temp directory: " + longLifeDir); + } + + public static File createTempFile(InputStream in, String namePrefix, String nameSufix) throws Exception + { + if (null == in) + { + return null; + } + + File file = createTempFile(namePrefix, nameSufix); + OutputStream out = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE); + try + { + byte[] buffer = new byte[BUFFER_SIZE]; + int i; + while ((i = in.read(buffer)) > -1) + { + out.write(buffer, 0, i); + } + } + catch (Exception e) + { + file.delete(); + throw e; + } + finally + { + in.close(); + out.flush(); + out.close(); + } + + return file; + } + + /** + * Is this a long life folder ? + * @param file + * @return true, this is a long life folder. + */ + private static boolean isLongLifeTempDir(File file) + { + if(file.isDirectory()) + { + if(file.getName().startsWith(ALFRESCO_LONG_LIFE_FILE_DIR)) + { + return true; + } + else + { + return false; + } + } + return false; + } + + /** + * Create a temp file in the alfresco temp dir. + * + * @return Returns a temp File that will be located in the + * Alfresco subdirectory of the default temp directory + * + * @see #ALFRESCO_TEMP_FILE_DIR + * @see File#createTempFile(java.lang.String, java.lang.String) + */ + public static File createTempFile(String prefix, String suffix) + { + File tempDir = TempFileProvider.getTempDir(); + // we have the directory we want to use + return createTempFile(prefix, suffix, tempDir); + } + + /** + * @return Returns a temp File that will be located in the + * given directory + * + * @see #ALFRESCO_TEMP_FILE_DIR + * @see File#createTempFile(java.lang.String, java.lang.String) + */ + public static File createTempFile(String prefix, String suffix, File directory) + { + try + { + File tempFile = File.createTempFile(prefix, suffix, directory); + if (logger.isDebugEnabled()) + { + logger.debug("Creating tmp file: " + tempFile); + } + return tempFile; + } catch (IOException e) + { + throw new AlfrescoRuntimeException("Failed to created temp file: \n" + + " prefix: " + prefix + "\n" + + " suffix: " + suffix + "\n" + + " directory: " + directory, + e); + } + } + + /** + * Cleans up all Alfresco temporary files that are older than the + * given number of hours. Subdirectories are emptied as well and all directories + * below the primary temporary subdirectory are removed. + *

+ * The job data must include a property protectHours, which is the + * number of hours to protect a temporary file from deletion since its last + * modification. + * + * @author Derek Hulley + */ + @AlfrescoPublicApi + public static class TempFileCleanerJob implements Job + { + public static final String KEY_PROTECT_HOURS = "protectHours"; + public static final String KEY_DIRECTORY_NAME = "directoryName"; + + /** + * Gets a list of all files in the {@link TempFileProvider#ALFRESCO_TEMP_FILE_DIR temp directory} + * and deletes all those that are older than the given number of hours. + */ + public void execute(JobExecutionContext context) throws JobExecutionException + { + // get the number of hours to protect the temp files + String strProtectHours = (String) context.getJobDetail().getJobDataMap().get(KEY_PROTECT_HOURS); + if (strProtectHours == null) + { + throw new JobExecutionException("Missing job data: " + KEY_PROTECT_HOURS); + } + int protectHours = -1; + try + { + protectHours = Integer.parseInt(strProtectHours); + } + catch (NumberFormatException e) + { + throw new JobExecutionException("Invalid job data " + KEY_PROTECT_HOURS + ": " + strProtectHours); + } + if (protectHours < 0 || protectHours > 8760) + { + throw new JobExecutionException("Hours to protect temp files must be 0 <= x <= 8760"); + } + + String directoryName = (String) context.getJobDetail().getJobDataMap().get(KEY_DIRECTORY_NAME); + + if (directoryName == null) + { + directoryName = ALFRESCO_TEMP_FILE_DIR; + } + + long now = System.currentTimeMillis(); + long aFewHoursBack = now - (3600L * 1000L * protectHours); + + long aLongTimeBack = now - (24 * 3600L * 1000L); + + File tempDir = TempFileProvider.getTempDir(directoryName); + int count = removeFiles(tempDir, aFewHoursBack, aLongTimeBack, false); // don't delete this directory + // done + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + count + " files from temp directory: " + tempDir); + } + } + + /** + * Removes all temporary files created before the given time. + *

+ * The delete will cascade down through directories as well. + * + * @param removeBefore only remove files created before this time + * @return Returns the number of files removed + */ + public static int removeFiles(long removeBefore) + { + File tempDir = TempFileProvider.getTempDir(); + return removeFiles(tempDir, removeBefore, removeBefore, false); + } + + /** + * @param directory the directory to clean out - the directory will optionally be removed + * @param removeBefore only remove files created before this time + * @param removeDir true if the directory must be removed as well, otherwise false + * @return Returns the number of files removed + */ + private static int removeFiles(File directory, long removeBefore, long longLifeBefore, boolean removeDir) + { + if (!directory.isDirectory()) + { + throw new IllegalArgumentException("Expected a directory to clear: " + directory); + } + // check if there is anything to to + if (!directory.exists()) + { + return 0; + } + // list all files + File[] files = directory.listFiles(); + int count = 0; + for (File file : files) + { + if (file.isDirectory()) + { + if(isLongLifeTempDir(file)) + { + // long life for this folder and its children + int countRemoved = removeFiles(file, longLifeBefore, longLifeBefore, true); + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + countRemoved + " files from temp directory: " + file); + } + } + else + { + // enter subdirectory and clean it out and remove itsynetics + int countRemoved = removeFiles(file, removeBefore, longLifeBefore, true); + if (logger.isDebugEnabled()) + { + logger.debug("Removed " + countRemoved + " files from directory: " + file); + } + } + } + else + { + // it is a file - check the created time + if (file.lastModified() > removeBefore) + { + // file is not old enough + continue; + } + // it is a file - attempt a delete + try + { + if(logger.isDebugEnabled()) + { + logger.debug("Deleting temp file: " + file); + } + file.delete(); + count++; + } + catch (Throwable e) + { + logger.info("Failed to remove temp file: " + file); + } + } + } + // must we delete the directory we are in? + if (removeDir) + { + // the directory must be removed if empty + try + { + File[] listing = directory.listFiles(); + if(listing != null && listing.length == 0) + { + // directory is empty + if(logger.isDebugEnabled()) + { + logger.debug("Deleting empty directory: " + directory); + } + directory.delete(); + } + } + catch (Throwable e) + { + logger.info("Failed to remove temp directory: " + directory, e); + } + } + // done + return count; + } + } +} diff --git a/src/main/java/org/alfresco/util/TraceableThreadFactory.java b/src/main/java/org/alfresco/util/TraceableThreadFactory.java new file mode 100644 index 0000000000..b59900b1c6 --- /dev/null +++ b/src/main/java/org/alfresco/util/TraceableThreadFactory.java @@ -0,0 +1,109 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A thread factory that spawns threads that are statically visible. Each factory uses a unique + * thread group. All the groups that have been used can be fetched using + * {@link #getActiveThreadGroups()}, allowing iteration of the the threads in the group. + * + * @since 2.1 + * @author Derek Hulley + */ +public class TraceableThreadFactory implements ThreadFactory +{ + private static final AtomicInteger factoryNumber = new AtomicInteger(1); + private static List activeThreadGroups = Collections.synchronizedList(new ArrayList(1)); + + /** + * Get a list of thread groups registered by the factory. + * + * @return Returns a snapshot of thread groups + */ + public static List getActiveThreadGroups() + { + return activeThreadGroups; + } + + private final ThreadGroup group; + private String namePrefix; + private final AtomicInteger threadNumber; + private boolean threadDaemon; + private int threadPriority; + + + public TraceableThreadFactory() + { + this.group = new ThreadGroup("TraceableThreadGroup-" + factoryNumber.getAndIncrement()); + TraceableThreadFactory.activeThreadGroups.add(this.group); + + this.namePrefix = "TraceableThread-" + factoryNumber.getAndIncrement() + "-thread-"; + this.threadNumber = new AtomicInteger(1); + + this.threadDaemon = true; + this.threadPriority = Thread.NORM_PRIORITY; + } + + /** + * @param daemon true if all threads created must be daemon threads + */ + public void setThreadDaemon(boolean daemon) + { + this.threadDaemon = daemon; + } + + /** + * + * @param threadPriority the threads priority from 1 (lowest) to 10 (highest) + */ + public void setThreadPriority(int threadPriority) + { + this.threadPriority = threadPriority; + } + + public Thread newThread(Runnable r) + { + Thread thread = new Thread( + group, + r, + namePrefix + threadNumber.getAndIncrement(), + 0); + thread.setDaemon(threadDaemon); + thread.setPriority(threadPriority); + + return thread; + } + + public void setNamePrefix(String namePrefix) + { + this.namePrefix = namePrefix; + } + + public String getNamePrefix() + { + return this.namePrefix; + } + +} diff --git a/src/main/java/org/alfresco/util/TriggerBean.java b/src/main/java/org/alfresco/util/TriggerBean.java new file mode 100644 index 0000000000..620e6a2aee --- /dev/null +++ b/src/main/java/org/alfresco/util/TriggerBean.java @@ -0,0 +1,103 @@ +/* + * 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.Date; + +import org.quartz.Scheduler; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; + +public class TriggerBean extends AbstractTriggerBean implements TriggerBeanSPI +{ + public long startDelay = 0; + + public long repeatInterval = 0; + + public int repeatCount = SimpleTrigger.REPEAT_INDEFINITELY; + + public TriggerBean() + { + super(); + } + + @Override + public int getRepeatCount() + { + return repeatCount; + } + + @Override + public void setRepeatCount(int repeatCount) + { + this.repeatCount = repeatCount; + } + + @Override + public long getRepeatInterval() + { + return repeatInterval; + } + + @Override + public void setRepeatInterval(long repeatInterval) + { + this.repeatInterval = repeatInterval; + } + + @Override + public void setRepeatIntervalMinutes(long repeatIntervalMinutes) + { + this.repeatInterval = repeatIntervalMinutes * 60L * 1000L; + } + + @Override + public long getStartDelay() + { + return startDelay; + } + + @Override + public void setStartDelay(long startDelay) + { + this.startDelay = startDelay; + } + + @Override + public void setStartDelayMinutes(long startDelayMinutes) + { + this.startDelay = startDelayMinutes * 60L * 1000L; + } + + @Override + public Trigger getTrigger() throws Exception + { + if ((repeatInterval <= 0) && (repeatCount != 0)) + { + logger.error("Job "+getBeanName()+" - repeatInterval/repeatIntervalMinutes cannot be 0 (or -ve) unless repeatCount is also 0"); + return null; + } + + SimpleTrigger trigger = new SimpleTrigger(getBeanName(), Scheduler.DEFAULT_GROUP); + trigger.setStartTime(new Date(System.currentTimeMillis() + this.startDelay)); + trigger.setRepeatCount(repeatCount); + trigger.setRepeatInterval(repeatInterval); + return trigger; + } +} diff --git a/src/main/java/org/alfresco/util/TriggerBeanSPI.java b/src/main/java/org/alfresco/util/TriggerBeanSPI.java new file mode 100644 index 0000000000..e4c5d662fc --- /dev/null +++ b/src/main/java/org/alfresco/util/TriggerBeanSPI.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.util; + +/** + * @author Andy + * + */ +public interface TriggerBeanSPI +{ + + /** + * @return int + */ + int getRepeatCount(); + + /** + * @param repeatCount int + */ + void setRepeatCount(int repeatCount); + + /** + * @return long + */ + long getRepeatInterval(); + + /** + * @param repeatInterval long + */ + void setRepeatInterval(long repeatInterval); + + /** + * @param repeatIntervalMinutes long + */ + void setRepeatIntervalMinutes(long repeatIntervalMinutes); + + /** + * @return long + */ + long getStartDelay(); + + /** + * @param startDelay long + */ + void setStartDelay(long startDelay); + + /** + * @param startDelayMinutes long + */ + void setStartDelayMinutes(long startDelayMinutes); + +} diff --git a/src/main/java/org/alfresco/util/Triple.java b/src/main/java/org/alfresco/util/Triple.java new file mode 100644 index 0000000000..24785f0191 --- /dev/null +++ b/src/main/java/org/alfresco/util/Triple.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.util; + +/** + * Utility class for containing three things that aren't like each other. + * + * @since 4.0 + */ +public final class Triple +{ + /** + * The first member of the triple. + */ + private final T first; + + /** + * The second member of the triple. + */ + private final U second; + + /** + * The third member of the triple. + */ + private final V third; + + /** + * Make a new one. + * + * @param first The first member. + * @param second The second member. + * @param third The third member. + */ + public Triple(final T first, final U second, final V third) + { + this.first = first; + this.second = second; + this.third = third; + } + + /** + * Get the first member of the tuple. + * @return The first member. + */ + public T getFirst() + { + return first; + } + + /** + * Get the second member of the tuple. + * @return The second member. + */ + public U getSecond() + { + return second; + } + + /** + * Get the third member of the tuple. + * @return The third member. + */ + public V getThird() + { + return third; + } + + /** + * Override of equals. + * @param other The thing to compare to. + * @return equality. + */ + public boolean equals(final Object other) + { + if (this == other) + { + return true; + } + + if (!(other instanceof Triple)) + { + return false; + } + + Triple o = (Triple)other; + return (first.equals(o.getFirst()) && + second.equals(o.getSecond()) && + third.equals(o.getThird())); + } + + /** + * Override of hashCode. + */ + public int hashCode() + { + return ((first == null ? 0 : first.hashCode()) + + (second == null ? 0 : second.hashCode()) + + (third == null ? 0 : third.hashCode())); + } + + /** + * @see java.lang.Object#toString() + */ + public String toString() + { + return "(" + first + ", " + second + ", " + third + ")"; + } +} diff --git a/src/main/java/org/alfresco/util/VersionNumber.java b/src/main/java/org/alfresco/util/VersionNumber.java new file mode 100644 index 0000000000..2c18bfba54 --- /dev/null +++ b/src/main/java/org/alfresco/util/VersionNumber.java @@ -0,0 +1,205 @@ +/* + * 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.io.Serializable; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Immutable class to encapsulate a version number string. + * + * A valid version number string can be made up of any number of numberical parts + * all delimited by '.'. + * + * @author Roy Wetherall + */ +@AlfrescoPublicApi +public final class VersionNumber implements Comparable, Serializable +{ + private static final long serialVersionUID = -1570247769786810251L; + + /** A convenient '0' version */ + public static final VersionNumber VERSION_ZERO = new VersionNumber("0"); + /** A convenient '999' version */ + public static final VersionNumber VERSION_BIG = new VersionNumber("999"); + + /** Version delimeter */ + private static final String DELIMITER = "\\."; + + /** Version parts */ + private final int[] parts; + + /** + * Constructror, expects a valid version string. + * + * A AlfrescoRuntimeException will be throw if an invalid version is encountered. + * + * @param version the version string + */ + public VersionNumber(String version) + { + // Split the version into its component parts + String[] versions = version.split(DELIMITER); + if (versions.length < 1) + { + throw new AlfrescoRuntimeException("The version string '" + version + "' is invalid."); + } + + try + { + // Set the parts of the version + int index = 0; + this.parts = new int[versions.length]; + for (String versionPart : versions) + { + int part = Integer.parseInt(versionPart); + this.parts[index] = part; + index++; + } + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException("The version string '" + version + "' is invalid."); + } + } + + /** + * Get the various parts of the version + * + * @return array containing the parts of the version + */ + public int[] getParts() + { + return this.parts.clone(); + } + + /** + * Compares the passed version to this. Determines whether they are equal, greater or less than this version. + * + * @param obj the other version number + * @return -1 if the passed version is less that this, 0 if they are equal, 1 if the passed version is greater + */ + public int compareTo(VersionNumber obj) + { + int result = 0; + + VersionNumber that = (VersionNumber)obj; + int length = 0; + if (this.parts.length > that.parts.length) + { + length = this.parts.length; + } + else + { + length = that.parts.length; + } + + for (int index = 0; index < length; index++) + { + int thisPart = this.getPart(index); + int thatPart = that.getPart(index); + + if (thisPart > thatPart) + { + result = 1; + break; + } + else if (thisPart < thatPart) + { + result = -1; + break; + } + } + + return result; + } + + /** + * Helper method to the the part based on the index, if an invalid index is supplied 0 is returned. + * + * @param index the index + * @return the part value, 0 if the index is invalid + */ + public int getPart(int index) + { + int result = 0; + if (index < this.parts.length) + { + result = this.parts[index]; + } + return result; + } + + /** + * Hash code implementation + */ + @Override + public int hashCode() + { + if (parts == null || parts.length == 0) + { + return 0; + } + else if (parts.length >= 2) + { + return parts[0] * 17 + parts[1]; + } + else + { + return parts[0]; + } + } + + /** + * Equals implementation + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (false == obj instanceof VersionNumber) + { + return false; + } + VersionNumber that = (VersionNumber) obj; + return this.compareTo(that) == 0; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (int part : parts) + { + if (!first) + { + sb.append("."); + } + first = false; + sb.append(part); + } + return sb.toString(); + } +} diff --git a/src/main/java/org/alfresco/util/VmShutdownListener.java b/src/main/java/org/alfresco/util/VmShutdownListener.java new file mode 100644 index 0000000000..8682b93c31 --- /dev/null +++ b/src/main/java/org/alfresco/util/VmShutdownListener.java @@ -0,0 +1,83 @@ +/* + * 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A class that keeps track of the VM shutdown status. It can be + * used by threads as a singleton to check if the + * VM shutdown status has been activated. + *

+ * NOTE: In order to prevent a proliferation of shutdown hooks, + * it is advisable to use instances as singletons only. + *

+ * This component should be used by long-running, but interruptable processes. + * + * @author Derek Hulley + */ +public class VmShutdownListener +{ + private Log logger; + private volatile boolean vmShuttingDown; + + /** + * Constructs this instance to listen to the VM shutdown call. + * + */ + public VmShutdownListener(final String name) + { + logger = LogFactory.getLog(VmShutdownListener.class); + + vmShuttingDown = false; + Runnable shutdownRunnable = new Runnable() + { + public void run() + { + vmShuttingDown = true; + if (logger.isDebugEnabled()) + { + logger.debug("VM shutdown detected by listener " + name); + } + }; + }; + Thread shutdownThread = new Thread(shutdownRunnable, "ShutdownListener-" + name); + Runtime.getRuntime().addShutdownHook(shutdownThread); + } + + /** + * @return Returns true if the VM shutdown signal was detected. + */ + public boolean isVmShuttingDown() + { + return vmShuttingDown; + } + + /** + * Message carrier to break out of loops using the callback. + * + * @author Derek Hulley + * @since 3.2.1 + */ + public static class VmShutdownException extends RuntimeException + { + private static final long serialVersionUID = -5876107469054587072L; + } +} diff --git a/src/main/java/org/alfresco/util/bean/BooleanBean.java b/src/main/java/org/alfresco/util/bean/BooleanBean.java new file mode 100644 index 0000000000..8d43227a1f --- /dev/null +++ b/src/main/java/org/alfresco/util/bean/BooleanBean.java @@ -0,0 +1,30 @@ +/* + * 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.bean; + +import org.alfresco.api.AlfrescoPublicApi; + +/** + * Interface that may be implemented to return a boolean value in Spring bean configuration. + */ +@AlfrescoPublicApi +public interface BooleanBean +{ + public boolean isTrue(); +} diff --git a/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java b/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java new file mode 100644 index 0000000000..f086229763 --- /dev/null +++ b/src/main/java/org/alfresco/util/bean/HierarchicalBeanLoader.java @@ -0,0 +1,251 @@ +/* + * 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.bean; + +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Factory bean to find beans using a class hierarchy to drive the lookup. The well-known + * placeholder {@link #DEFAULT_DIALECT_PLACEHOLDER} is replaced with successive class + * names starting from the {@link #setDialectClass(String) dialect class} and + * progressing up the hierarchy until the {@link #setDialectBaseClass(String) base class} + * is reached. The bean is looked up in the context at each point until the + * bean is found or the base of the class hierarchy is reached. + *

+ * For example assume bean names:
+ *

+ *    BEAN 1: contentDAO.org.hibernate.dialect.Dialect
+ *    BEAN 2: contentDAO.org.hibernate.dialect.MySQLInnoDBDialect
+ *    BEAN 3: propertyValueDAO.org.hibernate.dialect.Dialect
+ *    BEAN 4: propertyValueDAO.org.hibernate.dialect.MySQLDialect
+ * 
+ * and
+ *
+ *    dialectBaseClass = org.hibernate.dialect.Dialect
+ * 
+ * For dialect org.hibernate.dialect.MySQLInnoDBDialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 2
+ *    propertyValueDAO.bean.dialect == BEAN 4
+ * 
+ * For dialectorg.hibernate.dialect.MySQLDBDialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 1
+ *    propertyValueDAO.bean.dialect == BEAN 4
+ * 
+ * For dialectorg.hibernate.dialect.Dialect the following will be returned:
+ *
+ *    contentDAO.bean.dialect == BEAN 1
+ *    propertyValueDAO.bean.dialect == BEAN 3
+ * 
+ * + * @author Derek Hulley + * @since 3.2SP1 + */ +public class HierarchicalBeanLoader + implements InitializingBean, FactoryBean, ApplicationContextAware +{ + public static final String DEFAULT_DIALECT_PLACEHOLDER = "#bean.dialect#"; + public static final String DEFAULT_DIALECT_REGEX = "\\#bean\\.dialect\\#"; + + private ApplicationContext ctx; + private String targetBeanName; + private Class targetClass; + private String dialectBaseClass; + private String dialectClass; + + /** + * Create a new HierarchicalResourceLoader. + */ + public HierarchicalBeanLoader() + { + super(); + } + + /** + * The application context that this bean factory serves. + */ + public void setApplicationContext(ApplicationContext ctx) + { + this.ctx = ctx; + } + + /** + * @param targetBeanName the name of the target bean to return, + * including the {@link #DEFAULT_DIALECT_PLACEHOLDER} + * where the specific dialect must be replaced. + */ + public void setTargetBeanName(String targetBeanName) + { + this.targetBeanName = targetBeanName; + } + + /** + * Set the target class that will be returned by {@link #getObjectType()} + * + * @param targetClass the type that this factory returns + */ + public void setTargetClass(Class targetClass) + { + this.targetClass = targetClass; + } + + /** + * Set the class to be used during hierarchical dialect replacement. Searches for the + * configuration location will not go further up the hierarchy than this class. + * + * @param className the name of the class or interface + */ + public void setDialectBaseClass(String className) + { + this.dialectBaseClass = className; + } + + public void setDialectClass(String className) + { + this.dialectClass = className; + } + + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "targetBeanName", targetBeanName); + PropertyCheck.mandatory(this, "targetClass", targetClass); + PropertyCheck.mandatory(this, "dialectBaseClass", dialectBaseClass); + PropertyCheck.mandatory(this, "dialectClass", dialectClass); + } + + /** + * @return Returns {@link #setTargetClass(Class) target class} + */ + public Class getObjectType() + { + return targetClass; + } + + /** + * @return Returns true always + */ + public boolean isSingleton() + { + return true; + } + + /** + * Replaces the + */ + public Object getObject() throws Exception + { + if (dialectClass == null || dialectBaseClass == null) + { + ctx.getBean(targetBeanName); + } + + // If a property value has not been substituted, extract the property name and load from system + String dialectBaseClassStr = dialectBaseClass; + if (!PropertyCheck.isValidPropertyString(dialectBaseClass)) + { + String prop = PropertyCheck.getPropertyName(dialectBaseClass); + dialectBaseClassStr = System.getProperty(prop, dialectBaseClass); + } + String dialectClassStr = dialectClass; + if (!PropertyCheck.isValidPropertyString(dialectClass)) + { + String prop = PropertyCheck.getPropertyName(dialectClass); + dialectClassStr = System.getProperty(prop, dialectClass); + } + + Class dialectBaseClazz; + try + { + dialectBaseClazz = Class.forName(dialectBaseClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect base class not found: " + dialectBaseClassStr); + } + Class dialectClazz; + try + { + dialectClazz = Class.forName(dialectClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect class not found: " + dialectClassStr); + } + // Ensure that we are dealing with classes and not interfaces + if (!Object.class.isAssignableFrom(dialectBaseClazz)) + { + throw new RuntimeException( + "Dialect base class must be derived from java.lang.Object: " + + dialectBaseClazz.getName()); + } + if (!Object.class.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Dialect class must be derived from java.lang.Object: " + + dialectClazz.getName()); + } + // We expect these to be in the same hierarchy + if (!dialectBaseClazz.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Non-existent HierarchicalBeanLoader hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + + Class clazz = dialectClazz; + Object bean = null; + while (bean == null) + { + // Do replacement + String newBeanName = targetBeanName.replaceAll(DEFAULT_DIALECT_REGEX, clazz.getName()); + try + { + bean = ctx.getBean(newBeanName); + // Found it + break; + } + catch (NoSuchBeanDefinitionException e) + { + } + // Not found + bean = null; + // Are we at the base class? + if (clazz.equals(dialectBaseClazz)) + { + // We don't go any further + break; + } + // Move up the hierarchy + clazz = clazz.getSuperclass(); + if (clazz == null) + { + throw new RuntimeException( + "Non-existent HierarchicalBeanLoaderBean hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + } + return bean; + } +} diff --git a/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java b/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java new file mode 100644 index 0000000000..ce7012b112 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/AbstractAsynchronouslyRefreshedCache.java @@ -0,0 +1,732 @@ +/* + * 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.cache; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.alfresco.util.PropertyCheck; +import org.alfresco.util.transaction.TransactionListener; +import org.alfresco.util.transaction.TransactionSupportUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.InitializingBean; + +/** + * The base implementation for an asynchronously refreshed cache. + * + * Currently supports one value or a cache per key (such as tenant.) Implementors just need to provide buildCache(String key/tennnantId) + * + * @author Andy + * @since 4.1.3 + * + * @author mrogers + * MER 17/04/2014 Refactored to core and generalised tennancy + */ +public abstract class AbstractAsynchronouslyRefreshedCache + implements AsynchronouslyRefreshedCache, + RefreshableCacheListener, + Callable, + BeanNameAware, + InitializingBean, + TransactionListener +{ + private static final String RESOURCE_KEY_TXN_DATA = "AbstractAsynchronouslyRefreshedCache.TxnData"; + + private static Log logger = LogFactory.getLog(AbstractAsynchronouslyRefreshedCache.class); + + private enum RefreshState + { + IDLE, WAITING, RUNNING, DONE + }; + + private ThreadPoolExecutor threadPoolExecutor; + private AsynchronouslyRefreshedCacheRegistry registry; + + // State + + private List listeners = new LinkedList(); + protected final ReentrantReadWriteLock liveLock = new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock refreshLock = new ReentrantReadWriteLock(); + private final ReentrantReadWriteLock runLock = new ReentrantReadWriteLock(); + protected HashMap live = new HashMap(); + private LinkedHashSet refreshQueue = new LinkedHashSet(); + private String cacheId; + private RefreshState refreshState = RefreshState.IDLE; + private String resourceKeyTxnData; + + @Override + public void register(RefreshableCacheListener listener) + { + listeners.add(listener); + } + + /** + * @param threadPoolExecutor + * the threadPoolExecutor to set + */ + public void setThreadPoolExecutor(ThreadPoolExecutor threadPoolExecutor) + { + this.threadPoolExecutor = threadPoolExecutor; + } + + /** + * @param registry + * the registry to set + */ + public void setRegistry(AsynchronouslyRefreshedCacheRegistry registry) + { + this.registry = registry; + } + + + public void init() + { + registry.register(this); + } + + @Override + public String toString() + { + return "AbstractAsynchronouslyRefreshedCache [cacheId=" + cacheId + "]"; + } + + @Override + public T get(String key) + { + liveLock.readLock().lock(); + try + { + if (live.get(key) != null) + { + if (logger.isTraceEnabled()) + { + logger.trace("get() from cache for key " + key + " on " + this); + } + return live.get(key); + } + } + finally + { + liveLock.readLock().unlock(); + } + + if (logger.isDebugEnabled()) + { + logger.debug("get() miss, scheduling and waiting for key " + key + " on " + this); + } + + // There was nothing to return so we build and return + Refresh refresh = null; + refreshLock.writeLock().lock(); + try + { + // Is there anything we can wait for + for (Refresh existing : refreshQueue) + { + if (existing.getKey().equals(key)) + { + if (logger.isDebugEnabled()) + { + logger.debug("get() found existing build to wait for on " + this); + } + refresh = existing; + } + } + + if (refresh == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("get() building from scratch on " + this); + } + refresh = new Refresh(key); + refreshQueue.add(refresh); + } + + } + finally + { + refreshLock.writeLock().unlock(); + } + submit(); + waitForBuild(refresh); + + return get(key); + } + + /** + * Use the current thread to build and put a new version of the cache entry before returning. + * @param key the cache key + */ + public void forceInChangesForThisUncommittedTransaction(String key) + { + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for tenant " + key + " on " + this); + } + T cache = buildCache(key); + if (logger.isDebugEnabled()) + { + logger.debug("Cache built for tenant " + key + " on " + this); + } + + liveLock.writeLock().lock(); + try + { + live.put(key, cache); + } + finally + { + liveLock.writeLock().unlock(); + } + } + + protected void waitForBuild(Refresh refresh) + { + while (refresh.getState() != RefreshState.DONE) + { + synchronized (refresh) + { + try + { + refresh.wait(100); + } + catch (InterruptedException e) + { + } + } + } + } + + @Override + public void refresh(String key) + { + // String tenantId = tenantService.getCurrentUserDomain(); + if (logger.isDebugEnabled()) + { + logger.debug("Async cache refresh request for tenant " + key + " on " + this); + } + registry.broadcastEvent(new RefreshableCacheRefreshEvent(cacheId, key), true); + } + + @Override + public void onRefreshableCacheEvent(RefreshableCacheEvent refreshableCacheEvent) + { + // Ignore events not targeted for this cache + if (!refreshableCacheEvent.getCacheId().equals(cacheId)) + { + return; + } + if (logger.isDebugEnabled()) + { + logger.debug("Async cache onRefreshableCacheEvent " + refreshableCacheEvent + " on " + this); + } + + // If in a transaction delay the refresh until after it commits + + if (TransactionSupportUtil.getTransactionId() != null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Async cache adding" + refreshableCacheEvent.getKey() + " to post commit list: " + this); + } + TransactionData txData = getTransactionData(); + txData.keys.add(refreshableCacheEvent.getKey()); + } + else + { + LinkedHashSet keys = new LinkedHashSet(); + keys.add(refreshableCacheEvent.getKey()); + queueRefreshAndSubmit(keys); + } + } + + /** + * To be used in a transaction only. + */ + private TransactionData getTransactionData() + { + TransactionData data = (TransactionData) TransactionSupportUtil.getResource(resourceKeyTxnData); + if (data == null) + { + data = new TransactionData(); + // create and initialize caches + data.keys = new LinkedHashSet(); + + // ensure that we get the transaction callbacks as we have bound the unique + // transactional caches to a common manager + TransactionSupportUtil.bindListener(this, 0); + TransactionSupportUtil.bindResource(resourceKeyTxnData, data); + } + return data; + } + + private void queueRefreshAndSubmit(LinkedHashSet tenantIds) + { + if((tenantIds == null) || (tenantIds.size() == 0)) + { + return; + } + refreshLock.writeLock().lock(); + try + { + for (String tenantId : tenantIds) + { + if (logger.isDebugEnabled()) + { + logger.debug("Async cache adding refresh to queue for tenant " + tenantId + " on " + this); + } + refreshQueue.add(new Refresh(tenantId)); + } + } + finally + { + refreshLock.writeLock().unlock(); + } + submit(); + } + + @Override + public boolean isUpToDate(String key) + { + refreshLock.readLock().lock(); + try + { + for(Refresh refresh : refreshQueue) + { + if(refresh.getKey().equals(key)) + { + return false; + } + } + if (TransactionSupportUtil.getTransactionId() != null) + { + return (!getTransactionData().keys.contains(key)); + } + else + { + return true; + } + } + finally + { + refreshLock.readLock().unlock(); + } + } + + /** + * Must be run with runLock.writeLock + */ + private Refresh getNextRefresh() + { + if (runLock.writeLock().isHeldByCurrentThread()) + { + for (Refresh refresh : refreshQueue) + { + if (refresh.state == RefreshState.WAITING) + { + return refresh; + } + } + return null; + } + else + { + throw new IllegalStateException("Method should not be called without holding the write lock: " + this); + } + + } + + /** + * Must be run with runLock.writeLock + */ + private int countWaiting() + { + int count = 0; + if (runLock.writeLock().isHeldByCurrentThread()) + { + refreshLock.readLock().lock(); + try + { + for (Refresh refresh : refreshQueue) + { + if (refresh.state == RefreshState.WAITING) + { + count++; + } + } + return count; + } + finally + { + refreshLock.readLock().unlock(); + } + } + else + { + throw new IllegalStateException("Method should not be called without holding the write lock: " + this); + } + + } + + private void submit() + { + runLock.writeLock().lock(); + try + { + if (refreshState == RefreshState.IDLE) + { + if (logger.isDebugEnabled()) + { + logger.debug("submit() scheduling job: " + this); + } + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + } + finally + { + runLock.writeLock().unlock(); + } + } + + @Override + public Void call() + { + try + { + doCall(); + return null; + } + catch (Exception e) + { + logger.error("Cache update failed: " + this, e); + runLock.writeLock().lock(); + try + { + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + finally + { + runLock.writeLock().unlock(); + } + return null; + } + } + + private void doCall() throws Exception + { + Refresh refresh = setUpRefresh(); + if (refresh == null) + { + return; + } + + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for key" + refresh.getKey() + " on " + this); + } + + try + { + doRefresh(refresh); + } + catch (Exception e) + { + refresh.setState(RefreshState.WAITING); + throw e; + } + } + + private void doRefresh(Refresh refresh) + { + if (logger.isDebugEnabled()) + { + logger.debug("Building cache for tenant" + refresh.getKey() + ": " + this); + } + T cache = buildCache(refresh.getKey()); + if (logger.isDebugEnabled()) + { + logger.debug(".... cache built for tenant" + refresh.getKey()); + } + + liveLock.writeLock().lock(); + try + { + live.put(refresh.getKey(), cache); + } + finally + { + liveLock.writeLock().unlock(); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Cache entry updated for tenant" + refresh.getKey()); + } + + broadcastEvent(new RefreshableCacheRefreshedEvent(cacheId, refresh.key)); + + runLock.writeLock().lock(); + try + { + refreshLock.writeLock().lock(); + try + { + if (countWaiting() > 0) + { + if (logger.isDebugEnabled()) + { + logger.debug("Rescheduling more work: " + this); + } + threadPoolExecutor.submit(this); + refreshState = RefreshState.WAITING; + } + else + { + if (logger.isDebugEnabled()) + { + logger.debug("Nothing to do; going idle: " + this); + } + refreshState = RefreshState.IDLE; + } + refresh.setState(RefreshState.DONE); + refreshQueue.remove(refresh); + } + finally + { + refreshLock.writeLock().unlock(); + } + } + finally + { + runLock.writeLock().unlock(); + } + } + + private Refresh setUpRefresh() throws Exception + { + Refresh refresh = null; + runLock.writeLock().lock(); + try + { + if (refreshState == RefreshState.WAITING) + { + refreshLock.writeLock().lock(); + try + { + refresh = getNextRefresh(); + if (refresh != null) + { + refreshState = RefreshState.RUNNING; + refresh.setState(RefreshState.RUNNING); + return refresh; + } + else + { + refreshState = RefreshState.IDLE; + return null; + } + } + finally + { + refreshLock.writeLock().unlock(); + } + } + else + { + return null; + } + } + catch (Exception e) + { + if (refresh != null) + { + refresh.setState(RefreshState.WAITING); + } + throw e; + } + finally + { + runLock.writeLock().unlock(); + } + + } + + @Override + public void setBeanName(String name) + { + cacheId = name; + + } + + @Override + public String getCacheId() + { + return cacheId; + } + + /** + * Build the cache entry for the specific key. + * This method is called in a thread-safe manner i.e. it is only ever called by a single + * thread. + * + * @param key + * @return new Cache instance + */ + protected abstract T buildCache(String key); + + private static class Refresh + { + private String key; + + private volatile RefreshState state = RefreshState.WAITING; + + Refresh(String key) + { + this.key = key; + } + + /** + * @return the tenantId + */ + public String getKey() + { + return key; + } + + /** + * @return the state + */ + public RefreshState getState() + { + return state; + } + + /** + * @param state + * the state to set + */ + public void setState(RefreshState state) + { + this.state = state; + } + + @Override + public int hashCode() + { + // The bucked is determined by the tenantId alone - we are going to change the state + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Refresh other = (Refresh) obj; + if (state != other.state) + return false; + if (key == null) + { + if (other.key != null) + return false; + } + else if (!key.equals(other.key)) + return false; + return true; + } + + @Override + public String toString() + { + return "Refresh [key=" + key + ", state=" + state + ", hashCode()=" + hashCode() + "]"; + } + + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "threadPoolExecutor", threadPoolExecutor); + PropertyCheck.mandatory(this, "registry", registry); + registry.register(this); + + resourceKeyTxnData = RESOURCE_KEY_TXN_DATA + "." + cacheId; + + } + + public void broadcastEvent(RefreshableCacheEvent event) + { + if (logger.isDebugEnabled()) + { + logger.debug("Notifying cache listeners for " + getCacheId() + " " + event); + } + // If the system is up and running, broadcast the event immediately + for (RefreshableCacheListener listener : this.listeners) + { + listener.onRefreshableCacheEvent(event); + } + + } + + @Override + public void beforeCommit(boolean readOnly) + { + // Nothing + } + + @Override + public void beforeCompletion() + { + // Nothing + } + + @Override + public void afterCommit() + { + TransactionData txnData = getTransactionData(); + queueRefreshAndSubmit(txnData.keys); + } + + @Override + public void afterRollback() + { + // Nothing + } + + private static class TransactionData + { + LinkedHashSet keys; + } +} diff --git a/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java b/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java new file mode 100644 index 0000000000..d29a67a486 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/AbstractRefreshableCacheEvent.java @@ -0,0 +1,86 @@ +/* + * 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.util.cache; + +/** + * A generic event with the cache id and affected tenant + * + * @author Andy + */ +public abstract class AbstractRefreshableCacheEvent implements RefreshableCacheEvent +{ + private static final long serialVersionUID = 1324638640132648062L; + + private String cacheId; + private String key; + + AbstractRefreshableCacheEvent(String cacheId, String key) + { + this.cacheId = cacheId; + this.key = key; + } + + @Override + public String getCacheId() + { + return cacheId; + } + + @Override + public String getKey() + { + return key; + } + + @Override + public String toString() + { + return "AbstractRefreshableCacheEvent [cacheId=" + cacheId + ", tenantId=" + key + "]"; + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((cacheId == null) ? 0 : cacheId.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AbstractRefreshableCacheEvent other = (AbstractRefreshableCacheEvent) obj; + if (cacheId == null) + { + if (other.cacheId != null) return false; + } + else if (!cacheId.equals(other.cacheId)) return false; + if (key == null) + { + if (other.key != null) return false; + } + else if (!key.equals(other.key)) return false; + return true; + } +} diff --git a/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java b/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java new file mode 100644 index 0000000000..27534d1ae0 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCache.java @@ -0,0 +1,20 @@ +package org.alfresco.util.cache; + +public interface AsynchronouslyRefreshedCache extends RefreshableCache +{ + /** + * Get the cache id + * + * @return the cache ID + */ + String getCacheId(); + + /** + * Determine if the cache is up to date + * + * @param key tennant id + * @return true if the cache is not currently refreshing itself + */ + boolean isUpToDate(String key); + +} diff --git a/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java b/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java new file mode 100644 index 0000000000..8f093de68e --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/AsynchronouslyRefreshedCacheRegistry.java @@ -0,0 +1,19 @@ +package org.alfresco.util.cache; + + +public interface AsynchronouslyRefreshedCacheRegistry +{ + /** + * Register a listener + * @param listener + */ + public void register(RefreshableCacheListener listener); + + /** + * Fire an event + * @param event + * @param toAll - true goes to all listeners, false only to listeners that have a matching cacheId + */ + public void broadcastEvent(RefreshableCacheEvent event, boolean toAll); + +} diff --git a/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java b/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java new file mode 100644 index 0000000000..1e368bab87 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/DefaultAsynchronouslyRefreshedCacheRegistry.java @@ -0,0 +1,74 @@ +package org.alfresco.util.cache; +/* + * 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 . + */ +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Base registry implementation + * + * @author Andy + */ +public class DefaultAsynchronouslyRefreshedCacheRegistry implements AsynchronouslyRefreshedCacheRegistry +{ + private static Log logger = LogFactory.getLog(DefaultAsynchronouslyRefreshedCacheRegistry.class); + + private List listeners = new LinkedList(); + + @Override + public void register(RefreshableCacheListener listener) + { + if(logger.isDebugEnabled()) + { + logger.debug("Listener added for " + listener.getCacheId()); + } + listeners.add(listener); + } + + public void broadcastEvent(RefreshableCacheEvent event, boolean toAll) + { + // If the system is up and running, broadcast the event immediately + for (RefreshableCacheListener listener : this.listeners) + { + if (toAll) + { + if(logger.isDebugEnabled()) + { + logger.debug("Delivering event (" + event + ") to listener (" + listener + ")."); + } + listener.onRefreshableCacheEvent(event); + } + else + { + if (listener.getCacheId().equals(event.getCacheId())) + { + if(logger.isDebugEnabled()) + { + logger.debug("Delivering event (" + event + ") to listener (" + listener + ")."); + } + listener.onRefreshableCacheEvent(event); + } + } + } + } +} + diff --git a/src/main/java/org/alfresco/util/cache/RefreshableCache.java b/src/main/java/org/alfresco/util/cache/RefreshableCache.java new file mode 100644 index 0000000000..e9db992308 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/RefreshableCache.java @@ -0,0 +1,29 @@ +package org.alfresco.util.cache; + + +public interface RefreshableCache +{ + /** + * Get the cache. + * If there is no cache value this call will block. + * If the underlying cache is being refreshed, the old cache value will be returned until the refresh is complete. + * + * @return T + */ + public T get(String key); + + /** + * Refresh the cache asynchronously. + */ + public void refresh(String key); + + /** + * Register to be informed when the cache is updated in the background. + * + * Note: it is up to the implementation to provide any transactional wrapping. + * Transactional wrapping is not required to invalidate a shared cache entry directly via a transactional cache + * @param listener RefreshableCacheListener + */ + void register(RefreshableCacheListener listener); + +} diff --git a/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java b/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java new file mode 100644 index 0000000000..2f225a4f57 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/RefreshableCacheEvent.java @@ -0,0 +1,18 @@ +package org.alfresco.util.cache; + +import java.io.Serializable; + +public interface RefreshableCacheEvent extends Serializable +{ + /** + * Get the cache id + */ + public String getCacheId(); + + + /** + * Get the affected key/tenant id + */ + public String getKey(); + +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java b/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java new file mode 100644 index 0000000000..8a72faa8ef --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/RefreshableCacheListener.java @@ -0,0 +1,20 @@ +package org.alfresco.util.cache; + + +public interface RefreshableCacheListener +{ + /** + * Callback made when a cache refresh occurs + * + * @param refreshableCacheEvent the cache event + */ + public void onRefreshableCacheEvent(RefreshableCacheEvent refreshableCacheEvent); + + /** + * Cache id so broadcast can be constrained to matching caches + * + * @return the cache ID + */ + public String getCacheId(); + +} diff --git a/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java b/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java new file mode 100644 index 0000000000..161cb3ea4e --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshEvent.java @@ -0,0 +1,42 @@ +/* + * 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.util.cache; + +/** + * Describes an entry that is stale in the cache + * + * @author Andy + * + */ +public class RefreshableCacheRefreshEvent extends AbstractRefreshableCacheEvent +{ + /** + * @param cacheId + */ + RefreshableCacheRefreshEvent(String cacheId, String key) + { + super(cacheId, key); + } + + /** + * + */ + private static final long serialVersionUID = -8011932788039835334L; + +} diff --git a/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java b/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java new file mode 100644 index 0000000000..ef9833a126 --- /dev/null +++ b/src/main/java/org/alfresco/util/cache/RefreshableCacheRefreshedEvent.java @@ -0,0 +1,44 @@ +/* + * 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.util.cache; + +/** + * Describes a new entry has been inserted in the cache. + * + * @author Andy + * + */ +public class RefreshableCacheRefreshedEvent extends AbstractRefreshableCacheEvent +{ + + /** + * + */ + private static final long serialVersionUID = 2352511592269578075L; + + /** + * @param cacheId + * @param key - the key/ tennant id + */ + RefreshableCacheRefreshedEvent(String cacheId, String key) + { + super(cacheId, key); + } + +} diff --git a/src/main/java/org/alfresco/util/collections/CollectionUtils.java b/src/main/java/org/alfresco/util/collections/CollectionUtils.java new file mode 100644 index 0000000000..fc82aa000a --- /dev/null +++ b/src/main/java/org/alfresco/util/collections/CollectionUtils.java @@ -0,0 +1,647 @@ +/* + * 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.collections; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TreeSet; + +import org.alfresco.util.Pair; + +/** + * @author Nick Smith + * @author Neil Mc Erlean + * @since 4.0 + */ +public abstract class CollectionUtils +{ + public static boolean isEmpty(Map map) + { + if (map == null) + { + return true; + } + return map.isEmpty(); + } + + public static boolean isEmpty(Collection items) + { + if(items == null) + { + return true; + } + return items.isEmpty(); + } + + /** + * This method merges two sets returning the union of both sets. + * + * @param first first set. can be null. + * @param second second set. can be null. + * @return the union of both sets. will not be null + */ + public static Set nullSafeMerge(Set first, Set second) + { + return nullSafeMerge(first, second, false); + } + + /** + * This method merges two sets returning the union of both sets. + * + * @param first first set. can be null. + * @param second second set. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the union of both sets or null. + */ + public static Set nullSafeMerge(Set first, Set second, boolean emptyResultIsNull) + { + Set result = new HashSet(); + + if (first != null) result.addAll(first); + if (second != null) result.addAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + /** + * This method merges two maps returning the union of both maps. + * + * @param first first map. can be null. + * @param second second map. can be null. + * @return the union of both maps. will not be null + */ + public static Map nullSafeMerge(Map first, Map second) + { + return nullSafeMerge(first, second, false); + } + + /** + * This method merges two maps returning the union of both maps. + * + * @param first first map. can be null. + * @param second second map. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the union of both maps, or null. + */ + public static Map nullSafeMerge(Map first, Map second, boolean emptyResultIsNull) + { + Map result = new HashMap(); + + if (first != null) result.putAll(first); + if (second != null) result.putAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + /** + * This method joins two lists returning the a single list consisting of the first followed by the second. + * + * @param first first list. can be null. + * @param second second list. can be null. + * @return the concatenation of both lists. will not be null + */ + public static List nullSafeAppend(List first, List second) + { + return nullSafeAppend(first, second, false); + } + + /** + * This method joins two lists returning the a single list consisting of the first followed by the second. + * + * @param first first list. can be null. + * @param second second list. can be null. + * @param emptyResultIsNull if the result is empty, should we return null? + * @return the concatenation of both lists or null + */ + public static List nullSafeAppend(List first, List second, boolean emptyResultIsNull) + { + List result = new ArrayList(); + + if (first != null) result.addAll(first); + if (second != null) result.addAll(second); + + if (result.isEmpty() && emptyResultIsNull) + { + result = null; + } + return result; + } + + public static final Function TO_STRING_TRANSFORMER = new Function() + { + public String apply(Object value) + { + return value.toString(); + } + }; + + /** + * Converts a {@link Collection} of values of type F to a {@link Serializable} {@link List} of values of type T. + * Filters out all values converted to null. + * @param From type + * @param To type + * @param values the values to convert. + * @param transformer Used to convert values. + * @return List + */ + public static List transform(Collection values, Function transformer) + { + if(values == null || values.isEmpty()) + { + return new ArrayList(); + } + List results = new ArrayList(values.size()); + for (F value : values) + { + T result = transformer.apply(value); + if(result != null) + { + results.add(result); + } + } + return results; + } + + /** + * Converts a {@link Map} having keys of type F to a new {@link Map} instance having keys of type T. The object references + * in the value set are copied to the transformed map, thus reusing the same objects. + * @param From type + * @param To type + * @param The value type of the before and after maps. + * @param map the map to convert. + * @param transformer Used to convert keys. + * @return a new Map instance with transformed keys and unchanged values. These values will be the same object references. + */ + public static Map transformKeys(Map map, Function transformer) + { + if(map == null || map.isEmpty()) + { + return new HashMap(); + } + Map results = new HashMap(map.size()); + for (Entry entry : map.entrySet()) + { + T transformedKey = transformer.apply(entry.getKey()); + results.put(transformedKey, entry.getValue()); + } + return results; + } + + /** + * Converts a {@link Collection} of values of type F to a {@link Serializable} {@link List} of values of type T. + * Filters out all values converted to null. + * @param From type + * @param To type + * @param values the values to convert. + * @param transformer Used to convert values. + * @return List + */ + public static List transform(Function transformer, F... values) + { + if(values == null || values.length<1) + { + return new ArrayList(); + } + List results = new ArrayList(values.length); + for (F value : values) + { + T result = transformer.apply(value); + if(result != null) + { + results.add(result); + } + } + return results; + } + + public static List toListOfStrings(Collection values) + { + return transform(values, TO_STRING_TRANSFORMER); + } + + /** + * This utility method converts a vararg of Objects into a Set. + * + * @param objects the objects to be added to the set + * @return a Set of objects (any equal objects will of course not be duplicated) + * @throws ClassCastException if any of the supplied objects are not of type T. + */ + public static Set asSet(T... objects) + { + Set result = new HashSet<>(); + for (T obj : objects) + { + result.add(obj); + } + + return result; + } + + /** + * This utility method converts a vararg of Objects into a Set. + * + * @param clazz the Set type to return. + * @param objects the objects to be added to the set + * @return a Set of objects (any equal objects will of course not be duplicated) + * @throws ClassCastException if any of the supplied objects are not of type T. + */ + public static Set asSet(Class clazz, Object... objects) + { + Set result = new HashSet(); + for (Object obj : objects) + { + @SuppressWarnings("unchecked") + T cast = (T) obj; + result.add(cast); + } + + return result; + } + + /** + * Returns a filtered {@link List} of values. Only values for which filter.apply(T) returns true are included in the {@link List} or returned values. + * @param The type of the {@link Collection} + * @param values the {@link Collection} to be filtered. + * @param filter the {@link Function} used to filter the {@link Collection}. + * @return the filtered {@link List} of values. + */ + public static List filter(Collection values, final Function filter) + { + return transform(values, new Function() + { + public T apply(T value) + { + if(filter.apply(value)) + { + return value; + } + return null; + } + }); + } + + /** + * This method flattens the provided collection of collections of values into a single + * {@code List} object containing each of the elements from the provided sub-collections. + *

+ * For example, {@code flatten( [1, 2], [3], [], [4, 5, 6] )} would produce a List like {@code [1, 2, 3, 4, 5, 6]}. + * Here, "[]" represents any Java collection. + * + * @param the element type of the collections. Note that this must be the same for all collections. + * @param values a collection of collections of elements to be flattened. + * @return a List containing the flattened elements. + */ + public static List flatten(Collection> values) + { + List results = new ArrayList(); + for (Collection collection : values) + { + if (collection != null) { results.addAll(collection); } + } + return results; + } + + /** + * See {@link #flatten(Collection)} + * @param collections a vararg of Collection objects to be flattened into a list. + * @return A flat List containing the elements of the provided collections. + * @since 5.0 + */ + @SafeVarargs + public static List flatten(Collection... collections) + { + List> listOfCollections = Arrays.asList(collections); + return CollectionUtils.flatten(listOfCollections); + } + + public static List transformFlat(Collection values, Function> transformer) + { + return flatten(transform(values, transformer)); + } + + /** + * Finds the first value for which acceptor returns true. + * @param T + * @param values Collection + * @param acceptor Function + * @return returns accepted value or null. + */ + public static T findFirst(Collection values, Function acceptor) + { + if (values != null ) + { + for (T value : values) + { + if (acceptor.apply(value)) + { + return value; + } + } + } + return null; + } + + /** + * Returns an immutable Serializable Set containing the values. + * @param T + * @param values T... + * @return Set + */ + public static Set unmodifiableSet(T... values) + { + return unmodifiableSet(Arrays.asList(values)); + } + + /** + * Returns an immutable Serializable Set containing the values. + * @param T + * @param values Collection + * @return Set + */ + public static Set unmodifiableSet(Collection values) + { + TreeSet set = new TreeSet(values); + return Collections.unmodifiableSet(set); + } + + /** + * @param values Collection + * @param transformer Function + * @return Map + */ + public static Map transformToMap(Collection values, + Function transformer) + { + if(isEmpty(values)) + { + return Collections.emptyMap(); + } + HashMap results = new HashMap(values.size()); + for (F value : values) + { + T result = transformer.apply(value); + results.put(value, result); + } + return results; + } + + /** + * This method can be used to filter a Map. Any keys in the supplied map, for which the supplied {@link Function filter function} + * returns true, will be included in the resultant Map, else they will not. + * + * @param map the map whose entries are to be filtered. + * @param filter the filter function which is applied to the key. + * @return a filtered map. + */ + public static Map filterKeys(Map map, Function filter) + { + Map results = new HashMap(); + Set> entries = map.entrySet(); + for (Entry entry : entries) + { + K key = entry.getKey(); + if(filter.apply(key)) + { + results.put(key, entry.getValue()); + } + } + return results; + } + + public static Map transform(Map map, + Function, Pair> transformer ) + { + Map results = new HashMap(map.size()); + for (Entry entry : map.entrySet()) + { + Pair pair = transformer.apply(entry); + if(pair!=null) + { + TK key = pair.getFirst(); + if (key != null) + { + results.put(key, pair.getSecond()); + } + } + } + return results; + } + + public static Filter containsFilter(final Collection values) + { + return new Filter() + { + public Boolean apply(T value) + { + return values.contains(value); + } + }; + } + + /** + * This method returns a new ArrayList which is the intersection of the two List parameters, based on {@link Object#equals(Object) equality} + * of their elements. + * The intersection list will contain elements in the order they have in list1 and any references in the resultant list will be + * to elements within list1 also. + * + * @return a new ArrayList whose values represent the intersection of the two Lists. + */ + public static List intersect(List list1, List list2) + { + if (list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) + { + return Collections.emptyList(); + } + + List result = new ArrayList(); + result.addAll(list1); + + result.retainAll(list2); + + return result; + } + + /** + * This method returns a new HashMap which is the intersection of the two Map parameters, based on {@link Object#equals(Object) equality} + * of their entries. + * Any references in the resultant map will be to elements within map1. + * + * @return a new HashMap whose values represent the intersection of the two Maps. + */ + public static Map intersect(Map map1, Map map2) + { + if (map1 == null || map1.isEmpty() || map2 == null || map2.isEmpty()) + { + return Collections.emptyMap(); + } + + // We now know neither map is null. + Map result = new HashMap(); + for (Map.Entry item : map1.entrySet()) + { + V value = map2.get(item.getKey()); + if (value != null && value.equals(item.getValue())) + { + result.put(item.getKey(), item.getValue()); + } + } + + return result; + } + + /** + * This method returns a new HashSet which is the intersection of the two Set parameters, based on {@link Object#equals(Object) equality} + * of their elements. + * Any references in the resultant set will be to elements within set1. + * + * @return a new HashSet whose values represent the intersection of the two Sets. + */ + public static Set intersect(Set set1, Set set2) + { + if (set1 == null || set1.isEmpty() || set2 == null || set2.isEmpty()) + { + return Collections.emptySet(); + } + + Set result = new HashSet(); + result.addAll(set1); + + result.retainAll(set2); + + return result; + } + + /** + * Creates a new sorted map, based on the values from the given map and Comparator. + * + * @param map the map which needs to be sorted + * @param valueComparator the Comparator + * @return a new sorted map + */ + public static Map sortMapByValue(Map map, Comparator> valueComparator) + { + if (map == null) + { + return Collections.emptyMap(); + } + + List> entriesList = new LinkedList<>(map.entrySet()); + + // Sort based on the map's values + Collections.sort(entriesList, valueComparator); + + Map orderedMap = new LinkedHashMap<>(entriesList.size()); + for (Entry entry : entriesList) + { + orderedMap.put(entry.getKey(), entry.getValue()); + } + return orderedMap; + } + + /** + * This method offers convenient conversion from value-based comparators to entry-based comparators + * for use with {@link #sortMapByValue(Map, Comparator)} above. + *

+ * Call it like so: {@code CollectionUtils.toEntryComparator(valueComparator);} + * + * @param valueComparator a comparator which compares the value types from a Map. + * @return a comparator which takes Map.Entry objects from that Map and compares their values. + */ + public static Comparator> toEntryComparator(final Comparator valueComparator) + { + return new Comparator>() + { + @Override public int compare(Entry e1, Entry e2) + { + return valueComparator.compare(e1.getValue(), e2.getValue()); + } + }; + } + + /** + * This method returns a new List instance containing the same element objects as the provided + * list, but with the specified element having been moved left by the specified offset. + *

+ * If the offset would mean that the element would move beyond the start or end of the list, it will + * move only to the end. + * + * @param offset the number of places over which to move the specified element. + * @param element the element to be moved. + * @param list the list to be reordered. + * @return a new List instance containing the ordered elements. + * @throws NoSuchElementException if the list does not contain an element equal to the one specified. + */ + public static List moveLeft(int offset, T element, List list) + { + return moveRight(-offset, element, list); + } + + /** + * This method does the same as {@link #moveLeft(int, Object, List)} but it moves the specified element + * to the right instead of the left. + */ + public static List moveRight(int offset, T element, List list) + { + final int elementIndex = list.indexOf(element); + + if (elementIndex == -1) { throw new NoSuchElementException("Element not found in provided list."); } + + if (offset == 0) + { + return list; + } + else + { + int newElementIndex = elementIndex + offset; + + // Ensure that the element will not move off the end of the list. + if (newElementIndex >= list.size()) { newElementIndex = list.size() - 1; } + else if (newElementIndex < 0) { newElementIndex = 0; } + + List result = new ArrayList<>(list); + result.remove(element); + + result.add(newElementIndex, element); + + return result; + } + } +} diff --git a/src/main/java/org/alfresco/util/collections/EntryTransformer.java b/src/main/java/org/alfresco/util/collections/EntryTransformer.java new file mode 100644 index 0000000000..720db9b2b1 --- /dev/null +++ b/src/main/java/org/alfresco/util/collections/EntryTransformer.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.util.collections; + +import java.util.Map.Entry; + +import org.alfresco.util.Pair; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public interface EntryTransformer extends Function, Pair> +{ + //NOOP +} diff --git a/src/main/java/org/alfresco/util/collections/Filter.java b/src/main/java/org/alfresco/util/collections/Filter.java new file mode 100644 index 0000000000..412811355f --- /dev/null +++ b/src/main/java/org/alfresco/util/collections/Filter.java @@ -0,0 +1,30 @@ +/* + * 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.collections; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public interface Filter extends Function +{ + //NOOP +} diff --git a/src/main/java/org/alfresco/util/collections/Function.java b/src/main/java/org/alfresco/util/collections/Function.java new file mode 100644 index 0000000000..0150caf352 --- /dev/null +++ b/src/main/java/org/alfresco/util/collections/Function.java @@ -0,0 +1,38 @@ +/* + * 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.collections; + +/** + * + * @author Nick Smith + * @since 4.0 + * + * @param From type + * @param To type + */ +public interface Function +{ + /** + * Converts a value of type F to a result of type T. + * @param value F + * @return T + */ + T apply(F value); +} diff --git a/src/main/java/org/alfresco/util/collections/JsonUtils.java b/src/main/java/org/alfresco/util/collections/JsonUtils.java new file mode 100644 index 0000000000..de719834ff --- /dev/null +++ b/src/main/java/org/alfresco/util/collections/JsonUtils.java @@ -0,0 +1,59 @@ +/* + * 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.collections; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.json.JSONArray; + +/** + * @author Nick Smith + * @since 4.0 + * + */ +public class JsonUtils +{ + + @SuppressWarnings("unchecked") + public static List transform(JSONArray values, Function transformer) + { + if(values == null || values.length()<1) + { + return Collections.emptyList(); + } + ArrayList results = new ArrayList(values.length()); + for (int i = 0; i < values.length(); i++) + { + T result = transformer.apply((F)values.opt(i)); + if(result != null) + { + results.add(result); + } + } + return results; + } + + public static List toListOfStrings(JSONArray values) + { + return transform(values, CollectionUtils.TO_STRING_TRANSFORMER); + } +} diff --git a/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java b/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java new file mode 100644 index 0000000000..38601c4cbb --- /dev/null +++ b/src/main/java/org/alfresco/util/exec/ExecParameterTokenizer.java @@ -0,0 +1,264 @@ +/* + * 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.util.exec; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; + +import org.alfresco.util.Pair; + +/** + * This class is used to tokenize strings used as parameters for {@link RuntimeExec} objects. + * Examples of such strings are as follows (ImageMagick-like parameters): + *

    + *
  • -font Helvetica -pointsize 50
  • + *
  • -font Helvetica -pointsize 50 -draw "circle 100,100 150,150"
  • + *
  • -font Helvetica -pointsize 50 -draw "gravity south fill black text 0,12 'CopyRight'"
  • + *
+ * The first is the simple case which would be parsed into Strings as follows: + * "-font", "Helvetica", "-pointsize", "50" + *

+ * The second is more complex in that it includes a quoted parameter, which would be parsed as a single String: + * "-font", "Helvetica", "-pointsize", "50", "circle 100,100 150,150" + * Note however that the quotation characters will be stripped from the token. + *

+ * The third shows an example with embedded quotation marks, which would parse to: + * "-font", "Helvetica", "-pointsize", "50", "gravity south fill black text 0,12 'CopyRight'" + * In this case, the embedded quotation marks (which must be different from those surrounding the parameter) + * are preserved in the extracted token. + *

+ * The class does not understand escaped quotes such as p1 p2 "a b c \"hello\" d" p4 + * + * @author Neil Mc Erlean + * @since 3.4.2 + */ +public class ExecParameterTokenizer +{ + /** + * The string to be tokenized. + */ + private final String str; + + /** + * The list of tokens, which will take account of quoted sections. + */ + private List tokens; + + public ExecParameterTokenizer(String str) + { + this.str = str; + } + + /** + * This method returns the tokens in a parameter string. + * Any tokens not contained within single or double quotes will be tokenized in the normal + * way i.e. by using whitespace separators and the standard StringTokenizer algorithm. + * Any tokens which are contained within single or double quotes will be returned as single + * String instances and will have their quote marks removed. + *

+ * See above for examples. + * + * @throws NullPointerException if the string to be tokenized was null. + */ + public List getAllTokens() + { + if (this.str == null) + { + throw new NullPointerException("Illegal null string cannot be tokenized."); + } + + if (tokens == null) + { + tokens = new ArrayList(); + + // Preserve original behaviour from RuntimeExec. + if (str.indexOf('\'') == -1 && str.indexOf('"') == -1) + { + // Contains no quotes. + for (StringTokenizer standardTokenizer = new StringTokenizer(str); standardTokenizer.hasMoreTokens(); ) + { + tokens.add(standardTokenizer.nextToken()); + } + } + else + { + // There are either single or double quotes or both. + // So we need to identify the quoted regions within the string. + List> quotedRegions = new ArrayList>(); + + for (Pair next = identifyNextQuotedRegion(str, 0); next != null; ) + { + quotedRegions.add(next); + next = identifyNextQuotedRegion(str, next.getSecond() + 1); + } + + // Now we've got a List of index pairs identifying the quoted regions. + // We need to get substrings of quoted and unquoted blocks, whilst maintaining order. + List substrings = getSubstrings(str, quotedRegions); + + for (Substring r : substrings) + { + tokens.addAll(r.getTokens()); + } + } + } + + return this.tokens; + } + + /** + * The substrings will be a list of quoted and unquoted substrings. + * The unquoted ones need to be further tokenized in the normal way. + * The quoted ones must not be tokenized, but need their quotes stripped off. + */ + private List getSubstrings(String str, List> quotedRegionIndices) + { + List result = new ArrayList(); + + int cursorPosition = 0; + for (Pair nextQuotedRegionIndices : quotedRegionIndices) + { + if (cursorPosition < nextQuotedRegionIndices.getFirst()) + { + int startIndexOfNextQuotedRegion = nextQuotedRegionIndices.getFirst() - 1; + result.add(new UnquotedSubstring(str.substring(cursorPosition, startIndexOfNextQuotedRegion))); + cursorPosition = startIndexOfNextQuotedRegion; + } + result.add(new QuotedSubstring(str.substring(nextQuotedRegionIndices.getFirst(), nextQuotedRegionIndices.getSecond()))); + cursorPosition = nextQuotedRegionIndices.getSecond(); + } + + // We've processed all the quoted regions, but there may be a final unquoted region + if (cursorPosition < str.length() - 1) + { + result.add(new UnquotedSubstring(str.substring(cursorPosition, str.length() - 1))); + } + + return result; + } + + private Pair identifyNextQuotedRegion(String str, int startingIndex) + { + int indexOfNextSingleQuote = str.indexOf('\'', startingIndex); + int indexOfNextDoubleQuote = str.indexOf('"', startingIndex); + + if (indexOfNextSingleQuote == -1 && indexOfNextDoubleQuote == -1) + { + // If there are no more quoted regions + return null; + } + else if (indexOfNextSingleQuote > -1 && indexOfNextDoubleQuote > -1) + { + // If there are both single and double quotes in the remainder of the string + // Then select the closest quote. + int indexOfNextQuote = Math.min(indexOfNextSingleQuote, indexOfNextDoubleQuote); + char quoteChar = str.charAt(indexOfNextQuote); + + return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar); + } + else + { + // Only one of the quote characters is present. + + int indexOfNextQuote = Math.max(indexOfNextSingleQuote, indexOfNextDoubleQuote); + char quoteChar = str.charAt(indexOfNextQuote); + + return findIndexOfClosingQuote(str, indexOfNextQuote, quoteChar); + } + } + + private Pair findIndexOfClosingQuote(String str, int indexOfStartingQuote, char quoteChar) + { + // So we know which type of quote char we're dealing with. Either ' or ". + // Now we need to find the closing quote. + int indexAfterClosingQuote = str.indexOf(quoteChar, indexOfStartingQuote + 1) + 1; // + 1 to search after opening quote. + 1 to give result including closing quote. + + if (indexAfterClosingQuote == 0) // -1 + 1 + { + // If no closing quote. + throw new IllegalArgumentException("No closing " + quoteChar + "quote in" + str); + } + + return new Pair(indexOfStartingQuote, indexAfterClosingQuote); + } + + /** + * Utility interface for a substring in a parameter string. + */ + public interface Substring + { + /** + * Gets all the tokens in a parameter string. + */ + public List getTokens(); + } + + /** + * A substring that is not surrounded by (single or double) quotes. + */ + public class UnquotedSubstring implements Substring + { + private final String regionString; + public UnquotedSubstring(String str) + { + this.regionString = str; + } + + public List getTokens() + { + StringTokenizer t = new StringTokenizer(regionString); + List result = new ArrayList(); + while (t.hasMoreTokens()) + { + result.add(t.nextToken()); + } + return result; + } + + public String toString() + { + return UnquotedSubstring.class.getSimpleName() + ": '" + regionString + '\''; + } + } + + /** + * A substring that is surrounded by (single or double) quotes. + */ + public class QuotedSubstring implements Substring + { + private final String regionString; + public QuotedSubstring(String str) + { + this.regionString = str; + } + + public List getTokens() + { + String stringWithoutQuotes = regionString.substring(1, regionString.length() -1); + return Arrays.asList(new String[] {stringWithoutQuotes}); + } + + public String toString() + { + return QuotedSubstring.class.getSimpleName() + ": '" + regionString + '\''; + } + } +} diff --git a/src/main/java/org/alfresco/util/exec/RuntimeExec.java b/src/main/java/org/alfresco/util/exec/RuntimeExec.java new file mode 100644 index 0000000000..3db9814d9b --- /dev/null +++ b/src/main/java/org/alfresco/util/exec/RuntimeExec.java @@ -0,0 +1,1005 @@ +/* + * 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.util.exec; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.Timer; +import java.util.TimerTask; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This acts as a session similar to the java.lang.Process, but + * logs the system standard and error streams. + *

+ * The bean can be configured to execute a command directly, or be given a map + * of commands keyed by the os.name Java system property. In this map, + * the default key that is used when no match is found is the + * {@link #KEY_OS_DEFAULT *} key. + *

+ * Use the {@link #setProcessDirectory(String) processDirectory} property to change the default location + * from which the command executes. The process's environment can be configured using the + * {@link #setProcessProperties(Map) processProperties} property. + *

+ * Commands may use placeholders, e.g. + *


+ *    find
+ *    -name
+ *    ${filename}
+ * 
+ * The filename property will be substituted for any supplied value prior to + * each execution of the command. Currently, no checks are made to get or check the + * properties contained within the command string. It is up to the client code to + * dynamically extract the properties required if the required properties are not + * known up front. + *

+ * Sometimes, a variable may contain several arguments. . In this case, the arguments + * need to be tokenized using a standard StringTokenizer. To force tokenization + * of a value, use: + *


+ *    SPLIT:${userArgs}
+ * 
+ * You should not use this just to split up arguments that are known to require tokenization + * up front. The SPLIT: directive works for the entire argument and will not do anything + * if it is not at the beginning of the argument. Do not use SPLIT: to break up arguments + * that are fixed, so avoid doing this: + *

+ *    SPLIT:ls -lih
+ * 
+ * Instead, break the command up explicitly: + *

+ *    ls
+ *    -lih
+ * 
+ * + * Tokenization of quoted parameter values is handled by {@link ExecParameterTokenizer}, which + * describes the support in more detail. + * + * @author Derek Hulley + */ +public class RuntimeExec +{ + /** the key to use when specifying a command for any other OS: * */ + public static final String KEY_OS_DEFAULT = "*"; + + private static final String KEY_OS_NAME = "os.name"; + private static final int BUFFER_SIZE = 1024; + private static final String VAR_OPEN = "${"; + private static final String VAR_CLOSE = "}"; + private static final String DIRECTIVE_SPLIT = "SPLIT:"; + + private static Log logger = LogFactory.getLog(RuntimeExec.class); + private static Log transformerDebugLogger = LogFactory.getLog("org.alfresco.repo.content.transform.TransformerDebug"); + + private String[] command; + private Charset charset; + private boolean waitForCompletion; + private Map defaultProperties; + private String[] processProperties; + private File processDirectory; + private Set errCodes; + private Timer timer = new Timer(true); + + /** + * Default constructor. Initialize this instance by setting individual properties. + */ + public RuntimeExec() + { + this.charset = Charset.defaultCharset(); + this.waitForCompletion = true; + defaultProperties = Collections.emptyMap(); + processProperties = null; + processDirectory = null; + + // set default error codes + this.errCodes = new HashSet(2); + errCodes.add(1); + errCodes.add(2); + } + + public String toString() + { + + StringBuffer sb = new StringBuffer(256); + sb.append("RuntimeExec:\n") + .append(" command: "); + if (command == null) + { + // command is 'null', so there's nothing to toString + sb.append("'null'\n"); + } + else + { + for (String cmdStr : command) + { + sb.append(cmdStr).append(" "); + } + sb.append("\n"); + } + sb.append(" env props: ").append(Arrays.toString(processProperties)).append("\n") + .append(" dir: ").append(processDirectory).append("\n") + .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n"); + return sb.toString(); + } + + /** + * Set the command to execute regardless of operating system + * + * @param command an array of strings representing the command (first entry) and arguments + * + * @since 3.0 + */ + public void setCommand(String[] command) + { + this.command = command; + } + + /** + * Sets the assumed charset of OUT and ERR streams generated by the executed command. + * This defaults to the system default charset: {@link Charset#defaultCharset()}. + * + * @param charsetCode a supported character set code + * @throws UnsupportedCharsetException if the characterset code is not recognised by Java + */ + public void setCharset(String charsetCode) + { + this.charset = Charset.forName(charsetCode); + } + + /** + * Set whether to wait for completion of the command or not. If there is no wait for completion, + * then the return value of out and err buffers cannot be relied upon as the + * command may still be in progress. Failure is therefore not possible unless the calling thread + * waits for execution. + * + * @param waitForCompletion true (default) is to wait for the command to exit, + * or false to just return an exit code of 0 and whatever + * output is available at that point. + * + * @since 2.1 + */ + public void setWaitForCompletion(boolean waitForCompletion) + { + this.waitForCompletion = waitForCompletion; + } + + /** + * Supply a choice of commands to execute based on a mapping from the os.name system + * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used + * to get a command where there is not direct match to the operating system key. + *

+ * Each command is an array of strings, the first of which represents the command and all subsequent + * entries in the array represent the arguments. All elements of the array will be checked for + * the presence of any substitution parameters (e.g. '{dir}'). The parameters can be set using the + * {@link #setDefaultProperties(Map) defaults} or by passing the substitution values into the + * {@link #execute(Map)} command. + *

+ * If parameters passed may be multiple arguments, or if the values provided in the map are themselves + * collections of arguments (not recommended), then prefix the value with SPLIT: to ensure that + * the value is tokenized before being passed to the command. Any values that are not split, will be + * passed to the command as single arguments. For example:
+ * 'SPLIT: dir . ..' becomes 'dir', '.' and '..'.
+ * 'SPLIT: dir ${path}' (if path is '. ..') becomes 'dir', '.' and '..'.
+ * The splitting occurs post-subtitution. Where the arguments are known, it is advisable to avoid + * SPLIT:. + * + * @param commandsByOS a map of command string arrays, keyed by operating system names + * + * @see #setDefaultProperties(Map) + * + * @since 3.0 + */ + public void setCommandsAndArguments(Map commandsByOS) + { + // get the current OS + String serverOs = System.getProperty(KEY_OS_NAME); + // attempt to find a match + String[] command = commandsByOS.get(serverOs); + if (command == null) + { + // go through the commands keys, looking for one that matches by regular expression matching + for (String osName : commandsByOS.keySet()) + { + // Ignore * options. It is dealt with later. + if (osName.equals(KEY_OS_DEFAULT)) + { + continue; + } + // Do regex match + if (serverOs.matches(osName)) + { + command = commandsByOS.get(osName); + break; + } + } + // if there is still no command, then check for the wildcard + if (command == null) + { + command = commandsByOS.get(KEY_OS_DEFAULT); + } + } + // check + if (command == null) + { + throw new AlfrescoRuntimeException( + "No command found for OS " + serverOs + " or '" + KEY_OS_DEFAULT + "': \n" + + " commands: " + commandsByOS); + } + this.command = command; + } + + /** + * Supply a choice of commands to execute based on a mapping from the os.name system + * property to the command to execute. The {@link #KEY_OS_DEFAULT *} key can be used + * to get a command where there is not direct match to the operating system key. + * + * @param commandsByOS a map of command string keyed by operating system names + * + * @deprecated Use {@link #setCommandsAndArguments(Map)} + */ + public void setCommandMap(Map commandsByOS) + { + // This is deprecated, so issue a warning + logger.warn( + "The bean RuntimeExec property 'commandMap' has been deprecated;" + + " use 'commandsAndArguments' instead. See https://issues.alfresco.com/jira/browse/ETHREEOH-579."); + Map fixed = new LinkedHashMap(7); + for (Map.Entry entry : commandsByOS.entrySet()) + { + String os = entry.getKey(); + String unparsedCmd = entry.getValue(); + StringTokenizer tokenizer = new StringTokenizer(unparsedCmd); + String[] cmd = new String[tokenizer.countTokens()]; + for (int i = 0; i < cmd.length; i++) + { + cmd[i] = tokenizer.nextToken(); + } + fixed.put(os, cmd); + } + setCommandsAndArguments(fixed); + } + + /** + * Set the default command-line properties to use when executing the command. + * These are properties that substitute variables defined in the command string itself. + * Properties supplied during execution will overwrite the default properties. + *

+ * null properties will be treated as an empty string for substitution + * purposes. + * + * @param defaultProperties property values + */ + public void setDefaultProperties(Map defaultProperties) + { + this.defaultProperties = defaultProperties; + } + + /** + * Set additional runtime properties (environment properties) that will used + * by the executing process. + *

+ * Any keys or properties that start and end with ${...} will be removed on the assumption + * that these are unset properties. null values are translated to empty strings. + * All keys and values are trimmed of leading and trailing whitespace. + * + * @param processProperties Runtime process properties + * + * @see Runtime#exec(String, String[], java.io.File) + */ + public void setProcessProperties(Map processProperties) + { + ArrayList processPropList = new ArrayList(processProperties.size()); + boolean hasPath = false; + String systemPath = System.getenv("PATH"); + for (Map.Entry entry : processProperties.entrySet()) + { + String key = entry.getKey(); + String value = entry.getValue(); + if (key == null) + { + continue; + } + if (value == null) + { + value = ""; + } + key = key.trim(); + value = value.trim(); + if (key.startsWith(VAR_OPEN) && key.endsWith(VAR_CLOSE)) + { + continue; + } + if (value.startsWith(VAR_OPEN) && value.endsWith(VAR_CLOSE)) + { + continue; + } + // If a path is specified, prepend it to the existing path + if (key.equals("PATH")) + { + if (systemPath != null && systemPath.length() > 0) + { + processPropList.add(key + "=" + value + File.pathSeparator + systemPath); + } + else + { + processPropList.add(key + "=" + value); + } + hasPath = true; + } + else + { + processPropList.add(key + "=" + value); + } + } + // If a path was not specified, inherit the current one + if (!hasPath && systemPath != null && systemPath.length() > 0) + { + processPropList.add("PATH=" + systemPath); + } + this.processProperties = processPropList.toArray(new String[processPropList.size()]); + } + + /** + * Adds a property to existed processProperties. + * Property should not be null or empty. + * If property with the same value already exists then no change is made. + * If property exists with a different value then old value is replaced with the new one. + * @param name - property name + * @param value - property value + */ + public void setProcessProperty(String name, String value) + { + boolean set = false; + + if (name == null || value == null) + return; + + name = name.trim(); + value = value.trim(); + + if (name.isEmpty() || value.isEmpty()) + return; + + String property = name + "=" + value; + + for (String prop : this.processProperties) + { + if (prop.equals(property)) + { + set = true; + break; + } + + if (prop.startsWith(name)) + { + String oldValue = prop.split("=")[1]; + prop.replace(oldValue, value); + set = true; + } + } + + if (!set) + { + String[] existedProperties = this.processProperties; + int epl = existedProperties.length; + String[] newProperties = Arrays.copyOf(existedProperties, epl + 1); + newProperties[epl] = property; + this.processProperties = newProperties; + set = true; + } + } + + + /** + * Set the runtime location from which the command is executed. + *

+ * If the value is an unsubsititued variable (${...}) then it is ignored. + * If the location is not visible at the time of setting, a warning is issued only. + * + * @param processDirectory the runtime location from which to execute the command + */ + public void setProcessDirectory(String processDirectory) + { + if (processDirectory.startsWith(VAR_OPEN) && processDirectory.endsWith(VAR_CLOSE)) + { + this.processDirectory = null; + } + else + { + this.processDirectory = new File(processDirectory); + if (!this.processDirectory.exists()) + { + logger.warn( + "The runtime process directory is not visible when setting property 'processDirectory': \n" + + this); + } + } + } + + /** + * A comma or space separated list of values that, if returned by the executed command, + * indicate an error value. This defaults to "1, 2". + * + * @param errCodesStr the error codes for the execution + */ + public void setErrorCodes(String errCodesStr) + { + errCodes.clear(); + StringTokenizer tokenizer = new StringTokenizer(errCodesStr, " ,"); + while(tokenizer.hasMoreElements()) + { + String errCodeStr = tokenizer.nextToken(); + // attempt to convert it to an integer + try + { + int errCode = Integer.parseInt(errCodeStr); + this.errCodes.add(errCode); + } + catch (NumberFormatException e) + { + throw new AlfrescoRuntimeException( + "Property 'errorCodes' must be comma-separated list of integers: " + errCodesStr); + } + } + } + + /** + * Executes the command using the default properties + * + * @see #execute(Map) + */ + public ExecutionResult execute() + { + return execute(defaultProperties); + } + + /** + * Executes the statement that this instance was constructed with. + * + * @param properties the properties that the command might be executed with. + * null properties will be treated as an empty string for substitution + * purposes. + * + * @return Returns the full execution results + */ + public ExecutionResult execute(Map properties) + { + return execute(properties, -1); + } + + /** + * Executes the statement that this instance was constructed with an optional + * timeout after which the command is asked to + * + * @param properties the properties that the command might be executed with. + * null properties will be treated as an empty string for substitution + * purposes. + * @param timeoutMs a timeout after which {@link Process#destroy()} is called. + * ignored if less than or equal to zero. Note this method does not guarantee + * to terminate the process (it is not a kill -9). + * + * @return Returns the full execution results + */ + public ExecutionResult execute(Map properties, final long timeoutMs) + { + int defaultFailureExitValue = errCodes.size() > 0 ? ((Integer)errCodes.toArray()[0]) : 1; + + // check that the command has been set + if (command == null) + { + throw new AlfrescoRuntimeException("Runtime command has not been set: \n" + this); + } + + // create the properties + Runtime runtime = Runtime.getRuntime(); + Process process = null; + String[] commandToExecute = null; + try + { + // execute the command with full property replacement + commandToExecute = getCommand(properties); + final Process thisProcess = runtime.exec(commandToExecute, processProperties, processDirectory); + process = thisProcess; + if (timeoutMs > 0) + { + final String[] command = commandToExecute; + timer.schedule(new TimerTask() + { + @Override + public void run() + { + // Only try to kill the process if it is still running + try + { + thisProcess.exitValue(); + } + catch (IllegalThreadStateException stillRunning) + { + if (transformerDebugLogger.isDebugEnabled()) + { + transformerDebugLogger.debug("Process has taken too long ("+ + (timeoutMs/1000)+" seconds). Killing process "+ + Arrays.deepToString(command)); + } + thisProcess.destroy(); + } + } + }, timeoutMs); + } + } + catch (IOException e) + { + // The process could not be executed here, so just drop out with an appropriate error state + String execOut = ""; + String execErr = e.getMessage(); + int exitValue = defaultFailureExitValue; + ExecutionResult result = new ExecutionResult(null, commandToExecute, errCodes, exitValue, execOut, execErr); + logFullEnvironmentDump(result); + return result; + } + + // create the stream gobblers + InputStreamReaderThread stdOutGobbler = new InputStreamReaderThread(process.getInputStream(), charset); + InputStreamReaderThread stdErrGobbler = new InputStreamReaderThread(process.getErrorStream(), charset); + + // start gobbling + stdOutGobbler.start(); + stdErrGobbler.start(); + + // wait for the process to finish + int exitValue = 0; + try + { + if (waitForCompletion) + { + exitValue = process.waitFor(); + } + } + catch (InterruptedException e) + { + // process was interrupted - generate an error message + stdErrGobbler.addToBuffer(e.toString()); + exitValue = defaultFailureExitValue; + } + + if (waitForCompletion) + { + // ensure that the stream gobblers get to finish + stdOutGobbler.waitForCompletion(); + stdErrGobbler.waitForCompletion(); + } + + // get the stream values + String execOut = stdOutGobbler.getBuffer(); + String execErr = stdErrGobbler.getBuffer(); + + // construct the return value + ExecutionResult result = new ExecutionResult(process, commandToExecute, errCodes, exitValue, execOut, execErr); + + // done + logFullEnvironmentDump(result); + return result; + } + + /** + * Dump the full environment in debug mode + */ + private void logFullEnvironmentDump(ExecutionResult result) + { + if (logger.isTraceEnabled()) + { + StringBuilder sb = new StringBuilder(); + sb.append(result); + + // Environment variables modified by Alfresco + if (processProperties != null && processProperties.length > 0) + { + sb.append("\n modified environment: "); + for (int i=0; i envVariables = System.getenv(); + for (Map.Entry entry : envVariables.entrySet()) + { + String name = entry.getKey(); + String value = entry.getValue(); + sb.append("\n "); + sb.append(name + "=" + value); + } + + logger.trace(sb); + } + else if (logger.isDebugEnabled()) + { + logger.debug(result); + } + + // close output stream (connected to input stream of native subprocess) + } + + /** + * @return Returns the command that will be executed if no additional properties + * were to be supplied + */ + public String[] getCommand() + { + return getCommand(defaultProperties); + } + + /** + * Get the command that will be executed post substitution. + *

+ * null properties will be treated as an empty string for substitution + * purposes. + * + * @param properties the properties that the command might be executed with + * @return Returns the command that will be executed should the additional properties + * be supplied + */ + public String[] getCommand(Map properties) + { + Map execProperties = null; + if (properties == defaultProperties) + { + // we are just using the default properties + execProperties = defaultProperties; + } + else + { + execProperties = new HashMap(defaultProperties); + // overlay the supplied properties + execProperties.putAll(properties); + } + // Perform the substitution for each element of the command + ArrayList adjustedCommandElements = new ArrayList(20); + for (int i = 0; i < command.length; i++) + { + StringBuilder sb = new StringBuilder(command[i]); + for (Map.Entry entry : execProperties.entrySet()) + { + String key = entry.getKey(); + String value = entry.getValue(); + // ignore null + if (value == null) + { + value = ""; + } + // progressively replace the property in the command + key = (VAR_OPEN + key + VAR_CLOSE); + int index = sb.indexOf(key); + while (index > -1) + { + // replace + sb.replace(index, index + key.length(), value); + // get the next one + index = sb.indexOf(key, index + 1); + } + } + String adjustedValue = sb.toString(); + // Now SPLIT: it + if (adjustedValue.startsWith(DIRECTIVE_SPLIT)) + { + String unsplitAdjustedValue = sb.substring(DIRECTIVE_SPLIT.length()); + + // There may be quoted arguments here (see ALF-7482) + ExecParameterTokenizer quoteAwareTokenizer = new ExecParameterTokenizer(unsplitAdjustedValue); + List tokens = quoteAwareTokenizer.getAllTokens(); + adjustedCommandElements.addAll(tokens); + } + else + { + adjustedCommandElements.add(adjustedValue); + } + } + // done + return adjustedCommandElements.toArray(new String[adjustedCommandElements.size()]); + } + + /** + * Object to carry the results of an execution to the caller. + * + * @author Derek Hulley + */ + public static class ExecutionResult + { + private final Process process; + private final String[] command; + private final Set errCodes; + private final int exitValue; + private final String stdOut; + private final String stdErr; + + /** + * + * @param process the process attached to Java - null is allowed + */ + private ExecutionResult( + final Process process, + final String[] command, + final Set errCodes, + final int exitValue, + final String stdOut, + final String stdErr) + { + this.process = process; + this.command = command; + this.errCodes = errCodes; + this.exitValue = exitValue; + this.stdOut = stdOut; + this.stdErr = stdErr; + } + + @Override + public String toString() + { + String out = stdOut.length() > 250 ? stdOut.substring(0, 250) : stdOut; + String err = stdErr.length() > 250 ? stdErr.substring(0, 250) : stdErr; + + StringBuilder sb = new StringBuilder(128); + sb.append("Execution result: \n") + .append(" os: ").append(System.getProperty(KEY_OS_NAME)).append("\n") + .append(" command: ");appendCommand(sb, command).append("\n") + .append(" succeeded: ").append(getSuccess()).append("\n") + .append(" exit code: ").append(exitValue).append("\n") + .append(" out: ").append(out).append("\n") + .append(" err: ").append(err); + return sb.toString(); + } + + /** + * Appends the command in a form that make running from the command line simpler. + * It is not a real attempt at making a command given all the operating system + * and shell options, but makes copy, paste and edit a bit simpler. + */ + private StringBuilder appendCommand(StringBuilder sb, String[] command) + { + boolean arg = false; + for (String element: command) + { + if (element == null) + { + continue; + } + + if (arg) + { + sb.append(' '); + } + else + { + arg = true; + } + + boolean escape = element.indexOf(' ') != -1 || element.indexOf('>') != -1; + if (escape) + { + sb.append("\""); + } + sb.append(element); + if (escape) + { + sb.append("\""); + } + } + return sb; + } + + /** + * A helper method to force a kill of the process that generated this result. This is + * useful in cases where the process started is not expected to exit, or doesn't exit + * quickly. If the {@linkplain RuntimeExec#setWaitForCompletion(boolean) "wait for completion"} + * flag is false then the process may still be running when this result is returned. + * + * @return + * true if the process was killed, otherwise false + */ + public boolean killProcess() + { + if (process == null) + { + return true; + } + try + { + process.destroy(); + return true; + } + catch (Throwable e) + { + logger.warn(e.getMessage()); + return false; + } + } + + /** + * @param exitValue the command exit value + * @return Returns true if the code is a listed failure code + * + * @see #setErrorCodes(String) + */ + private boolean isFailureCode(int exitValue) + { + return errCodes.contains((Integer)exitValue); + } + + /** + * @return Returns true if the command was deemed to be successful according to the + * failure codes returned by the execution. + */ + public boolean getSuccess() + { + return !isFailureCode(exitValue); + } + + public int getExitValue() + { + return exitValue; + } + + public String getStdOut() + { + return stdOut; + } + + public String getStdErr() + { + return stdErr; + } + } + + /** + * Gobbles an InputStream and writes it into a + * StringBuffer + *

+ * The reading of the input stream is buffered. + */ + public static class InputStreamReaderThread extends Thread + { + private final InputStream is; + private final Charset charset; + private final StringBuffer buffer; // we require the synchronization + private boolean completed; + + /** + * @param is an input stream to read - it will be wrapped in a buffer + * for reading + */ + public InputStreamReaderThread(InputStream is, Charset charset) + { + super(); + setDaemon(true); // must not hold up the VM if it is terminating + this.is = is; + this.charset = charset; + this.buffer = new StringBuffer(BUFFER_SIZE); + this.completed = false; + } + + public synchronized void run() + { + completed = false; + + byte[] bytes = new byte[BUFFER_SIZE]; + InputStream tempIs = null; + try + { + tempIs = new BufferedInputStream(is, BUFFER_SIZE); + int count = -2; + while (count != -1) + { + // do we have something previously read? + if (count > 0) + { + String toWrite = new String(bytes, 0, count, charset.name()); + buffer.append(toWrite); + } + // read the next set of bytes + count = tempIs.read(bytes); + } + // done + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Unable to read stream", e); + } + finally + { + // close the input stream + if (tempIs != null) + { + try + { + tempIs.close(); + } + catch (Exception e) + { + } + } + // The thread has finished consuming the stream + completed = true; + // Notify waiters + this.notifyAll(); // Note: Method is synchronized + } + } + + /** + * Waits for the run to complete. + *

+ * Remember to start the thread first + */ + public synchronized void waitForCompletion() + { + while (!completed) + { + try + { + // release our lock and wait a bit + this.wait(1000L); // 200 ms + } + catch (InterruptedException e) + { + } + } + } + + /** + * @param msg the message to add to the buffer + */ + public void addToBuffer(String msg) + { + buffer.append(msg); + } + + public boolean isComplete() + { + return completed; + } + + /** + * @return Returns the current state of the buffer + */ + public String getBuffer() + { + return buffer.toString(); + } + } +} diff --git a/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java b/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java new file mode 100644 index 0000000000..b1293f16fd --- /dev/null +++ b/src/main/java/org/alfresco/util/exec/RuntimeExecBootstrapBean.java @@ -0,0 +1,257 @@ +/* + * 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.exec; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.alfresco.util.bean.BooleanBean; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * Application bootstrap bean that is able to execute one or more + * native executable statements upon startup and shutdown. + * + * @author Derek Hulley + */ +public class RuntimeExecBootstrapBean extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(RuntimeExecBootstrapBean.class); + + private List startupCommands; + private boolean failOnError; + private boolean killProcessesOnShutdown; + private boolean enabled; + + /** Keep track of the processes so that we can kill them on shutdown */ + private List executionResults; + + private Thread shutdownThread; + + /** + * Initializes the bean + *

    + *
  • failOnError = true
  • + *
  • killProcessesOnShutdown = true
  • + *
  • enabled = true
  • + *
+ */ + public RuntimeExecBootstrapBean() + { + this.startupCommands = Collections.emptyList(); + this.executionResults = new ArrayList(1); + failOnError = true; + killProcessesOnShutdown = true; + enabled = true; + } + + /** + * Set the commands to execute, in sequence, when the application context + * is initialized. + * + * @param startupCommands list of commands + */ + public void setStartupCommands(List startupCommands) + { + this.startupCommands = startupCommands; + } + + /** + * Set whether a process failure generates an error or not. Deviation from the default is + * useful if use as part of a process where the command or the codes generated by the + * execution may be ignored or avoided by the system. + * + * @param failOnError true (default) to issue an error message and throw an + * exception if the process fails to execute or generates an error + * return value. + * + * @since 2.1 + */ + public void setFailOnError(boolean failOnError) + { + this.failOnError = failOnError; + } + + /** + * Set whether or not to force a shutdown of successfully started processes. As most + * bootstrap processes are kicked off in order to provide the server with some or other + * service, this is true by default. + * + * @param killProcessesOnShutdown + * true to force any successfully executed commands' processes to + * be forcibly killed when the server shuts down. + * + * @since 2.1.0 + */ + public void setKillProcessesOnShutdown(boolean killProcessesOnShutdown) + { + this.killProcessesOnShutdown = killProcessesOnShutdown; + } + + /** + * Set whether or not the process should be disabled at ApplicationContext bootstrap. + * If a RuntimeExecBootstrapBean is disabled, then the command will not be executed. + * This property is not required and is false by default. + *

+ * This method has been deprecated in favour of a clearer name introduced in 3.3. + * See {@link #setEnabled}. + * + * @param disabledAtStartUp any String which equalsIgnoreCase("true") + * to prevent the command from being executed. + * @since 3.2.1 + * @deprecated Use {@link #setEnabled} instead, remembering that the boolean property should be inverted. + */ + public void setDisabledAtStartUp(String disabledAtStartUp) + { + boolean disabled = Boolean.parseBoolean(disabledAtStartUp); + this.setEnabled(Boolean.toString(!disabled)); + } + + /** + * Set whether or not the process should be enabled at ApplicationContext bootstrap. + * If a RuntimeExecBootstrapBean is not enabled, then the command will not be executed. + * This property is not required and is true by default. + * + * @param enabled any String which does not equalsIgnoreCase("true") + * will prevent the command from being executed. + * + * @since 3.3 + */ + public void setEnabled(String enabled) + { + // A String parameter rather than a boolean parameter is used here in order to allow + // the injection of properties ${foo.bar}. In this way undefined properties (which will + // be injected as "${foo.bar}") will mean the parameter is equivalent to false. + this.enabled = Boolean.parseBoolean(enabled); + } + + public void setEnabledFromBean(BooleanBean enabled) + { + this.enabled = enabled.isTrue(); + } + + @Override + protected synchronized void onBootstrap(ApplicationEvent event) + { + // If the command is disabled then do nothing. + if (this.enabled == false) + { + if (logger.isDebugEnabled()) + { + logger.debug("Bootstrap execution of " + startupCommands.size() + " was not enabled"); + } + return; + } + // execute + for (RuntimeExec command : startupCommands) + { + ExecutionResult result = command.execute(); + // check for failure + if (!result.getSuccess()) + { + String msg = "Bootstrap command failed: \n" + result; + if (failOnError) + { + throw new AlfrescoRuntimeException(msg); + } + else + { + logger.error(msg); + } + } + else + { + // It executed, so keep track of it + executionResults.add(result); + } + } + if (killProcessesOnShutdown) + { + // Force a shutdown on VM termination as we can't rely on the Spring context termination + this.shutdownThread = new KillProcessShutdownThread(); + Runtime.getRuntime().addShutdownHook(this.shutdownThread); + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bootstrap execution of " + startupCommands.size() + " commands was successful"); + } + } + + /** + * A thread that serves to kill the successfully created process, if required + * + * @since 2.1 + * @author Derek Hulley + */ + private class KillProcessShutdownThread extends Thread + { + public KillProcessShutdownThread() + { + super(RuntimeExecBootstrapBean.class.getName()); + } + @Override + public void run() + { + doShutdown(); + } + } + + /** + * Handle the shutdown of a subsystem but not the entire VM + */ + @Override + protected synchronized void onShutdown(ApplicationEvent event) + { + if (this.enabled == false) + { + return; + } + + try + { + // We managed to stop the process ourselves (e.g. on subsystem shutdown). Remove the shutdown hook + Runtime.getRuntime().removeShutdownHook(this.shutdownThread); + doShutdown(); + } + catch (IllegalStateException e) + { + // The system is shutting down - we'll have to let the shutdown hook run + } + } + + private void doShutdown() + { + if (!killProcessesOnShutdown) + { + // Do not force a kill + return; + } + for (ExecutionResult executionResult : executionResults) + { + executionResult.killProcess(); + } + } +} diff --git a/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java b/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java new file mode 100644 index 0000000000..842d217fa2 --- /dev/null +++ b/src/main/java/org/alfresco/util/exec/RuntimeExecShutdownBean.java @@ -0,0 +1,165 @@ +/* + * 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.exec; + +import java.util.Collections; +import java.util.List; + +import org.springframework.extensions.surf.util.AbstractLifecycleBean; +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEvent; + +/** + * This bean executes a list of shutdown commands when either the VM shuts down + * or the application context closes. In both cases, the commands are only + * executed if the application context was started. + * + * @author Derek Hulley + */ +public class RuntimeExecShutdownBean extends AbstractLifecycleBean +{ + private static Log logger = LogFactory.getLog(RuntimeExecShutdownBean.class); + + /** the commands to execute on context closure or VM shutdown */ + private List shutdownCommands; + /** the registered shutdown hook */ + private Thread shutdownHook; + /** ensures that commands don't get executed twice */ + private boolean executed; + + /** + * Initializes the bean with empty defaults, i.e. it will do nothing + */ + public RuntimeExecShutdownBean() + { + this.shutdownCommands = Collections.emptyList(); + this.executed = false; + } + + /** + * Set the commands to execute, in sequence, when the application context + * is initialized. + * + * @param startupCommands list of commands + */ + public void setShutdownCommands(List startupCommands) + { + this.shutdownCommands = startupCommands; + } + + private synchronized void execute() + { + // have we already done this? + if (executed) + { + return; + } + executed = true; + for (RuntimeExec command : shutdownCommands) + { + ExecutionResult result = command.execute(); + // check for failure + if (!result.getSuccess()) + { + logger.error("Shutdown command execution failed. Continuing with other commands.: \n" + result); + } + } + // done + if (logger.isDebugEnabled()) + { + logger.debug("Executed shutdown commands"); + } + } + + /** + * The thread that will call the shutdown commands. + * + * @author Derek Hulley + */ + private class ShutdownThread extends Thread + { + private ShutdownThread() + { + super(RuntimeExecShutdownBean.class.getName()); + this.setDaemon(true); + } + + @Override + public void run() + { + execute(); + } + } + + @Override + protected void onBootstrap(ApplicationEvent event) + { + // register shutdown hook + shutdownHook = new ShutdownThread(); + Runtime.getRuntime().addShutdownHook(shutdownHook); + + if (logger.isDebugEnabled()) + { + logger.debug("Registered shutdown hook"); + } + } + + @Override + protected void onShutdown(ApplicationEvent event) + { + // remove shutdown hook and execute + if (shutdownHook != null) + { + // execute + execute(); + // remove hook + try + { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } + catch (IllegalStateException e) + { + // VM is already shutting down + } + shutdownHook = null; + + if (logger.isDebugEnabled()) + { + logger.debug("Deregistered shutdown hook"); + } + } + } + +} + + + + + + + + + + + + + + diff --git a/src/main/java/org/alfresco/util/log/NDC.java b/src/main/java/org/alfresco/util/log/NDC.java new file mode 100644 index 0000000000..302f5a46ce --- /dev/null +++ b/src/main/java/org/alfresco/util/log/NDC.java @@ -0,0 +1,77 @@ +/* + * 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.log; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * A stand in for the org.apache.log4j.NDC class that avoids introducing runtime dependencies against the otherwise + * optional log4j. + * + * @author dward + */ +public class NDC +{ + private static Log logger = LogFactory.getLog(NDC.class); + + /** Log4J delegate for NDC */ + private static NDCDelegate ndcDelegate; + + static + { + if (logger.isDebugEnabled()) + { + try + { + ndcDelegate = (NDCDelegate) Class.forName("org.alfresco.util.log.log4j.Log4JNDC").newInstance(); + } + catch (Throwable e) + { + // We just ignore it + } + } + } + + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public static void push(String message) + { + if (ndcDelegate != null) + { + ndcDelegate.push(message); + } + } + + /** + * Remove the diagnostic context for this thread. + */ + static public void remove() + { + if (ndcDelegate != null) + { + ndcDelegate.remove(); + } + } +} diff --git a/src/main/java/org/alfresco/util/log/NDCDelegate.java b/src/main/java/org/alfresco/util/log/NDCDelegate.java new file mode 100644 index 0000000000..1d9bbebcb2 --- /dev/null +++ b/src/main/java/org/alfresco/util/log/NDCDelegate.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.log; + +/** + * @author dward + * + */ +public interface NDCDelegate +{ + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public void push(String message); + + /** + * Remove the diagnostic context for this thread. + */ + public void remove(); +} diff --git a/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java b/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java new file mode 100644 index 0000000000..49f599f054 --- /dev/null +++ b/src/main/java/org/alfresco/util/log/log4j/Log4JNDC.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.util.log.log4j; + +import org.alfresco.util.log.NDCDelegate; +import org.apache.log4j.NDC; + +/** + * A stand in for the org.apache.log4j.NDC class that avoids introducing runtime dependencies against the otherwise + * optional log4j. + * + * @author dward + */ +public class Log4JNDC implements NDCDelegate +{ + // Force resolution of the log4j NDC class by the classloader (thus forcing an error if unavailable) + @SuppressWarnings("unused") + private static final Class NDC_REF = NDC.class; + + /** + * Push new diagnostic context information for the current thread. + * + * @param message + * The new diagnostic context information. + */ + public void push(String message) + { + NDC.push(message); + } + + /** + * Remove the diagnostic context for this thread. + */ + public void remove() + { + NDC.remove(); + } +} diff --git a/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java b/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java new file mode 100644 index 0000000000..a8c0f71d50 --- /dev/null +++ b/src/main/java/org/alfresco/util/random/NormalDistributionHelper.java @@ -0,0 +1,66 @@ +/* + * 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.util.random; + +import org.apache.commons.math3.distribution.NormalDistribution; + +/** + * Utility functions guided by the + * Normal Distribution. + * + * @author Derek Hulley + * @since 5.1 + */ +public class NormalDistributionHelper +{ + private final NormalDistribution normalDistribution; + + /** + * Use a simple normal distribution to generate random numbers + */ + public NormalDistributionHelper() + { + this.normalDistribution = new NormalDistribution(); + } + + /** + * Get a random long where a standard deviation of 1.0 corresponds to the + * min and max values provided. The sampling is repeated until a value is + * found within the range given. + */ + public long getValue(long min, long max) + { + if (min > max) + { + throw new IllegalArgumentException("Min must less than or equal to max."); + } + + double sample = -2.0; + // Keep sampling until we get something within bounds of the standard deviation + while (sample < -1.0 || sample > 1.0) + { + sample = normalDistribution.sample(); + } + long halfRange = (max - min)/2L; + long mean = min + halfRange; + long ret = mean + (long) (halfRange * sample); + // Done + return ret; + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java b/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java new file mode 100644 index 0000000000..19e92cc1d9 --- /dev/null +++ b/src/main/java/org/alfresco/util/registry/NamedObjectRegistry.java @@ -0,0 +1,216 @@ +/* + * 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.registry; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.alfresco.api.AlfrescoPublicApi; +import org.alfresco.error.AlfrescoRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ParameterCheck; + +/** + * An generic registry of objects held by name. This is effectively a strongly-typed, + * synchronized map. + * + * @author Derek Hulley + * @since 3.2 + */ +@AlfrescoPublicApi +public class NamedObjectRegistry +{ + private static final Log logger = LogFactory.getLog(NamedObjectRegistry.class); + + private final ReentrantReadWriteLock.ReadLock readLock; + private final ReentrantReadWriteLock.WriteLock writeLock; + + private Class storageType; + private Pattern namePattern; + private final Map objects; + + /** + * Default constructor. The {@link #setStorageType(Class)} method must be called. + */ + public NamedObjectRegistry() + { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + this.namePattern = null; // Deliberately null + this.storageType = null; // Deliberately null + this.objects = new HashMap(13); + } + + /** + * Constructor that takes care of {@link #setStorageType(Class)}. + * + * @see #setStorageType(Class) + */ + public NamedObjectRegistry(Class type) + { + this(); + setStorageType(type); + } + + /** + * Set the type of class that the registry holds. Any attempt to register a + * an instance of another type will be rejected. + * + * @param clazz the type to store + */ + public void setStorageType(Class clazz) + { + writeLock.lock(); + try + { + this.storageType = clazz; + } + finally + { + writeLock.unlock(); + } + } + + /** + * Optionally set a pattern to which all object names must conform + * @param namePattern a regular expression + */ + public void setNamePattern(String namePattern) + { + writeLock.lock(); + try + { + this.namePattern = Pattern.compile(namePattern); + } + catch (PatternSyntaxException e) + { + throw new AlfrescoRuntimeException( + "Regular expression compilation failed for property 'namePrefix': " + e.getMessage(), + e); + } + finally + { + writeLock.unlock(); + } + } + + /** + * Register a named object instance. + * + * @param name the name of the object + * @param object the instance to register, which correspond to the type + */ + public void register(String name, T object) + { + ParameterCheck.mandatoryString("name", name); + ParameterCheck.mandatory("object", object); + + if (!storageType.isAssignableFrom(object.getClass())) + { + throw new IllegalArgumentException( + "This NameObjectRegistry only accepts objects of type " + storageType); + } + writeLock.lock(); + try + { + if (storageType == null) + { + throw new IllegalStateException( + "The registry has not been configured (setStorageType not yet called yet)"); + } + if (namePattern != null) + { + if (!namePattern.matcher(name).matches()) + { + throw new IllegalArgumentException( + "Object name '" + name + "' does not match required pattern: " + namePattern); + } + } + T prevObject = objects.put(name, object); + if (prevObject != null && prevObject != object) + { + logger.warn( + "Overwriting name object in registry: \n" + + " Previous: " + prevObject + "\n" + + " New: " + object); + } + } + finally + { + writeLock.unlock(); + } + } + + /** + * Get a named object if it has been registered + * + * @param name the name of the object to retrieve + * @return Returns the instance of the object, which will necessarily + * be of the correct type, or null + */ + public T getNamedObject(String name) + { + readLock.lock(); + try + { + // Get it + return objects.get(name); + } + finally + { + readLock.unlock(); + } + } + + /** + * @return Returns a copy of the map of instances + */ + public Map getAllNamedObjects() + { + readLock.lock(); + try + { + // Get it + return new HashMap(objects); + } + finally + { + readLock.unlock(); + } + } + + public void reset() + { + writeLock.lock(); + try + { + if (storageType == null) + objects.clear(); + } + finally + { + writeLock.unlock(); + } + } +} diff --git a/src/main/java/org/alfresco/util/resource/HierarchicalResourceLoader.java b/src/main/java/org/alfresco/util/resource/HierarchicalResourceLoader.java new file mode 100644 index 0000000000..87d845867f --- /dev/null +++ b/src/main/java/org/alfresco/util/resource/HierarchicalResourceLoader.java @@ -0,0 +1,203 @@ +/* + * 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.resource; + +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; + +/** + * Locate resources by using a class hierarchy to drive the search. The well-known + * placeholder {@link #DEFAULT_DIALECT_PLACEHOLDER} is replaced with successive class + * names starting from the {@link #setDialectClass(String) dialect class} and + * progressing up the hierarchy until the {@link #setDialectBaseClass(String) base class} + * is reached. A full resource search using Spring's {@link DefaultResourceLoader} is + * done at each point until the resource is found or the base of the class hierarchy is + * reached. + *

+ * For example assume classpath resources:
+ *

+ *    RESOURCE 1: config/ibatis/org.hibernate.dialect.Dialect/SqlMap-DOG.xml
+ *    RESOURCE 2: config/ibatis/org.hibernate.dialect.MySQLInnoDBDialect/SqlMap-DOG.xml
+ *    RESOURCE 3: config/ibatis/org.hibernate.dialect.Dialect/SqlMap-CAT.xml
+ *    RESOURCE 4: config/ibatis/org.hibernate.dialect.MySQLDialect/SqlMap-CAT.xml
+ * 
+ * and
+ *
+ *    dialectBaseClass = org.hibernate.dialect.Dialect
+ * 
+ * For dialect org.hibernate.dialect.MySQLInnoDBDialect the following will be returned:
+ *
+ *    config/ibatis/#resource.dialect#/SqlMap-DOG.xml == RESOURCE 2
+ *    config/ibatis/#resource.dialect#/SqlMap-CAT.xml == RESOURCE 4
+ * 
+ * For dialectorg.hibernate.dialect.MySQLDBDialect the following will be returned:
+ *
+ *    config/ibatis/#resource.dialect#/SqlMap-DOG.xml == RESOURCE 1
+ *    config/ibatis/#resource.dialect#/SqlMap-CAT.xml == RESOURCE 4
+ * 
+ * For dialectorg.hibernate.dialect.Dialect the following will be returned:
+ *
+ *    config/ibatis/#resource.dialect#/SqlMap-DOG.xml == RESOURCE 1
+ *    config/ibatis/#resource.dialect#/SqlMap-CAT.xml == RESOURCE 3
+ * 
+ * + * @author Derek Hulley + * @since 3.2 (Mobile) + */ +public class HierarchicalResourceLoader extends DefaultResourceLoader implements InitializingBean +{ + public static final String DEFAULT_DIALECT_PLACEHOLDER = "#resource.dialect#"; + public static final String DEFAULT_DIALECT_REGEX = "\\#resource\\.dialect\\#"; + + private String dialectBaseClass; + private String dialectClass; + + /** + * Create a new HierarchicalResourceLoader. + */ + public HierarchicalResourceLoader() + { + super(); + } + + /** + * Set the class to be used during hierarchical dialect replacement. Searches for the + * configuration location will not go further up the hierarchy than this class. + * + * @param className the name of the class or interface + */ + public void setDialectBaseClass(String className) + { + this.dialectBaseClass = className; + } + + public void setDialectClass(String className) + { + this.dialectClass = className; + } + + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "dialectBaseClass", dialectBaseClass); + PropertyCheck.mandatory(this, "dialectClass", dialectClass); + } + + /** + * Get a resource using the defined class hierarchy as a search path. + * + * @param location the location including a {@link #DEFAULT_DIALECT_PLACEHOLDER placeholder} + * @return a resource found by successive searches using class name replacement, or + * null if not found. + */ + @SuppressWarnings("unchecked") + @Override + public Resource getResource(String location) + { + if (dialectClass == null || dialectBaseClass == null) + { + return super.getResource(location); + } + + // If a property value has not been substituted, extract the property name and load from system + String dialectBaseClassStr = dialectBaseClass; + if (!PropertyCheck.isValidPropertyString(dialectBaseClass)) + { + String prop = PropertyCheck.getPropertyName(dialectBaseClass); + dialectBaseClassStr = System.getProperty(prop, dialectBaseClass); + } + String dialectClassStr = dialectClass; + if (!PropertyCheck.isValidPropertyString(dialectClass)) + { + String prop = PropertyCheck.getPropertyName(dialectClass); + dialectClassStr = System.getProperty(prop, dialectClass); + } + + Class dialectBaseClazz; + try + { + dialectBaseClazz = Class.forName(dialectBaseClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect base class not found: " + dialectBaseClassStr); + } + Class dialectClazz; + try + { + dialectClazz = Class.forName(dialectClassStr); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("Dialect class not found: " + dialectClassStr); + } + // Ensure that we are dealing with classes and not interfaces + if (!Object.class.isAssignableFrom(dialectBaseClazz)) + { + throw new RuntimeException( + "Dialect base class must be derived from java.lang.Object: " + + dialectBaseClazz.getName()); + } + if (!Object.class.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Dialect class must be derived from java.lang.Object: " + + dialectClazz.getName()); + } + // We expect these to be in the same hierarchy + if (!dialectBaseClazz.isAssignableFrom(dialectClazz)) + { + throw new RuntimeException( + "Non-existent HierarchicalResourceLoader hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + + Class clazz = dialectClazz; + Resource resource = null; + while (resource == null) + { + // Do replacement + String newLocation = location.replaceAll(DEFAULT_DIALECT_REGEX, clazz.getName()); + resource = super.getResource(newLocation); + if (resource != null && resource.exists()) + { + // Found + break; + } + // Not found + resource = null; + // Are we at the base class? + if (clazz.equals(dialectBaseClazz)) + { + // We don't go any further + break; + } + // Move up the hierarchy + clazz = clazz.getSuperclass(); + if (clazz == null) + { + throw new RuntimeException( + "Non-existent HierarchicalResourceLoaderBean hierarchy: " + + dialectBaseClazz.getName() + " is not a superclass of " + dialectClazz); + } + } + return resource; + } +} diff --git a/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java b/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java new file mode 100644 index 0000000000..979d25471b --- /dev/null +++ b/src/main/java/org/alfresco/util/shard/ExplicitShardingPolicy.java @@ -0,0 +1,104 @@ +/* + * 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.util.shard; + +import java.util.LinkedList; +import java.util.List; + +/** + * Common ACL based index sharding behaviour for SOLR and the repository + * + * @author Andy + */ +public class ExplicitShardingPolicy +{ + private int numShards; + + private int replicationFactor; + + private int numNodes; + + public ExplicitShardingPolicy(int numShards, int replicationFactor, int numNodes) + { + this.numShards = numShards; + this.replicationFactor = replicationFactor; + this.numNodes = numNodes; + } + + public boolean configurationIsValid() + { + if ((numShards * replicationFactor) % numNodes != 0) + { + return false; + } + + int shardsPerNode = numShards * replicationFactor / numNodes; + if ((shardsPerNode > numShards) || (shardsPerNode < 1)) + { + return false; + } + + return true; + } + + public List getShardIdsForNode(int nodeInstance) + { + LinkedList shardIds = new LinkedList(); + int test = 0; + for (int replica = 0; replica < replicationFactor; replica++) + { + for (int shard = replica; shard < numShards + replica; shard++) + { + if (test % numNodes == nodeInstance - 1) + { + shardIds.add(shard % numShards); + } + test++; + } + + } + return shardIds; + } + + public List getNodeInstancesForShardId(int shardId) + { + LinkedList nodeInstances = new LinkedList(); + for (int nodeInstance = 1; nodeInstance <= numNodes; nodeInstance++) + { + int test = 0; + for (int replica = 0; replica < replicationFactor; replica++) + { + for (int shard = replica; shard < numShards + replica; shard++) + { + if (test % numNodes == nodeInstance - 1) + { + if(shard % numShards == shardId) + { + nodeInstances.add(nodeInstance); + } + } + test++; + } + + } + } + return nodeInstances; + } + +} diff --git a/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java b/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java new file mode 100644 index 0000000000..c2886846c8 --- /dev/null +++ b/src/main/java/org/alfresco/util/transaction/ConnectionPoolException.java @@ -0,0 +1,52 @@ +/* + * 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.transaction; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Exception wraps {@link java.util.NoSuchElementException} from {@link org.apache.commons.dbcp.BasicDataSource} + * + * @author alex.mukha + * @since 4.1.9 + */ +public class ConnectionPoolException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 1L; + + public ConnectionPoolException(String msgId, Object[] msgParams, Throwable cause) + { + super(msgId, msgParams, cause); + } + + public ConnectionPoolException(String msgId, Object[] msgParams) + { + super(msgId, msgParams); + } + + public ConnectionPoolException(String msgId, Throwable cause) + { + super(msgId, cause); + } + + public ConnectionPoolException(String msgId) + { + super(msgId); + } +} diff --git a/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java b/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java new file mode 100644 index 0000000000..08f41c6c7d --- /dev/null +++ b/src/main/java/org/alfresco/util/transaction/SpringAwareUserTransaction.java @@ -0,0 +1,618 @@ +/* + * 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.transaction; + +import java.lang.reflect.Method; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import org.alfresco.error.StackTraceUtil; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.transaction.interceptor.TransactionAttributeSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * A UserTransaction that will allow the thread using it to participate + * in transactions that are normally only begun and committed by the SpringFramework + * transaction aware components. + *

+ * Client code can use this class directly, but should be very careful to handle the exception + * conditions with the appropriate finally blocks and rollback code. + * It is recommended that clients use this class indirectly via an instance of the + * {@link org.alfresco.repo.transaction.RetryingTransactionHelper}. + *

+ * Nested user transaction are allowed. + *

+ * Logging:
+ * To dump exceptions during commits, turn debugging on for this class.
+ * To log leaked transactions i.e. a begin() is not matched by a commit() or rollback(), + * add .trace to the usual classname-based debug category and set to WARN log + * level. This will log the first detection of a leaked transaction and automatically enable + * transaction call stack logging for subsequent leaked transactions. To enforce + * call stack logging from the start set the .trace log level to DEBUG. Call stack + * logging will hamper performance but is useful when it appears that something is eating + * connections or holding onto resources - usually a sign that client code hasn't handled all + * possible exception conditions. + * + * @see org.springframework.transaction.PlatformTransactionManager + * @see org.springframework.transaction.support.DefaultTransactionDefinition + * + * @author Derek Hulley + */ +public class SpringAwareUserTransaction + extends TransactionAspectSupport + implements UserTransaction, TransactionAttributeSource, TransactionAttribute +{ + /* + * There is some extra work in here to perform safety checks against the thread ID. + * This is because this class doesn't operate in an environment that guarantees that the + * thread coming into the begin() method is the same as the thread forcing commit() or + * rollback(). + */ + + private static final long serialVersionUID = 3762538897183224373L; + + + private static final String NAME = "UserTransaction"; + + private static final Log logger = LogFactory.getLog(SpringAwareUserTransaction.class); + + + /* + * Leaked Transaction Logging + */ + private static final Log traceLogger = LogFactory.getLog(SpringAwareUserTransaction.class.getName() + ".trace"); + private static volatile boolean isCallStackTraced = false; + + static + { + if (traceLogger.isDebugEnabled()) + { + isCallStackTraced = true; + traceLogger.warn("Logging of transaction call stack is enforced and will affect performance"); + } + } + + + static boolean isCallStackTraced() + { + return isCallStackTraced; + } + + /** stores whether begin() & commit()/rollback() methods calls are balanced */ + private boolean isBeginMatched = true; + /** stores the begin() call stack when auto tracing */ + private StackTraceElement[] beginCallStack; + + + private boolean readOnly; + private int isolationLevel; + private int propagationBehaviour; + private int timeout; + + /** Stores the user transaction current status as affected by explicit operations */ + private int internalStatus = Status.STATUS_NO_TRANSACTION; + /** the transaction information used to check for mismatched begin/end */ + private TransactionInfo internalTxnInfo; + /** keep the thread that the transaction was started on to perform thread safety checks */ + private long threadId = Long.MIN_VALUE; + /** make sure that we clean up the thread transaction stack properly */ + private boolean finalized = false; + + /** + * Creates a user transaction that defaults to {@link TransactionDefinition#PROPAGATION_REQUIRED}. + * + * @param transactionManager the transaction manager to use + * @param readOnly true to force a read-only transaction + * @param isolationLevel one of the + * {@link TransactionDefinition#ISOLATION_DEFAULT TransactionDefinition.ISOLATION_XXX} + * constants + * @param propagationBehaviour one of the + * {@link TransactionDefinition#PROPAGATION_MANDATORY TransactionDefinition.PROPAGATION__XXX} + * constants + * @param timeout the transaction timeout in seconds. + * + * @see TransactionDefinition#getTimeout() + */ + public SpringAwareUserTransaction( + PlatformTransactionManager transactionManager, + boolean readOnly, + int isolationLevel, + int propagationBehaviour, + int timeout) + { + super(); + setTransactionManager(transactionManager); + setTransactionAttributeSource(this); + this.readOnly = readOnly; + this.isolationLevel = isolationLevel; + this.propagationBehaviour = propagationBehaviour; + this.timeout = timeout; + } + + @Override + public String toString() + { + StringBuilder sb = new StringBuilder(256); + sb.append("UserTransaction") + .append("[object=").append(super.toString()) + .append(", status=").append(internalStatus) + .append("]"); + return sb.toString(); + } + + /** + * This class carries all the information required to fullfil requests about the transaction + * attributes. It acts as a source of the transaction attributes. + * + * @return Return this instance + */ + public TransactionAttribute getTransactionAttribute(Method method, Class targetClass) + { + return this; + } + + /** + * Return a qualifier value associated with this transaction attribute. This is not used as the transaction manager + * has been selected for us. + * + * @return null always + */ + public String getQualifier() + { + return null; + } + + /** + * The {@link UserTransaction } must rollback regardless of the error. The + * {@link #rollback() rollback} behaviour is implemented by simulating a caught + * exception. As this method will always return true, the rollback + * behaviour will be to rollback the transaction or mark it for rollback. + * + * @return Returns true always + */ + public boolean rollbackOn(Throwable ex) + { + return true; + } + + /** + * @see #NAME + */ + public String getName() + { + return NAME; + } + + public boolean isReadOnly() + { + return readOnly; + } + + public int getIsolationLevel() + { + return isolationLevel; + } + + public int getPropagationBehavior() + { + return propagationBehaviour; + } + + public int getTimeout() + { + return timeout; + } + + /** + * Implementation required for {@link UserTransaction}. + */ + public void setTransactionTimeout(int timeout) throws SystemException + { + if (internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Can only set the timeout before begin"); + } + this.timeout = timeout; + } + + /** + * Gets the current transaction info, or null if none exists. + *

+ * A check is done to ensure that the transaction info on the stack is exactly + * the same instance used when this transaction was started. + * The internal status is also checked against the transaction info. + * These checks ensure that the transaction demarcation is done correctly and that + * thread safety is adhered to. + * + * @return Returns the current transaction + */ + private TransactionInfo getTransactionInfo() + { + // a few quick self-checks + if (threadId < 0 && internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Transaction has been started but there is no thread ID"); + } + else if (threadId >= 0 && internalStatus == Status.STATUS_NO_TRANSACTION) + { + throw new RuntimeException("Transaction has not been started but a thread ID has been recorded"); + } + + TransactionInfo txnInfo = null; + try + { + txnInfo = TransactionAspectSupport.currentTransactionInfo(); + // we are in a transaction + } + catch (NoTransactionException e) + { + // No transaction. It is possible that the transaction threw an exception during commit. + } + // perform checks for active transactions + if (internalStatus == Status.STATUS_ACTIVE) + { + if (Thread.currentThread().getId() != threadId) + { + // the internally stored transaction info (retrieved in begin()) should match the info + // on the thread + throw new RuntimeException("UserTransaction may not be accessed by multiple threads"); + } + else if (txnInfo == null) + { + // internally we recorded a transaction starting, but there is nothing on the thread + throw new RuntimeException("Transaction boundaries have been made to overlap in the stack"); + } + else if (txnInfo != internalTxnInfo) + { + // the transaction info on the stack isn't the one we started with + throw new RuntimeException("UserTransaction begin/commit mismatch"); + } + } + return txnInfo; + } + + /** + * This status is a combination of the internal status, as recorded during explicit operations, + * and the status provided by the Spring support. + * + * @see Status + */ + public synchronized int getStatus() throws SystemException + { + TransactionInfo txnInfo = getTransactionInfo(); + + // if the txn info is null, then we are outside a transaction + if (txnInfo == null) + { + return internalStatus; // this is checked in getTransactionInfo + } + + // normally the internal status is correct, but we only need to double check + // for the case where the transaction was marked for rollback, or rolledback + // in a deeper transaction + TransactionStatus txnStatus = txnInfo.getTransactionStatus(); + if (internalStatus == Status.STATUS_ROLLEDBACK) + { + // explicitly rolled back at some point + return internalStatus; + } + else if (txnStatus.isRollbackOnly()) + { + // marked for rollback at some point in the stack + return Status.STATUS_MARKED_ROLLBACK; + } + else + { + // just rely on the internal status + return internalStatus; + } + } + + public synchronized void setRollbackOnly() throws IllegalStateException, SystemException + { + // just a check + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_MARKED_ROLLBACK) + { + // this is acceptable + } + else if (status == Status.STATUS_NO_TRANSACTION) + { + throw new IllegalStateException("The transaction has not been started yet"); + } + else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new IllegalStateException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (status != Status.STATUS_ACTIVE) + { + throw new IllegalStateException("The transaction is not active: " + status); + } + + // mark for rollback + txnInfo.getTransactionStatus().setRollbackOnly(); + // make sure that we record the fact that we have been marked for rollback + internalStatus = Status.STATUS_MARKED_ROLLBACK; + // done + if (logger.isDebugEnabled()) + { + logger.debug("Set transaction status to rollback only: " + this); + } + } + + /** + * @throws NotSupportedException if an attempt is made to reuse this instance + */ + public synchronized void begin() throws NotSupportedException, SystemException + { + // make sure that the status and info align - the result may or may not be null + @SuppressWarnings("unused") + TransactionInfo txnInfo = getTransactionInfo(); + if (internalStatus != Status.STATUS_NO_TRANSACTION) + { + throw new NotSupportedException("The UserTransaction may not be reused"); + } + + // check + + if( (propagationBehaviour != TransactionDefinition.PROPAGATION_REQUIRES_NEW)) + { + if(!readOnly && + TransactionSynchronizationManager.isSynchronizationActive() && + TransactionSynchronizationManager.isCurrentTransactionReadOnly() + ) + { + throw new IllegalStateException("Nested writable transaction in a read only transaction"); + } + } + + // begin a transaction + try + { + internalTxnInfo = createTransactionIfNecessary( + (Method) null, + (Class) null); // super class will just pass nulls back to us + } + catch (CannotCreateTransactionException e) + { + throw new ConnectionPoolException("The DB connection pool is depleted.", e); + } + + internalStatus = Status.STATUS_ACTIVE; + threadId = Thread.currentThread().getId(); + + // Record that transaction details now that begin was successful + isBeginMatched = false; + if (isCallStackTraced) + { + // get the stack trace + Exception e = new Exception(); + e.fillInStackTrace(); + beginCallStack = e.getStackTrace(); + } + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Began user transaction: " + this); + } + } + + /** + * @throws IllegalStateException if a transaction was not started + */ + public synchronized void commit() + throws RollbackException, HeuristicMixedException, HeuristicRollbackException, + SecurityException, IllegalStateException, SystemException + { + // perform checks + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_NO_TRANSACTION) + { + throw new IllegalStateException("The transaction has not yet begun"); + } + else if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new RollbackException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_MARKED_ROLLBACK) + { + throw new RollbackException("The transaction has already been marked for rollback"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (status != Status.STATUS_ACTIVE || txnInfo == null) + { + throw new IllegalStateException("No user transaction is active"); + } + + if (!finalized) + { + try + { + // the status seems correct - we can try a commit + commitTransactionAfterReturning(txnInfo); + } + catch (Throwable e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Transaction didn't commit", e); + } + // commit failed + internalStatus = Status.STATUS_ROLLEDBACK; + RollbackException re = new RollbackException("Transaction didn't commit: " + e.getMessage()); + // Stick the originating reason for failure into the exception. + re.initCause(e); + throw re; + } + finally + { + // make sure that we clean up the stack + cleanupTransactionInfo(txnInfo); + finalized = true; + // clean up leaked transaction logging + isBeginMatched = true; + beginCallStack = null; + } + } + + // regardless of whether the transaction was finally committed or not, the status + // as far as UserTransaction is concerned should be 'committed' + + // keep track that this UserTransaction was explicitly committed + internalStatus = Status.STATUS_COMMITTED; + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Committed user transaction: " + this); + } + } + + public synchronized void rollback() + throws IllegalStateException, SecurityException, SystemException + { + // perform checks + TransactionInfo txnInfo = getTransactionInfo(); + + int status = getStatus(); + // check the status + if (status == Status.STATUS_ROLLING_BACK || status == Status.STATUS_ROLLEDBACK) + { + throw new IllegalStateException("The transaction has already been rolled back"); + } + else if (status == Status.STATUS_COMMITTING || status == Status.STATUS_COMMITTED) + { + throw new IllegalStateException("The transaction has already been committed"); + } + else if (txnInfo == null) + { + throw new IllegalStateException("No user transaction is active"); + } + + if (!finalized) + { + try + { + // force a rollback by generating an exception that will trigger a rollback + completeTransactionAfterThrowing(txnInfo, new Exception()); + } + finally + { + // make sure that we clean up the stack + cleanupTransactionInfo(txnInfo); + finalized = true; + // clean up leaked transaction logging + isBeginMatched = true; + beginCallStack = null; + } + } + + // the internal status notes that we were specifically rolled back + internalStatus = Status.STATUS_ROLLEDBACK; + + // done + if (logger.isDebugEnabled()) + { + logger.debug("Rolled back user transaction: " + this); + } + } + + @Override + protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) + { + if (logger.isDebugEnabled()) + { + logger.debug("Exception attempting to pass transaction boundaries.", ex); + } + super.completeTransactionAfterThrowing(txInfo, ex); + } + + @Override + protected String methodIdentification(Method method) + { + // note: override for debugging purposes - this method called by Spring + return NAME; + } + + @Override + protected void finalize() throws Throwable + { + if (!isBeginMatched) + { + if (isCallStackTraced) + { + if (beginCallStack == null) + { + traceLogger.error("UserTransaction being garbage collected without a commit() or rollback(). " + + "NOTE: Prior to transaction call stack logging."); + } + else + { + StringBuilder sb = new StringBuilder(1024); + StackTraceUtil.buildStackTrace( + "UserTransaction being garbage collected without a commit() or rollback().", + beginCallStack, + sb, + -1); + traceLogger.error(sb); + } + } + else + { + traceLogger.error("Detected first UserTransaction which is being garbage collected without a commit() or rollback()"); + traceLogger.error("Logging of transaction call stack is now enabled and will affect performance"); + isCallStackTraced = true; + } + } + } +} diff --git a/src/main/java/org/alfresco/util/transaction/TransactionListener.java b/src/main/java/org/alfresco/util/transaction/TransactionListener.java new file mode 100644 index 0000000000..e97222ad5a --- /dev/null +++ b/src/main/java/org/alfresco/util/transaction/TransactionListener.java @@ -0,0 +1,70 @@ +/* + * 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.transaction; + +/** + * Listener for Alfresco-specific transaction callbacks. + * + * @see org.alfresco.repo.transaction.AlfrescoTransactionSupport + * + * @author Derek Hulley + */ +public interface TransactionListener +{ + + /** + * Called before a transaction is committed. + *

+ * All transaction resources are still available. + * + * @param readOnly true if the transaction is read-only + */ + void beforeCommit(boolean readOnly); + + /** + * Invoked before transaction commit/rollback. Will be called after + * {@link #beforeCommit(boolean) } even if {@link #beforeCommit(boolean)} + * failed. + *

+ * All transaction resources are still available. + */ + void beforeCompletion(); + + /** + * Invoked after transaction commit. + *

+ * Any exceptions generated here will only be logged and will have no effect + * on the state of the transaction. + *

+ * Although all transaction resources are still available, this method should + * be used only for cleaning up resources after a commit has occured. + */ + void afterCommit(); + + /** + * Invoked after transaction rollback. + *

+ * Any exceptions generated here will only be logged and will have no effect + * on the state of the transaction. + *

+ * Although all transaction resources are still available, this method should + * be used only for cleaning up resources after a rollback has occured. + */ + void afterRollback(); +} diff --git a/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java b/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java new file mode 100644 index 0000000000..efd42e3b17 --- /dev/null +++ b/src/main/java/org/alfresco/util/transaction/TransactionListenerAdapter.java @@ -0,0 +1,60 @@ +/* + * 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.transaction; + +/** + * NO-OP listener. + * + * @author Derek Hulley + * @since 5.0 + */ +public abstract class TransactionListenerAdapter implements TransactionListener +{ + /** + * {@inheritDoc} + */ + @Override + public void beforeCommit(boolean readOnly) + { + } + + /** + * {@inheritDoc} + */ + @Override + public void beforeCompletion() + { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterCommit() + { + } + + /** + * {@inheritDoc} + */ + @Override + public void afterRollback() + { + } +} diff --git a/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java b/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java new file mode 100644 index 0000000000..e6b2d50066 --- /dev/null +++ b/src/main/java/org/alfresco/util/transaction/TransactionSupportUtil.java @@ -0,0 +1,639 @@ +/* + * Copyright (C) 2014-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.transaction; + + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentSkipListSet; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.util.GUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.extensions.surf.util.ParameterCheck; +import org.springframework.orm.hibernate3.SessionFactoryUtils; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Helper class to manage transaction synchronization. This provides helpers to + * ensure that the necessary TransactionSynchronization instances + * are registered on behalf of the application code. + * + * @author mrogers + */ +public abstract class TransactionSupportUtil +{ + private static Log logger = LogFactory.getLog(TransactionSupportUtil.class); + + /** + * The order of synchronization set to be 100 less than the Hibernate synchronization order + */ + public static final int SESSION_SYNCHRONIZATION_ORDER = + SessionFactoryUtils.SESSION_SYNCHRONIZATION_ORDER - 100; + + /** resource key to store the transaction synchronizer instance */ + protected static final String RESOURCE_KEY_TXN_SYNCH = "txnSynch"; + /** resource binding during after-completion phase */ + protected static final String RESOURCE_KEY_TXN_COMPLETING = "AlfrescoTransactionSupport.txnCompleting"; + + /** + * @return Returns the system time when the transaction started, or -1 if there is no current transaction. + */ + public static long getTransactionStartTime() + { + /* + * This method can be called outside of a transaction, so we can go direct to the synchronizations. + */ + TransactionSynchronizationImpl txnSynch = + (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (txnSynch == null) + { + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + // need to lazily register synchronizations + return registerSynchronizations().getTransactionStartTime(); + } + else + { + return -1; // not in a transaction + } + } + else + { + return txnSynch.getTransactionStartTime(); + } + } + + /** + * Get a unique identifier associated with each transaction of each thread. Null is returned if + * no transaction is currently active. + * + * @return Returns the transaction ID, or null if no transaction is present + */ + public static String getTransactionId() + { + /* + * Go direct to the synchronizations as we don't want to register a resource if one doesn't exist. + * This method is heavily used, so the simple Map lookup on the ThreadLocal is the fastest. + */ + + TransactionSynchronizationImpl txnSynch = + (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (txnSynch == null) + { + if (TransactionSynchronizationManager.isSynchronizationActive()) + { + // need to lazily register synchronizations + return registerSynchronizations().getTransactionId(); + } + else + { + return null; // not in a transaction + } + } + else + { + return txnSynch.getTransactionId(); + } + } + + public static boolean isActualTransactionActive() + { + return TransactionSynchronizationManager.isActualTransactionActive(); + } + + /** + * Gets a resource associated with the current transaction, which must be active. + *

+ * All necessary synchronization instances will be registered automatically, if required. + * + * + * @param key the thread resource map key + * @return Returns a thread resource of null if not present + * + * @see org.alfresco.repo.transaction.TransactionalResourceHelper for helper methods to create and bind common collection types + */ + @SuppressWarnings("unchecked") + public static R getResource(Object key) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // get the resource + Object resource = txnSynch.resources.get(key); + // done + if (logger.isTraceEnabled()) + { + logger.trace("Fetched resource: \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + return (R) resource; + } + + /** + * Gets the current transaction synchronization instance, which contains the locally bound + * resources that are available to {@link #getResource(Object) retrieve} or + * {@link #bindResource(Object, Object) add to}. + *

+ * This method also ensures that the transaction binding has been performed. + * + * @return Returns the common synchronization instance used + */ + private static TransactionSynchronizationImpl getSynchronization() + { + // ensure synchronizations + return registerSynchronizations(); + } + + /** + * Binds the Alfresco-specific to the transaction resources + * + * @return Returns the current or new synchronization implementation + */ + private static TransactionSynchronizationImpl registerSynchronizations() + { + /* + * No thread synchronization or locking required as the resources are all threadlocal + */ + if (!TransactionSynchronizationManager.isSynchronizationActive()) + { + Thread currentThread = Thread.currentThread(); + throw new AlfrescoRuntimeException("Transaction must be active and synchronization is required: " + currentThread); + } + TransactionSynchronizationImpl txnSynch = + (TransactionSynchronizationImpl) TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (txnSynch != null) + { + // synchronization already registered + return txnSynch; + } + // we need a unique ID for the transaction + String txnId = GUID.generate(); + // register the synchronization + txnSynch = new TransactionSynchronizationImpl(txnId); + TransactionSynchronizationManager.registerSynchronization(txnSynch); + // register the resource that will ensure we don't duplication the synchronization + TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bound txn synch: " + txnSynch); + } + return txnSynch; + } + + /** + * Cleans out transaction resources if present + */ + private static void clearSynchronization() + { + if (TransactionSynchronizationManager.hasResource(RESOURCE_KEY_TXN_SYNCH)) + { + Object txnSynch = TransactionSynchronizationManager.unbindResource(RESOURCE_KEY_TXN_SYNCH); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Unbound txn synch:" + txnSynch); + } + } + } + + /** + * Helper method to rebind the synchronization to the transaction + * + * @param txnSynch TransactionSynchronizationImpl + */ + private static void rebindSynchronization(TransactionSynchronizationImpl txnSynch) + { + TransactionSynchronizationManager.bindResource(RESOURCE_KEY_TXN_SYNCH, txnSynch); + if (logger.isDebugEnabled()) + { + logger.debug("Bound (rebind) txn synch: " + txnSynch); + } + } + + /** + * Binds a resource to the current transaction, which must be active. + *

+ * All necessary synchronization instances will be registered automatically, if required. + * + * @param key Object + * @param resource Object + */ + public static void bindResource(Object key, Object resource) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // bind the resource + txnSynch.resources.put(key, resource); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Bound resource: \n" + + " key: " + key + "\n" + + " resource: " + resource); + } + } + + /** + * Unbinds a resource from the current transaction, which must be active. + *

+ * All necessary synchronization instances will be registered automatically, if required. + * + * @param key Object + */ + public static void unbindResource(Object key) + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + // remove the resource + txnSynch.resources.remove(key); + // done + if (logger.isDebugEnabled()) + { + logger.debug("Unbound resource: \n" + + " key: " + key); + } + } + + /** + * Bind listener to the specified priority. Duplicate bindings + * + * The priority specifies the position for the listener during commit. + * For example flushing of caches needs to happen very late. + * @param listener the listener to bind. + * @param priority 0 = Normal priority + * @return true if the new listener was bound. False if the listener was already bound. + */ + public static boolean bindListener(TransactionListener listener, int priority) + { + if (logger.isDebugEnabled()) + { + logger.debug("Bind Listener listener: " + listener + ", priority: " + priority); + } + TransactionSynchronizationImpl synch = getSynchronization(); + return synch.addListener(listener, priority); + } + + /** + * @return Returns all the listeners in a list disconnected from the original set + */ + public static Set getListeners() + { + // get the synchronization + TransactionSynchronizationImpl txnSynch = getSynchronization(); + + return txnSynch.getListenersIterable(); + + } + + /** + * Handler of txn synchronization callbacks specific to internal + * application requirements + */ + private static class TransactionSynchronizationImpl extends TransactionSynchronizationAdapter + { + private long txnStartTime; + private final String txnId; + private final Map resources; + + /** + * priority to listeners + */ + private final Map>priorityLookup = new HashMap>(); + + /** + * Sets up the resource map + * + * @param txnId String + */ + public TransactionSynchronizationImpl(String txnId) + { + this.txnStartTime = System.currentTimeMillis(); + this.txnId = txnId; + priorityLookup.put(0, new LinkedHashSet(5)); + resources = new HashMap(17); + } + + public long getTransactionStartTime() + { + return txnStartTime; + } + + public String getTransactionId() + { + return txnId; + } + + /** + * Add a trasaction listener + * + * @return true if the listener was added, false if it already existed. + */ + public boolean addListener(TransactionListener listener, int priority) + { + ParameterCheck.mandatory("listener", listener); + + if(this.priorityLookup.containsKey(priority)) + { + Set listeners = priorityLookup.get(priority); + return listeners.add(listener); + } + else + { + synchronized (priorityLookup) + { + if(priorityLookup.containsKey(priority)) + { + Set listeners = priorityLookup.get(priority); + return listeners.add(listener); + } + else + { + Set listeners = new LinkedHashSet(5); + priorityLookup.put(priority, listeners); + return listeners.add(listener); + } + } + } + } + + /** + * Return the level zero (normal) listeners + * + * @return Returns the level zero (normal) listeners in a list disconnected from the original set + */ + private List getLevelZeroListenersIterable() + { + Setlisteners = priorityLookup.get(0); + return new ArrayList(listeners); + } + + /** + * @return all the listeners regardless of priority + */ + private Set getListenersIterable() + { + Set ret = new LinkedHashSet(); + Set>> entries = priorityLookup.entrySet(); + + for(Entry> entry : entries) + { + ret.addAll((Set)entry.getValue()); + } + + return ret; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(50); + sb.append("TransactionSychronizationImpl") + .append("[ txnId=").append(txnId) + .append("]"); + return sb.toString(); + } + + /** + * @see org.alfresco.repo.transaction.AlfrescoTransactionSupport#SESSION_SYNCHRONIZATION_ORDER + */ + @Override + public int getOrder() + { + return TransactionSupportUtil.SESSION_SYNCHRONIZATION_ORDER; + } + + @Override + public void suspend() + { + if (logger.isDebugEnabled()) + { + logger.debug("Suspending transaction: " + this); + } + TransactionSupportUtil.clearSynchronization(); + } + + @Override + public void resume() + { + if (logger.isDebugEnabled()) + { + logger.debug("Resuming transaction: " + this); + } + TransactionSupportUtil.rebindSynchronization(this); + } + + /** + * Pre-commit cleanup. + *

+ * Ensures that the session transaction listeners are property executed. + * + * The Lucene indexes are then prepared. + */ + @Override + public void beforeCommit(boolean readOnly) + { + if (logger.isDebugEnabled()) + { + logger.debug("Before commit " + (readOnly ? "read-only" : "" ) + this); + } + // get the txn ID + TransactionSynchronizationImpl synch = (TransactionSynchronizationImpl) + TransactionSynchronizationManager.getResource(RESOURCE_KEY_TXN_SYNCH); + if (synch == null) + { + throw new AlfrescoRuntimeException("No synchronization bound to thread"); + } + + logger.trace("Before Prepare - level 0"); + + // Run the priority 0 (normal) listeners + // These are still considered part of the transaction so are executed here + doBeforeCommit(readOnly); + + // Now run the > 0 listeners beforeCommit + Set priorities = priorityLookup.keySet(); + + SortedSet sortedPriorities = new ConcurrentSkipListSet(FORWARD_INTEGER_ORDER); + sortedPriorities.addAll(priorities); + sortedPriorities.remove(0); // already done level 0 above + + if(logger.isDebugEnabled()) + { + logger.debug("Before Prepare priorities:" + sortedPriorities); + } + for(Integer priority : sortedPriorities) + { + Set listeners = priorityLookup.get(priority); + + for(TransactionListener listener : listeners) + { + listener.beforeCommit(readOnly); + } + } + if(logger.isDebugEnabled()) + { + logger.debug("Prepared"); + } + } + + /** + * Execute the beforeCommit event handlers for the registered listeners + * + * @param readOnly is read only + */ + private void doBeforeCommit(boolean readOnly) + { + doBeforeCommit(new HashSet(), readOnly); + } + + /** + * Executes the beforeCommit event handlers for the outstanding listeners. + * This process is iterative as the process of calling listeners may lead to more listeners + * being added. The new listeners will be processed until there no listeners remaining. + * + * @param visitedListeners a set containing the already visited listeners + * @param readOnly is read only + */ + private void doBeforeCommit(Set visitedListeners, boolean readOnly) + { + Set listeners = priorityLookup.get(0); + Set pendingListeners = new HashSet(listeners); + pendingListeners.removeAll(visitedListeners); + + if (pendingListeners.size() != 0) + { + for (TransactionListener listener : pendingListeners) + { + listener.beforeCommit(readOnly); + visitedListeners.add(listener); + } + + doBeforeCommit(visitedListeners, readOnly); + } + } + + @Override + public void beforeCompletion() + { + if (logger.isDebugEnabled()) + { + logger.debug("Before completion: " + this); + } + // notify listeners + for (TransactionListener listener : getLevelZeroListenersIterable()) + { + listener.beforeCompletion(); + } + } + + + @Override + public void afterCompletion(int status) + { + String statusStr = "unknown"; + switch (status) + { + case TransactionSynchronization.STATUS_COMMITTED: + statusStr = "committed"; + break; + case TransactionSynchronization.STATUS_ROLLED_BACK: + statusStr = "rolled-back"; + break; + default: + } + if (logger.isDebugEnabled()) + { + logger.debug("After completion (" + statusStr + "): " + this); + } + + // Force any queries for read-write state to return TXN_READ_ONLY + // This will be cleared with the synchronization, so we don't need to clear it out + TransactionSupportUtil.bindResource(RESOURCE_KEY_TXN_COMPLETING, Boolean.TRUE); + + Set priorities = priorityLookup.keySet(); + + SortedSet sortedPriorities = new ConcurrentSkipListSet(REVERSE_INTEGER_ORDER); + sortedPriorities.addAll(priorities); + + // Need to run these in reverse order cache,lucene,listeners + for(Integer priority : sortedPriorities) + { + Set listeners = new HashSet(priorityLookup.get(priority)); + + for(TransactionListener listener : listeners) + { + try + { + if (status == TransactionSynchronization.STATUS_COMMITTED) + { + listener.afterCommit(); + } + else + { + listener.afterRollback(); + } + } + catch (RuntimeException e) + { + logger.error("After completion (" + statusStr + ") TransactionalCache exception", e); + } + } + } + if(logger.isDebugEnabled()) + { + logger.debug("After Completion: DONE"); + } + + + // clear the thread's registrations and synchronizations + TransactionSupportUtil.clearSynchronization(); + } + } + + static private Comparator FORWARD_INTEGER_ORDER = new Comparator() + { + @Override + public int compare(Integer arg0, Integer arg1) + { + return arg0.intValue() - arg1.intValue(); + } + } ; + + static private Comparator REVERSE_INTEGER_ORDER = new Comparator() + { + @Override + public int compare(Integer arg0, Integer arg1) + { + return arg1.intValue() - arg0.intValue(); + } + } ; + +} diff --git a/src/main/java/org/alfresco/util/xml/SchemaHelper.java b/src/main/java/org/alfresco/util/xml/SchemaHelper.java new file mode 100644 index 0000000000..65ae33fd31 --- /dev/null +++ b/src/main/java/org/alfresco/util/xml/SchemaHelper.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.xml; + +import java.io.File; +import java.net.URL; + +import org.springframework.util.ResourceUtils; +import org.xml.sax.InputSource; +import org.xml.sax.SAXParseException; + +import com.sun.codemodel.JCodeModel; +import com.sun.tools.xjc.api.ErrorListener; +import com.sun.tools.xjc.api.S2JJAXBModel; +import com.sun.tools.xjc.api.SchemaCompiler; +import com.sun.tools.xjc.api.XJC; + +/** + * Helper to generate code from XSD files. + * + * @author Derek Hulley + * @since 3.2 + */ +public class SchemaHelper +{ + public static void main(String ... args) + { + if (args.length < 2 || !args[0].startsWith("--compile-xsd=") && !args[1].startsWith("--output-dir=")) + { + System.out.println("Usage: SchemaHelper --compile-xsd= --output-dir="); + System.exit(1); + } + final String urlStr = args[0].substring(14); + if (urlStr.length() == 0) + { + System.out.println("Usage: SchemaHelper --compile-xsd= --output-dir="); + System.exit(1); + } + final String dirStr = args[1].substring(13); + if (dirStr.length() == 0) + { + System.out.println("Usage: SchemaHelper --compile-xsd= --output-dir="); + System.exit(1); + } + try + { + URL url = ResourceUtils.getURL(urlStr); + File dir = new File(dirStr); + if (!dir.exists() || !dir.isDirectory()) + { + System.out.println("Output directory not found: " + dirStr); + System.exit(1); + } + + ErrorListener errorListener = new ErrorListener() + { + public void warning(SAXParseException e) + { + System.out.println("WARNING: " + e.getMessage()); + } + public void info(SAXParseException e) + { + System.out.println("INFO: " + e.getMessage()); + } + public void fatalError(SAXParseException e) + { + handleException(urlStr, e); + } + public void error(SAXParseException e) + { + handleException(urlStr, e); + } + }; + + SchemaCompiler compiler = XJC.createSchemaCompiler(); + compiler.setErrorListener(errorListener); + compiler.parseSchema(new InputSource(url.toExternalForm())); + S2JJAXBModel model = compiler.bind(); + if (model == null) + { + System.out.println("Failed to produce binding model for URL " + urlStr); + System.exit(1); + } + JCodeModel codeModel = model.generateCode(null, errorListener); + codeModel.build(dir); + } + catch (Throwable e) + { + handleException(urlStr, e); + System.exit(1); + } + } + private static void handleException(String urlStr, Throwable e) + { + System.out.println("Error processing XSD " + urlStr); + e.printStackTrace(); + } +} diff --git a/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java b/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java new file mode 100644 index 0000000000..0a77cdfcea --- /dev/null +++ b/src/main/java/org/alfresco/web/scripts/servlet/StaticAssetCacheFilter.java @@ -0,0 +1,79 @@ +/* + * 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.web.scripts.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Simple servlet filter to add a 'Cache-Control' HTTP header to a response. + * The Cache-Control header is set to a max-age value by a configurable setting + * in the 'expires' init parameters - values are in days. + * + * WebScripts or other servlets that happen to match the response type + * configured for the filter (e.g. "*.js") should override cache settings + * as required. + * + * @author Kevin Roast + */ +public class StaticAssetCacheFilter implements Filter +{ + private static final long DAY_S = 60L*60L*24L; // 1 day in seconds + private static final long DEFAULT_30DAYS = 30L; // default of 30 days if not configured + + private long expire = DAY_S * DEFAULT_30DAYS; // initially set to default value of 30 days + + + /* (non-Javadoc) + * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) + */ + public void init(FilterConfig config) throws ServletException + { + String expireParam = config.getInitParameter("expires"); + if (expireParam != null) + { + this.expire = Long.parseLong(expireParam) * DAY_S; + } + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, + ServletException + { + ((HttpServletResponse)res).setHeader("Cache-Control", "must-revalidate, max-age=" + Long.toString(this.expire)); + chain.doFilter(req, res); + } + + /* (non-Javadoc) + * @see javax.servlet.Filter#destroy() + */ + public void destroy() + { + this.expire = DAY_S * DEFAULT_30DAYS; + } +} \ No newline at end of file diff --git a/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java b/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java new file mode 100644 index 0000000000..11ddffdcdc --- /dev/null +++ b/src/main/java/org/alfresco/web/scripts/servlet/X509ServletFilterBase.java @@ -0,0 +1,326 @@ +/* +* 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.web.scripts.servlet; + +import javax.management.*; +import javax.security.auth.x500.X500Principal; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * The X509ServletFilterBase enforces X509 Authentication. + * + * Optional Init Param: + * cert-contains : Ensure that the principal subject of the cert contains a specific string. + * + * The X509ServletFilter will also ensure that the cert is present in the request, which will only happen if there + * is a successful SSL handshake which includes client authentication. This handshake is handled by the Application Server. + * A SSL handshake that does not include client Authentication will receive a 403 error response. + * + * The checkInforce method must be implemented to determine if the X509 Authentication is turned on. This allows + * applications to turn on/off X509 Authentication based on parameters outside of the web.xml. + * + * */ + +public abstract class X509ServletFilterBase implements Filter +{ + + protected boolean enforce; + private String httpsPort; + private String certContains; + private static Log logger = LogFactory.getLog(X509ServletFilterBase.class); + + public void init(FilterConfig config) throws ServletException + { + try + { + /* + * Find out if we are enforcing. + */ + + if(logger.isDebugEnabled()) + { + logger.debug("Initializing X509ServletFilter"); + } + + this.handleClientAuth(); + + this.enforce = checkEnforce(config.getServletContext()); + + if(logger.isDebugEnabled()) + { + logger.debug("Enforcing X509 Authentication:"+this.enforce); + } + + if (this.enforce) + { + /* + * We are enforcing so get the cert-contains string. + */ + + this.certContains = config.getInitParameter("cert-contains"); + + if(logger.isDebugEnabled()) + { + if(certContains == null) + { + logger.debug("Not enforcing cert-contains"); + } + else + { + logger.debug("Enforcing cert-contains:" + this.certContains); + } + } + } + } + catch (Exception e) + { + throw new ServletException(e); + } + } + + public void setHttpsPort(int port) + { + this.httpsPort = Integer.toString(port); + } + + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, + ServletException + { + + HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + /* + * Test if we are enforcing X509. + */ + if(this.enforce) + { + if(logger.isDebugEnabled()) + { + logger.debug("Enforcing X509 request"); + } + + X509Certificate[] certs = (X509Certificate[])httpRequest.getAttribute("javax.servlet.request.X509Certificate"); + if(validCert(certs)) + { + + if(logger.isDebugEnabled()) + { + logger.debug("Cert is valid"); + } + + /* + * The cert is valid so forward the request. + */ + + chain.doFilter(request,response); + } + else + { + if(logger.isDebugEnabled()) + { + logger.debug("Cert is invalid"); + } + + if(!httpRequest.isSecure()) + { + if(this.httpsPort != null) + { + String redirectUrl = httpRequest.getRequestURL().toString(); + int port = httpRequest.getLocalPort(); + String httpPort = Integer.toString(port); + redirectUrl = redirectUrl.replace(httpPort, httpsPort); + redirectUrl = redirectUrl.replace("http", "https"); + String query = httpRequest.getQueryString(); + if(query != null) + { + redirectUrl = redirectUrl+"?"+query; + } + + if(logger.isDebugEnabled()) + { + logger.debug("Redirecting to:"+redirectUrl); + } + httpResponse.sendRedirect(redirectUrl); + return; + } + } + /* + * Invalid cert so send 403. + */ + httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "X509 Authentication failure"); + } + } + else + { + /* + * We are not enforcing X509 so forward the request + */ + chain.doFilter(request,response); + } + } + + + /** + * + * @param servletContext + * @return true if enforcing X509 false if not enforcing X509 + * @throws IOException + * + * The checkInforce method is called during the initialization of the Filter. Implement this method to decide if + * X509 security is being enforced. + * + **/ + + protected abstract boolean checkEnforce(ServletContext servletContext) throws IOException; + + private boolean validCert(X509Certificate[] certs) + { + /* + * If the cert is null then the it's not valid. + */ + + if(certs == null) + { + return false; + } + + /* + * Get the first certificate in the chain. The first certificate is the client certificate. + */ + + X509Certificate cert = certs[0]; + try + { + /* + * check the certificate has not expired. + */ + if(logger.isDebugEnabled()) + { + logger.debug("Checking cert is valid"); + } + cert.checkValidity(); + } + catch (Exception e) + { + logger.error("Cert is invalid", e); + return false; + } + + X500Principal x500Principal = cert.getSubjectX500Principal(); + String name = x500Principal.getName(); + + /* + * Cert contains is an optional check + */ + + if(this.certContains == null) + { + return true; + } + + /* + * Check that the cert contains the specified value. + */ + + if(name.contains(this.certContains)) + { + if(logger.isDebugEnabled()) + { + logger.debug("Cert: "+ name + " contains: "+ this.certContains); + } + + return true; + } + else + { + logger.error("Cert: " + name + " does not contain: " + this.certContains); + return false; + } + } + + public void destroy() + { + } + + private void handleClientAuth() + { + try + { + MBeanServer mBeanServer = MBeanServerFactory.findMBeanServer(null).get(0); + + //Are we Tomcat + ObjectName catalina = new ObjectName("Catalina", "type", "Engine"); + Set objectNames = mBeanServer.queryNames(catalina, null); + if(objectNames == null || objectNames.size() == 0) + { + //We do not appear to be Tomcat + return; + } + + //We are Tomcat so look for the clientAuth + QueryExp query = Query.or(Query.eq(Query.attr("clientAuth"), Query.value("want")), + Query.eq(Query.attr("clientAuth"), Query.value(true))); + + objectNames = mBeanServer.queryNames(null, query); + + if (objectNames != null && objectNames.size() == 0) + { + logger.warn("clientAuth does not appear to be set for Tomcat. clientAuth must be set to 'want' for X509 Authentication"); + logger.warn("Attempting to set clientAuth=want through JMX..."); + + query = Query.eq(Query.attr("secure"), Query.value(true)); + + Set objectNames1 = mBeanServer.queryNames(null, query); + + if(objectNames1 != null) + { + for(ObjectName objectName : objectNames1) + { + if(objectName.toString().contains("ProtocolHandler")) + { + logger.warn("Setting clientAuth=want on MBean:" + objectName.toString()); + mBeanServer.setAttribute(objectName, new Attribute("clientAuth", "want")); + return; + } + } + } + + logger.warn("Unable to set clientAuth=want through JMX."); + } + } + catch(Throwable t) + { + logger.warn("An error occurred while checking for clientAuth. Turn on debug logging to see the stacktrace."); + logger.debug("Error while handling clientAuth",t); + } + } +} \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000000..b4a8ca66bf --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,264 @@ +# Set root logger level to error +log4j.rootLogger=error, Console, File + +###### Console appender definition ####### + +# All outputs currently set to be a ConsoleAppender. +log4j.appender.Console=org.apache.log4j.ConsoleAppender +log4j.appender.Console.layout=org.apache.log4j.PatternLayout + +# use log4j NDC to replace %x with tenant domain / username +log4j.appender.Console.layout.ConversionPattern=%d{ISO8601} %x %-5p [%c{3}] [%t] %m%n +#log4j.appender.Console.layout.ConversionPattern=%d{ABSOLUTE} %-5p [%c] %m%n + +###### File appender definition ####### +log4j.appender.File=org.apache.log4j.DailyRollingFileAppender +log4j.appender.File.File=alfresco.log +log4j.appender.File.Append=true +log4j.appender.File.DatePattern='.'yyyy-MM-dd +log4j.appender.File.layout=org.apache.log4j.PatternLayout +log4j.appender.File.layout.ConversionPattern=%d{yyyy-MM-dd} %d{ABSOLUTE} %-5p [%c] [%t] %m%n + +###### Hibernate specific appender definition ####### +#log4j.appender.file=org.apache.log4j.FileAppender +#log4j.appender.file.File=hibernate.log +#log4j.appender.file.layout=org.apache.log4j.PatternLayout +#log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n + +###### Log level overrides ####### + +# Commented-in loggers will be exposed as JMX MBeans (refer to org.alfresco.repo.admin.Log4JHierarchyInit) +# Hence, generally useful loggers should be listed with at least ERROR level to allow simple runtime +# control of the level via a suitable JMX Console. Also, any other loggers can be added transiently via +# Log4j addLoggerMBean as long as the logger exists and has been loaded. + +# Hibernate +log4j.logger.org.hibernate=error +log4j.logger.org.hibernate.util.JDBCExceptionReporter=fatal +log4j.logger.org.hibernate.event.def.AbstractFlushingEventListener=fatal +log4j.logger.org.hibernate.type=warn +log4j.logger.org.hibernate.cfg.SettingsFactory=warn + +# Spring +log4j.logger.org.springframework=warn +# Turn off Spring remoting warnings that should really be info or debug. +log4j.logger.org.springframework.remoting.support=error +log4j.logger.org.springframework.util=error + +# Axis/WSS4J +log4j.logger.org.apache.axis=info +log4j.logger.org.apache.ws=info + +# CXF +log4j.logger.org.apache.cxf=error + +# MyFaces +log4j.logger.org.apache.myfaces.util.DebugUtils=info +log4j.logger.org.apache.myfaces.el.VariableResolverImpl=error +log4j.logger.org.apache.myfaces.application.jsp.JspViewHandlerImpl=error +log4j.logger.org.apache.myfaces.taglib=error + +# OpenOfficeConnection +log4j.logger.net.sf.jooreports.openoffice.connection=fatal + +# log prepared statement cache activity ### +log4j.logger.org.hibernate.ps.PreparedStatementCache=info + +# Alfresco +log4j.logger.org.alfresco=error +log4j.logger.org.alfresco.repo.admin=info +log4j.logger.org.alfresco.repo.transaction=warn +log4j.logger.org.alfresco.repo.cache.TransactionalCache=warn +log4j.logger.org.alfresco.repo.model.filefolder=warn +log4j.logger.org.alfresco.repo.tenant=info +log4j.logger.org.alfresco.config=warn +log4j.logger.org.alfresco.config.JndiObjectFactoryBean=warn +log4j.logger.org.alfresco.config.JBossEnabledWebApplicationContext=warn +log4j.logger.org.alfresco.repo.management.subsystems=warn +log4j.logger.org.alfresco.repo.management.subsystems.ChildApplicationContextFactory=info +log4j.logger.org.alfresco.repo.management.subsystems.ChildApplicationContextFactory$ChildApplicationContext=warn +log4j.logger.org.alfresco.repo.security.sync=info +log4j.logger.org.alfresco.repo.security.person=info + +log4j.logger.org.alfresco.sample=info +log4j.logger.org.alfresco.web=info +#log4j.logger.org.alfresco.web.app.AlfrescoNavigationHandler=debug +#log4j.logger.org.alfresco.web.ui.repo.component.UIActions=debug +#log4j.logger.org.alfresco.web.ui.repo.tag.PageTag=debug +#log4j.logger.org.alfresco.web.bean.clipboard=debug +log4j.logger.org.alfresco.service.descriptor.DescriptorService=info +#log4j.logger.org.alfresco.web.page=debug + +log4j.logger.org.alfresco.repo.importer.ImporterBootstrap=error +#log4j.logger.org.alfresco.repo.importer.ImporterBootstrap=info + +log4j.logger.org.alfresco.repo.admin.patch.PatchExecuter=info +log4j.logger.org.alfresco.repo.domain.patch.ibatis.PatchDAOImpl=info + +# Specific patches +log4j.logger.org.alfresco.repo.admin.patch.impl.DeploymentMigrationPatch=info +log4j.logger.org.alfresco.repo.version.VersionMigrator=info + +log4j.logger.org.alfresco.repo.module.ModuleServiceImpl=info +log4j.logger.org.alfresco.repo.domain.schema.SchemaBootstrap=info +log4j.logger.org.alfresco.repo.admin.ConfigurationChecker=info +log4j.logger.org.alfresco.repo.node.index.AbstractReindexComponent=warn +log4j.logger.org.alfresco.repo.node.index.IndexTransactionTracker=warn +log4j.logger.org.alfresco.repo.node.index.FullIndexRecoveryComponent=info +log4j.logger.org.alfresco.util.OpenOfficeConnectionTester=info +log4j.logger.org.alfresco.repo.node.db.hibernate.HibernateNodeDaoServiceImpl=warn +log4j.logger.org.alfresco.repo.domain.hibernate.DirtySessionMethodInterceptor=warn +log4j.logger.org.alfresco.repo.transaction.RetryingTransactionHelper=warn +log4j.logger.org.alfresco.util.transaction.SpringAwareUserTransaction.trace=warn +log4j.logger.org.alfresco.util.AbstractTriggerBean=warn +log4j.logger.org.alfresco.enterprise.repo.cluster=info +log4j.logger.org.alfresco.repo.version.Version2ServiceImpl=warn + +#log4j.logger.org.alfresco.web.app.DebugPhaseListener=debug +log4j.logger.org.alfresco.repo.node.db.NodeStringLengthWorker=info + +log4j.logger.org.alfresco.repo.workflow=info + +# CIFS server debugging +log4j.logger.org.alfresco.smb.protocol=error +#log4j.logger.org.alfresco.smb.protocol.auth=debug +#log4j.logger.org.alfresco.acegi=debug + +# FTP server debugging +log4j.logger.org.alfresco.ftp.protocol=error +#log4j.logger.org.alfresco.ftp.server=debug + +# WebDAV debugging +#log4j.logger.org.alfresco.webdav.protocol=debug +log4j.logger.org.alfresco.webdav.protocol=info + +# NTLM servlet filters +#log4j.logger.org.alfresco.web.app.servlet.NTLMAuthenticationFilter=debug +#log4j.logger.org.alfresco.repo.webdav.auth.NTLMAuthenticationFilter=debug + +# Kerberos servlet filters +#log4j.logger.org.alfresco.web.app.servlet.KerberosAuthenticationFilter=debug +#log4j.logger.org.alfresco.repo.webdav.auth.KerberosAuthenticationFilter=debug + +# File servers +log4j.logger.org.alfresco.fileserver=warn + +# Repo filesystem debug logging +#log4j.logger.org.alfresco.filesys.repo.ContentDiskDriver=debug + +# Integrity message threshold - if 'failOnViolation' is off, then WARNINGS are generated +log4j.logger.org.alfresco.repo.node.integrity=ERROR + +# Indexer debugging +log4j.logger.org.alfresco.repo.search.Indexer=error +#log4j.logger.org.alfresco.repo.search.Indexer=debug + +log4j.logger.org.alfresco.repo.search.impl.lucene.index=error +log4j.logger.org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexerImpl=warn +#log4j.logger.org.alfresco.repo.search.impl.lucene.index=DEBUG + +# Audit debugging +# log4j.logger.org.alfresco.repo.audit=DEBUG +# log4j.logger.org.alfresco.repo.audit.model=DEBUG + +# Property sheet and modelling debugging +# change to error to hide the warnings about missing properties and associations +log4j.logger.alfresco.missingProperties=warn + +# Dictionary/Model debugging +log4j.logger.org.alfresco.repo.dictionary=warn +log4j.logger.org.alfresco.repo.dictionary.types.period=warn + +# Virtualization Server Registry +log4j.logger.org.alfresco.mbeans.VirtServerRegistry=error + +# Spring context runtime property setter +log4j.logger.org.alfresco.util.RuntimeSystemPropertiesSetter=info + +# Debugging options for clustering +log4j.logger.org.alfresco.repo.content.ReplicatingContentStore=error +log4j.logger.org.alfresco.repo.content.replication=error + +#log4j.logger.org.alfresco.repo.deploy.DeploymentServiceImpl=debug + +# Activity service +log4j.logger.org.alfresco.repo.activities=warn + +# User usage tracking +log4j.logger.org.alfresco.repo.usage=info + +# Sharepoint +log4j.logger.org.alfresco.module.vti=info + +# Forms Engine +log4j.logger.org.alfresco.web.config.forms=info +log4j.logger.org.alfresco.web.scripts.forms=info + +# CMIS +log4j.logger.org.alfresco.opencmis=error +log4j.logger.org.alfresco.opencmis.AlfrescoCmisServiceInterceptor=error +log4j.logger.org.alfresco.cmis=error +log4j.logger.org.alfresco.cmis.dictionary=warn +log4j.logger.org.apache.chemistry.opencmis=info +log4j.logger.org.apache.chemistry.opencmis.server.impl.browser.CmisBrowserBindingServlet=OFF +log4j.logger.org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet=OFF + +# IMAP +log4j.logger.org.alfresco.repo.imap=info + +# JBPM +# Note: non-fatal errors (eg. logged during job execution) should be handled by Alfresco's retrying transaction handler +log4j.logger.org.jbpm.graph.def.GraphElement=fatal + +#log4j.logger.org.alfresco.repo.googledocs=debug + +###### Scripting ####### + +# Web Framework +log4j.logger.org.springframework.extensions.webscripts=info +log4j.logger.org.springframework.extensions.webscripts.ScriptLogger=warn +log4j.logger.org.springframework.extensions.webscripts.ScriptDebugger=off + +# Repository +log4j.logger.org.alfresco.repo.web.scripts=warn +log4j.logger.org.alfresco.repo.web.scripts.BaseWebScriptTest=info +log4j.logger.org.alfresco.repo.web.scripts.AlfrescoRhinoScriptDebugger=off +log4j.logger.org.alfresco.repo.jscript=error +log4j.logger.org.alfresco.repo.jscript.ScriptLogger=warn +log4j.logger.org.alfresco.repo.cmis.rest.CMISTest=info + +log4j.logger.org.alfresco.repo.domain.schema.script.ScriptBundleExecutorImpl=off +log4j.logger.org.alfresco.repo.domain.schema.script.ScriptExecutorImpl=info + +log4j.logger.org.alfresco.repo.search.impl.solr.facet.SolrFacetServiceImpl=info + +# Bulk Filesystem Import Tool +log4j.logger.org.alfresco.repo.bulkimport=warn + +# Freemarker +# Note the freemarker.runtime logger is used to log non-fatal errors that are handled by Alfresco's retrying transaction handler +log4j.logger.freemarker.runtime= + +# Metadata extraction +log4j.logger.org.alfresco.repo.content.metadata.AbstractMappingMetadataExtracter=warn + +# Reduces PDFont error level due to ALF-7105 +log4j.logger.org.apache.pdfbox.pdmodel.font.PDSimpleFont=fatal +log4j.logger.org.apache.pdfbox.pdmodel.font.PDFont=fatal +log4j.logger.org.apache.pdfbox.pdmodel.font.PDCIDFont=fatal + +# no index support +log4j.logger.org.alfresco.repo.search.impl.noindex.NoIndexIndexer=fatal +log4j.logger.org.alfresco.repo.search.impl.noindex.NoIndexSearchService=fatal + +# lucene index warnings +log4j.logger.org.alfresco.repo.search.impl.lucene.index.IndexInfo=warn + +# Warn about RMI socket bind retries. +log4j.logger.org.alfresco.util.remote.server.socket.HostConfigurableSocketFactory=warn + +log4j.logger.org.alfresco.repo.usage.RepoUsageMonitor=info + +# Authorization +log4j.logger.org.alfresco.enterprise.repo.authorization.AuthorizationService=info +log4j.logger.org.alfresco.enterprise.repo.authorization.AuthorizationsConsistencyMonitor=warn diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties new file mode 100644 index 0000000000..9e65a1a5ac --- /dev/null +++ b/src/main/resources/logging.properties @@ -0,0 +1,8 @@ +handlers = java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + +org.activiti.level = FATAL +#Uncomment this to log schema creation statements from Activiti +#org.activiti.engine.impl.db.DbSqlSession.level = FINE diff --git a/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java b/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java new file mode 100644 index 0000000000..4d91bc75f0 --- /dev/null +++ b/src/test/java/org/alfresco/config/SystemPropertiesSetterBeanTest.java @@ -0,0 +1,88 @@ +/* + * 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 junit.framework.TestCase; + +/** + * @see SystemPropertiesSetterBean + * + * @author Derek Hulley + */ +public class SystemPropertiesSetterBeanTest extends TestCase +{ + private static final String KEY_A = "SystemPropertiesSetterBeanTest.A"; + private static final String KEY_B = "SystemPropertiesSetterBeanTest.B"; + private static final String KEY_C = "SystemPropertiesSetterBeanTest.C"; + private static final String KEY_EXISTING = "SystemPropertiesSetterBeanTest.EXISTING "; + private static final String KEY_PLACEHOLDER = "SystemPropertiesSetterBeanTest.PLACEHOLDER"; + private static final String KEY_EMPTY_STRING = "SystemPropertiesSetterBeanTest.EMPTY_STRING"; + private static final String KEY_NULL = "SystemPropertiesSetterBeanTest.NULL"; + private static final String VALUE_A = "A"; + private static final String VALUE_B = "B"; + private static final String VALUE_C = "C"; + private static final String VALUE_EXISTING = "EXISTING"; + private static final String VALUE_PLACEHOLDER = "${OOPS}"; + private static final String VALUE_EMPTY_STRING = ""; + private static final String VALUE_NULL = null; + + SystemPropertiesSetterBean setter; + private Map propertyMap; + + public void setUp() throws Exception + { + System.setProperty(KEY_EXISTING, VALUE_EXISTING); + + propertyMap = new HashMap(7); + propertyMap.put(KEY_A, VALUE_A); + propertyMap.put(KEY_B, VALUE_B); + propertyMap.put(KEY_C, VALUE_C); + propertyMap.put(KEY_EXISTING, "SHOULD NOT HAVE OVERRIDDEN EXISTING PROPERTY"); + propertyMap.put(KEY_PLACEHOLDER, VALUE_PLACEHOLDER); + propertyMap.put(KEY_EMPTY_STRING, VALUE_EMPTY_STRING); + propertyMap.put(KEY_NULL, VALUE_NULL); + + setter = new SystemPropertiesSetterBean(); + setter.setPropertyMap(propertyMap); + } + + public void testSetUp() + { + assertEquals(VALUE_EXISTING, System.getProperty(KEY_EXISTING)); + assertNull(System.getProperty(KEY_A)); + assertNull(System.getProperty(KEY_B)); + assertNull(System.getProperty(KEY_C)); + } + + public void testSettingOfSystemProperties() + { + setter.init(); + // Check + assertEquals(VALUE_A, System.getProperty(KEY_A)); + assertEquals(VALUE_B, System.getProperty(KEY_B)); + assertEquals(VALUE_C, System.getProperty(KEY_C)); + assertEquals(VALUE_EXISTING, System.getProperty(KEY_EXISTING)); + assertNull("Property placeholder not detected", System.getProperty(KEY_PLACEHOLDER)); + assertNull("Empty string not detected", System.getProperty(KEY_EMPTY_STRING)); + assertNull("Null string not detected", System.getProperty(KEY_NULL)); + } +} diff --git a/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java b/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java new file mode 100644 index 0000000000..9a22b69a75 --- /dev/null +++ b/src/test/java/org/alfresco/encryption/EncryptingOutputStreamTest.java @@ -0,0 +1,69 @@ +/* + * 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.util.Arrays; + +import junit.framework.TestCase; + +/** + * Tests that the EncryptingOutputStream and EncryptingInputStream classes work correctly. + */ +public class EncryptingOutputStreamTest extends TestCase +{ + + /** + * Tests encryption / decryption by attempting to encrypt and decrypt the bytes forming this class definition and + * comparing it with the unencrypted bytes. + * + * @throws Exception + * an exception + */ + public void testEncrypt() throws Exception + { + final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + final byte[] seed = getClass().getName().getBytes("UTF-8"); + random.setSeed(seed); + keyGen.initialize(1024, random); + final KeyPair pair = keyGen.generateKeyPair(); + + final ByteArrayOutputStream buff = new ByteArrayOutputStream(2048); + final OutputStream encrypting = new EncryptingOutputStream(buff, pair.getPublic(), random); + final ByteArrayOutputStream plainText1 = new ByteArrayOutputStream(2048); + + final InputStream in = getClass().getResourceAsStream(getClass().getSimpleName() + ".class"); + final byte[] inbuff = new byte[17]; + int bytesRead; + while ((bytesRead = in.read(inbuff)) != -1) + { + encrypting.write(inbuff, 0, bytesRead); + plainText1.write(inbuff, 0, bytesRead); + } + in.close(); + encrypting.close(); + plainText1.close(); + final InputStream decrypting = new DecryptingInputStream(new ByteArrayInputStream(buff.toByteArray()), pair + .getPrivate()); + final ByteArrayOutputStream plainText2 = new ByteArrayOutputStream(2048); + while ((bytesRead = decrypting.read(inbuff)) != -1) + { + plainText2.write(inbuff, 0, bytesRead); + } + + assertTrue(Arrays.equals(plainText1.toByteArray(), plainText2.toByteArray())); + + } +} diff --git a/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java b/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java new file mode 100644 index 0000000000..76a228a3c0 --- /dev/null +++ b/src/test/java/org/alfresco/error/AlfrescoRuntimeExceptionTest.java @@ -0,0 +1,116 @@ +/* + * 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.net.URL; +import java.util.Locale; +import java.util.ResourceBundle; + +import junit.framework.TestCase; + +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Alfresco runtime exception test + * + * @author Roy Wetherall + */ +public class AlfrescoRuntimeExceptionTest extends TestCase +{ + private static final String BASE_RESOURCE_NAME = "org.alfresco.i18n.testMessages"; + private static final String PARAM_VALUE = "television"; + private static final String MSG_PARAMS = "msg_params"; + private static final String MSG_ERROR = "msg_error"; + private static final String VALUE_ERROR = "This is an error message. \n This is on a new line."; + private static final String VALUE_FR_ERROR = "C'est un message d'erreur. \n C'est sur une nouvelle ligne."; + private static final String VALUE_PARAMS = "What no " + PARAM_VALUE + "?"; + private static final String VALUE_FR_PARAMS = "Que non " + PARAM_VALUE + "?"; + private static final String NON_I18NED_MSG = "This is a non i18ned error message."; + private static final String NON_EXISTING_MSG = "non.existing.msgId"; + + @Override + protected void setUp() throws Exception + { + // Re-set the current locale to be the default + Locale.setDefault(Locale.ENGLISH); + I18NUtil.setLocale(Locale.getDefault()); + } + + public void testI18NBehaviour() + { + // Ensure that the bundle is present on the classpath + String baseResourceAsProperty = BASE_RESOURCE_NAME.replace('.', '/') + ".properties"; + URL baseResourceURL = AlfrescoRuntimeExceptionTest.class.getClassLoader().getResource(baseResourceAsProperty); + assertNotNull(baseResourceURL); + + baseResourceAsProperty = BASE_RESOURCE_NAME.replace('.', '/') + "_fr_FR" + ".properties"; + baseResourceURL = AlfrescoRuntimeExceptionTest.class.getClassLoader().getResource(baseResourceAsProperty); + assertNotNull(baseResourceURL); + + // Ensure we can load it as a resource bundle + ResourceBundle properties = ResourceBundle.getBundle(BASE_RESOURCE_NAME); + assertNotNull(properties); + properties = ResourceBundle.getBundle(BASE_RESOURCE_NAME, new Locale("fr", "FR")); + assertNotNull(properties); + + + // From here on in, we use Spring + + // Register the bundle + I18NUtil.registerResourceBundle(BASE_RESOURCE_NAME); + + AlfrescoRuntimeException exception1 = new AlfrescoRuntimeException(MSG_PARAMS, new Object[]{PARAM_VALUE}); + assertTrue(exception1.getMessage().contains(VALUE_PARAMS)); + AlfrescoRuntimeException exception3 = new AlfrescoRuntimeException(MSG_ERROR); + assertTrue(exception3.getMessage().contains(VALUE_ERROR)); + + // Change the locale and re-test + I18NUtil.setLocale(new Locale("fr", "FR")); + + AlfrescoRuntimeException exception2 = new AlfrescoRuntimeException(MSG_PARAMS, new Object[]{PARAM_VALUE}); + assertTrue(exception2.getMessage().contains(VALUE_FR_PARAMS)); + AlfrescoRuntimeException exception4 = new AlfrescoRuntimeException(MSG_ERROR); + assertTrue(exception4.getMessage().contains(VALUE_FR_ERROR)); + + AlfrescoRuntimeException exception5 = new AlfrescoRuntimeException(NON_I18NED_MSG); + assertTrue(exception5.getMessage().contains(NON_I18NED_MSG)); + + // MNT-13028 + String param1 = PARAM_VALUE + "_1"; + String param2 = PARAM_VALUE + "_2"; + String param3 = PARAM_VALUE + "_3"; + AlfrescoRuntimeException exception6 = new AlfrescoRuntimeException(NON_EXISTING_MSG, new Object[]{param1, param2, param3}); + String message6 = exception6.getMessage(); + assertTrue(message6.contains(NON_EXISTING_MSG)); + assertTrue(message6.contains(param1)); + assertTrue(message6.contains(param2)); + assertTrue(message6.contains(param3)); + } + + public void testMakeRuntimeException() + { + Throwable e = new RuntimeException("sfsfs"); + RuntimeException ee = AlfrescoRuntimeException.makeRuntimeException(e, "Test"); + assertTrue("Exception should not have been changed", ee == e); + + e = new Exception(); + ee = AlfrescoRuntimeException.makeRuntimeException(e, "Test"); + assertTrue("Expected an AlfrescoRuntimeException instance", ee instanceof AlfrescoRuntimeException); + } +} diff --git a/src/test/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBeanTest.java b/src/test/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBeanTest.java new file mode 100644 index 0000000000..6f1721313c --- /dev/null +++ b/src/test/java/org/alfresco/ibatis/HierarchicalSqlSessionFactoryBeanTest.java @@ -0,0 +1,227 @@ +/* + * 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.AbstractCollection; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.TreeSet; + +import junit.framework.TestCase; + +import org.alfresco.util.resource.HierarchicalResourceLoader; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +/** + * @see HierarchicalSqlSessionFactoryBean + * @see HierarchicalXMLConfigBuilder + * @see HierarchicalResourceLoader + * + * @author Derek Hulley, janv + * @since 4.0 + */ +public class HierarchicalSqlSessionFactoryBeanTest extends TestCase +{ + private static final String QUERY_OBJECT = Object.class.getName(); + private static final String QUERY_ABSTRACTCOLLECTION = "org.alfresco.ibatis.abstractcollection."+AbstractCollection.class.getName().replace(".", "_"); + private static final String QUERY_ABSTRACTLIST = "org.alfresco.ibatis.abstractlist."+AbstractList.class.getName().replace(".", "_"); + private static final String QUERY_TREESET = "org.alfresco.ibatis.treeset."+TreeSet.class.getName().replace(".", "_"); + + private static Log logger = LogFactory.getLog(HierarchicalSqlSessionFactoryBeanTest.class); + + private ClassPathXmlApplicationContext ctx; + private TestDAO testDao; + + @Override + public void setUp() throws Exception + { + testDao = new TestDAO(); + testDao.setId(5L); + testDao.setPropOne("prop-one"); + testDao.setPropTwo("prop-two"); + } + + @Override + public void tearDown() throws Exception + { + try + { + if (ctx != null) + { + ctx.close(); + } + } + catch (Throwable e) + { + logger.error("Failed to neatly close application context", e); + } + } + + /** + * Pushes the dialect class into the system properties, closes an current context and + * recreates it; the MyBatis Configuration is then returned. + */ + @SuppressWarnings("unchecked") + private Configuration getConfiguration(Class dialectClass) throws Exception + { + System.setProperty("hierarchy-test.dialect", dialectClass.getName()); + if (ctx != null) + { + try + { + ctx.close(); + ctx = null; + } + catch (Throwable e) + { + logger.error("Failed to neatly close application context", e); + } + } + ctx = new ClassPathXmlApplicationContext("ibatis/hierarchy-test/hierarchy-test-context.xml"); + return ((SqlSessionFactory)ctx.getBean("mybatisConfig")).getConfiguration(); + } + + /** + * Check context startup and shutdown + */ + public void testContextStartup() throws Exception + { + getConfiguration(TreeSet.class); + getConfiguration(HashSet.class); + getConfiguration(ArrayList.class); + getConfiguration(AbstractCollection.class); + try + { + getConfiguration(Collection.class); + fail("Failed to detect incompatible class hierarchy"); + } + catch (Throwable e) + { + // Expected + } + } + + public void testHierarchyTreeSet() throws Exception + { + Configuration mybatisConfig = getConfiguration(TreeSet.class); + MappedStatement stmt = mybatisConfig.getMappedStatement(QUERY_TREESET); + assertNotNull("Query missing for " + QUERY_TREESET + " using " + TreeSet.class, stmt); + try + { + mybatisConfig.getMappedStatement(QUERY_ABSTRACTCOLLECTION); + fail("Query not missing for " + QUERY_ABSTRACTCOLLECTION + " using " + TreeSet.class); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + public void testHierarchyHashSet() throws Exception + { + Configuration mybatisConfig = getConfiguration(HashSet.class); + MappedStatement stmt = mybatisConfig.getMappedStatement(QUERY_ABSTRACTCOLLECTION); + assertNotNull("Query missing for " + QUERY_ABSTRACTCOLLECTION + " using " + HashSet.class, stmt); + try + { + mybatisConfig.getMappedStatement(QUERY_OBJECT); + fail("Query not missing for " + QUERY_OBJECT + " using " + HashSet.class); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + public void testHierarchyArrayList() throws Exception + { + Configuration mybatisConfig = getConfiguration(ArrayList.class); + MappedStatement stmt = mybatisConfig.getMappedStatement(QUERY_ABSTRACTLIST); + assertNotNull("Query missing for " + QUERY_ABSTRACTLIST + " using " + ArrayList.class, stmt); + try + { + mybatisConfig.getMappedStatement(QUERY_ABSTRACTCOLLECTION); + fail("Query not missing for " + QUERY_ABSTRACTCOLLECTION + " using " + ArrayList.class); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + public void testHierarchyAbstractCollection() throws Exception + { + Configuration mybatisConfig = getConfiguration(AbstractCollection.class); + MappedStatement stmt = mybatisConfig.getMappedStatement(QUERY_ABSTRACTCOLLECTION); + assertNotNull("Query missing for " + QUERY_ABSTRACTCOLLECTION + " using " + AbstractCollection.class, stmt); + try + { + mybatisConfig.getMappedStatement(QUERY_OBJECT); + fail("Query not missing for " + QUERY_OBJECT + " using " + AbstractCollection.class); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + /** + * Helper class that iBatis will use in the test mappings + * @author Derek Hulley + */ + public static class TestDAO + { + private Long id; + private String propOne; + private String propTwo; + + public Long getId() + { + return id; + } + public void setId(Long id) + { + this.id = id; + } + public String getPropOne() + { + return propOne; + } + public void setPropOne(String propOne) + { + this.propOne = propOne; + } + public String getPropTwo() + { + return propTwo; + } + public void setPropTwo(String propTwo) + { + this.propTwo = propTwo; + } + } +} diff --git a/src/test/java/org/alfresco/query/CannedQueryTest.java b/src/test/java/org/alfresco/query/CannedQueryTest.java new file mode 100644 index 0000000000..73c2b96a56 --- /dev/null +++ b/src/test/java/org/alfresco/query/CannedQueryTest.java @@ -0,0 +1,366 @@ +/* + * 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.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.TestCase; + +import org.alfresco.query.CannedQuerySortDetails.SortOrder; +import org.alfresco.util.Pair; +import org.alfresco.util.registry.NamedObjectRegistry; + +/** + * Tests the {@link CannedQuery name query} infrastructure. + * + * @author Derek Hulley + * @since 4.0 + */ +public class CannedQueryTest extends TestCase +{ + private static final String QUERY_TEST_ONE = "test.query.one"; + private static final String QUERY_TEST_TWO = "test.query.two"; + + private static final List RESULTS_ONE; + private static final List RESULTS_TWO; + private static final Set ANTI_RESULTS; + + static + { + RESULTS_ONE = new ArrayList(10); + for (int i = 0; i < 10; i++) + { + RESULTS_ONE.add("ONE_" + i); + } + RESULTS_TWO = new ArrayList(10); + for (int i = 0; i < 10; i++) + { + RESULTS_TWO.add(new Long(i)); + } + ANTI_RESULTS = new HashSet(); + ANTI_RESULTS.add("ONE_5"); + ANTI_RESULTS.add(new Long(5)); + } + + @SuppressWarnings("rawtypes") + private NamedObjectRegistry namedQueryFactoryRegistry; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public void setUp() throws Exception + { + // Create the registry + namedQueryFactoryRegistry = new NamedObjectRegistry(); + namedQueryFactoryRegistry.setStorageType(CannedQueryFactory.class); + namedQueryFactoryRegistry.setNamePattern("test\\.query\\..*"); + // Registry the query factories + // ONE + TestCannedQueryFactory namedQueryFactoryOne = new TestCannedQueryFactory(RESULTS_ONE); + namedQueryFactoryOne.setBeanName(QUERY_TEST_ONE); + namedQueryFactoryOne.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryOne.afterPropertiesSet(); + // TWO + TestCannedQueryFactory namedQueryFactoryTwo = new TestCannedQueryFactory(RESULTS_TWO); + namedQueryFactoryTwo.setBeanName(QUERY_TEST_TWO); + namedQueryFactoryTwo.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryTwo.afterPropertiesSet(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testRegistry() throws Exception + { + CannedQueryFactory one = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + assertNotNull("No factory for " + QUERY_TEST_ONE, one); + CannedQueryFactory two = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_TWO); + assertNotNull("No factory for " + QUERY_TEST_TWO, two); + // Kick out registrations with incorrect naming convention + try + { + TestCannedQueryFactory namedQueryFactoryBogus = new TestCannedQueryFactory(RESULTS_TWO); + namedQueryFactoryBogus.setBeanName("test_query_blah"); + namedQueryFactoryBogus.setRegistry(namedQueryFactoryRegistry); + namedQueryFactoryBogus.afterPropertiesSet(); + fail("Should have kicked out incorrectly-named registered queries"); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + @SuppressWarnings("unchecked") + public void testQueryAllResults() throws Exception + { + // An instance of the CannedQueryFactory could be injected or constructed as well + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryParameters params = new CannedQueryParameters(null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Attempt to reuse the query + try + { + qOne.execute(); + fail("Second execution of same instance must not be allowed."); + } + catch (IllegalStateException e) + { + // Expected + } + // Get the number of results when not requested + try + { + qrOne.getTotalResultCount(); + fail("Expected failure when requesting total count without explicit request."); + } + catch (IllegalStateException e) + { + // Expected + } + // Get the paged result count + int pagedResultCount = qrOne.getPagedResultCount(); + assertEquals("Incorrect number of results", 9, pagedResultCount); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryMaxResults() throws Exception + { + // An instance of the CannedQueryFactory could be injected or constructed as well + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQuery qOne = qfOne.getCannedQuery(null, 0, 9, null); + CannedQueryResults qrOne = qOne.execute(); + + // Get the paged result count + int pagedResultCount = qrOne.getPagedResultCount(); + assertEquals("Incorrect number of results", 9, pagedResultCount); + assertEquals("Incorrect number of pages", 1, qrOne.getPageCount()); + List> pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 1, pages.size()); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("No sorting was specified in the parameters", "ONE_9", qrOne.getPages().get(0).get(8)); + assertFalse("Should have more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPagedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryPageDetails qPageDetails = new CannedQueryPageDetails(0, 5, 1, 2); + CannedQueryParameters params = new CannedQueryParameters(null, qPageDetails, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("No sorting was specified in the parameters", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("No sorting was specified in the parameters", "ONE_9", qrOne.getPages().get(1).get(3)); + List> pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 2, pages.size()); + assertEquals("Incorrect results on page", 5, pages.get(0).size()); + assertEquals("Incorrect results on page", 4, pages.get(1).size()); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + + // Skip some results and use different page sizes + qPageDetails = new CannedQueryPageDetails(2, 3, 1, 3); + params = new CannedQueryParameters(null, qPageDetails, null); + qOne = qfOne.getCannedQuery(params); + qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 7, qrOne.getPagedResultCount()); + assertEquals("Incorrect number of pages", 3, qrOne.getPageCount()); + pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 3, pages.size()); + assertEquals("Incorrect results on page", 3, pages.get(0).size()); + assertEquals("Incorrect results on page", 3, pages.get(1).size()); + assertEquals("Incorrect results on page", 1, pages.get(2).size()); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + + // Skip some results and return less pages + qPageDetails = new CannedQueryPageDetails(2, 3, 1, 2); + params = new CannedQueryParameters(null, qPageDetails, null); + qOne = qfOne.getCannedQuery(params); + qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 6, qrOne.getPagedResultCount()); + assertEquals("Incorrect number of pages", 2, qrOne.getPageCount()); + pages = qrOne.getPages(); + assertEquals("Incorrect number of pages", 2, pages.size()); + assertEquals("Incorrect results on page", 3, pages.get(0).size()); + assertEquals("Incorrect results on page", 3, pages.get(1).size()); + assertTrue("Should have more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQuerySortedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQuerySortDetails qSortDetails = new CannedQuerySortDetails( + new Pair("blah", SortOrder.DESCENDING)); + CannedQueryParameters params = new CannedQueryParameters(null, null, qSortDetails); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("Expected inverse sorting", "ONE_9", qrOne.getPages().get(0).get(0)); + assertEquals("Expected inverse sorting", "ONE_0", qrOne.getPages().get(0).get(8)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPermissionCheckedResults() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryParameters params = new CannedQueryParameters(null, null, null, 0, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of results", 9, qrOne.getPagedResultCount()); + assertEquals("Incorrect result order", "ONE_0", qrOne.getPages().get(0).get(0)); + assertEquals("Incorrect result order", "ONE_1", qrOne.getPages().get(0).get(1)); + assertEquals("Incorrect result order", "ONE_2", qrOne.getPages().get(0).get(2)); + assertEquals("Incorrect result order", "ONE_3", qrOne.getPages().get(0).get(3)); + assertEquals("Incorrect result order", "ONE_4", qrOne.getPages().get(0).get(4)); // << missing 5! + assertEquals("Incorrect result order", "ONE_6", qrOne.getPages().get(0).get(5)); + assertEquals("Incorrect result order", "ONE_7", qrOne.getPages().get(0).get(6)); + assertEquals("Incorrect result order", "ONE_8", qrOne.getPages().get(0).get(7)); + assertEquals("Incorrect result order", "ONE_9", qrOne.getPages().get(0).get(8)); + assertFalse("Should NOT have any more pages/items", qrOne.hasMoreItems()); + } + + @SuppressWarnings("unchecked") + public void testQueryPermissionCheckedPagedTotalCount() throws Exception + { + CannedQueryFactory qfOne = namedQueryFactoryRegistry.getNamedObject(QUERY_TEST_ONE); + CannedQueryPageDetails qPageDetails = new CannedQueryPageDetails(5, 1, 1, 1); + CannedQuerySortDetails qSortDetails = new CannedQuerySortDetails( + new Pair("blah", SortOrder.DESCENDING)); + CannedQueryParameters params = new CannedQueryParameters(null, qPageDetails, qSortDetails, 1000, null); + CannedQuery qOne = qfOne.getCannedQuery(params); + CannedQueryResults qrOne = qOne.execute(); + // Check pages + assertEquals("Incorrect number of total results", + new Pair(9,9), qrOne.getTotalResultCount()); // Pre-paging + assertEquals("Incorrect number of paged results", 1, qrOne.getPagedResultCount()); // Skipped 5 + assertEquals("Incorrect result order", "ONE_3", qrOne.getPages().get(0).get(0)); // Order reversed + assertTrue("Should have more pages/items", qrOne.hasMoreItems()); + } + + /** + * Test factory to generate "queries" that just return a list of Strings. + * + * @param the type of the results + * + * @author Derek Hulley + * @since 4.0 + */ + private static class TestCannedQueryFactory extends AbstractCannedQueryFactory + { + private final List results; + private TestCannedQueryFactory(List results) + { + this.results = results; + } + + @Override + public CannedQuery getCannedQuery(CannedQueryParameters parameters) + { + String queryExecutionId = super.getQueryExecutionId(parameters); + return new TestCannedQuery(parameters, queryExecutionId, results, ANTI_RESULTS); + } + } + + /** + * Test query that just returns values passed in + * + * @param the type of the results + * + * @author Derek Hulley + * @since 4.0 + */ + private static class TestCannedQuery extends AbstractCannedQuery + { + private final List results; + private final Set antiResults; + private TestCannedQuery( + CannedQueryParameters params, + String queryExecutionId, List results, Set antiResults) + { + super(params); + this.results = results; + this.antiResults = antiResults; + } + + @Override + protected List queryAndFilter(CannedQueryParameters parameters) + { + return results; + } + + @Override + protected boolean isApplyPostQuerySorting() + { + return true; + } + + @Override + protected List applyPostQuerySorting(List results, CannedQuerySortDetails sortDetails) + { + if (sortDetails.getSortPairs().size() == 0) + { + // Nothing to sort on + return results; + } + List ret = new ArrayList(results); + Collections.reverse(ret); + return ret; + } + + @Override + protected boolean isApplyPostQueryPermissions() + { + return true; + } + + @Override + protected List applyPostQueryPermissions(List results, int requestedCount) + { + boolean cutoffAllowed = (getParameters().getTotalResultCountMax() == 0); + + final List ret = new ArrayList(results.size()); + for (T t : results) + { + if (!antiResults.contains(t)) + { + ret.add(t); + } + // Cut off if we have enough results + if (cutoffAllowed && ret.size() == requestedCount) + { + break; + } + } + + return ret; + } + } +} diff --git a/src/test/java/org/alfresco/util/BaseTest.java b/src/test/java/org/alfresco/util/BaseTest.java new file mode 100644 index 0000000000..6caebc4856 --- /dev/null +++ b/src/test/java/org/alfresco/util/BaseTest.java @@ -0,0 +1,152 @@ +/* + * 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.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.extensions.config.ConfigSource; +import org.springframework.extensions.config.source.ClassPathConfigSource; +import org.springframework.extensions.config.source.FileConfigSource; +import org.springframework.extensions.config.xml.XMLConfigService; + +/** + * Base class for all JUnit tests + * + * @author gavinc, Neil McErlean + */ +public abstract class BaseTest extends TestCase +{ + protected String resourcesDir; + + protected boolean loadFromClasspath; + + public BaseTest() + { + // GC: Added this to allow flexible test resources folder configuration + // Try to get resources dir from a system property otherwise uses the default hardcoded + // backward compatible + String resourcesDir = null; + // This allows subclasses to override the getResourcesDir method (e.g. by passing classpath: ) + if(getResourcesDir()==null) + { + resourcesDir = System.getProperty("alfresco.test.resources.dir"); + if(resourcesDir == null || resourcesDir.equals("")) + { + // defaults to source/test-resources + resourcesDir = System.getProperty("user.dir") + File.separator + "source" + File.separator + "test-resources"; + } + } + else + { + resourcesDir = getResourcesDir(); + } + loadFromClasspath = resourcesDir.startsWith("classpath:"); + // Returns the resources dir with trailing separator or just the classpath: string in case that was specified + this.resourcesDir = resourcesDir + ((loadFromClasspath) ? "" : File.separator); + } + + /** + * Override this method to pass a custom resource dir. + * Valid values are a file system path or the string "classpath:" + * @return + */ + public String getResourcesDir() + { + return this.resourcesDir; + } + + /** + * Checks for validity of the resource location. + * + * In case of file resource, provide the full file path + * In case of classpath resources, please pass the full resource URI, prepended with the classpath: string + * @param fullFileName + */ + protected void assertFileIsValid(String fullFileName) + { + if(loadFromClasspath) + { + // if we load from classpath, we need to remove the "classpath:" trailing string + String resourceName = fullFileName.substring(fullFileName.indexOf(":") + 1); + assertNotNull(resourceName); + URL configResourceUrl = getClass().getClassLoader().getResource(resourceName); + assertNotNull(configResourceUrl); + } + else + { + File f = new File(fullFileName); + assertTrue("Required file missing: " + fullFileName, f.exists()); + assertTrue("Required file not readable: " + fullFileName, f.canRead()); + } + } + + /** + * Loads a config file from filesystem or classpath + * + * In case of file resource, just provide the file name relative to resourceDir + * In case of classpath resources, just provide the resource URI, without with the prepending classpath: string + * @param xmlConfigFile + * @return + */ + protected XMLConfigService initXMLConfigService(String xmlConfigFile) + { + XMLConfigService svc = null; + if(loadFromClasspath) + { + svc = new XMLConfigService(new ClassPathConfigSource(xmlConfigFile)); + } + else + { + String fullFileName = getResourcesDir() + xmlConfigFile; + assertFileIsValid(fullFileName); + svc = new XMLConfigService(new FileConfigSource(fullFileName)); + } + svc.initConfig(); + return svc; + } + + protected XMLConfigService initXMLConfigService(String xmlConfigFile, String overridingXmlConfigFile) + { + List files = new ArrayList(2); + files.add(xmlConfigFile); + files.add(overridingXmlConfigFile); + return initXMLConfigService(files); + } + + protected XMLConfigService initXMLConfigService(List xmlConfigFilenames) + { + List configFiles = new ArrayList(); + for (String filename : xmlConfigFilenames) + { + // if we load from classpath then no need to prepend the resources dir (which will be .equals("classpath:") + String path = ((loadFromClasspath) ? "" : getResourcesDir()) + filename; + assertFileIsValid(path); + configFiles.add(path); + } + ConfigSource configSource = (loadFromClasspath) ? new ClassPathConfigSource(configFiles) : new FileConfigSource(configFiles); + XMLConfigService svc = new XMLConfigService(configSource); + svc.initConfig(); + return svc; + } +} diff --git a/src/test/java/org/alfresco/util/BridgeTableTest.java b/src/test/java/org/alfresco/util/BridgeTableTest.java new file mode 100644 index 0000000000..2ea97a5bb0 --- /dev/null +++ b/src/test/java/org/alfresco/util/BridgeTableTest.java @@ -0,0 +1,362 @@ +/* + * 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.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +import org.junit.Test; + +/** + * @author Andy + * + */ +public class BridgeTableTest extends TestCase +{ + + @Test + public void testBasic() + { + BridgeTable bridgeTable = new BridgeTable(); + bridgeTable.addLink("A", "B"); + bridgeTable.addLink("C", "D"); + bridgeTable.addLink("E", "F"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(0, bridgeTable.getAncestors("C").size()); + assertEquals(1, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(1, bridgeTable.getDescendants("A").size()); + assertEquals(0, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.addLink("B", "C"); + + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(3, bridgeTable.getDescendants("A").size()); + assertEquals(2, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.addLink("D", "E"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertTrue(bridgeTable.getAncestors("B", 1).contains("A")); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertTrue(bridgeTable.getAncestors("C", 1).contains("B")); + assertTrue(bridgeTable.getAncestors("C", 2).contains("A")); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertTrue(bridgeTable.getAncestors("D", 1).contains("C")); + assertTrue(bridgeTable.getAncestors("D", 2).contains("B")); + assertTrue(bridgeTable.getAncestors("D", 3).contains("A")); + assertEquals(4, bridgeTable.getAncestors("E").size()); + assertTrue(bridgeTable.getAncestors("E", 1).contains("D")); + assertTrue(bridgeTable.getAncestors("E", 2).contains("C")); + assertTrue(bridgeTable.getAncestors("E", 3).contains("B")); + assertTrue(bridgeTable.getAncestors("E", 4).contains("A")); + assertEquals(5, bridgeTable.getAncestors("F").size()); + assertTrue(bridgeTable.getAncestors("F", 1).contains("E")); + assertTrue(bridgeTable.getAncestors("F", 2).contains("D")); + assertTrue(bridgeTable.getAncestors("F", 3).contains("C")); + assertTrue(bridgeTable.getAncestors("F", 4).contains("B")); + assertTrue(bridgeTable.getAncestors("F", 5).contains("A")); + + assertEquals(5, bridgeTable.getDescendants("A").size()); + assertTrue(bridgeTable.getDescendants("A", 1).contains("B")); + assertTrue(bridgeTable.getDescendants("A", 2).contains("C")); + assertTrue(bridgeTable.getDescendants("A", 3).contains("D")); + assertTrue(bridgeTable.getDescendants("A", 4).contains("E")); + assertTrue(bridgeTable.getDescendants("A", 5).contains("F")); + assertEquals(4, bridgeTable.getDescendants("B").size()); + assertTrue(bridgeTable.getDescendants("B", 1).contains("C")); + assertTrue(bridgeTable.getDescendants("B", 2).contains("D")); + assertTrue(bridgeTable.getDescendants("B", 3).contains("E")); + assertTrue(bridgeTable.getDescendants("B", 4).contains("F")); + assertEquals(3, bridgeTable.getDescendants("C").size()); + assertTrue(bridgeTable.getDescendants("C", 1).contains("D")); + assertTrue(bridgeTable.getDescendants("C", 2).contains("E")); + assertTrue(bridgeTable.getDescendants("C", 3).contains("F")); + assertEquals(2, bridgeTable.getDescendants("D").size()); + assertTrue(bridgeTable.getDescendants("D", 1).contains("E")); + assertTrue(bridgeTable.getDescendants("D", 2).contains("F")); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertTrue(bridgeTable.getDescendants("E", 1).contains("F")); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.removeLink("D", "E"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(2, bridgeTable.getAncestors("C").size()); + assertEquals(3, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(3, bridgeTable.getDescendants("A").size()); + assertEquals(2, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + + bridgeTable.removeLink("B", "C"); + + assertEquals(0, bridgeTable.getAncestors("A").size()); + assertEquals(1, bridgeTable.getAncestors("B").size()); + assertEquals(0, bridgeTable.getAncestors("C").size()); + assertEquals(1, bridgeTable.getAncestors("D").size()); + assertEquals(0, bridgeTable.getAncestors("E").size()); + assertEquals(1, bridgeTable.getAncestors("F").size()); + + assertEquals(1, bridgeTable.getDescendants("A").size()); + assertEquals(0, bridgeTable.getDescendants("B").size()); + assertEquals(1, bridgeTable.getDescendants("C").size()); + assertEquals(0, bridgeTable.getDescendants("D").size()); + assertEquals(1, bridgeTable.getDescendants("E").size()); + assertEquals(0, bridgeTable.getDescendants("F").size()); + } + +// @Test +// public void test_1M() +// { +// // 1M = 21 +// for (int i = 0; i < 20; i++) +// { +// BridgeTable bridgeTable = new BridgeTable(); +// long start = System.nanoTime(); +// bridgeTable.addLinks(getTreeLinks(i)); +// long end = System.nanoTime(); +// double elapsed = ((end - start) / 1e9); +// System.out.println("" + bridgeTable.size() + " in " + elapsed); +// assertTrue(elapsed < 60); +// } +// } + + @Test + public void test_16k() + { + // 1M = 21 + for (int i = 0; i < 15; i++) + { + BridgeTable bridgeTable = new BridgeTable(); + long start = System.nanoTime(); + bridgeTable.addLinks(getTreeLinks(i)); + long end = System.nanoTime(); + double elapsed = ((end - start) / 1e9); + System.out.println("" + bridgeTable.size() + " in " + elapsed); + assertTrue(elapsed < 60); + } + } + +// @Test +// public void test_1000x1000() +// { +// BridgeTable bridgeTable = new BridgeTable(); +// HashSet> links = new HashSet>(); +// for (int i = 0; i < 10; i++) +// { +// for (int j = 0; j < 100; j++) +// { +// links.addAll(getTreeLinks(10)); +// } +// +// +// long start = System.nanoTime(); +// bridgeTable.addLinks(links); +// long end = System.nanoTime(); +// System.out.println("Trees " + bridgeTable.size() + " in " + ((end - start) / 1e9)); +// +// start = System.nanoTime(); +// for (String key : bridgeTable.keySet()) +// { +// bridgeTable.getAncestors(key); +// } +// end = System.nanoTime(); +// System.out.println("By key " + bridgeTable.size() + " in " + ((end - start) / 1e9)); +// } +// } + + + @Test + public void test_100x100() + { + BridgeTable bridgeTable = new BridgeTable(); + HashSet> links = new HashSet>(); + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < 10; j++) + { + links.addAll(getTreeLinks(7)); + } + + + long start = System.nanoTime(); + bridgeTable.addLinks(links); + long end = System.nanoTime(); + System.out.println("Trees " + bridgeTable.size() + " in " + ((end - start) / 1e9)); + + start = System.nanoTime(); + for (String key : bridgeTable.keySet()) + { + bridgeTable.getAncestors(key); + } + end = System.nanoTime(); + System.out.println("By key " + bridgeTable.size() + " in " + ((end - start) / 1e9)); + } + } + + @Test + public void testSecondary() + { + BridgeTable bridgeTable = new BridgeTable(); + + bridgeTable.addLink("A", "B"); + bridgeTable.addLink("A", "C"); + bridgeTable.addLink("B", "D"); + bridgeTable.addLink("B", "E"); + bridgeTable.addLink("C", "F"); + bridgeTable.addLink("C", "G"); + + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(4, bridgeTable.getDescendants("A", 2).size()); + assertEquals(6, bridgeTable.getDescendants("A", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + assertEquals(0, bridgeTable.getDescendants("B", 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1, 2).size()); + + bridgeTable.addLink("N", "O"); + bridgeTable.addLink("N", "P"); + bridgeTable.addLink("O", "Q"); + bridgeTable.addLink("O", "R"); + bridgeTable.addLink("P", "S"); + bridgeTable.addLink("P", "T"); + + assertEquals(2, bridgeTable.getDescendants("N", 1).size()); + assertEquals(4, bridgeTable.getDescendants("N", 2).size()); + assertEquals(6, bridgeTable.getDescendants("N", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1).size()); + assertEquals(0, bridgeTable.getDescendants("O", 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1, 2).size()); + + bridgeTable.addLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("B", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.addLink("B", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("A", "N"); + assertEquals(3, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("A", "N"); + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("B", "N"); + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(3, bridgeTable.getDescendants("B", 1).size()); + + bridgeTable.removeLink("B", "N"); + + assertEquals(2, bridgeTable.getDescendants("A", 1).size()); + assertEquals(4, bridgeTable.getDescendants("A", 2).size()); + assertEquals(6, bridgeTable.getDescendants("A", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1).size()); + assertEquals(0, bridgeTable.getDescendants("B", 2).size()); + assertEquals(2, bridgeTable.getDescendants("B", 1, 2).size()); + + + assertEquals(2, bridgeTable.getDescendants("N", 1).size()); + assertEquals(4, bridgeTable.getDescendants("N", 2).size()); + assertEquals(6, bridgeTable.getDescendants("N", 1, 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1).size()); + assertEquals(0, bridgeTable.getDescendants("O", 2).size()); + assertEquals(2, bridgeTable.getDescendants("O", 1, 2).size()); + + } + + private Set> getTreeLinks(int depth) + { + int count = 0; + String base = "" + System.nanoTime(); + + HashSet currentRow = new HashSet(); + HashSet lastRow = new HashSet(); + + HashSet> links = new HashSet>(); + + for (int i = 0; i < depth; i++) + { + if (lastRow.size() == 0) + { + currentRow.add("GROUP_" + base + "_" + count); + count++; + } + else + { + for (String group : lastRow) + { + String newGroup = "GROUP_" + base + "_" + count; + currentRow.add(newGroup); + count++; + links.add(new Pair(group, newGroup)); + + newGroup = "GROUP_" + base + "_" + count; + currentRow.add(newGroup); + count++; + links.add(new Pair(group, newGroup)); + } + } + lastRow = currentRow; + currentRow = new HashSet(); + } + + return links; + + } + +} diff --git a/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java b/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java new file mode 100644 index 0000000000..c5ace65cdf --- /dev/null +++ b/src/test/java/org/alfresco/util/DynamicallySizedThreadPoolExecutorTest.java @@ -0,0 +1,334 @@ +/* + * 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 java.util.Map.Entry; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import junit.framework.TestCase; + +/** + * Tests for our instance of {@link java.util.concurrent.ThreadPoolExecutor} + * + * @author Nick Burch + */ +public class DynamicallySizedThreadPoolExecutorTest extends TestCase +{ + + private static Log logger = LogFactory.getLog(DynamicallySizedThreadPoolExecutorTest.class); + private static final int DEFAULT_KEEP_ALIVE_TIME = 90; + + @Override + protected void setUp() throws Exception + { + SleepUntilAllWake.reset(); + } + + public void testUpToCore() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(5,10, DEFAULT_KEEP_ALIVE_TIME); + + assertEquals(0, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(5, exec.getPoolSize()); + } + + public void testPastCoreButNotHugeQueue() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(5,10, DEFAULT_KEEP_ALIVE_TIME); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + // Need to hit max pool size before it adds more + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(5, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(5, exec.getPoolSize()); + assertEquals(7, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(5, exec.getPoolSize()); + } + + public void testToExpandQueue() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(2,4,1); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // Next should add one + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(3, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // But no more will be added, as we're at max + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + + // All threads still running, as 1 second timeout + assertEquals(4, exec.getPoolSize()); + } + + public void offTestToExpandThenContract() throws Exception + { + DynamicallySizedThreadPoolExecutor exec = createInstance(2,4,1); + exec.setKeepAliveTime(30, TimeUnit.MILLISECONDS); + + assertEquals(0, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // Next should add one + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(3, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again + exec.execute(new SleepUntilAllWake()); + Thread.sleep(20); // Let the new thread spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // But no more will be added, as we're at max + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + + // Wait longer than the timeout without any work, which should + // let all the extra threads go away + // (Depending on how closely your JVM follows the specification, + // we may fall back to the core size which is correct, or we + // may go to zero which is wrong, but hey, it's the JVM...) + logger.debug("Core pool size is " + exec.getCorePoolSize()); + logger.debug("Current pool size is " + exec.getPoolSize()); + logger.debug("Queue size is " + exec.getQueue().size()); + assertTrue( + "Pool size should be 0-2 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() >= 0 + ); + assertTrue( + "Pool size should be 0-2 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() <= 2 + ); + + SleepUntilAllWake.reset(); + + // Add 2 new jobs, will stay/ go to at 2 threads + assertEquals(0, exec.getQueue().size()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + + // Let the idle threads grab them, then check + Thread.sleep(20); + assertEquals(2, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + // 3 more, still at 2 threads + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + assertEquals(2, exec.getPoolSize()); + assertEquals(3, exec.getQueue().size()); + + // And again wait for it all + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertEquals(2, exec.getPoolSize()); + + + // Now decrease the overall pool size + // Will rise and fall to there now + exec.setCorePoolSize(1); + + // Run a quick job, to ensure that the + // "can I kill one yet" logic is applied + SleepUntilAllWake.reset(); + exec.execute(new SleepUntilAllWake()); + SleepUntilAllWake.wakeAll(); + + Thread.sleep(100); + assertEquals(1, exec.getPoolSize()); + assertEquals(0, exec.getQueue().size()); + + SleepUntilAllWake.reset(); + + + // Push enough on to go up to 4 active threads + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + exec.execute(new SleepUntilAllWake()); + + Thread.sleep(20); // Let the new threads spin up + assertEquals(4, exec.getPoolSize()); + assertEquals(6, exec.getQueue().size()); + + // Wait for them all to finish, should drop back to 1 now + // (Or zero, if your JVM can't read the specification...) + SleepUntilAllWake.wakeAll(); + Thread.sleep(100); + assertTrue( + "Pool size should be 0 or 1 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() >= 0 + ); + assertTrue( + "Pool size should be 0 or 1 as everything is idle, was " + exec.getPoolSize(), + exec.getPoolSize() <= 1 + ); + } + + private DynamicallySizedThreadPoolExecutor createInstance(int corePoolSize, int maximumPoolSize, int keepAliveTime) + { + // We need a thread factory + TraceableThreadFactory threadFactory = new TraceableThreadFactory(); + threadFactory.setThreadDaemon(true); + threadFactory.setThreadPriority(Thread.NORM_PRIORITY); + + BlockingQueue workQueue = new LinkedBlockingQueue(); + + return new DynamicallySizedThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + TimeUnit.SECONDS, + workQueue, + threadFactory, + new ThreadPoolExecutor.CallerRunsPolicy()); + } + + public static class SleepUntilAllWake implements Runnable + { + private static ConcurrentMap sleeping = new ConcurrentHashMap(); + private static boolean allAwake = false; + + @Override + public void run() + { + if(allAwake) return; + + // Track us, and wait for the bang + logger.debug("Adding thread: " + Thread.currentThread().getName()); + sleeping.put(Thread.currentThread().getName(), Thread.currentThread()); + try + { + Thread.sleep(30*1000); + System.err.println("Warning - Thread finished sleeping without wake!"); + } + catch(InterruptedException e) + { + logger.debug("Interrupted thread: " + Thread.currentThread().getName()); + } + } + + public static void wakeAll() + { + allAwake = true; + for(Entry t : sleeping.entrySet()) + { + logger.debug("Interrupting thread: " + t.getKey()); + t.getValue().interrupt(); + } + } + public static void reset() + { + logger.debug("Resetting."); + allAwake = false; + sleeping.clear(); + } + } +} diff --git a/src/test/java/org/alfresco/util/EqualsHelperTest.java b/src/test/java/org/alfresco/util/EqualsHelperTest.java new file mode 100644 index 0000000000..c40d8afa34 --- /dev/null +++ b/src/test/java/org/alfresco/util/EqualsHelperTest.java @@ -0,0 +1,107 @@ +/* + * 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.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.EqualsHelper.MapValueComparison; + +import junit.framework.TestCase; + +/** + * @see EqualsHelper + * + * @author Derek Hulley + * @since 3.1SP2 + */ +public class EqualsHelperTest extends TestCase +{ + private File fileOne; + private File fileTwo; + + @Override + public void setUp() throws Exception + { + fileOne = TempFileProvider.createTempFile(getName(), "-one.txt"); + fileTwo = TempFileProvider.createTempFile(getName(), "-two.txt"); + + OutputStream osOne = new FileOutputStream(fileOne); + osOne.write("1234567890 - ONE".getBytes("UTF-8")); + osOne.close(); + + OutputStream osTwo = new FileOutputStream(fileTwo); + osTwo.write("1234567890 - TWO".getBytes("UTF-8")); + osTwo.close(); + } + + public void testStreamsNotEqual() throws Exception + { + InputStream isLeft = new FileInputStream(fileOne); + InputStream isRight = new FileInputStream(fileTwo); + boolean equal = EqualsHelper.binaryStreamEquals(isLeft, isRight); + assertFalse("Should not be the same", equal); + } + + public void testStreamsEqual() throws Exception + { + InputStream isLeft = new FileInputStream(fileOne); + InputStream isRight = new FileInputStream(fileOne); + boolean equal = EqualsHelper.binaryStreamEquals(isLeft, isRight); + assertTrue("Should be the same", equal); + } + + public void testMapComparison() throws Exception + { + Map left = new HashMap(); + Map right = new HashMap(); + // EQUAL + left.put(0, "A"); + right.put(0, "A"); + // NOT_EQUAL + left.put(1, "A"); + right.put(1, "B"); + // EQUAL + left.put(2, null); + right.put(2, null); + // NOT_EQUAL + left.put(3, null); + right.put(3, "B"); + // NOT_EQUAL + left.put(4, "A"); + right.put(4, null); + // RIGHT_ONLY + right.put(5, "B"); + // LEFT_ONLY + left.put(6, "A"); + Map diff = EqualsHelper.getMapComparison(left, right); + assertEquals("'EQUAL' check failed", MapValueComparison.EQUAL, diff.get(0)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(1)); + assertEquals("'EQUAL' check failed", MapValueComparison.EQUAL, diff.get(2)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(3)); + assertEquals("'NOT_EQUAL' check failed", MapValueComparison.NOT_EQUAL, diff.get(4)); + assertEquals("'RIGHT_ONLY' check failed", MapValueComparison.RIGHT_ONLY, diff.get(5)); + assertEquals("'LEFT_ONLY' check failed", MapValueComparison.LEFT_ONLY, diff.get(6)); + } +} diff --git a/src/test/java/org/alfresco/util/GuidTest.java b/src/test/java/org/alfresco/util/GuidTest.java new file mode 100644 index 0000000000..a99f96b00d --- /dev/null +++ b/src/test/java/org/alfresco/util/GuidTest.java @@ -0,0 +1,104 @@ +/* + * 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.util; + +import java.lang.Thread.State; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import junit.framework.TestCase; + +import org.junit.Assert; + +/** + * Test class for GUID generation + * + * @author Andreea Dragoi + * + */ +public class GuidTest extends TestCase +{ + class GuidRunner implements Runnable + { + @Override + public void run() + { + GUID.generate(); + } + } + + /** + * Tests the improvement added by using a SecureRandom pool when generating GUID's + */ + public void testGuid() + { + // warm-up (to pre-init the secureRandomArray) + GUID.generate(); + + List threads = new ArrayList<>(); + int n = 30; + + for (int i = 0; i < n; i++) + { + Thread thread = new Thread(new GuidRunner()); + threads.add(thread); + thread.start(); + } + + Set blocked = new HashSet(); + Set terminated = new HashSet(); + + int maxItemsBlocked = 0; + + while (terminated.size() != n) + { + for (Thread current : threads) + { + State state = current.getState(); + String name = current.getName(); + + if (state == State.BLOCKED) + { + if (!blocked.contains(name)) + { + blocked.add(name); + maxItemsBlocked = blocked.size() > maxItemsBlocked ? blocked.size() : maxItemsBlocked; + } + } + else // not BLOCKED, eg. RUNNABLE, TERMINATED, ... + { + blocked.remove(name); + if (state == State.TERMINATED && !terminated.contains(name)) + { + terminated.add(name); + } + } + } + } + + //worst case scenario : max number of threads blocked at a moment = number of threads - 2 ( usually ~5 for 30 threads) + //the implementation without RandomSecure pool reaches constantly (number of threads - 1) max blocked threads + Assert.assertTrue("Exceeded number of blocked threads : " + maxItemsBlocked, maxItemsBlocked < n-2); + } + +} + diff --git a/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java b/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java new file mode 100644 index 0000000000..96c23f4314 --- /dev/null +++ b/src/test/java/org/alfresco/util/ISO8601DateFormatTest.java @@ -0,0 +1,287 @@ +/* + * 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.util; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; + +public class ISO8601DateFormatTest extends TestCase +{ + public void testConversion() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "2005-09-16T17:01:03.456+01:00"; + String test2 = "1801-09-16T17:01:03.456+01:00"; + // convert to a date + Date date = ISO8601DateFormat.parse(test); + Date date2 = ISO8601DateFormat.parse(test2); + // get the string form + String strDate = ISO8601DateFormat.format(date); + String strDate2 = ISO8601DateFormat.format(date2); + // convert back to a date from the converted string + Date dateAfter = ISO8601DateFormat.parse(strDate); + Date dateAfter2 = ISO8601DateFormat.parse(strDate2); + // make sure the date objects match, test this instead of the + // string as the string form will be different in different + // locales + assertEquals(date, dateAfter); + assertEquals(date2, dateAfter2); + } + + public void testGetCalendarMethod() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Calendar calendarGMT = ISO8601DateFormat.getCalendar(); + + TimeZone.setDefault(TimeZone.getTimeZone("BST")); + Calendar calendarBST = ISO8601DateFormat.getCalendar(); + + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Calendar calendarGMT1 = ISO8601DateFormat.getCalendar(); + + assertNotSame(calendarGMT, calendarBST); + assertSame(calendarGMT, calendarGMT1); + } + + public void testDateParser() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + String test = "2005-09-16T17:01:03.456+01:00"; + String test2 = "1801-09-16T17:01:03.456+01:00"; + + String isoFormattedDate = "2005-09-16T16:01:03.456Z"; + String isoFormattedDate2 = "1801-09-16T16:01:03.456Z"; + + Date testDate = getDateValue(2005, 9, 16, 17, 1, 3, 456, 60); + Date testDate2 = getDateValue(1801, 9, 16, 17, 1, 3, 456, 60); + + // convert to a date + Date date = ISO8601DateFormat.parse(test); + Date date2 = ISO8601DateFormat.parse(test2); + // check converted to date value + assertEquals(testDate, date); + assertEquals(testDate2, date2); + + // get the string form + String strDate = ISO8601DateFormat.format(date); + String strDate2 = ISO8601DateFormat.format(date2); + // check the date converted to sting + assertEquals(isoFormattedDate, strDate); + assertEquals(isoFormattedDate2, strDate2); + } + + private Date getDateValue(int year, int month, int day, int hours, int minutes, int sec, int msec, int offsetInMinutes) + { + // minute in millis + int millisInMinute = 1000 * 60; + + GregorianCalendar gc = new GregorianCalendar(); + + // set correct offset + String[] tzArray = TimeZone.getAvailableIDs(millisInMinute * offsetInMinutes); + if (tzArray.length > 0) + { + gc.setTimeZone(TimeZone.getTimeZone(tzArray[0])); + } + + // set date + gc.set(GregorianCalendar.YEAR, year); + gc.set(GregorianCalendar.MONTH, month - 1); + gc.set(GregorianCalendar.DAY_OF_MONTH, day); + gc.set(GregorianCalendar.HOUR_OF_DAY, hours); + gc.set(GregorianCalendar.MINUTE, minutes); + gc.set(GregorianCalendar.SECOND, sec); + gc.set(GregorianCalendar.MILLISECOND, msec); + + return gc.getTime(); + } + + public void testMiliseconds() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + + // ALF-3803 bug fix, milliseconds are optional + String testA = "2005-09-16T17:01:03.456Z"; + String testB = "2005-09-16T17:01:03Z"; + String testBms = "2005-09-16T17:01:03.000Z"; + String testC = "1801-09-16T17:01:03Z"; + String testCms = "1801-09-16T17:01:03.000Z"; + + Date dateA = ISO8601DateFormat.parse(testA); + Date dateB = ISO8601DateFormat.parse(testB); + Date dateC = ISO8601DateFormat.parse(testC); + + assertEquals(testA, ISO8601DateFormat.format(dateA)); + + assertEquals(testBms, ISO8601DateFormat.format(dateB)); + + assertEquals(testCms, ISO8601DateFormat.format(dateC)); + + // The official ISO 8601.2004 spec doesn't say much helpful about milliseconds + // The W3C version says it's up to different + // implementations to put bounds on them + // We can silently ignore anything beyond 3 digits, see ALF-14687 + String testCms3 = "2005-09-16T17:01:03.123+01:00"; + String testCms4 = "2005-09-16T17:01:03.1234+01:00"; + String testCms5 = "2005-09-16T17:01:03.12345+01:00"; + String testCms6 = "2005-09-16T17:01:03.123456+01:00"; + String testCms7 = "2005-09-16T17:01:03.1234567+01:00"; + + Date testCDate = ISO8601DateFormat.parse(testCms3); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms4)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms5)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms6)); + assertEquals(testCDate, ISO8601DateFormat.parse(testCms7)); + } + + public void testTimezones() + { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + Date date = null; + + TimeZone tz = TimeZone.getTimeZone("Australia/Sydney"); + String testSydney = "2011-02-04T16:13:14"; + String testUTC = "2011-02-04T05:13:14.000Z"; + + //Sydney + date = ISO8601DateFormat.parse(testSydney, tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + // Check with ms too + date = ISO8601DateFormat.parse(testSydney + ".000", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + //Sydney with an offset and timezone + date = ISO8601DateFormat.parse(testSydney+"+11:00", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + + // Check with ms too + date = ISO8601DateFormat.parse(testSydney + ".000"+"+11:00", tz); + assertEquals(testUTC, ISO8601DateFormat.format(date)); + } + + public void testToZulu(){ + String base = "2011-02-04T16:13:14.000"; + String zulu = base + "Z"; + String utc0 = base + "+00:00"; + String utc1 = "2011-02-04T17:13:14" + "+01:00"; + String utcMinus1 = "2011-02-04T15:13:14" + "-01:00"; + + assertEquals(zulu, ISO8601DateFormat.formatToZulu(zulu)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utc1)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utc0)); + assertEquals(zulu, ISO8601DateFormat.formatToZulu(utcMinus1)); + } + + public void testDayOnly() + { + Date date = null; + + // Test simple parsing + TimeZone tz = TimeZone.getTimeZone("Europe/London"); + date = ISO8601DateFormat.parseDayOnly("2012-05-21", tz); + + Calendar cal = Calendar.getInstance(tz); + cal.setTime(date); + + // Check date and time component + assertEquals(2012, cal.get(Calendar.YEAR)); + assertEquals(4, cal.get(Calendar.MONTH)); + assertEquals(21, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, cal.get(Calendar.HOUR)); + assertEquals(0, cal.get(Calendar.MINUTE)); + assertEquals(0, cal.get(Calendar.SECOND)); + assertEquals(0, cal.get(Calendar.MILLISECOND)); + + // Check time is ignored on full ISO8601-string + date = ISO8601DateFormat.parseDayOnly("2012-05-21T12:13:14Z", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + + assertEquals(2012, cal.get(Calendar.YEAR)); + assertEquals(4, cal.get(Calendar.MONTH)); + assertEquals(21, cal.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, cal.get(Calendar.HOUR)); + assertEquals(0, cal.get(Calendar.MINUTE)); + assertEquals(0, cal.get(Calendar.SECOND)); + assertEquals(0, cal.get(Calendar.MILLISECOND)); + + // Check year signs + date = ISO8601DateFormat.parseDayOnly("+2012-05-21", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + assertEquals(GregorianCalendar.AD, cal.get(Calendar.ERA)); + + date = ISO8601DateFormat.parseDayOnly("-2012-05-21", tz); + cal = Calendar.getInstance(tz); + cal.setTime(date); + assertEquals(GregorianCalendar.BC, cal.get(Calendar.ERA)); + + + // Check illegal format + try + { + ISO8601DateFormat.parseDayOnly("2011-02-0", tz); + fail("Exception expected on illegal format"); + } + catch(AlfrescoRuntimeException e) {} + try + { + ISO8601DateFormat.parseDayOnly("201a-02-02", tz); + fail("Exception expected on illegal format"); + } + catch(AlfrescoRuntimeException e) {} + } + + public void testDSTParser() + { + TimeZone tz = TimeZone.getTimeZone("America/Sao_Paulo"); + TimeZone.setDefault(tz); + // MNT-15454: This date is invalid as the 00:00 hour became 01:00 because of daylight saving time. + String test1 = "2014-10-19T"; + String test2 = "2014-10-19T00:01:01.000"; + + String isoFormattedDate = "2014-10-19T03:00:00.000Z"; + + // Sun Oct 19 01:00:00 BRST 2014 + Date testDate = getDateValue(2014, 10, 19, 0, 0, 0, 0, - 3*60); + // convert to a date + Date date = ISO8601DateFormat.parse(test1, tz); + // Check converted to date value + assertEquals(testDate, date); + + // Convert to a date + date = ISO8601DateFormat.parse(test2, tz); + // Check converted to date value + assertEquals(testDate, date); + + // Get the string form + String strDate = ISO8601DateFormat.format(date); + // Check the date converted to sting + assertEquals(isoFormattedDate, strDate); + } +} diff --git a/src/test/java/org/alfresco/util/LogAdapterTest.java b/src/test/java/org/alfresco/util/LogAdapterTest.java new file mode 100644 index 0000000000..6d707e77af --- /dev/null +++ b/src/test/java/org/alfresco/util/LogAdapterTest.java @@ -0,0 +1,212 @@ +/* + * 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.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.logging.Log; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for LogAdapter. + * + * @author Alan Davis + */ +public class LogAdapterTest +{ + @Mock + Log log; + + LogAdapter adapter; + + Throwable throwable; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + + adapter = new LogAdapter(log) { }; + throwable = new Exception(); + } + + @Test + public void traceTest() + { + adapter.trace(""); + adapter.trace("", throwable); + verify(log).trace("", null); + verify(log).trace("", throwable); + + when(log.isTraceEnabled()).thenReturn(true); + assertTrue("", adapter.isTraceEnabled()); + + when(log.isTraceEnabled()).thenReturn(false); + assertFalse("", adapter.isTraceEnabled()); + } + + @Test + public void debugTest() + { + adapter.debug(""); + adapter.debug("", throwable); + verify(log).debug("", null); + verify(log).debug("", throwable); + + when(log.isDebugEnabled()).thenReturn(true); + assertTrue("", adapter.isDebugEnabled()); + + when(log.isDebugEnabled()).thenReturn(false); + assertFalse("", adapter.isDebugEnabled()); + } + + @Test + public void infoTest() + { + adapter.info(""); + adapter.info("", throwable); + verify(log).info("", null); + verify(log).info("", throwable); + + when(log.isInfoEnabled()).thenReturn(true); + assertTrue("", adapter.isInfoEnabled()); + + when(log.isInfoEnabled()).thenReturn(false); + assertFalse("", adapter.isInfoEnabled()); + } + + @Test + public void warnTest() + { + adapter.warn(""); + adapter.warn("", throwable); + verify(log).warn("", null); + verify(log).warn("", throwable); + + when(log.isWarnEnabled()).thenReturn(true); + assertTrue("", adapter.isWarnEnabled()); + + when(log.isWarnEnabled()).thenReturn(false); + assertFalse("", adapter.isWarnEnabled()); + } + + @Test + public void errorTest() + { + adapter.error(""); + adapter.error("", throwable); + verify(log).error("", null); + verify(log).error("", throwable); + + when(log.isErrorEnabled()).thenReturn(true); + assertTrue("", adapter.isErrorEnabled()); + + when(log.isErrorEnabled()).thenReturn(false); + assertFalse("", adapter.isErrorEnabled()); + } + + @Test + public void fatalTest() + { + adapter.fatal(""); + adapter.fatal("", throwable); + verify(log).fatal("", null); + verify(log).fatal("", throwable); + + when(log.isFatalEnabled()).thenReturn(true); + assertTrue("", adapter.isFatalEnabled()); + + when(log.isFatalEnabled()).thenReturn(false); + assertFalse("", adapter.isFatalEnabled()); + } + + @Test + public void nullTest() + { + adapter = new LogAdapter(null) { }; + + adapter.trace(""); + adapter.trace("", throwable); + adapter.debug(""); + adapter.debug("", throwable); + adapter.info(""); + adapter.info("", throwable); + adapter.warn(""); + adapter.warn("", throwable); + adapter.error(""); + adapter.error("", throwable); + adapter.fatal(""); + adapter.fatal("", throwable); + verify(log, times(0)).trace("", null); + verify(log, times(0)).trace("", throwable); + verify(log, times(0)).debug("", null); + verify(log, times(0)).debug("", throwable); + verify(log, times(0)).info("", null); + verify(log, times(0)).info("", throwable); + verify(log, times(0)).warn("", null); + verify(log, times(0)).warn("", throwable); + verify(log, times(0)).error("", null); + verify(log, times(0)).error("", throwable); + verify(log, times(0)).fatal("", null); + verify(log, times(0)).fatal("", throwable); + + when(log.isTraceEnabled()).thenReturn(true); + assertFalse("", adapter.isTraceEnabled()); + + when(log.isTraceEnabled()).thenReturn(false); + assertFalse("", adapter.isTraceEnabled()); + + when(log.isDebugEnabled()).thenReturn(true); + assertFalse("", adapter.isDebugEnabled()); + + when(log.isDebugEnabled()).thenReturn(false); + assertFalse("", adapter.isDebugEnabled()); + + when(log.isInfoEnabled()).thenReturn(true); + assertFalse("", adapter.isInfoEnabled()); + + when(log.isInfoEnabled()).thenReturn(false); + assertFalse("", adapter.isInfoEnabled()); + + when(log.isWarnEnabled()).thenReturn(true); + assertFalse("", adapter.isWarnEnabled()); + + when(log.isWarnEnabled()).thenReturn(false); + assertFalse("", adapter.isWarnEnabled()); + + when(log.isErrorEnabled()).thenReturn(true); + assertFalse("", adapter.isErrorEnabled()); + + when(log.isErrorEnabled()).thenReturn(false); + assertFalse("", adapter.isErrorEnabled()); + + when(log.isFatalEnabled()).thenReturn(true); + assertFalse("", adapter.isFatalEnabled()); + + when(log.isFatalEnabled()).thenReturn(false); + assertFalse("", adapter.isFatalEnabled()); + } +} diff --git a/src/test/java/org/alfresco/util/LogTeeTest.java b/src/test/java/org/alfresco/util/LogTeeTest.java new file mode 100644 index 0000000000..660fc1e812 --- /dev/null +++ b/src/test/java/org/alfresco/util/LogTeeTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 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.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.commons.logging.Log; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Test class for LogTee. + * + * @author Alan Davis + */ +public class LogTeeTest +{ + @Mock + Log log1; + + @Mock + Log log2; + + LogTee tee; + + Throwable throwable; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + + tee = new LogTee(log1, log2) { }; + throwable = new Exception(); + } + + @Test + public void traceTest() + { + tee.trace(""); + tee.trace("", throwable); + verify(log1).trace("", null); + verify(log1).trace("", throwable); + verify(log2).trace("", null); + verify(log2).trace("", throwable); + + when(log1.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + + when(log1.isTraceEnabled()).thenReturn(false); + assertTrue("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(false); + assertFalse("", tee.isTraceEnabled()); + + when(log2.isTraceEnabled()).thenReturn(true); + assertTrue("", tee.isTraceEnabled()); + } + + @Test + public void debugTest() + { + tee.debug(""); + tee.debug("", throwable); + verify(log1).debug("", null); + verify(log1).debug("", throwable); + verify(log2).debug("", null); + verify(log2).debug("", throwable); + + + when(log1.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + + when(log1.isDebugEnabled()).thenReturn(false); + assertTrue("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(false); + assertFalse("", tee.isDebugEnabled()); + + when(log2.isDebugEnabled()).thenReturn(true); + assertTrue("", tee.isDebugEnabled()); + } + + @Test + public void infoTest() + { + tee.info(""); + tee.info("", throwable); + verify(log1).info("", null); + verify(log1).info("", throwable); + verify(log2).info("", null); + verify(log2).info("", throwable); + + + when(log1.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + + when(log1.isInfoEnabled()).thenReturn(false); + assertTrue("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(false); + assertFalse("", tee.isInfoEnabled()); + + when(log2.isInfoEnabled()).thenReturn(true); + assertTrue("", tee.isInfoEnabled()); + } + + @Test + public void warnTest() + { + tee.warn(""); + tee.warn("", throwable); + verify(log1).warn("", null); + verify(log1).warn("", throwable); + verify(log2).warn("", null); + verify(log2).warn("", throwable); + + + when(log1.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + + when(log1.isWarnEnabled()).thenReturn(false); + assertTrue("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(false); + assertFalse("", tee.isWarnEnabled()); + + when(log2.isWarnEnabled()).thenReturn(true); + assertTrue("", tee.isWarnEnabled()); + } + + @Test + public void errorTest() + { + tee.error(""); + tee.error("", throwable); + verify(log1).error("", null); + verify(log1).error("", throwable); + verify(log2).error("", null); + verify(log2).error("", throwable); + + + when(log1.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + + when(log1.isErrorEnabled()).thenReturn(false); + assertTrue("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(false); + assertFalse("", tee.isErrorEnabled()); + + when(log2.isErrorEnabled()).thenReturn(true); + assertTrue("", tee.isErrorEnabled()); + } + + @Test + public void fatalTest() + { + tee.fatal(""); + tee.fatal("", throwable); + verify(log1).fatal("", null); + verify(log1).fatal("", throwable); + verify(log2).fatal("", null); + verify(log2).fatal("", throwable); + + + when(log1.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + + when(log1.isFatalEnabled()).thenReturn(false); + assertTrue("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(false); + assertFalse("", tee.isFatalEnabled()); + + when(log2.isFatalEnabled()).thenReturn(true); + assertTrue("", tee.isFatalEnabled()); + } +} diff --git a/src/test/java/org/alfresco/util/PathMapperTest.java b/src/test/java/org/alfresco/util/PathMapperTest.java new file mode 100644 index 0000000000..f9455e1edd --- /dev/null +++ b/src/test/java/org/alfresco/util/PathMapperTest.java @@ -0,0 +1,95 @@ +/* + * 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; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * @see PathMapper + * + * @author Derek Hulley + * @since 3.2 + */ +public class PathMapperTest extends TestCase +{ + private PathMapper mapper; + + @Override + protected void setUp() throws Exception + { + mapper = new PathMapper(); + mapper.addPathMap("/a/b/c", "/1/2/3"); + mapper.addPathMap("/a/b/c", "/one/two/three"); + mapper.addPathMap("/a/c/c", "/1/3/3"); + mapper.addPathMap("/a/c/c", "/one/three/three"); + mapper.addPathMap("/A/B/C", "/1/2/3"); + mapper.addPathMap("/A/B/C", "/ONE/TWO/THREE"); + mapper.addPathMap("/A/C/C", "/1/3/3"); + mapper.addPathMap("/A/C/C", "/ONE/THREE/THREE"); + } + + public void testConvertValueMap() + { + Map inputMap = new HashMap(5); + inputMap.put("/a/a/a/111", 111); + inputMap.put("/a/b/c/123", 123); + inputMap.put("/a/b/b/122", 122); + inputMap.put("/a/c/c/133", 133); + inputMap.put("/A/A/A/111", 111); + inputMap.put("/A/B/C/123", 123); + inputMap.put("/A/B/B/122", 122); + inputMap.put("/A/C/C/133", 133); + + Map expectedOutputMap = new HashMap(5); + expectedOutputMap.put("/1/2/3/123", 123); + expectedOutputMap.put("/one/two/three/123", 123); + expectedOutputMap.put("/1/3/3/133", 133); + expectedOutputMap.put("/one/three/three/133", 133); + expectedOutputMap.put("/1/2/3/123", 123); + expectedOutputMap.put("/ONE/TWO/THREE/123", 123); + expectedOutputMap.put("/1/3/3/133", 133); + expectedOutputMap.put("/ONE/THREE/THREE/133", 133); + + Map outputMap = mapper.convertMap(inputMap); + + String diff = EqualsHelper.getMapDifferenceReport(outputMap, expectedOutputMap); + if (diff != null) + { + fail(diff); + } + } + + public void testPathMatchesExact() + { + Set mappedPaths = mapper.getMappedPaths("/a/b/c"); + assertEquals("Exact matches expected", 2, mappedPaths.size()); + mappedPaths = mapper.getMappedPaths("/a"); + assertEquals("Exact match NOT expected", 0, mappedPaths.size()); + } + + public void testPathMatchesPartial() + { + Set mappedPaths = mapper.getMappedPathsWithPartialMatch("/a"); + assertEquals("Partial matches expected", 4, mappedPaths.size()); + } +} diff --git a/src/test/java/org/alfresco/util/TempFileProviderTest.java b/src/test/java/org/alfresco/util/TempFileProviderTest.java new file mode 100644 index 0000000000..cd5b1c8358 --- /dev/null +++ b/src/test/java/org/alfresco/util/TempFileProviderTest.java @@ -0,0 +1,95 @@ +/* + * 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.io.File; + +import junit.framework.TestCase; + +/** + * Unit test for TempFileProvider + * + * @see org.alfresco.util.TempFileProvider + * + * @author Derek Hulley + */ +public class TempFileProviderTest extends TestCase +{ + /** + * test of getTempDir + * + * @throws Exception + */ + public void testTempDir() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + assertTrue("Not a directory", tempDir.isDirectory()); + File tempDirParent = tempDir.getParentFile(); + + // create a temp file + File tempFile = File.createTempFile("AAAA", ".tmp"); + File tempFileParent = tempFile.getParentFile(); + + // they should be equal + assertEquals("Our temp dir not subdirectory system temp directory", + tempFileParent, tempDirParent); + } + + /** + * test create a temporary file + * + * create another file with the same prefix and suffix. + * @throws Exception + */ + public void testTempFile() throws Exception + { + File tempFile = TempFileProvider.createTempFile("AAAA", ".tmp"); + File tempFileParent = tempFile.getParentFile(); + File tempDir = TempFileProvider.getTempDir(); + assertEquals("Temp file not located in our temp directory", + tempDir, tempFileParent); + + /** + * Create another temp file and then delete it. + */ + File tempFile2 = TempFileProvider.createTempFile("AAAA", ".tmp"); + tempFile2.delete(); + } + + /** + * test create a temporary file with a directory + * + * create another file with the same prefix and suffix. + * @throws Exception + */ + public void testTempFileWithDir() throws Exception + { + File tempDir = TempFileProvider.getTempDir(); + File tempFile = TempFileProvider.createTempFile("AAAA", ".tmp", tempDir); + File tempFileParent = tempFile.getParentFile(); + assertEquals("Temp file not located in our temp directory", + tempDir, tempFileParent); + + /** + * Create another temp file and then delete it. + */ + File tempFile2 = TempFileProvider.createTempFile("AAAA", ".tmp", tempDir); + tempFile2.delete(); + } +} diff --git a/src/test/java/org/alfresco/util/VersionNumberTest.java b/src/test/java/org/alfresco/util/VersionNumberTest.java new file mode 100644 index 0000000000..3a1bd79b60 --- /dev/null +++ b/src/test/java/org/alfresco/util/VersionNumberTest.java @@ -0,0 +1,123 @@ +/* + * 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 junit.framework.TestCase; + +/** + * Test for extension version class. + * + * @author Roy Wetherall + */ +public class VersionNumberTest extends TestCase +{ + public void testCreate() + { + VersionNumber version1 = new VersionNumber("1"); + int[] parts1 = version1.getParts(); + assertNotNull(parts1); + assertEquals(1, parts1.length); + assertEquals(1, parts1[0]); + + VersionNumber version2 = new VersionNumber("1.2"); + int[] parts2 = version2.getParts(); + assertNotNull(parts2); + assertEquals(2, parts2.length); + assertEquals(1, parts2[0]); + assertEquals(2, parts2[1]); + + VersionNumber version3 = new VersionNumber("1.2.3"); + int[] parts3 = version3.getParts(); + assertNotNull(parts3); + assertEquals(3, parts3.length); + assertEquals(1, parts3[0]); + assertEquals(2, parts3[1]); + assertEquals(3, parts3[2]); + + try + { + new VersionNumber("xxx"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + try + { + new VersionNumber("1-1-2"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + try + { + new VersionNumber("1.2.3a"); + fail("Should not have created an invalid version"); + } catch (Exception exception) + { + // OK + } + } + + public void testEquals() + { + VersionNumber version0 = new VersionNumber("1"); + VersionNumber version1 = new VersionNumber("1.2"); + VersionNumber version2 = new VersionNumber("1.2"); + VersionNumber version3 = new VersionNumber("1.2.3"); + VersionNumber version4 = new VersionNumber("1.2.3"); + VersionNumber version5 = new VersionNumber("1.3.3"); + VersionNumber version6 = new VersionNumber("1.0"); + + assertFalse(version0.equals(version1)); + assertTrue(version1.equals(version2)); + assertFalse(version2.equals(version3)); + assertTrue(version3.equals(version4)); + assertFalse(version4.equals(version5)); + assertTrue(version0.equals(version6)); + } + + public void testCompare() + { + VersionNumber version0 = new VersionNumber("1"); + VersionNumber version1 = new VersionNumber("1.2"); + VersionNumber version2 = new VersionNumber("1.2"); + VersionNumber version3 = new VersionNumber("1.2.3"); + VersionNumber version4 = new VersionNumber("1.11"); + VersionNumber version5 = new VersionNumber("1.3.3"); + VersionNumber version6 = new VersionNumber("2.0"); + VersionNumber version7 = new VersionNumber("2.0.1"); + VersionNumber version8 = new VersionNumber("10.0"); + VersionNumber version9 = new VersionNumber("10.3"); + VersionNumber version10 = new VersionNumber("11.1"); + + assertEquals(-1, version0.compareTo(version1)); + assertEquals(1, version1.compareTo(version0)); + assertEquals(0, version1.compareTo(version2)); + assertEquals(-1, version2.compareTo(version3)); + assertEquals(-1, version2.compareTo(version4)); + assertEquals(-1, version3.compareTo(version5)); + assertEquals(1, version6.compareTo(version5)); + assertEquals(-1, version6.compareTo(version7)); + assertEquals(-1, version1.compareTo(version8)); + assertEquals(-1, version8.compareTo(version9)); + assertEquals(-1, version9.compareTo(version10)); + } +} diff --git a/src/test/java/org/alfresco/util/bean/HierarchicalBeanLoaderTest.java b/src/test/java/org/alfresco/util/bean/HierarchicalBeanLoaderTest.java new file mode 100644 index 0000000000..1cd11a6022 --- /dev/null +++ b/src/test/java/org/alfresco/util/bean/HierarchicalBeanLoaderTest.java @@ -0,0 +1,95 @@ +/* + * 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.bean; + +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.Collection; +import java.util.TreeSet; + +import junit.framework.TestCase; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @see HierarchicalBeanLoader + * + * @author Derek Hulley + * @since 3.2SP1 + */ +public class HierarchicalBeanLoaderTest extends TestCase +{ + private ClassPathXmlApplicationContext ctx; + + private String getBean(Class clazz, boolean setBeforeInit) throws Exception + { + if (setBeforeInit) + { + System.setProperty("hierarchy-test.dialect", clazz.getName()); + } + ctx = new ClassPathXmlApplicationContext("bean-loader/hierarchical-bean-loader-test-context.xml"); + if (!setBeforeInit) + { + System.setProperty("hierarchy-test.dialect", clazz.getName()); + } + return (String) ctx.getBean("test.someString"); + } + + public void tearDown() + { + try + { + ctx.close(); + } + catch (Throwable e) + { + } + } + + public void testSuccess1() throws Throwable + { + String str = getBean(TreeSet.class, true); + assertEquals("Bean value incorrect", "TreeSet", str); + } + + public void testSuccess2() throws Throwable + { + String str = getBean(AbstractList.class, true); + assertEquals("Bean value incorrect", "AbstractList", str); + } + + public void testSuccess3() throws Throwable + { + String str = getBean(AbstractCollection.class, true); + assertEquals("Bean value incorrect", "AbstractCollection", str); + } + + public void testFailure1() throws Throwable + { + try + { + getBean(Collection.class, true); + fail("Should not be able to retrieve bean using class " + Collection.class); + } + catch (Throwable e) + { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java b/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java new file mode 100644 index 0000000000..18ac6af79a --- /dev/null +++ b/src/test/java/org/alfresco/util/collections/CollectionUtilsTest.java @@ -0,0 +1,206 @@ +/* + * 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.collections; + +import static java.util.Arrays.asList; + +import static org.alfresco.util.collections.CollectionUtils.asSet; +import static org.alfresco.util.collections.CollectionUtils.nullSafeMerge; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for {@link CollectionUtils}. + * + * @author Neil Mc Erlean + */ +public class CollectionUtilsTest +{ + private static Set stooges; + + private static Map primes; + private static Map squares; + private static Map nullMap; + private static Map nerdsBirthdays; + + @Before public void initData() + { + stooges = new HashSet<>(); + stooges.add("Larry"); + stooges.add("Curly"); + stooges.add("Moe"); + + primes = new HashMap<>(); + primes.put("two", 2); + primes.put("three", 3); + primes.put("five", 5); + + squares = new HashMap<>(); + squares.put("one", 1); + squares.put("two", 4); + squares.put("three", 9); + + nerdsBirthdays = new HashMap<>(); + nerdsBirthdays.put("Alan Turing", 1912); + nerdsBirthdays.put("Charles Babbage", 1791); + nerdsBirthdays.put("Matthew Smith", 1966); + nerdsBirthdays.put("Paul Dirac", 1902); + nerdsBirthdays.put("Robert Boyle", 1627); + nerdsBirthdays.put("Robert Hooke", 1635); + nerdsBirthdays.put("J. Robert Oppenheimer", 1904); + } + + @Test public void varArgsAsSet() { + assertEquals(stooges, asSet("Larry", "Curly", "Moe")); + + assertEquals(stooges, CollectionUtils.asSet(String.class, "Larry", "Curly", "Moe")); + } + + @Test public void nullSafeMergeMaps() + { + assertNull(nullSafeMerge(nullMap, nullMap, true)); + + assertEquals(Collections.emptyMap(), nullSafeMerge(nullMap, nullMap)); + assertEquals(primes, nullSafeMerge(nullMap, primes)); + assertEquals(primes, nullSafeMerge(primes, nullMap)); + + Map primesAndSquares = new HashMap<>(); + primesAndSquares.putAll(primes); + primesAndSquares.putAll(squares); + + assertEquals(primesAndSquares, nullSafeMerge(primes, squares)); + } + + @Test public void collectionFiltering() throws Exception + { + Function johnFilter = new KeySubstringFilter("John"); + assertEquals(0, CollectionUtils.filterKeys(nerdsBirthdays, johnFilter).size()); + + Function robertFilter = new KeySubstringFilter("Robert"); + assertEquals(3, CollectionUtils.filterKeys(nerdsBirthdays, robertFilter).size()); + } + + private static final class KeySubstringFilter implements Function + { + private final String substring; + public KeySubstringFilter(String substring) { this.substring = substring; } + @Override public Boolean apply(String value) + { + return value.contains(substring); + } + } + + @Test public void sortMapsByEntry() throws Exception + { + final Map expectedSorting = getNerdsSortedByBirthDate(); + + Comparator> entryComparator = new Comparator>() + { + @Override public int compare(Entry e1, Entry e2) + { + return e1.getValue().intValue() - e2.getValue().intValue(); + } + }; + + final Map actualSorting = CollectionUtils.sortMapByValue(nerdsBirthdays, entryComparator); + + assertEquals(expectedSorting, actualSorting); + } + + @Test public void sortMapsByValue() throws Exception + { + final Map expectedSorting = getNerdsSortedByBirthDate(); + + Comparator valueComparator = new Comparator() + { + @Override public int compare(Integer i1, Integer i2) + { + return i1.intValue() - i2.intValue(); + } + }; + + Comparator> entryComparator = CollectionUtils.toEntryComparator(valueComparator); + + final Map actualSorting = CollectionUtils.sortMapByValue(nerdsBirthdays, entryComparator); + + assertEquals(expectedSorting, actualSorting); + } + + private Map getNerdsSortedByBirthDate() + { + final Map result = new LinkedHashMap<>(); // maintains insertion order + result.put("Robert Boyle", 1627); + result.put("Robert Hooke", 1635); + result.put("Charles Babbage", 1791); + result.put("Paul Dirac", 1902); + result.put("J. Robert Oppenheimer", 1904); + result.put("Alan Turing", 1912); + result.put("Matthew Smith", 1966); + return result; + } + + @Test public void moveItemInList() throws Exception + { + final List input = asList("a", "b", "c"); + + assertEquals(asList("a", "b", "c"), CollectionUtils.moveRight(0, "b", input)); + assertEquals(asList("a", "c", "b"), CollectionUtils.moveRight(1, "b", input)); + assertEquals(asList("a", "c", "b"), CollectionUtils.moveRight(5, "b", input)); + + assertEquals(asList("c", "a", "b"), CollectionUtils.moveRight(-2, "c", input)); + assertEquals(asList("c", "a", "b"), CollectionUtils.moveRight(-5, "c", input)); + + assertEquals(asList("a", "b", "c"), CollectionUtils.moveLeft(0, "b", input)); + assertEquals(asList("b", "a", "c"), CollectionUtils.moveLeft(1, "b", input)); + assertEquals(asList("b", "a", "c"), CollectionUtils.moveLeft(5, "b", input)); + + assertEquals(asList("b", "c", "a"), CollectionUtils.moveLeft(-2, "a", input)); + assertEquals(asList("b", "c", "a"), CollectionUtils.moveLeft(-5, "a", input)); + + try { CollectionUtils.moveRight(1, "x", input); } + catch (NoSuchElementException expected) { return; } + + fail("Expected exception was not thrown."); + } + + @Test public void flattenCollections() throws Exception + { + final List list1 = CollectionUtils.toListOfStrings(stooges); + Collections.sort(list1); + final List list2 = asList("Hello", "World"); + + assertEquals(asList("Curly", "Larry", "Moe", "Hello", "World"), + CollectionUtils.flatten(list1, list2)); + } +} diff --git a/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java b/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java new file mode 100644 index 0000000000..c2d660da46 --- /dev/null +++ b/src/test/java/org/alfresco/util/exec/ExecParameterTokenizerTest.java @@ -0,0 +1,161 @@ +/* + * 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.util.exec; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +/** + * Unit test class for {@link ExecParameterTokenizer}. + * + * @author Neil Mc Erlean + * @since 3.4.2 + */ +public class ExecParameterTokenizerTest +{ + @Test public void tokenizeEmptyString() + { + final String str1 = ""; + final String str2 = " \t "; + + List expectedTokens = Arrays.asList(new String[0]); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test(expected=NullPointerException.class) public void tokenizeNullString() + { + final String str1 = null; + + List expectedTokens = Arrays.asList(new String[0]); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeSimpleParameterString() + { + final String str1 = "-font Helvetica -pointsize 50"; + final String str2 = " -font Helvetica -pointsize 50 "; + + List expectedTokens = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringEntirelyQuoted() + { + final String str1 = "\"circle 100,100 150,150\""; + final String str2 = "'circle 100,100 150,150'"; + + List expectedTokens = Arrays.asList(new String[] {"circle 100,100 150,150"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithUnclosedSingleQuote() + { + final String str = "-font Helvetica -pointsize 50 -draw 'circle"; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithUnclosedDoubleQuote() + { + final String str = "-font Helvetica -pointsize 50 -draw \"circle"; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test(expected=IllegalArgumentException.class) + public void tokenizeParameterStringWithMalformedQuoteNesting() + { + final String str = " \"foo 'bar baz\" hello' "; + + ExecParameterTokenizer t = new ExecParameterTokenizer(str); + t.getAllTokens(); + } + + @Test public void tokenizeParameterStringWithQuotedParam() + { + final String str1 = "-font Helvetica -pointsize 50 -draw \"circle 100,100 150,150\""; + final String str2 = "-font Helvetica -pointsize 50 -draw 'circle 100,100 150,150'"; + + List expectedTokens = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "circle 100,100 150,150"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringWithQuotedParam_MixedQuotes() + { + final String str1 = "'Hello world' middle \"Goodbye world\""; + final String str2 = "\"Hello world\" middle 'Goodbye world'"; + + List expectedTokens = Arrays.asList(new String[] {"Hello world", "middle", "Goodbye world"}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens, t.getAllTokens()); + } + + @Test public void tokenizeParameterStringWithQuotedParamContainingQuotes() + { + final String str1 = "-font Helvetica -pointsize 50 -draw \"gravity south fill black text 0,12 'CopyRight'\""; + final String str2 = "-font Helvetica -pointsize 50 -draw 'gravity south fill black text 0,12 \"CopyRight\"'"; + + List expectedTokens1 = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "gravity south fill black text 0,12 'CopyRight'"}); + + List expectedTokens2 = Arrays.asList(new String[] {"-font", "Helvetica", "-pointsize", "50", + "-draw", "gravity south fill black text 0,12 \"CopyRight\""}); + + ExecParameterTokenizer t = new ExecParameterTokenizer(str1); + assertEquals("Wrong tokens", expectedTokens1, t.getAllTokens()); + + t = new ExecParameterTokenizer(str2); + assertEquals("Wrong tokens", expectedTokens2, t.getAllTokens()); + } +} diff --git a/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java b/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java new file mode 100644 index 0000000000..464d980ba3 --- /dev/null +++ b/src/test/java/org/alfresco/util/exec/RuntimeExecBeansTest.java @@ -0,0 +1,227 @@ +/* + * 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.exec; + +import java.io.File; +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @see org.alfresco.util.exec.RuntimeExecBootstrapBean + * + * @author Derek Hulley + */ +public class RuntimeExecBeansTest extends TestCase +{ + private static Log logger = LogFactory.getLog(RuntimeExecBeansTest.class); + + private static final String APP_CONTEXT_XML = + "classpath:org/alfresco/util/exec/RuntimeExecBeansTest-context.xml"; + private static final String DIR = "dir RuntimeExecBootstrapBeanTest"; + + private File dir; + + public void setUp() throws Exception + { + dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + } + + public void testBootstrapAndShutdown() throws Exception + { + // now bring up the bootstrap + ApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + + // the folder should be gone + assertFalse("Folder was not deleted by bootstrap", dir.exists()); + + // now create the folder again + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + // announce that the context is closing + ctx.publishEvent(new ContextClosedEvent(ctx)); + + // the folder should be gone + assertFalse("Folder was not deleted by shutdown", dir.exists()); + } + + public void testSimpleSuccess() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec dirRootExec = (RuntimeExec) ctx.getBean("commandListRootDir"); + assertNotNull(dirRootExec); + // Execute it + dirRootExec.execute(); + } + finally + { + ctx.close(); + } + } + + public void testDeprecatedSetCommandMap() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec deprecatedExec = (RuntimeExec) ctx.getBean("commandCheckDeprecatedSetCommandMap"); + assertNotNull(deprecatedExec); + // Execute it + deprecatedExec.execute(); + } + finally + { + ctx.close(); + } + // The best we can do is look at the log manually + logger.warn("There should be a warning re. the use of deprecated 'setCommandMap'."); + } + + public void testSplitArguments() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec splitExec = (RuntimeExec) ctx.getBean("commandSplitArguments"); + assertNotNull(splitExec); + String[] splitCommand = splitExec.getCommand(); + assertTrue( + "Command arguments not split into 'dir', '.' and '..' :" + Arrays.deepToString(splitCommand), + Arrays.deepEquals(new String[] {"dir", ".", ".."}, splitCommand)); + } + finally + { + ctx.close(); + } + } + + public void testSplitArgumentsAsSingleValue() throws Exception + { + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec splitExec = (RuntimeExec) ctx.getBean("commandSplitArgumentsAsSingleValue"); + assertNotNull(splitExec); + String[] splitCommand = splitExec.getCommand(); + assertTrue( + "Command arguments not split into 'dir', '.' and '..' : " + Arrays.deepToString(splitCommand), + Arrays.deepEquals(new String[] {"dir", ".", ".."}, splitCommand)); + } + finally + { + ctx.close(); + } + } + + public void testFailureModeOfMissingCommand() + { + File dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec failureExec = (RuntimeExec) ctx.getBean("commandFailureGuaranteed"); + assertNotNull(failureExec); + // Execute it + ExecutionResult result = failureExec.execute(); + assertEquals("Expected first error code in list", 666, result.getExitValue()); + } + finally + { + ctx.close(); + } + } + +// /** +// * Checks that the encoding setting feeds through to the streams. +// */ +// public void testStreamReading() throws Exception +// { +// String manglingCharsetName = "UTF-16"; +// +// File dir = new File(DIR); +// dir.mkdir(); +// assertTrue("Directory not created", dir.exists()); +// +// ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); +// try +// { +// RuntimeExec dirRootExec = (RuntimeExec) ctx.getBean("commandListRootDir"); +// assertNotNull(dirRootExec); +// // Execute it +// ExecutionResult result = dirRootExec.execute(); +// +// // Get the error stream +// String defaultStdOut = result.getStdOut(); +// +// // Change the encoding +// dirRootExec.setCharset(manglingCharsetName); +// result = dirRootExec.execute(); +// String mangledStdOut = result.getStdOut(); +// // The two error strings must not be the same +// assertNotSame("Differently encoded strings should differ", defaultStdOut, mangledStdOut); +// +// // Now convert the Shift-JIS string and ensure it's the same as originally expected +// Charset defaultCharset = Charset.defaultCharset(); +// byte[] mangledBytes = mangledStdOut.getBytes(manglingCharsetName); +// String convertedStrOut = new String(mangledBytes, defaultCharset.name()); +// // Check, catering for any mangled characters +// assertTrue("Expected to be able to convert value back to default charset.", convertedStrOut.contains(defaultStdOut)); +// } +// finally +// { +// ctx.close(); +// } +// } +// + public void testExecOfNeverEndingProcess() + { + File dir = new File(DIR); + dir.mkdir(); + assertTrue("Directory not created", dir.exists()); + + ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(APP_CONTEXT_XML); + try + { + RuntimeExec failureExec = (RuntimeExec) ctx.getBean("commandNeverEnding"); + assertNotNull(failureExec); + // Execute it + failureExec.execute(); + // The command is never-ending, so this should be out immediately + } + finally + { + ctx.close(); + } + } +} diff --git a/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java b/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java new file mode 100644 index 0000000000..9144a2fb53 --- /dev/null +++ b/src/test/java/org/alfresco/util/exec/RuntimeExecTest.java @@ -0,0 +1,166 @@ +/* + * 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.exec; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.util.exec.RuntimeExec.ExecutionResult; + +import junit.framework.TestCase; + +/** + * @see org.alfresco.util.exec.RuntimeExec + * + * @author Derek Hulley + */ +public class RuntimeExecTest extends TestCase +{ + public void testStreams() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // This test will return different results on Windows and Linux! + // note that some Unix variants will error without a path + HashMap commandMap = new HashMap(5); + commandMap.put("*", new String[] {"find", "/", "-maxdepth", "1", "-name", "var"}); + commandMap.put("Windows.*", new String[] {"find", "/?"}); + exec.setCommandsAndArguments(commandMap); + // execute + ExecutionResult ret = exec.execute(); + + String out = ret.getStdOut(); + String err = ret.getStdErr(); + + assertEquals("Didn't expect error code", 0, ret.getExitValue()); + assertEquals("Didn't expect any error output", 0, err.length()); + assertTrue("No output found", out.length() > 0); + } + + public void testWildcard() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // set the command + Map commandMap = new HashMap(3, 1.0f); + commandMap.put(".*", (new String[]{"TEST"})); + exec.setCommandsAndArguments(commandMap); + + String[] commandStr = exec.getCommand(); + assertTrue("Expected default match to work", Arrays.deepEquals(new String[] {"TEST"}, commandStr)); + } + + public void testWithProperties() throws Exception + { + RuntimeExec exec = new RuntimeExec(); + + // set the command + Map commandMap = new HashMap(3, 1.0f); + commandMap.put("Windows.*", new String[]{"dir", "${path}"}); + commandMap.put("Linux", new String[] {"ls", "${path}"}); + commandMap.put("Mac OS X", new String[]{"ls", "${path}"}); + commandMap.put("*", new String[]{"wibble", "${path}"}); + exec.setCommandsAndArguments(commandMap); + + // set the default properties + Map defaultProperties = new HashMap(1, 1.0f); + defaultProperties.put("path", "."); + exec.setDefaultProperties(defaultProperties); + + // check that the command lines generated are correct + String defaultCommand[] = exec.getCommand(); + String dynamicCommand[] = exec.getCommand(Collections.singletonMap("path", "./")); + // check + String os = System.getProperty("os.name"); + String[] defaultCommandCheck = null; + String[] dynamicCommandCheck = null; + if (os.matches("Windows.*")) + { + defaultCommandCheck = new String[]{"dir", "."}; + dynamicCommandCheck = new String[]{"dir", "./"}; + } + else if (os.equals("Linux") || os.equals("Mac OS X")) + { + defaultCommandCheck = new String[]{"ls", "."}; + dynamicCommandCheck = new String[]{"ls", "./"}; + } + else + { + defaultCommandCheck = new String[]{"wibble", "."}; + dynamicCommandCheck = new String[]{"wibble", "./"}; + } + assertTrue("Default command for OS " + os + " is incorrect", Arrays.deepEquals(defaultCommandCheck, defaultCommand)); + assertTrue("Dynamic command for OS " + os + " is incorrect", Arrays.deepEquals(dynamicCommandCheck, dynamicCommand)); + } + + public void testNoTimeout() throws Exception + { + long timeout = -1; + int runFor = 10000; + + longishRunningProcess(runFor, timeout); + } + + public void testTimeout() throws Exception + { + long timeout = 5000; + int runFor = 10000; + + longishRunningProcess(runFor, timeout); + } + + private void longishRunningProcess(int runFor, long timeout) + { + long marginOfError = 3000; + boolean shouldComplete = timeout <= 0; + + assertTrue("The timeout when set must be more than "+marginOfError+"ms", shouldComplete || timeout >= marginOfError); + assertTrue("The timeout when set plus "+marginOfError+"ms must less than the runFor value", shouldComplete || timeout+marginOfError <= runFor); + + long minTime = (shouldComplete ? runFor : timeout) - marginOfError; + long maxTime = (shouldComplete ? runFor : timeout) + marginOfError; + + RuntimeExec exec = new RuntimeExec(); + + // This test will return different results on Windows and Linux! + // note that some Unix variants will error without a path + HashMap commandMap = new HashMap(); + commandMap.put("*", new String[] {"sleep", ""+(runFor/1000)}); + commandMap.put("Windows.*", new String[] {"ping", "-n", ""+(runFor/1000+1), "127.0.0.1"}); // don't you just love Microsoft + + // execute + exec.setCommandsAndArguments(commandMap); + long time = System.currentTimeMillis(); + ExecutionResult ret = exec.execute(Collections.emptyMap(), timeout); + time = System.currentTimeMillis()-time; + + String out = ret.getStdOut(); + String err = ret.getStdErr(); + + assertTrue("Command was too fast "+time+"ms", time >= minTime); + assertTrue("Command was too slow "+time+"ms", time <= maxTime); + + if (shouldComplete) + assertEquals("Didn't expect error code", 0, ret.getExitValue()); + else + assertFalse("Didn't expect success code", 0 == ret.getExitValue()); + } +} diff --git a/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java b/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java new file mode 100644 index 0000000000..47d4c56a8d --- /dev/null +++ b/src/test/java/org/alfresco/util/random/NormalDistributionHelperTest.java @@ -0,0 +1,69 @@ +/* + * 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.util.random; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @see NormalDistributionHelper + * + * @author Derek Hulley + * @since 5.1 + */ +public class NormalDistributionHelperTest +{ + private NormalDistributionHelper normalDistribution = new NormalDistributionHelper(); + + @Test + public void testGetValue_Fail() + { + try + { + normalDistribution.getValue(5L, -5L); + fail("Min-max relation not detected."); + } + catch (IllegalArgumentException e) + { + // Expected + } + } + + @Test + public void testGetValue_Precise() + { + assertEquals(10L, normalDistribution.getValue(10L, 10L)); + assertEquals(0L, normalDistribution.getValue(0L, 0L)); + assertEquals(-10L, normalDistribution.getValue(-10L, -10L)); + } + + @Test + public void testGetValue_Repeated() + { + for (int i = 0; i < 1000; i++) + { + long value = normalDistribution.getValue(-1*i, i); + assertTrue("Min not respected", value >= -1*i); + assertTrue("Max not respected", value <= i); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/alfresco/util/resource/HierarchicalResourceLoaderTest.java b/src/test/java/org/alfresco/util/resource/HierarchicalResourceLoaderTest.java new file mode 100644 index 0000000000..1fa11f6b97 --- /dev/null +++ b/src/test/java/org/alfresco/util/resource/HierarchicalResourceLoaderTest.java @@ -0,0 +1,170 @@ +/* + * 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.resource; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.AbstractCollection; +import java.util.AbstractList; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.TreeSet; + +import junit.framework.TestCase; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.springframework.core.io.Resource; + +/** + * @see HierarchicalResourceLoader + * + * @author Derek Hulley + * @since 3.2 (Mobile) + */ +public class HierarchicalResourceLoaderTest extends TestCase +{ + private HierarchicalResourceLoader getLoader( + Class baseClazz, + Class clazz) throws Throwable + { + HierarchicalResourceLoader loader = new HierarchicalResourceLoader(); + loader.setDialectBaseClass(baseClazz.getName()); + loader.setDialectClass(clazz.getName()); + loader.afterPropertiesSet(); + return loader; + } + + /** + * Check that unmatched hierarchies are detected + */ + public void testMismatchDetection() throws Throwable + { + // First, do a successful few + getLoader(AbstractCollection.class, TreeSet.class); + getLoader(AbstractCollection.class, HashSet.class); + getLoader(AbstractCollection.class, ArrayList.class); + getLoader(AbstractCollection.class, AbstractCollection.class); + // Now blow up a bit + try + { + getLoader(Collection.class, Object.class).getResource("abc"); + fail("Failed to detect incompatible class hierarchy"); + } + catch (RuntimeException e) + { + // Expected + } + try + { + getLoader(ArrayList.class, AbstractCollection.class).getResource("abc"); + fail("Failed to detect incompatible class hierarchy"); + } + catch (RuntimeException e) + { + // Expected + } + } + + private void checkResource(Resource resource, String check) throws Throwable + { + assertNotNull("Resource not found", resource); + assertTrue("Resource doesn't exist", resource.exists()); + InputStream is = resource.getInputStream(); + StringBuilder builder = new StringBuilder(128); + byte[] bytes = new byte[128]; + InputStream tempIs = null; + try + { + tempIs = new BufferedInputStream(is, 128); + int count = -2; + while (count != -1) + { + // do we have something previously read? + if (count > 0) + { + String toWrite = new String(bytes, 0, count, "UTF-8"); + builder.append(toWrite); + } + // read the next set of bytes + count = tempIs.read(bytes); + } + } + catch (IOException e) + { + throw new AlfrescoRuntimeException("Unable to read stream", e); + } + finally + { + // close the input stream + try + { + is.close(); + } + catch (Exception e) + { + } + } + // The string + String fileValue = builder.toString(); + assertEquals("Incorrect file retrieved: ", check, fileValue); + } + + private static final String RESOURCE = "classpath:resource-loader/#resource.dialect#/file.txt"; + /** + * Check that resource loading works. + * + * The data available is: + *
+     * classpatch:resource-loader/
+     *    java.util.AbstractCollection
+     *    java.util.AbstractList
+     *    java.util.TreeSet 
+     * 
+ * With each folder containing a text file with the name of the folder. + */ + public void testResourceLoading() throws Throwable + { + // First, do a successful few + HierarchicalResourceLoader bean; + Resource resource; + + bean = getLoader(AbstractCollection.class, TreeSet.class); + resource = bean.getResource(RESOURCE); + checkResource(resource, "java.util.TreeSet"); + + bean = getLoader(AbstractCollection.class, AbstractSet.class); + resource = bean.getResource(RESOURCE); + checkResource(resource, "java.util.AbstractCollection"); + + bean = getLoader(AbstractCollection.class, AbstractCollection.class); + resource = bean.getResource(RESOURCE); + checkResource(resource, "java.util.AbstractCollection"); + + bean = getLoader(AbstractCollection.class, ArrayList.class); + resource = bean.getResource(RESOURCE); + checkResource(resource, "java.util.AbstractList"); + + bean = getLoader(AbstractCollection.class, AbstractList.class); + resource = bean.getResource(RESOURCE); + checkResource(resource, "java.util.AbstractList"); + } +} diff --git a/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java b/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java new file mode 100644 index 0000000000..937c647225 --- /dev/null +++ b/src/test/java/org/alfresco/util/shard/ExplicitShardingPolicyTest.java @@ -0,0 +1,201 @@ +/* + * 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.util.shard; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +/** + * @author Andy + */ +public class ExplicitShardingPolicyTest +{ + + @Test + public void tenShards_noReplication_oneNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 1, 1); + assertTrue(policy.configurationIsValid()); + List shardIds = policy.getShardIdsForNode(1); + assertEquals(10, shardIds.size()); + for (int i = 0; i < 10; i++) + { + assertTrue(shardIds.contains(i)); + } + assertEquals(0, policy.getShardIdsForNode(2).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(1, nodeInstances.size()); + } + } + + @Test + public void tenShards_noReplication_tenNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 1, 10); + assertTrue(policy.configurationIsValid()); + + for (int i = 0; i < 10; i++) + { + List shardIds = policy.getShardIdsForNode(i + 1); + assertEquals(1, shardIds.size()); + assertTrue(shardIds.contains(i)); + } + assertEquals(0, policy.getShardIdsForNode(11).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(1, nodeInstances.size()); + + } + } + + @Test + public void tenShards_doubled_tenNodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 2, 10); + assertTrue(policy.configurationIsValid()); + + for (int i = 0; i < 10; i++) + { + List shardIds = policy.getShardIdsForNode(i + 1); + assertEquals(2, shardIds.size()); + assertTrue(shardIds.contains(i)); + assertTrue(shardIds.contains((i + 1) % 10)); + } + assertEquals(0, policy.getShardIdsForNode(11).size()); + + for (int i = 0; i < 10; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(2, nodeInstances.size()); + + } + } + + @Test + public void check_24_3() + { + buildAndTest(24, 3, 72); + buildAndTest(24, 3, 36); + buildAndTest(24, 3, 24); + buildAndTest(24, 3, 18); + buildAndTest(24, 3, 12); + buildAndTest(24, 3, 9); + buildAndTest(24, 3, 8); + buildAndTest(24, 3, 6); + buildAndTest(24, 3, 4); + buildAndTest(24, 3, 3); + } + + @Test + + public void failing() + { + buildAndTest(10, 2, 4); + } + + @Test + public void check_10_2() + { + buildAndTest(10, 2, 20); + buildAndTest(10, 2, 10); + buildAndTest(10, 2, 5); + buildAndTest(10, 2, 4); + buildAndTest(10, 2, 2); + } + + @Test + public void check_12_2() + { + buildAndTest(12, 2, 24); + buildAndTest(12, 2, 12); + buildAndTest(12, 2, 8); + buildAndTest(12, 2, 6); + buildAndTest(12, 2, 4); + buildAndTest(12, 2, 3); + buildAndTest(12, 2, 2); + } + + @Test + public void invalidConfiguration_nodes() + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(10, 2, 11); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(10, 0, 10); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(0, 2, 10); + assertFalse(policy.configurationIsValid()); + + policy = new ExplicitShardingPolicy(10, 11, 10); + assertFalse(policy.configurationIsValid()); + } + + private void buildAndTest(int numShards, int replicationFactor, int numNodes) + { + ExplicitShardingPolicy policy = new ExplicitShardingPolicy(numShards, replicationFactor, numNodes); + assertTrue(policy.configurationIsValid()); + + int[] found = new int[numShards]; + for (int i = 0; i < numNodes; i++) + { + List shardIds = policy.getShardIdsForNode(i + 1); + assertEquals(numShards * replicationFactor / numNodes, shardIds.size()); + for (Integer shardId : shardIds) + { + found[shardId]++; + } + } + check(found, replicationFactor); + assertEquals(0, policy.getShardIdsForNode(numNodes + 1).size()); + + + int[] nodes = new int[numNodes]; + for(int i = 0; i < numShards; i++) + { + List nodeInstances = policy.getNodeInstancesForShardId(i); + assertEquals(replicationFactor, nodeInstances.size()); + for (Integer nodeInstance : nodeInstances) + { + nodes[nodeInstance-1]++; + } + } + check(nodes, numShards * replicationFactor / numNodes); + } + + /** + * @param found + * @param i + */ + private void check(int[] found, int count) + { + for (int i = 0; i < found.length; i++) + { + assertEquals(count, found[i]); + } + } +} diff --git a/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java b/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java new file mode 100644 index 0000000000..5c218bbafe --- /dev/null +++ b/src/test/java/org/alfresco/util/transaction/SpringAwareUserTransactionTest.java @@ -0,0 +1,404 @@ +/* + * 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.transaction; + +import java.util.NoSuchElementException; + +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.interceptor.TransactionAspectSupport; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +/** + * @see org.alfresco.util.transaction.SpringAwareUserTransaction + * + * @author Derek Hulley + */ +public class SpringAwareUserTransactionTest extends TestCase +{ + private DummyTransactionManager transactionManager; + private FailingTransactionManager failingTransactionManager; + private UserTransaction txn; + + public SpringAwareUserTransactionTest() + { + super(); + } + + @Override + protected void setUp() throws Exception + { + transactionManager = new DummyTransactionManager(); + failingTransactionManager = new FailingTransactionManager(); + txn = getTxn(); + } + + private UserTransaction getTxn() + { + return new SpringAwareUserTransaction( + transactionManager, + false, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + } + + public void testSetUp() throws Exception + { + assertNotNull(transactionManager); + assertNotNull(txn); + } + + private void checkNoStatusOnThread() + { + try + { + TransactionAspectSupport.currentTransactionStatus(); + fail("Spring transaction info is present outside of transaction boundaries"); + } + catch (NoTransactionException e) + { + // expected + } + } + + public void testNoTxnStatus() throws Exception + { + checkNoStatusOnThread(); + assertEquals("Transaction status is not correct", + Status.STATUS_NO_TRANSACTION, + txn.getStatus()); + assertEquals("Transaction manager not set up correctly", + txn.getStatus(), + transactionManager.getStatus()); + } + + public void testSimpleTxnWithCommit() throws Throwable + { + testNoTxnStatus(); + try + { + txn.begin(); + assertEquals("Transaction status is not correct", + Status.STATUS_ACTIVE, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + + txn.commit(); + assertEquals("Transaction status is not correct", + Status.STATUS_COMMITTED, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + } + catch (Throwable e) + { + // unexpected exception - attempt a cleanup + try + { + txn.rollback(); + } + catch (Throwable ee) + { + e.printStackTrace(); + } + throw e; + } + checkNoStatusOnThread(); + } + + public void testSimpleTxnWithRollback() throws Exception + { + testNoTxnStatus(); + try + { + txn.begin(); + + throw new Exception("Blah"); + } + catch (Throwable e) + { + txn.rollback(); + } + assertEquals("Transaction status is not correct", + Status.STATUS_ROLLEDBACK, + txn.getStatus()); + assertEquals("Transaction manager not called correctly", + txn.getStatus(), + transactionManager.getStatus()); + checkNoStatusOnThread(); + } + + public void testNoBeginCommit() throws Exception + { + testNoTxnStatus(); + try + { + txn.commit(); + fail("Failed to detected no begin"); + } + catch (IllegalStateException e) + { + // expected + } + checkNoStatusOnThread(); + } + + public void testPostRollbackCommitDetection() throws Exception + { + testNoTxnStatus(); + + txn.begin(); + txn.rollback(); + try + { + txn.commit(); + fail("Failed to detect rolled back txn"); + } + catch (RollbackException e) + { + // expected + } + checkNoStatusOnThread(); + } + + public void testPostSetRollbackOnlyCommitDetection() throws Exception + { + testNoTxnStatus(); + + txn.begin(); + txn.setRollbackOnly(); + try + { + txn.commit(); + fail("Failed to detect set rollback"); + } + catch (RollbackException e) + { + // expected + txn.rollback(); + } + checkNoStatusOnThread(); + } + + public void testMismatchedBeginCommit() throws Exception + { + UserTransaction txn1 = getTxn(); + UserTransaction txn2 = getTxn(); + + testNoTxnStatus(); + + txn1.begin(); + txn2.begin(); + + txn2.commit(); + txn1.commit(); + + checkNoStatusOnThread(); + + txn1 = getTxn(); + txn2 = getTxn(); + + txn1.begin(); + txn2.begin(); + + try + { + txn1.commit(); + fail("Failure to detect mismatched transaction begin/commit"); + } + catch (RuntimeException e) + { + // expected + } + txn2.commit(); + txn1.commit(); + + checkNoStatusOnThread(); + } + + /** + * Test for leaked transactions (no guarantee it will succeed due to reliance + * on garbage collector), so disabled by default. + * + * Also, if it succeeds, transaction call stack tracing will be enabled + * potentially hitting the performance of all subsequent tests. + * + * @throws Exception + */ + public void xtestLeakedTransactionLogging() throws Exception + { + assertFalse(SpringAwareUserTransaction.isCallStackTraced()); + + TrxThread t1 = new TrxThread(); + t1.start(); + System.gc(); + Thread.sleep(1000); + + TrxThread t2 = new TrxThread(); + t2.start(); + System.gc(); + Thread.sleep(1000); + + assertTrue(SpringAwareUserTransaction.isCallStackTraced()); + + TrxThread t3 = new TrxThread(); + t3.start(); + System.gc(); + Thread.sleep(3000); + System.gc(); + Thread.sleep(3000); + } + + private class TrxThread extends Thread + { + public void run() + { + try + { + getTrx(); + } + catch (Exception e) {} + } + + public void getTrx() throws Exception + { + UserTransaction txn = getTxn(); + txn.begin(); + txn = null; + } + } + + public void testConnectionPoolException() throws Exception + { + testNoTxnStatus(); + txn = getFailingTxn(); + try + { + txn.begin(); + fail("ConnectionPoolException should be thrown."); + } + catch (ConnectionPoolException cpe) + { + // Expected fail + } + } + + private UserTransaction getFailingTxn() + { + return new SpringAwareUserTransaction( + failingTransactionManager, + false, + TransactionDefinition.ISOLATION_DEFAULT, + TransactionDefinition.PROPAGATION_REQUIRED, + TransactionDefinition.TIMEOUT_DEFAULT); + } + + /** + * Used to check that the transaction manager is being called correctly + * + * @author Derek Hulley + */ + @SuppressWarnings("serial") + private static class DummyTransactionManager extends AbstractPlatformTransactionManager + { + private int status = Status.STATUS_NO_TRANSACTION; + private Object txn = new Object(); + + /** + * @return Returns one of the {@link Status Status.STATUS_XXX} constants + */ + public int getStatus() + { + return status; + } + + protected void doBegin(Object arg0, TransactionDefinition arg1) + { + status = Status.STATUS_ACTIVE; + } + + protected void doCommit(DefaultTransactionStatus arg0) + { + status = Status.STATUS_COMMITTED; + } + + protected Object doGetTransaction() + { + return txn; + } + + protected void doRollback(DefaultTransactionStatus arg0) + { + status = Status.STATUS_ROLLEDBACK; + } + } + + /** + * Throws {@link NoSuchElementException} on begin() + * + * @author alex.mukha + */ + private static class FailingTransactionManager extends AbstractPlatformTransactionManager + { + private static final long serialVersionUID = 1L; + private int status = Status.STATUS_NO_TRANSACTION; + private Object txn = new Object(); + + /** + * @return Returns one of the {@link Status Status.STATUS_XXX} constants + */ + @SuppressWarnings("unused") + public int getStatus() + { + return status; + } + + protected void doBegin(Object arg0, TransactionDefinition arg1) + { + throw new CannotCreateTransactionException("Test exception."); + } + + protected void doCommit(DefaultTransactionStatus arg0) + { + status = Status.STATUS_COMMITTED; + } + + protected Object doGetTransaction() + { + return txn; + } + + protected void doRollback(DefaultTransactionStatus arg0) + { + status = Status.STATUS_ROLLEDBACK; + } + } +} diff --git a/src/test/resources/bean-loader/hierarchical-bean-loader-test-context.xml b/src/test/resources/bean-loader/hierarchical-bean-loader-test-context.xml new file mode 100644 index 0000000000..3cf31df419 --- /dev/null +++ b/src/test/resources/bean-loader/hierarchical-bean-loader-test-context.xml @@ -0,0 +1,44 @@ + + + + + + + + false + + + SYSTEM_PROPERTIES_MODE_OVERRIDE + + + false + + + + + + test.someString.#bean.dialect# + + + java.lang.String + + + java.util.AbstractCollection + + + ${hierarchy-test.dialect} + + + + + + + + + + + + + + diff --git a/src/test/resources/config-areas.xml b/src/test/resources/config-areas.xml new file mode 100644 index 0000000000..594902261c --- /dev/null +++ b/src/test/resources/config-areas.xml @@ -0,0 +1,13 @@ + + + + + value + + + + + A value + + + \ No newline at end of file diff --git a/src/test/resources/config-multi.xml b/src/test/resources/config-multi.xml new file mode 100644 index 0000000000..36b3116dde --- /dev/null +++ b/src/test/resources/config-multi.xml @@ -0,0 +1,30 @@ + + + + + + + + + + Another global value + true + + child two value + + + + + Another value + + + + the overridden first value + second value + + child two value + child three value + + + + \ No newline at end of file diff --git a/src/test/resources/config-props.properties b/src/test/resources/config-props.properties new file mode 100644 index 0000000000..fd5e95097e --- /dev/null +++ b/src/test/resources/config-props.properties @@ -0,0 +1,5 @@ +globalValue=globalValue +childOneValue=childOneValue +theValue=theValue +theAttr=attrValue +true=true diff --git a/src/test/resources/config-props.xml b/src/test/resources/config-props.xml new file mode 100644 index 0000000000..d975b5ce0d --- /dev/null +++ b/src/test/resources/config-props.xml @@ -0,0 +1,19 @@ + + + + The global value + ${globalValue} + ${missingGlobalValue} + false + + + + The value + ${theValue} + ${missingTheValue} + true + ${true} + ${missingTrue} + + + \ No newline at end of file diff --git a/src/test/resources/config-replace.xml b/src/test/resources/config-replace.xml new file mode 100644 index 0000000000..c95276c335 --- /dev/null +++ b/src/test/resources/config-replace.xml @@ -0,0 +1,19 @@ + + + + The replaced global value + + child custom value + + + + + the replaced first value + new fourth value + + child two value + child three value + + + + \ No newline at end of file diff --git a/src/test/resources/config.xml b/src/test/resources/config.xml new file mode 100644 index 0000000000..116b828479 --- /dev/null +++ b/src/test/resources/config.xml @@ -0,0 +1,57 @@ + + + + The global value + false + + child one value + + + child one value + child two value + child three value + + + + + The value + true + + + + first value + second value + third value + + child one value + + + + + first value + second value + third value + + child one value + + + + + + child one value + child two value + + + grand child one value + grand child two value + + + child four value + + + + + A value + + + \ No newline at end of file diff --git a/src/test/resources/ibatis/hierarchy-test/hierarchy-test-SqlMapConfig.xml b/src/test/resources/ibatis/hierarchy-test/hierarchy-test-SqlMapConfig.xml new file mode 100644 index 0000000000..cd3ffda6a4 --- /dev/null +++ b/src/test/resources/ibatis/hierarchy-test/hierarchy-test-SqlMapConfig.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/test/resources/ibatis/hierarchy-test/hierarchy-test-context.xml b/src/test/resources/ibatis/hierarchy-test/hierarchy-test-context.xml new file mode 100644 index 0000000000..34ab7f88f2 --- /dev/null +++ b/src/test/resources/ibatis/hierarchy-test/hierarchy-test-context.xml @@ -0,0 +1,51 @@ + + + + + + + + false + + + SYSTEM_PROPERTIES_MODE_OVERRIDE + + + false + + + + + + java.util.AbstractCollection + + + ${hierarchy-test.dialect} + + + + + + + + + + classpath:ibatis/hierarchy-test/hierarchy-test-SqlMapConfig.xml + + + + + + + + + + java:comp/env/jdbc/dataSource + + + + + + + diff --git a/src/test/resources/ibatis/hierarchy-test/java.util.AbstractCollection/hierarchy-test-SqlMap.xml b/src/test/resources/ibatis/hierarchy-test/java.util.AbstractCollection/hierarchy-test-SqlMap.xml new file mode 100644 index 0000000000..e1895e60bc --- /dev/null +++ b/src/test/resources/ibatis/hierarchy-test/java.util.AbstractCollection/hierarchy-test-SqlMap.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/ibatis/hierarchy-test/java.util.AbstractList/hierarchy-test-SqlMap.xml b/src/test/resources/ibatis/hierarchy-test/java.util.AbstractList/hierarchy-test-SqlMap.xml new file mode 100644 index 0000000000..72698980ea --- /dev/null +++ b/src/test/resources/ibatis/hierarchy-test/java.util.AbstractList/hierarchy-test-SqlMap.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/ibatis/hierarchy-test/java.util.TreeSet/hierarchy-test-SqlMap.xml b/src/test/resources/ibatis/hierarchy-test/java.util.TreeSet/hierarchy-test-SqlMap.xml new file mode 100644 index 0000000000..b15273e2d8 --- /dev/null +++ b/src/test/resources/ibatis/hierarchy-test/java.util.TreeSet/hierarchy-test-SqlMap.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/org/alfresco/i18n/testMessages.properties b/src/test/resources/org/alfresco/i18n/testMessages.properties new file mode 100644 index 0000000000..6d54f72162 --- /dev/null +++ b/src/test/resources/org/alfresco/i18n/testMessages.properties @@ -0,0 +1,4 @@ +msg_yes=Yes +msg_no=No +msg_params=What no {0}? +msg_error=This is an error message. \n This is on a new line. \ No newline at end of file diff --git a/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties b/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties new file mode 100644 index 0000000000..7db02235fb --- /dev/null +++ b/src/test/resources/org/alfresco/i18n/testMessages_fr_FR.properties @@ -0,0 +1,4 @@ +msg_yes=Oui +msg_no=Non +msg_params=Que non {0}? +msg_error=C'est un message d'erreur. \n C'est sur une nouvelle ligne. \ No newline at end of file diff --git a/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml b/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml new file mode 100644 index 0000000000..e68189f6b1 --- /dev/null +++ b/src/test/resources/org/alfresco/util/exec/RuntimeExecBeansTest-context.xml @@ -0,0 +1,225 @@ + + + + + + + + + + + dir + + + + + ls + + + + + + + + value1 + + + null + + + ${env.prop3.unsubstituted} + + + + + . + + + true + + + 1, 2 + + + + + + + + + dir + SPLIT:${paths} + + + + + + + . .. + + + + 1, 2 + + + + + + + + + SPLIT: dir . .. + + + + + + 1, 2 + + + + + + + + + dir c: + + + ls / + + + + + true + + + 1 + + + + + + + + + cmd + /C + rmdir + ${dir} + + + + + rm + -rf + ${dir} + + + + + rm + -rf + ${dir} + + + + + wibble + + + + + + + dir RuntimeExecBootstrapBeanTest + + + + 1, 2 + + + + + + + + + + wibble + + + + + + 666 + + + + + + + + + + + + + + + + + + + + + + + + + + cmd + + + + + ls + + + + + + false + + + 1 + + + + + + + + + + dir + ${dir} + + + + + ls + ${dir} + + + + + + true + + + 1 + + + + diff --git a/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt b/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt new file mode 100644 index 0000000000..284f2bfe50 --- /dev/null +++ b/src/test/resources/resource-loader/java.util.AbstractCollection/file.txt @@ -0,0 +1 @@ +java.util.AbstractCollection \ No newline at end of file diff --git a/src/test/resources/resource-loader/java.util.AbstractList/file.txt b/src/test/resources/resource-loader/java.util.AbstractList/file.txt new file mode 100644 index 0000000000..f6ba039304 --- /dev/null +++ b/src/test/resources/resource-loader/java.util.AbstractList/file.txt @@ -0,0 +1 @@ +java.util.AbstractList \ No newline at end of file diff --git a/src/test/resources/resource-loader/java.util.TreeSet/file.txt b/src/test/resources/resource-loader/java.util.TreeSet/file.txt new file mode 100644 index 0000000000..8817feae36 --- /dev/null +++ b/src/test/resources/resource-loader/java.util.TreeSet/file.txt @@ -0,0 +1 @@ +java.util.TreeSet \ No newline at end of file diff --git a/src/test/resources/test-config-forms-basic-override.xml b/src/test/resources/test-config-forms-basic-override.xml new file mode 100644 index 0000000000..604074e48f --- /dev/null +++ b/src/test/resources/test-config-forms-basic-override.xml @@ -0,0 +1,99 @@ + + + + + + + 999 + + + Goodbye + + This is new + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + 500px + bar + + + + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + +
+
+
+ + + +
+ + + +
+
+
+ +
+ \ No newline at end of file diff --git a/src/test/resources/test-config-forms-basic.xml b/src/test/resources/test-config-forms-basic.xml new file mode 100644 index 0000000000..1127991ca9 --- /dev/null +++ b/src/test/resources/test-config-forms-basic.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + 50 + + + Hello + + + + + + 1 + Hello + For ever and ever. + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + 500px + + + + + + + + 10 + 500px + + + + + + + +
+ +
+
+
+
+ + + +
+ + + + + + + + + + + + + +
+
+ + + +
+ + + + +
+
+
+ + + +
+ + + +
+
+
+ + + +
+ + + +
+
+
+ + + +
+ + + + + + + + +
+
+
+ + + +
+ + + + + + + + + +
+
+ + +
diff --git a/src/test/resources/test-config-forms-negative.xml b/src/test/resources/test-config-forms-negative.xml new file mode 100644 index 0000000000..35abe68e9f --- /dev/null +++ b/src/test/resources/test-config-forms-negative.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/src/test/resources/test-config-forms.xml b/src/test/resources/test-config-forms.xml new file mode 100644 index 0000000000..6010638f2f --- /dev/null +++ b/src/test/resources/test-config-forms.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + 50 + + + + + 1 + Hello + Greetings + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + bar + + + + + + + + + + + + un + deux + + + quatre + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+ + + + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file

+ * This class is thread-safe in that it will detect multithreaded access and throw + * exceptions. Therefore do not use on multiple threads. Instances should be + * used only for the duration of the required user transaction and then discarded. + * Any attempt to reuse an instance will result in failure. + *