diff options
164 files changed, 15194 insertions, 435 deletions
diff --git a/archiva-base/archiva-common/pom.xml b/archiva-base/archiva-common/pom.xml index d520be144..ff8a9c5eb 100644 --- a/archiva-base/archiva-common/pom.xml +++ b/archiva-base/archiva-common/pom.xml @@ -30,7 +30,7 @@ <dependencies> <!-- TO OTHER DEVELOPERS: This module should depend on NO OTHER ARCHIVA MODULES. - If you feel tempted to add one, discuss it first in the + If you feel tempted to add one, discuss it first in the archiva-dev@maven.apache.org mailing-list. joakime@apache.org --> @@ -56,7 +56,23 @@ </dependency> <dependency> <groupId>org.codehaus.plexus</groupId> - <artifactId>plexus-container-default</artifactId> + <artifactId>plexus-slf4j-logging</artifactId> + </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-spring</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>xalan</groupId> + <artifactId>xalan</artifactId> + <version>2.7.0</version> + </dependency> + <dependency> + <groupId>dom4j</groupId> + <artifactId>dom4j</artifactId> + <version>1.6.1</version> + <scope>test</scope> </dependency> </dependencies> <build> diff --git a/archiva-base/archiva-configuration/src/main/java/org/apache/maven/archiva/configuration/DefaultArchivaConfiguration.java b/archiva-base/archiva-configuration/src/main/java/org/apache/maven/archiva/configuration/DefaultArchivaConfiguration.java index 4c9259a27..28ceeaa7b 100644 --- a/archiva-base/archiva-configuration/src/main/java/org/apache/maven/archiva/configuration/DefaultArchivaConfiguration.java +++ b/archiva-base/archiva-configuration/src/main/java/org/apache/maven/archiva/configuration/DefaultArchivaConfiguration.java @@ -102,11 +102,13 @@ public class DefaultArchivaConfiguration /** * @plexus.requirement role="org.apache.maven.archiva.policies.PreDownloadPolicy" + * @todo these don't strictly belong in here */ private Map<String, PreDownloadPolicy> prePolicies; /** * @plexus.requirement role="org.apache.maven.archiva.policies.PostDownloadPolicy" + * @todo these don't strictly belong in here */ private Map<String, PostDownloadPolicy> postPolicies; diff --git a/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/ArchivaConfigurationTest.java b/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/ArchivaConfigurationTest.java index ab6b80c38..d96965c98 100644 --- a/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/ArchivaConfigurationTest.java +++ b/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/ArchivaConfigurationTest.java @@ -19,24 +19,34 @@ package org.apache.maven.archiva.configuration; * under the License. */ +import java.io.File; +import java.util.List; +import java.util.Map; + import org.apache.commons.io.FileUtils; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.registry.RegistryException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.custommonkey.xmlunit.XMLAssert; import org.easymock.MockControl; -import java.io.File; -import java.util.List; -import java.util.Map; - /** * Test the configuration store. * * @author <a href="mailto:brett@apache.org">Brett Porter</a> */ public class ArchivaConfigurationTest - extends PlexusTestCase + extends PlexusInSpringTestCase { + /** + * {@inheritDoc} + * @see org.codehaus.plexus.spring.PlexusInSpringTestCase#getSpringConfigLocation() + */ + protected String getSpringConfigLocation() + throws Exception + { + return "org/apache/maven/archiva/configuration/spring-context.xml"; + } + public void testGetConfigurationFromRegistryWithASingleNamedConfigurationResource() throws Exception { @@ -474,7 +484,7 @@ public class ArchivaConfigurationTest (ArchivaConfiguration) lookup( ArchivaConfiguration.class.getName(), "test-not-allowed-to-write-to-user" ); Configuration config = archivaConfiguration.getConfiguration(); archivaConfiguration.save( config ); - // No Exception == test passes. + // No Exception == test passes. // Expected Path is: Should not have thrown an exception. } @@ -538,7 +548,7 @@ public class ArchivaConfigurationTest archivaConfiguration.save( configuration ); // Release existing - release( archivaConfiguration ); +// FIXME spring equivalent ? release( archivaConfiguration ); // Reload. archivaConfiguration = diff --git a/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/MavenProxyPropertyLoaderTest.java b/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/MavenProxyPropertyLoaderTest.java index db3049277..8bfc6e81d 100644 --- a/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/MavenProxyPropertyLoaderTest.java +++ b/archiva-base/archiva-configuration/src/test/java/org/apache/maven/archiva/configuration/MavenProxyPropertyLoaderTest.java @@ -19,22 +19,32 @@ package org.apache.maven.archiva.configuration; * under the License. */ -import org.codehaus.plexus.PlexusTestCase; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Map; import java.util.Properties; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; + /** * @author Edwin Punzalan */ public class MavenProxyPropertyLoaderTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private MavenProxyPropertyLoader loader; + /** + * {@inheritDoc} + * @see org.codehaus.plexus.spring.PlexusInSpringTestCase#getSpringConfigLocation() + */ + protected String getSpringConfigLocation() + throws Exception + { + return "org/apache/maven/archiva/configuration/spring-context.xml"; + } + public void testLoadValidMavenProxyConfiguration() throws IOException, InvalidConfigurationException { diff --git a/archiva-base/archiva-consumers/archiva-core-consumers/src/test/java/org/apache/maven/archiva/consumers/core/repository/AbstractRepositoryPurgeTest.java b/archiva-base/archiva-consumers/archiva-core-consumers/src/test/java/org/apache/maven/archiva/consumers/core/repository/AbstractRepositoryPurgeTest.java index 623853824..828aa74c4 100644 --- a/archiva-base/archiva-consumers/archiva-core-consumers/src/test/java/org/apache/maven/archiva/consumers/core/repository/AbstractRepositoryPurgeTest.java +++ b/archiva-base/archiva-consumers/archiva-core-consumers/src/test/java/org/apache/maven/archiva/consumers/core/repository/AbstractRepositoryPurgeTest.java @@ -25,9 +25,9 @@ import org.apache.maven.archiva.database.ArchivaDatabaseException; import org.apache.maven.archiva.database.ArtifactDAO; import org.apache.maven.archiva.model.ArchivaArtifact; import org.apache.maven.archiva.repository.ManagedRepositoryContent; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.jdo.DefaultConfigurableJdoFactory; import org.codehaus.plexus.jdo.JdoFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.jpox.SchemaTool; import java.io.File; @@ -46,7 +46,7 @@ import javax.jdo.PersistenceManagerFactory; * @author <a href="mailto:oching@apache.org">Maria Odea Ching</a> */ public abstract class AbstractRepositoryPurgeTest - extends PlexusTestCase + extends PlexusInSpringTestCase { public static final String TEST_REPO_ID = "test-repo"; diff --git a/archiva-base/archiva-consumers/archiva-database-consumers/src/test/java/org/apache/maven/archiva/consumers/database/AbstractDatabaseCleanupTest.java b/archiva-base/archiva-consumers/archiva-database-consumers/src/test/java/org/apache/maven/archiva/consumers/database/AbstractDatabaseCleanupTest.java index 098e8a0a6..51f9412bc 100644 --- a/archiva-base/archiva-consumers/archiva-database-consumers/src/test/java/org/apache/maven/archiva/consumers/database/AbstractDatabaseCleanupTest.java +++ b/archiva-base/archiva-consumers/archiva-database-consumers/src/test/java/org/apache/maven/archiva/consumers/database/AbstractDatabaseCleanupTest.java @@ -19,15 +19,15 @@ package org.apache.maven.archiva.consumers.database; * under the License. */ -import org.codehaus.plexus.PlexusTestCase; import org.apache.commons.io.FileUtils; import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; -import org.apache.maven.archiva.repository.RepositoryContentFactory; import org.apache.maven.archiva.model.ArchivaArtifact; import org.apache.maven.archiva.model.ArchivaArtifactModel; import org.apache.maven.archiva.model.ArchivaProjectModel; +import org.apache.maven.archiva.repository.RepositoryContentFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.io.File; @@ -35,7 +35,7 @@ import java.io.File; * @author <a href="mailto:oching@apache.org">Maria Odea Ching</a> */ public abstract class AbstractDatabaseCleanupTest - extends PlexusTestCase + extends PlexusInSpringTestCase { ArchivaConfiguration archivaConfig; diff --git a/archiva-base/archiva-consumers/archiva-lucene-consumers/src/test/java/org/apache/maven/archiva/consumers/lucene/LuceneCleanupRemoveIndexedConsumerTest.java b/archiva-base/archiva-consumers/archiva-lucene-consumers/src/test/java/org/apache/maven/archiva/consumers/lucene/LuceneCleanupRemoveIndexedConsumerTest.java index 64eeae2bd..22070f151 100644 --- a/archiva-base/archiva-consumers/archiva-lucene-consumers/src/test/java/org/apache/maven/archiva/consumers/lucene/LuceneCleanupRemoveIndexedConsumerTest.java +++ b/archiva-base/archiva-consumers/archiva-lucene-consumers/src/test/java/org/apache/maven/archiva/consumers/lucene/LuceneCleanupRemoveIndexedConsumerTest.java @@ -22,7 +22,7 @@ package org.apache.maven.archiva.consumers.lucene; import org.apache.maven.archiva.consumers.DatabaseCleanupConsumer; import org.apache.maven.archiva.model.ArchivaArtifact; import org.apache.maven.archiva.model.ArchivaArtifactModel; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; /** * LuceneCleanupRemoveIndexedConsumerTest @@ -31,7 +31,7 @@ import org.codehaus.plexus.PlexusTestCase; * @version */ public class LuceneCleanupRemoveIndexedConsumerTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private DatabaseCleanupConsumer luceneCleanupRemoveIndexConsumer; diff --git a/archiva-base/archiva-converter/src/test/java/org/apache/maven/archiva/converter/RepositoryConverterTest.java b/archiva-base/archiva-converter/src/test/java/org/apache/maven/archiva/converter/RepositoryConverterTest.java index 70a5ca1ff..4dbc2a434 100644 --- a/archiva-base/archiva-converter/src/test/java/org/apache/maven/archiva/converter/RepositoryConverterTest.java +++ b/archiva-base/archiva-converter/src/test/java/org/apache/maven/archiva/converter/RepositoryConverterTest.java @@ -26,8 +26,8 @@ import org.apache.maven.artifact.factory.ArtifactFactory; import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.repository.ArtifactRepositoryFactory; import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.i18n.I18N; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.io.File; import java.io.IOException; @@ -44,7 +44,7 @@ import java.util.List; * @todo group metadata */ public class RepositoryConverterTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private ArtifactRepository sourceRepository; diff --git a/archiva-base/archiva-dependency-graph/src/test/java/org/apache/maven/archiva/dependency/graph/AllTests.java b/archiva-base/archiva-dependency-graph/src/test/java/org/apache/maven/archiva/dependency/graph/AllTests.java index f99e786c9..24f12c12b 100644 --- a/archiva-base/archiva-dependency-graph/src/test/java/org/apache/maven/archiva/dependency/graph/AllTests.java +++ b/archiva-base/archiva-dependency-graph/src/test/java/org/apache/maven/archiva/dependency/graph/AllTests.java @@ -23,7 +23,7 @@ import junit.framework.Test; import junit.framework.TestSuite; /** - * Utility class to aide IDE developers. + * Utility class to aide IDE developers. * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ @@ -39,7 +39,7 @@ public class AllTests suite.addTestSuite( GraphvizDotTool.class ); suite.addTestSuite( DepManDeepVersionDependencyGraphTest.class ); suite.addTestSuite( SimpleDependencyGraphTest.class ); - suite.addTestSuite( MavenProjectInfoReportsPluginDependencyGraphTest.class ); +// suite.addTestSuite( MavenProjectInfoReportsPluginDependencyGraphTest.class ); suite.addTestSuite( ArchivaCommonDependencyGraphTest.class ); suite.addTestSuite( WagonManagerDependencyGraphTest.class ); suite.addTestSuite( ContinuumStoreDependencyGraphTest.class ); diff --git a/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaArtifactTest.java b/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaArtifactTest.java index 822f59833..80f53030f 100644 --- a/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaArtifactTest.java +++ b/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaArtifactTest.java @@ -22,26 +22,27 @@ package org.apache.maven.archiva.model; import java.util.Date; import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; /** - * ArchivaModelClonerTest + * ArchivaModelClonerTest * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id: ArchivaModelClonerTest.java 525951 2007-04-05 20:11:19Z joakime $ */ public class ArchivaArtifactTest - extends PlexusTestCase + extends PlexusInSpringTestCase { public void testArtifactModelProcessed() { ArchivaArtifactModel model = new ArchivaArtifactModel(); - + assertNull( "whenProcessed", model.getWhenProcessed() ); assertFalse( "isProcessed", model.isProcessed() ); - + model.setWhenProcessed( new Date() ); - + assertTrue( "isProcessed", model.isProcessed() ); } - + } diff --git a/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaModelClonerTest.java b/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaModelClonerTest.java index fc124f1a5..4f1af59da 100644 --- a/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaModelClonerTest.java +++ b/archiva-base/archiva-model/src/test/java/org/apache/maven/archiva/model/ArchivaModelClonerTest.java @@ -20,15 +20,16 @@ package org.apache.maven.archiva.model; */ import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; /** - * ArchivaModelClonerTest + * ArchivaModelClonerTest * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ */ public class ArchivaModelClonerTest - extends PlexusTestCase + extends PlexusInSpringTestCase { public void testCloneProjectModelWithParent() { diff --git a/archiva-base/archiva-policies/pom.xml b/archiva-base/archiva-policies/pom.xml index 4e3acf2b6..d8e061667 100644 --- a/archiva-base/archiva-policies/pom.xml +++ b/archiva-base/archiva-policies/pom.xml @@ -33,6 +33,11 @@ <artifactId>archiva-common</artifactId> </dependency> <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-spring</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> </dependency> diff --git a/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/CachedFailuresPolicy.java b/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/CachedFailuresPolicy.java index 9f4495dc9..77bd3b0ba 100644 --- a/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/CachedFailuresPolicy.java +++ b/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/CachedFailuresPolicy.java @@ -47,7 +47,7 @@ public class CachedFailuresPolicy * All resource requests are allowed thru to the remote repo. */ public static final String NO = "no"; - + /** * The YES policy setting means that the existence of old failures is checked, and will * prevent the request from being performed against the remote repo. @@ -55,7 +55,7 @@ public class CachedFailuresPolicy public static final String YES = "yes"; /** - * @plexus.requirement role-hint="default" + * @plexus.requirement */ private UrlFailureCache urlFailureCache; @@ -72,9 +72,9 @@ public class CachedFailuresPolicy { if ( !options.contains( policySetting ) ) { - // Not a valid code. - throw new PolicyConfigurationException( "Unknown cache-failues policy setting [" + policySetting - + "], valid settings are [" + StringUtils.join( options.iterator(), "," ) + "]" ); + // Not a valid code. + throw new PolicyConfigurationException( "Unknown cache-failues policy setting [" + policySetting + + "], valid settings are [" + StringUtils.join( options.iterator(), "," ) + "]" ); } if ( NO.equals( policySetting ) ) @@ -90,7 +90,8 @@ public class CachedFailuresPolicy { if ( urlFailureCache.hasFailedBefore( url ) ) { - throw new PolicyViolationException( "NO to fetch, check-failures detected previous failure on url: " + url ); + throw new PolicyViolationException( + "NO to fetch, check-failures detected previous failure on url: " + url ); } } diff --git a/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/urlcache/DefaultUrlFailureCache.java b/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/urlcache/DefaultUrlFailureCache.java index a520a151c..cb2c66e71 100644 --- a/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/urlcache/DefaultUrlFailureCache.java +++ b/archiva-base/archiva-policies/src/main/java/org/apache/maven/archiva/policies/urlcache/DefaultUrlFailureCache.java @@ -28,18 +28,20 @@ import java.util.Date; * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ - * - * @plexus.component role="org.apache.maven.archiva.policies.urlcache.UrlFailureCache" - * role-hint="default" */ public class DefaultUrlFailureCache implements UrlFailureCache { /** - * @plexus.requirement role-hint="url-failures-cache" + * @todo spring cache instead */ private Cache urlCache; + public DefaultUrlFailureCache( Cache urlCache ) + { + this.urlCache = urlCache; + } + public void cacheFailure( String url ) { urlCache.register( url, new Date() ); diff --git a/archiva-base/archiva-policies/src/main/resources/META-INF/spring-context.xml b/archiva-base/archiva-policies/src/main/resources/META-INF/spring-context.xml new file mode 100644 index 000000000..840223b1a --- /dev/null +++ b/archiva-base/archiva-policies/src/main/resources/META-INF/spring-context.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="urlFailureCache" class="org.apache.maven.archiva.policies.urlcache.DefaultUrlFailureCache"> + <!-- collaborators and configuration for this bean go here --> + <constructor-arg ref="cache#url-failures-cache" type="org.codehaus.plexus.cache.Cache"/> + </bean> + +</beans>
\ No newline at end of file diff --git a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/CachedFailuresPolicyTest.java b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/CachedFailuresPolicyTest.java index 61e89ab3e..c7083d93d 100644 --- a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/CachedFailuresPolicyTest.java +++ b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/CachedFailuresPolicyTest.java @@ -19,31 +19,25 @@ package org.apache.maven.archiva.policies; * under the License. */ -import org.apache.maven.archiva.policies.urlcache.UrlFailureCache; -import org.codehaus.plexus.PlexusTestCase; - import java.io.File; import java.util.Properties; +import org.apache.maven.archiva.policies.urlcache.UrlFailureCache; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; + /** - * CachedFailuresPolicyTest + * CachedFailuresPolicyTest * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ */ public class CachedFailuresPolicyTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private DownloadPolicy lookupPolicy() throws Exception { - return (DownloadPolicy) lookup( PreDownloadPolicy.class.getName(), "cache-failures" ); - } - - private UrlFailureCache lookupUrlFailureCache() - throws Exception - { - return (UrlFailureCache) lookup( UrlFailureCache.class.getName(), "default" ); + return (DownloadPolicy) lookup( PreDownloadPolicy.class, "cache-failures" ); } private File getFile() @@ -85,14 +79,13 @@ public class CachedFailuresPolicyTest public void testPolicyYesInCache() throws Exception { - UrlFailureCache urlFailureCache = lookupUrlFailureCache(); - DownloadPolicy policy = lookupPolicy(); File localFile = getFile(); Properties request = createRequest(); String url = "http://a.bad.hostname.maven.org/path/to/resource.txt"; + UrlFailureCache urlFailureCache = (UrlFailureCache) lookup( "urlFailureCache" ); urlFailureCache.cacheFailure( url ); request.setProperty( "url", url ); diff --git a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ChecksumPolicyTest.java b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ChecksumPolicyTest.java index 335a7a72e..81f140237 100644 --- a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ChecksumPolicyTest.java +++ b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ChecksumPolicyTest.java @@ -19,22 +19,22 @@ package org.apache.maven.archiva.policies; * under the License. */ -import org.apache.commons.io.FileUtils; -import org.codehaus.plexus.PlexusTestCase; - import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.Properties; +import org.apache.commons.io.FileUtils; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; + /** - * ChecksumPolicyTest + * ChecksumPolicyTest * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ */ public class ChecksumPolicyTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String GOOD = "good"; @@ -166,7 +166,7 @@ public class ChecksumPolicyTest Properties request = createRequest(); boolean actualResult; - + try { policy.applyPolicy( ChecksumPolicy.FAIL, request, localFile ); @@ -195,7 +195,7 @@ public class ChecksumPolicyTest Properties request = createRequest(); boolean actualResult; - + try { policy.applyPolicy( ChecksumPolicy.FIX, request, localFile ); @@ -205,7 +205,7 @@ public class ChecksumPolicyTest { actualResult = false; } - + assertEquals( createMessage( ChecksumPolicy.FIX, md5State, sha1State ), expectedResult, actualResult ); // End result should be legitimate SHA1 and MD5 files. diff --git a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ReleasePolicyTest.java b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ReleasePolicyTest.java index 747241ca2..044c7220e 100644 --- a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ReleasePolicyTest.java +++ b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/ReleasePolicyTest.java @@ -20,19 +20,19 @@ package org.apache.maven.archiva.policies; */ import org.apache.commons.io.FileUtils; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.io.File; import java.util.Properties; /** - * ReleasePolicyTest + * ReleasePolicyTest * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ */ public class ReleasePolicyTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String PATH_VERSION_METADATA = "org/apache/archiva/archiva-testable/1.0-SNAPSHOT/maven-metadata.xml"; diff --git a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/SnapshotsPolicyTest.java b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/SnapshotsPolicyTest.java index c57381a44..330db534b 100644 --- a/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/SnapshotsPolicyTest.java +++ b/archiva-base/archiva-policies/src/test/java/org/apache/maven/archiva/policies/SnapshotsPolicyTest.java @@ -19,12 +19,12 @@ package org.apache.maven.archiva.policies; * under the License. */ -import org.apache.commons.io.FileUtils; -import org.codehaus.plexus.PlexusTestCase; - import java.io.File; import java.util.Properties; +import org.apache.commons.io.FileUtils; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; + /** * SnapshotsPolicyTest * @@ -32,7 +32,7 @@ import java.util.Properties; * @version $Id$ */ public class SnapshotsPolicyTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String PATH_VERSION_METADATA = "org/apache/archiva/archiva-testable/1.0-SNAPSHOT/maven-metadata.xml"; diff --git a/archiva-base/archiva-policies/src/test/resources/log4j.xml b/archiva-base/archiva-policies/src/test/resources/log4j.xml index 901c99f33..9bb5e1e5a 100644 --- a/archiva-base/archiva-policies/src/test/resources/log4j.xml +++ b/archiva-base/archiva-policies/src/test/resources/log4j.xml @@ -39,6 +39,14 @@ <level value="error"/> </logger> + <logger name="org.codehaus.plexus.spring"> + <level value="warn"/> + </logger> + + <logger name="org.springframework"> + <level value="warn"/> + </logger> + <root> <priority value ="info" /> <appender-ref ref="console" /> diff --git a/archiva-base/archiva-proxy/pom.xml b/archiva-base/archiva-proxy/pom.xml index 2a12567e7..2dc32b1ae 100644 --- a/archiva-base/archiva-proxy/pom.xml +++ b/archiva-base/archiva-proxy/pom.xml @@ -29,6 +29,11 @@ <name>Archiva Base :: Proxy</name> <dependencies> <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-spring</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <dependency> <groupId>org.apache.maven.archiva</groupId> <artifactId>archiva-configuration</artifactId> </dependency> diff --git a/archiva-base/archiva-proxy/src/main/java/org/apache/maven/archiva/proxy/DefaultRepositoryProxyConnectors.java b/archiva-base/archiva-proxy/src/main/java/org/apache/maven/archiva/proxy/DefaultRepositoryProxyConnectors.java index 63bd89e7e..f0b867481 100644 --- a/archiva-base/archiva-proxy/src/main/java/org/apache/maven/archiva/proxy/DefaultRepositoryProxyConnectors.java +++ b/archiva-base/archiva-proxy/src/main/java/org/apache/maven/archiva/proxy/DefaultRepositoryProxyConnectors.java @@ -116,7 +116,7 @@ public class DefaultRepositoryProxyConnectors private Map<String, PostDownloadPolicy> postDownloadPolicies; /** - * @plexus.requirement role-hint="default" + * @plexus.requirement */ private UrlFailureCache urlFailureCache; @@ -526,10 +526,10 @@ public class DefaultRepositoryProxyConnectors log.info( emsg ); return null; } - + Wagon wagon = null; try - { + { RepositoryURL repoUrl = remoteRepository.getURL(); String protocol = repoUrl.getProtocol(); wagon = (Wagon) wagons.get( protocol ); @@ -618,7 +618,7 @@ public class DefaultRepositoryProxyConnectors { String url = remoteRepository.getURL().getUrl() + remotePath; - // Transfer checksum does not use the policy. + // Transfer checksum does not use the policy. if ( urlFailureCache.hasFailedBefore( url + type ) ) { return; @@ -833,7 +833,7 @@ public class DefaultRepositoryProxyConnectors //Convert seconds to milliseconds int timeoutInMilliseconds = remoteRepository.getRepository().getTimeout() * 1000; - + //Set timeout wagon.setTimeout(timeoutInMilliseconds); diff --git a/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/AbstractProxyTestCase.java b/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/AbstractProxyTestCase.java index 8527ade4f..c6a5f3e7f 100644 --- a/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/AbstractProxyTestCase.java +++ b/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/AbstractProxyTestCase.java @@ -19,6 +19,17 @@ package org.apache.maven.archiva.proxy; * under the License. */ +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Locale; + import org.apache.commons.io.FileUtils; import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; @@ -28,22 +39,12 @@ import org.apache.maven.archiva.policies.CachedFailuresPolicy; import org.apache.maven.archiva.policies.ChecksumPolicy; import org.apache.maven.archiva.policies.ReleasesPolicy; import org.apache.maven.archiva.policies.SnapshotsPolicy; -import org.apache.maven.archiva.policies.urlcache.UrlFailureCache; import org.apache.maven.archiva.repository.ManagedRepositoryContent; import org.apache.maven.wagon.Wagon; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusClassPathXmlApplicationContext; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; +import org.springframework.beans.factory.BeanFactory; /** * AbstractProxyTestCase @@ -52,7 +53,7 @@ import java.util.Locale; * @version $Id$ */ public abstract class AbstractProxyTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { protected static final String ID_LEGACY_PROXIED = "legacy-proxied"; @@ -259,14 +260,6 @@ public abstract class AbstractProxyTestCase return repoContent; } - protected UrlFailureCache lookupUrlFailureCache() - throws Exception - { - UrlFailureCache failurlCache = (UrlFailureCache) lookup( UrlFailureCache.class.getName(), "default" ); - assertNotNull( "URL Failure Cache cannot be null.", failurlCache ); - return failurlCache; - } - /** * Read the first line from the checksum file, and return it (trimmed). */ @@ -378,6 +371,17 @@ public abstract class AbstractProxyTestCase return repoLocation; } + /** + * {@inheritDoc} + * @see org.codehaus.plexus.spring.PlexusInSpringTestCase#getConfigLocation() + */ + @Override + protected String getSpringConfigLocation() + throws Exception + { + return "org/apache/maven/archiva/proxy/spring-context.xml"; + } + @Override protected void setUp() throws Exception @@ -493,7 +497,7 @@ public abstract class AbstractProxyTestCase { assertTrue( "Managed File should exist: ", managedFile.exists() ); assertTrue( "Remote File should exist: ", remoteFile.exists() ); - + managedFile.setLastModified( remoteFile.lastModified() + 55000 ); } @@ -501,13 +505,13 @@ public abstract class AbstractProxyTestCase { assertTrue( "Managed File should exist: ", managedFile.exists() ); assertTrue( "Remote File should exist: ", remoteFile.exists() ); - + managedFile.setLastModified( remoteFile.lastModified() - 55000 ); } protected void assertNotModified( File file, long expectedModificationTime ) { - assertEquals( "File <" + file.getAbsolutePath() + "> not have been modified.", + assertEquals( "File <" + file.getAbsolutePath() + "> not have been modified.", expectedModificationTime, file.lastModified() ); } @@ -516,11 +520,11 @@ public abstract class AbstractProxyTestCase { String managedLegacyPath = managedLegacyDir.getCanonicalPath(); String testFile = file.getCanonicalPath(); - + assertTrue( "Unit Test Failure: File <" + testFile + "> should be have been defined within the legacy managed path of <" + managedLegacyPath + ">", testFile .startsWith( managedLegacyPath ) ); - + assertFalse( "File < " + testFile + "> should not exist in managed legacy repository.", file.exists() ); } @@ -529,11 +533,11 @@ public abstract class AbstractProxyTestCase { String managedDefaultPath = managedDefaultDir.getCanonicalPath(); String testFile = file.getCanonicalPath(); - + assertTrue( "Unit Test Failure: File <" + testFile + "> should be have been defined within the managed default path of <" + managedDefaultPath + ">", testFile .startsWith( managedDefaultPath ) ); - + assertFalse( "File < " + testFile + "> should not exist in managed default repository.", file.exists() ); } diff --git a/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.java b/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.java index 73452adac..f2796727b 100644 --- a/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.java +++ b/archiva-base/archiva-proxy/src/test/java/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.java @@ -49,9 +49,9 @@ public class CacheFailuresTransferTest String path = "org/apache/maven/test/get-in-second-proxy/1.0/get-in-second-proxy-1.0.jar"; File expectedFile = new File( managedDefaultDir.getAbsoluteFile(), path ); setupTestableManagedRepository( path ); - + assertNotExistsInManagedDefaultRepo( expectedFile ); - + ArtifactReference artifact = managedDefaultRepository.toArtifactReference( path ); // Configure Repository (usually done within archiva.xml configuration) @@ -72,13 +72,13 @@ public class CacheFailuresTransferTest File downloadedFile = proxyHandler.fetchFromProxies( managedDefaultRepository, artifact ); wagonMockControl.verify(); - + // Second attempt to download same artifact use cache wagonMockControl.reset(); wagonMockControl.replay(); downloadedFile = proxyHandler.fetchFromProxies( managedDefaultRepository, artifact ); wagonMockControl.verify(); - + assertNotDownloaded( downloadedFile ); assertNoTempFiles( expectedFile ); } @@ -91,7 +91,7 @@ public class CacheFailuresTransferTest setupTestableManagedRepository( path ); assertNotExistsInManagedDefaultRepo( expectedFile ); - + ArtifactReference artifact = managedDefaultRepository.toArtifactReference( path ); // Configure Repository (usually done within archiva.xml configuration) @@ -118,11 +118,11 @@ public class CacheFailuresTransferTest wagonMock.get( path, new File( expectedFile.getParentFile(), expectedFile.getName() + ".tmp" ) ); wagonMockControl.setThrowable( new ResourceDoesNotExistException( "resource does not exist." ), 2 ); wagonMockControl.replay(); - + downloadedFile = proxyHandler.fetchFromProxies( managedDefaultRepository, artifact ); - + wagonMockControl.verify(); - + assertNotDownloaded( downloadedFile ); assertNoTempFiles( expectedFile ); } @@ -156,4 +156,12 @@ public class CacheFailuresTransferTest assertFileEquals( expectedFile, downloadedFile, proxied2File ); assertNoTempFiles( expectedFile ); } + + protected UrlFailureCache lookupUrlFailureCache() + throws Exception + { + UrlFailureCache urlFailureCache = (UrlFailureCache) lookup( "urlFailureCache" ); + assertNotNull( "URL Failure Cache cannot be null.", urlFailureCache ); + return urlFailureCache; + } } diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.xml index d6b58680e..848a73057 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/CacheFailuresTransferTest.xml @@ -60,9 +60,11 @@ <requirement> <role>org.apache.maven.archiva.repository.RepositoryContentFactory</role> <role-hint>mocked</role-hint> + <field-name>repositoryFactory</field-name> </requirement> <requirement> <role>org.apache.maven.archiva.repository.metadata.MetadataTools</role> + <field-name>metadataTools</field-name> </requirement> <requirement> <role>org.apache.maven.archiva.policies.PreDownloadPolicy</role> @@ -73,17 +75,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -103,12 +104,12 @@ <!-- 30 minutes = 1800 seconds --> <time-to-live-seconds>1800</time-to-live-seconds> </configuration> - </component> - + </component> + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ChecksumTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ChecksumTransferTest.xml index d678aaedc..9d90e2250 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ChecksumTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ChecksumTransferTest.xml @@ -73,17 +73,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -104,11 +103,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedDefaultTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedDefaultTransferTest.xml index d678aaedc..9d90e2250 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedDefaultTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedDefaultTransferTest.xml @@ -73,17 +73,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -104,11 +103,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedLegacyTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedLegacyTransferTest.xml index d678aaedc..9d90e2250 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedLegacyTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/ManagedLegacyTransferTest.xml @@ -73,17 +73,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -104,11 +103,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/MetadataTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/MetadataTransferTest.xml index d63f77c61..e7b75187c 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/MetadataTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/MetadataTransferTest.xml @@ -42,8 +42,8 @@ </requirement> </requirements> </component> - - + + <component> <role>org.apache.maven.archiva.repository.metadata.MetadataTools</role> <implementation>org.apache.maven.archiva.repository.metadata.MetadataTools</implementation> @@ -64,7 +64,7 @@ </requirement> </requirements> </component> - + <component> <role>org.apache.maven.archiva.proxy.RepositoryProxyConnectors</role> <role-hint>default</role-hint> @@ -96,17 +96,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -127,11 +126,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/RelocateTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/RelocateTransferTest.xml index d678aaedc..9d90e2250 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/RelocateTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/RelocateTransferTest.xml @@ -73,17 +73,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -104,11 +103,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/SnapshotTransferTest.xml b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/SnapshotTransferTest.xml index d678aaedc..9d90e2250 100644 --- a/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/SnapshotTransferTest.xml +++ b/archiva-base/archiva-proxy/src/test/resources/org/apache/maven/archiva/proxy/SnapshotTransferTest.xml @@ -73,17 +73,16 @@ <field-name>postDownloadPolicies</field-name> </requirement> <requirement> - <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> - <role-hint>default</role-hint> - <field-name>urlFailureCache</field-name> - </requirement> - <requirement> <role>org.apache.maven.archiva.repository.scanner.RepositoryContentConsumers</role> <field-name>consumers</field-name> </requirement> + <requirement> + <role>org.apache.maven.archiva.policies.urlcache.UrlFailureCache</role> + <field-name>urlFailureCache</field-name> + </requirement> </requirements> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> @@ -104,11 +103,11 @@ <time-to-live-seconds>1800</time-to-live-seconds> </configuration> </component> - + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> </components> -</component-set>
\ No newline at end of file +</component-set> diff --git a/archiva-base/archiva-repository-layer/pom.xml b/archiva-base/archiva-repository-layer/pom.xml index bc837346f..7877f44a6 100644 --- a/archiva-base/archiva-repository-layer/pom.xml +++ b/archiva-base/archiva-repository-layer/pom.xml @@ -29,6 +29,11 @@ <name>Archiva Repository Interface Layer</name> <dependencies> <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-spring</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <dependency> <groupId>org.apache.maven.archiva</groupId> <artifactId>archiva-configuration</artifactId> </dependency> diff --git a/archiva-base/archiva-repository-layer/src/test/java/org/apache/maven/archiva/repository/AbstractRepositoryLayerTestCase.java b/archiva-base/archiva-repository-layer/src/test/java/org/apache/maven/archiva/repository/AbstractRepositoryLayerTestCase.java index 3636e9dee..199e813d9 100644 --- a/archiva-base/archiva-repository-layer/src/test/java/org/apache/maven/archiva/repository/AbstractRepositoryLayerTestCase.java +++ b/archiva-base/archiva-repository-layer/src/test/java/org/apache/maven/archiva/repository/AbstractRepositoryLayerTestCase.java @@ -21,19 +21,30 @@ package org.apache.maven.archiva.repository; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.io.File; /** - * AbstractRepositoryLayerTestCase + * AbstractRepositoryLayerTestCase * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ */ public abstract class AbstractRepositoryLayerTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { + /** + * {@inheritDoc} + * @see org.codehaus.plexus.spring.PlexusInSpringTestCase#getSpringConfigLocation() + */ + @Override + protected String getSpringConfigLocation() + throws Exception + { + return "org/apache/maven/archiva/repository/spring-context.xml"; + } + protected ManagedRepositoryConfiguration createRepository( String id, String name, File location ) { ManagedRepositoryConfiguration repo = new ManagedRepositoryConfiguration(); @@ -51,7 +62,7 @@ public abstract class AbstractRepositoryLayerTestCase repo.setUrl( url ); return repo; } - + protected ManagedRepositoryContent createManagedRepositoryContent( String id, String name, File location, String layout ) throws Exception { diff --git a/archiva-database/src/test/java/org/apache/maven/archiva/database/AbstractArchivaDatabaseTestCase.java b/archiva-database/src/test/java/org/apache/maven/archiva/database/AbstractArchivaDatabaseTestCase.java index be87fc857..d40fed4d8 100644 --- a/archiva-database/src/test/java/org/apache/maven/archiva/database/AbstractArchivaDatabaseTestCase.java +++ b/archiva-database/src/test/java/org/apache/maven/archiva/database/AbstractArchivaDatabaseTestCase.java @@ -26,9 +26,9 @@ import org.apache.maven.archiva.database.updater.TestDatabaseCleanupConsumer; import org.apache.maven.archiva.database.updater.TestDatabaseUnprocessedConsumer; import org.apache.maven.archiva.model.ArtifactReference; import org.apache.maven.archiva.model.VersionedReference; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.jdo.DefaultConfigurableJdoFactory; import org.codehaus.plexus.jdo.JdoFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.jpox.SchemaTool; import java.io.File; @@ -49,7 +49,7 @@ import javax.jdo.PersistenceManagerFactory; * @version $Id$ */ public abstract class AbstractArchivaDatabaseTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String TIMESTAMP = "yyyy/MM/dd HH:mm:ss"; diff --git a/archiva-database/src/test/java/org/apache/maven/archiva/database/browsing/RepositoryBrowsingTest.java b/archiva-database/src/test/java/org/apache/maven/archiva/database/browsing/RepositoryBrowsingTest.java index 8f2e6bf27..3eb408b9c 100644 --- a/archiva-database/src/test/java/org/apache/maven/archiva/database/browsing/RepositoryBrowsingTest.java +++ b/archiva-database/src/test/java/org/apache/maven/archiva/database/browsing/RepositoryBrowsingTest.java @@ -61,7 +61,7 @@ public class RepositoryBrowsingTest public RepositoryBrowsing lookupBrowser() throws Exception { - RepositoryBrowsing browser = (RepositoryBrowsing) lookup( RepositoryBrowsing.class.getName() ); + RepositoryBrowsing browser = (RepositoryBrowsing) lookup( RepositoryBrowsing.class ); assertNotNull( "RepositoryBrowsing should not be null.", browser ); return browser; } diff --git a/archiva-database/src/test/java/org/apache/maven/archiva/database/updater/DatabaseConsumersTest.java b/archiva-database/src/test/java/org/apache/maven/archiva/database/updater/DatabaseConsumersTest.java index 18265657a..575774283 100644 --- a/archiva-database/src/test/java/org/apache/maven/archiva/database/updater/DatabaseConsumersTest.java +++ b/archiva-database/src/test/java/org/apache/maven/archiva/database/updater/DatabaseConsumersTest.java @@ -20,7 +20,7 @@ package org.apache.maven.archiva.database.updater; */ import org.apache.commons.collections.CollectionUtils; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.util.List; @@ -31,7 +31,7 @@ import java.util.List; * @version $Id$ */ public class DatabaseConsumersTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private DatabaseConsumers lookupDbConsumers() throws Exception diff --git a/archiva-reporting/archiva-artifact-reports/src/test/java/org/apache/maven/archiva/reporting/artifact/AbstractArtifactReportsTestCase.java b/archiva-reporting/archiva-artifact-reports/src/test/java/org/apache/maven/archiva/reporting/artifact/AbstractArtifactReportsTestCase.java index 40630faa4..577cdba68 100644 --- a/archiva-reporting/archiva-artifact-reports/src/test/java/org/apache/maven/archiva/reporting/artifact/AbstractArtifactReportsTestCase.java +++ b/archiva-reporting/archiva-artifact-reports/src/test/java/org/apache/maven/archiva/reporting/artifact/AbstractArtifactReportsTestCase.java @@ -20,9 +20,9 @@ package org.apache.maven.archiva.reporting.artifact; */ import org.apache.maven.archiva.database.ArchivaDAO; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.jdo.DefaultConfigurableJdoFactory; import org.codehaus.plexus.jdo.JdoFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.jpox.SchemaTool; import java.io.File; @@ -41,7 +41,7 @@ import javax.jdo.PersistenceManagerFactory; * @version $Id$ */ public abstract class AbstractArtifactReportsTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { protected ArchivaDAO dao; diff --git a/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaDatabaseUpdateTaskExecutorTest.java b/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaDatabaseUpdateTaskExecutorTest.java index ebdb592c3..94f5d34bb 100644 --- a/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaDatabaseUpdateTaskExecutorTest.java +++ b/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaDatabaseUpdateTaskExecutorTest.java @@ -26,9 +26,9 @@ import org.apache.maven.archiva.database.ArtifactDAO; import org.apache.maven.archiva.database.constraints.ArtifactsProcessedConstraint; import org.apache.maven.archiva.model.ArchivaArtifact; import org.apache.maven.archiva.scheduled.tasks.DatabaseTask; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.jdo.DefaultConfigurableJdoFactory; import org.codehaus.plexus.jdo.JdoFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.codehaus.plexus.taskqueue.execution.TaskExecutor; import org.jpox.SchemaTool; @@ -51,7 +51,7 @@ import javax.jdo.PersistenceManagerFactory; * @version $Id:$ */ public class ArchivaDatabaseUpdateTaskExecutorTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private TaskExecutor taskExecutor; diff --git a/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaRepositoryScanningTaskExecutorTest.java b/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaRepositoryScanningTaskExecutorTest.java index 8f935289f..4570ea7cf 100644 --- a/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaRepositoryScanningTaskExecutorTest.java +++ b/archiva-scheduled/src/test/java/org/apache/maven/archiva/scheduled/executors/ArchivaRepositoryScanningTaskExecutorTest.java @@ -26,9 +26,9 @@ import org.apache.maven.archiva.database.ArchivaDAO; import org.apache.maven.archiva.database.ArtifactDAO; import org.apache.maven.archiva.database.constraints.ArtifactsProcessedConstraint; import org.apache.maven.archiva.scheduled.tasks.RepositoryTask; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.jdo.DefaultConfigurableJdoFactory; import org.codehaus.plexus.jdo.JdoFactory; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.codehaus.plexus.taskqueue.execution.TaskExecutor; import org.jpox.SchemaTool; @@ -49,7 +49,7 @@ import javax.jdo.PersistenceManagerFactory; * @version $Id$ */ public class ArchivaRepositoryScanningTaskExecutorTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private TaskExecutor taskExecutor; diff --git a/archiva-web/archiva-security/src/test/java/org/apache/maven/archiva/security/SecurityStartupTest.java b/archiva-web/archiva-security/src/test/java/org/apache/maven/archiva/security/SecurityStartupTest.java index f3924f934..7096133cc 100644 --- a/archiva-web/archiva-security/src/test/java/org/apache/maven/archiva/security/SecurityStartupTest.java +++ b/archiva-web/archiva-security/src/test/java/org/apache/maven/archiva/security/SecurityStartupTest.java @@ -19,7 +19,7 @@ package org.apache.maven.archiva.security; * under the License. */ -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; /** * SecurityStartupTest @@ -28,7 +28,7 @@ import org.codehaus.plexus.PlexusTestCase; * @version $Id$ */ public class SecurityStartupTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private SecurityStartup secStart; diff --git a/archiva-web/archiva-webapp/pom.xml b/archiva-web/archiva-webapp/pom.xml index be1612e5e..38afffcb9 100644 --- a/archiva-web/archiva-webapp/pom.xml +++ b/archiva-web/archiva-webapp/pom.xml @@ -181,14 +181,19 @@ </dependency> <!-- Other dependencies --> <dependency> - <groupId>org.codehaus.plexus.webdav</groupId> - <artifactId>plexus-webdav-simple</artifactId> + <groupId>org.apache.maven.archiva</groupId> + <artifactId>archiva-webdav</artifactId> </dependency> <dependency> <groupId>org.codehaus.plexus</groupId> <artifactId>plexus-xwork-integration</artifactId> </dependency> <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-web</artifactId> + <version>2.5.1</version> + </dependency> + <dependency> <groupId>org.apache.derby</groupId> <artifactId>derby</artifactId> <scope>provided</scope> @@ -221,6 +226,14 @@ <artifactId>xmlunit</artifactId> </dependency> <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-quartz</artifactId> + </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-taskqueue</artifactId> + </dependency> + <dependency> <groupId>org.codehaus.plexus.redback</groupId> <artifactId>redback-keys-memory</artifactId> <version>${redback.version}</version> diff --git a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoader.java b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoader.java index 2a821848b..b289188ed 100644 --- a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoader.java +++ b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoader.java @@ -19,9 +19,9 @@ package org.apache.maven.archiva.web.repository; * under the License. */ +import org.apache.maven.archiva.webdav.util.MimeTypes; import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; -import org.codehaus.plexus.webdav.util.MimeTypes; import java.io.IOException; import java.net.URL; diff --git a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ProxiedDavServer.java b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ProxiedDavServer.java index 027f58f54..bafea90af 100644 --- a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ProxiedDavServer.java +++ b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/ProxiedDavServer.java @@ -19,18 +19,6 @@ package org.apache.maven.archiva.web.repository; * under the License. */ -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletResponse; - import org.apache.maven.archiva.common.utils.PathUtil; import org.apache.maven.archiva.model.ArtifactReference; import org.apache.maven.archiva.model.ProjectReference; @@ -49,24 +37,35 @@ import org.apache.maven.archiva.repository.layout.LayoutException; import org.apache.maven.archiva.repository.metadata.MetadataTools; import org.apache.maven.archiva.repository.metadata.RepositoryMetadataException; import org.apache.maven.archiva.security.ArchivaUser; +import org.apache.maven.archiva.webdav.AbstractDavServerComponent; +import org.apache.maven.archiva.webdav.DavServerComponent; +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.DavServerListener; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.util.WebdavMethodUtil; import org.apache.maven.model.DistributionManagement; import org.apache.maven.model.Model; import org.apache.maven.model.Relocation; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -import org.codehaus.plexus.webdav.AbstractDavServerComponent; -import org.codehaus.plexus.webdav.DavServerComponent; -import org.codehaus.plexus.webdav.DavServerException; -import org.codehaus.plexus.webdav.DavServerListener; -import org.codehaus.plexus.webdav.servlet.DavServerRequest; -import org.codehaus.plexus.webdav.util.WebdavMethodUtil; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; /** * ProxiedDavServer * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ - * @plexus.component role="org.codehaus.plexus.webdav.DavServerComponent" + * @plexus.component role="org.apache.maven.archiva.webdav.DavServerComponent" * role-hint="proxied" instantiation-strategy="per-lookup" */ public class ProxiedDavServer diff --git a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/RepositoryServlet.java b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/RepositoryServlet.java index f0fed7486..c3121a2cb 100644 --- a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/RepositoryServlet.java +++ b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/repository/RepositoryServlet.java @@ -24,6 +24,11 @@ import org.apache.maven.archiva.configuration.ConfigurationEvent; import org.apache.maven.archiva.configuration.ConfigurationListener; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.security.ArchivaRoleConstants; +import org.apache.maven.archiva.webdav.DavServerComponent; +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.servlet.multiplexed.MultiplexedWebDavServlet; +import org.apache.maven.archiva.webdav.util.WebdavMethodUtil; import org.codehaus.plexus.redback.authentication.AuthenticationException; import org.codehaus.plexus.redback.authentication.AuthenticationResult; import org.codehaus.plexus.redback.authorization.AuthorizationException; @@ -33,21 +38,17 @@ import org.codehaus.plexus.redback.policy.MustChangePasswordException; import org.codehaus.plexus.redback.system.SecuritySession; import org.codehaus.plexus.redback.system.SecuritySystem; import org.codehaus.plexus.redback.xwork.filter.authentication.HttpAuthenticator; -import org.codehaus.plexus.webdav.DavServerComponent; -import org.codehaus.plexus.webdav.DavServerException; -import org.codehaus.plexus.webdav.DavServerManager; -import org.codehaus.plexus.webdav.servlet.DavServerRequest; -import org.codehaus.plexus.webdav.servlet.multiplexed.MultiplexedWebDavServlet; -import org.codehaus.plexus.webdav.util.WebdavMethodUtil; - -import java.io.File; -import java.io.IOException; -import java.util.Map; +import org.codehaus.plexus.spring.PlexusToSpringUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.Map; /** * RepositoryServlet @@ -69,25 +70,24 @@ public class RepositoryServlet private ArchivaMimeTypeLoader mimeTypeLoader; - public synchronized void initComponents() - throws ServletException + public synchronized void initServers( ServletConfig servletConfig ) + throws DavServerException { - super.initComponents(); - - mimeTypeLoader = (ArchivaMimeTypeLoader) lookup( ArchivaMimeTypeLoader.class.getName() ); - - securitySystem = (SecuritySystem) lookup( SecuritySystem.ROLE ); - httpAuth = (HttpAuthenticator) lookup( HttpAuthenticator.ROLE, "basic" ); - - configuration = (ArchivaConfiguration) lookup( ArchivaConfiguration.class.getName() ); + WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext( servletConfig.getServletContext() ); + + mimeTypeLoader = (ArchivaMimeTypeLoader) wac.getBean( + PlexusToSpringUtils.buildSpringId( ArchivaMimeTypeLoader.class.getName() ) ); + + securitySystem = (SecuritySystem) wac.getBean( PlexusToSpringUtils.buildSpringId( SecuritySystem.ROLE ) ); + httpAuth = + (HttpAuthenticator) wac.getBean( PlexusToSpringUtils.buildSpringId( HttpAuthenticator.ROLE, "basic" ) ); + + configuration = (ArchivaConfiguration) wac.getBean( + PlexusToSpringUtils.buildSpringId( ArchivaConfiguration.class.getName() ) ); configuration.addListener( this ); repositoryMap = configuration.getConfiguration().getManagedRepositoriesAsMap(); - } - public synchronized void initServers( ServletConfig servletConfig ) - throws DavServerException - { for ( ManagedRepositoryConfiguration repo : repositoryMap.values() ) { File repoDir = new File( repo.getLocation() ); @@ -109,45 +109,6 @@ public class RepositoryServlet } @Override - public void destroy() - { - try - { - release( securitySystem ); - } - catch ( ServletException e ) - { - log( "Unable to release SecuritySystem : " + e.getMessage(), e ); - } - try - { - release( httpAuth ); - } - catch ( ServletException e ) - { - log( "Unable to release HttpAuth : " + e.getMessage(), e ); - } - try - { - release( configuration ); - } - catch ( ServletException e ) - { - log( "Unable to release ArchivaConfiguration : " + e.getMessage(), e ); - } - try - { - release( mimeTypeLoader ); - } - catch ( ServletException e ) - { - log( "Unable to release ArchivaMimeTypeLoader : " + e.getMessage(), e ); - } - - super.destroy(); - } - - @Override protected void service( HttpServletRequest httpRequest, HttpServletResponse httpResponse ) throws ServletException, IOException { @@ -271,14 +232,12 @@ public class RepositoryServlet repositoryMap.clear(); repositoryMap.putAll( configuration.getConfiguration().getManagedRepositoriesAsMap() ); } - - DavServerManager davManager = getDavManager(); - + synchronized ( davManager ) { // Clear out the old servers. davManager.removeAllServers(); - + // Create new servers. try { @@ -290,4 +249,9 @@ public class RepositoryServlet } } } + + ArchivaConfiguration getConfiguration() + { + return configuration; + } } diff --git a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/ArchivaStartup.java b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/ArchivaStartup.java index 2dab098d9..82113ed76 100644 --- a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/ArchivaStartup.java +++ b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/ArchivaStartup.java @@ -19,55 +19,53 @@ package org.apache.maven.archiva.web.startup; * under the License. */ +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; + import org.apache.maven.archiva.common.ArchivaException; import org.apache.maven.archiva.scheduled.ArchivaTaskScheduler; import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; +import org.codehaus.plexus.spring.PlexusToSpringUtils; +import org.codehaus.plexus.taskqueue.execution.TaskQueueExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; /** * ArchivaStartup - the startup of all archiva features in a deterministic order. * * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> * @version $Id$ - * - * @plexus.component - * role="org.apache.maven.archiva.web.startup.ArchivaStartup" - * role-hint="default" */ public class ArchivaStartup - implements Initializable + implements ServletContextListener { - /** - * @plexus.requirement role-hint="default" - */ - private SecuritySynchronization securitySync; - - /** - * @plexus.requirement role-hint="default" - */ - private ResolverFactoryInit resolverFactory; - - /** - * @plexus.requirement role-hint="default" - */ - private ArchivaTaskScheduler taskScheduler; + public void contextDestroyed(ServletContextEvent arg0) { + } - public void initialize() - throws InitializationException - { - Banner.display( ArchivaVersion.determineVersion( this.getClass().getClassLoader() ) ); + public void contextInitialized(ServletContextEvent arg0) { + WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(arg0.getServletContext()); + + SecuritySynchronization securitySync = (SecuritySynchronization) wac.getBean(PlexusToSpringUtils.buildSpringId(SecuritySynchronization.class)); + ResolverFactoryInit resolverFactory = (ResolverFactoryInit) wac.getBean(PlexusToSpringUtils.buildSpringId(ResolverFactoryInit.class)); + ArchivaTaskScheduler taskScheduler = (ArchivaTaskScheduler) wac.getBean(PlexusToSpringUtils.buildSpringId(ArchivaTaskScheduler.class)); + TaskQueueExecutor databaseUpdateQueue = (TaskQueueExecutor) wac.getBean(PlexusToSpringUtils.buildSpringId(TaskQueueExecutor.class, "database-update")); + TaskQueueExecutor repositoryScanningQueue = (TaskQueueExecutor) wac.getBean(PlexusToSpringUtils.buildSpringId(TaskQueueExecutor.class, "repository-scanning")); + Banner banner = (Banner) wac.getBean(PlexusToSpringUtils.buildSpringId(Banner.class)); try { securitySync.startup(); resolverFactory.startup(); taskScheduler.startup(); + banner.display(); } catch ( ArchivaException e ) { - throw new InitializationException( "Unable to properly startup archiva: " + e.getMessage(), e ); + throw new RuntimeException( "Unable to properly startup archiva: " + e.getMessage(), e ); } } diff --git a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/Banner.java b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/Banner.java index bb7fcf87e..76a5223ee 100644 --- a/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/Banner.java +++ b/archiva-web/archiva-webapp/src/main/java/org/apache/maven/archiva/web/startup/Banner.java @@ -222,15 +222,9 @@ public class Banner return injectVersion( decode( encodedBanner ), version ); } - public static void display( String version ) + public void display() { - String banner = getBanner( version ); + String banner = getBanner( ArchivaVersion.determineVersion( this.getClass().getClassLoader() ) ); LoggerFactory.getLogger( Banner.class ).info( StringUtils.repeat( "_", 25 ) + "\n" + banner ); } - - public void initialize() - throws InitializationException - { - Banner.display( ArchivaVersion.determineVersion( this.getClass().getClassLoader() ) ); - } } diff --git a/archiva-web/archiva-webapp/src/main/resources/META-INF/plexus/application.xml b/archiva-web/archiva-webapp/src/main/resources/META-INF/plexus/application.xml index aed45580b..8d42dcfd2 100644 --- a/archiva-web/archiva-webapp/src/main/resources/META-INF/plexus/application.xml +++ b/archiva-web/archiva-webapp/src/main/resources/META-INF/plexus/application.xml @@ -19,21 +19,6 @@ --> <plexus> - <load-on-start> - <component> - <role>org.apache.maven.archiva.web.startup.ArchivaStartup</role> - <role-hint>default</role-hint> - </component> - <component> - <role>org.codehaus.plexus.taskqueue.execution.TaskQueueExecutor</role> - <role-hint>database-update</role-hint> - </component> - <component> - <role>org.codehaus.plexus.taskqueue.execution.TaskQueueExecutor</role> - <role-hint>repository-scanning</role-hint> - </component> - </load-on-start> - <components> <component> <role>org.codehaus.plexus.registry.Registry</role> @@ -82,13 +67,16 @@ </component> <component> - <role>org.codehaus.plexus.webdav.DavServerManager</role> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> <role-hint>default</role-hint> - <implementation>org.codehaus.plexus.webdav.DefaultDavServerManager</implementation> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> <description>DefaultDavServerManager</description> - <configuration> - <provider-hint>proxied</provider-hint> - </configuration> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>proxied</role-hint> + </requirement> + </requirements> </component> <component> @@ -200,8 +188,8 @@ </component> <component> - <role>org.codehaus.plexus.webdav.util.MimeTypes</role> - <implementation>org.codehaus.plexus.webdav.util.MimeTypes</implementation> + <role>org.apache.maven.archiva.webdav.util.MimeTypes</role> + <implementation>org.apache.maven.archiva.webdav.util.MimeTypes</implementation> <description>MimeTypes</description> <configuration> <resource>archiva-mime-types.txt</resource> diff --git a/archiva-web/archiva-webapp/src/main/resources/webwork.properties b/archiva-web/archiva-webapp/src/main/resources/webwork.properties index 02b991ff9..a73c02d3c 100644 --- a/archiva-web/archiva-webapp/src/main/resources/webwork.properties +++ b/archiva-web/archiva-webapp/src/main/resources/webwork.properties @@ -19,7 +19,7 @@ # define our own action mapper here webwork.mapper.class = org.apache.maven.archiva.web.mapper.RepositoryActionMapper -webwork.objectFactory = org.codehaus.plexus.xwork.PlexusObjectFactory +webwork.objectFactory = org.codehaus.plexus.spring.WebWorkPlexusInSpringObjectFactory webwork.url.includeParams = none webwork.devMode = true diff --git a/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/applicationContext.xml b/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/applicationContext.xml new file mode 100644 index 000000000..f6b0fa81d --- /dev/null +++ b/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/applicationContext.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + <bean id="loggerManager" class="org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager" + init-method="initialize"/> + + <bean id="urlFailureCache" class="org.apache.maven.archiva.policies.urlcache.DefaultUrlFailureCache"> + <constructor-arg ref="cache#url-failures-cache" type="org.codehaus.plexus.cache.Cache"/> + </bean> + +</beans> diff --git a/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/web.xml b/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/web.xml index f47d6ebc1..82d730be3 100644 --- a/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/web.xml +++ b/archiva-web/archiva-webapp/src/main/webapp/WEB-INF/web.xml @@ -56,8 +56,28 @@ </filter-mapping> <listener> - <listener-class>org.codehaus.plexus.xwork.PlexusLifecycleListener</listener-class> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> + <listener> + <!-- TODO: some Spring technique for this? --> + <listener-class>org.apache.maven.archiva.web.startup.ArchivaStartup</listener-class> + </listener> + + <context-param> + <param-name>contextClass</param-name> + <param-value>org.codehaus.plexus.spring.PlexusWebApplicationContext</param-value> + </context-param> + + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value> + classpath*:/META-INF/plexus/components.xml + classpath*:/META-INF/spring-context.xml + /WEB-INF/classes/META-INF/plexus/application.xml + /WEB-INF/classes/META-INF/plexus/components.xml + /WEB-INF/applicationContext.xml + </param-value> + </context-param> <servlet> <servlet-name>RepositoryServlet</servlet-name> @@ -72,22 +92,22 @@ </servlet-mapping> <resource-ref> - <res-ref-name>jdbc/users</res-ref-name> - <res-type>javax.sql.DataSource</res-type> - <res-auth>Container</res-auth> - <res-sharing-scope>Shareable</res-sharing-scope> + <res-ref-name>jdbc/users</res-ref-name> + <res-type>javax.sql.DataSource</res-type> + <res-auth>Container</res-auth> + <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> <resource-ref> - <res-ref-name>jdbc/archiva</res-ref-name> - <res-type>javax.sql.DataSource</res-type> - <res-auth>Container</res-auth> - <res-sharing-scope>Shareable</res-sharing-scope> + <res-ref-name>jdbc/archiva</res-ref-name> + <res-type>javax.sql.DataSource</res-type> + <res-auth>Container</res-auth> + <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> <resource-ref> - <res-ref-name>mail/Session</res-ref-name> - <res-type>javax.mail.Session</res-type> - <res-auth>Container</res-auth> - <res-sharing-scope>Shareable</res-sharing-scope> + <res-ref-name>mail/Session</res-ref-name> + <res-type>javax.mail.Session</res-type> + <res-auth>Container</res-auth> + <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> </web-app> diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/AbstractWebworkTestCase.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/AbstractWebworkTestCase.java index bfbd6ddbe..35f526e4f 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/AbstractWebworkTestCase.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/AbstractWebworkTestCase.java @@ -20,9 +20,8 @@ package org.apache.maven.archiva.web.action; */ import com.opensymphony.xwork.ActionSupport; - import org.apache.commons.lang.StringUtils; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import java.lang.reflect.Method; import java.util.Collection; @@ -35,7 +34,7 @@ import java.util.List; * @version $Id$ */ public abstract class AbstractWebworkTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { /** * This is a conveinence method for mimicking how the webwork interceptors diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddManagedRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddManagedRepositoryActionTest.java index 8a870cc5c..ed0501636 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddManagedRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddManagedRepositoryActionTest.java @@ -20,16 +20,15 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.commons.io.FileUtils; import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.security.ArchivaRoleConstants; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.role.RoleManager; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.io.File; @@ -42,7 +41,7 @@ import java.util.Collections; * @version $Id$ */ public class AddManagedRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private AddManagedRepositoryAction action; @@ -59,7 +58,7 @@ public class AddManagedRepositoryActionTest private File location; @Override - protected String getCustomConfigurationName() + protected String getPlexusConfigLocation() { return AbstractManagedRepositoriesAction.class.getName().replace( '.', '/' ) + "Test.xml"; } diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddRemoteRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddRemoteRepositoryActionTest.java index c7beec7ca..5089b07a3 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddRemoteRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/AddRemoteRepositoryActionTest.java @@ -20,13 +20,12 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.util.Collections; @@ -38,7 +37,7 @@ import java.util.Collections; * @version $Id$ */ public class AddRemoteRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private AddRemoteRepositoryAction action; diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.java index c0582812c..f9e205f4c 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.java @@ -20,24 +20,20 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.IndeterminateConfigurationException; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; - -import org.apache.maven.archiva.model.ArchivaProjectModel; - import org.apache.maven.archiva.configuration.ProxyConnectorConfiguration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; - +import org.apache.maven.archiva.model.ArchivaProjectModel; import org.apache.maven.archiva.security.ArchivaRoleConstants; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.role.RoleManager; import org.codehaus.plexus.redback.role.RoleManagerException; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; import org.codehaus.plexus.registry.RegistryException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.io.File; @@ -50,7 +46,7 @@ import java.util.Collections; * @version $Id$ */ public class DeleteManagedRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private DeleteManagedRepositoryAction action; @@ -67,7 +63,7 @@ public class DeleteManagedRepositoryActionTest private File location; @Override - protected String getCustomConfigurationName() + protected String getPlexusConfigLocation() { return AbstractManagedRepositoriesAction.class.getName().replace( '.', '/' ) + "Test.xml"; } diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteRemoteRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteRemoteRepositoryActionTest.java index a6b5b7eaa..1457f5952 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteRemoteRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/DeleteRemoteRepositoryActionTest.java @@ -20,15 +20,14 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.IndeterminateConfigurationException; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.configuration.ProxyConnectorConfiguration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.registry.RegistryException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.util.Collections; @@ -40,7 +39,7 @@ import java.util.Collections; * @version $Id$ */ public class DeleteRemoteRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String REPO_ID = "remote-repo-ident"; diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditManagedRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditManagedRepositoryActionTest.java index 407ffd710..c109a8eaa 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditManagedRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditManagedRepositoryActionTest.java @@ -20,15 +20,14 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.security.ArchivaRoleConstants; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.role.RoleManager; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.io.File; @@ -41,7 +40,7 @@ import java.util.Collections; * @version $Id$ */ public class EditManagedRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private EditManagedRepositoryAction action; @@ -58,7 +57,7 @@ public class EditManagedRepositoryActionTest private File location; @Override - protected String getCustomConfigurationName() + protected String getPlexusConfigLocation() { return AbstractManagedRepositoriesAction.class.getName().replace( '.', '/' ) + "Test.xml"; } diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditRemoteRepositoryActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditRemoteRepositoryActionTest.java index cd1a9018d..eb8a92920 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditRemoteRepositoryActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/EditRemoteRepositoryActionTest.java @@ -20,13 +20,12 @@ package org.apache.maven.archiva.web.action.admin.repositories; */ import com.opensymphony.xwork.Action; - import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; import org.easymock.MockControl; import java.util.Collections; @@ -38,7 +37,7 @@ import java.util.Collections; * @version $Id$ */ public class EditRemoteRepositoryActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private static final String REPO_ID = "remote-repo-ident"; diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/RepositoriesActionTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/RepositoriesActionTest.java index 375a142d0..1b372a1da 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/RepositoriesActionTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/action/admin/repositories/RepositoriesActionTest.java @@ -22,15 +22,15 @@ package org.apache.maven.archiva.web.action.admin.repositories; import com.meterware.servletunit.ServletRunner; import com.meterware.servletunit.ServletUnitClient; import com.opensymphony.xwork.Action; -import org.codehaus.plexus.PlexusTestCase; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionBundle; import org.codehaus.plexus.redback.xwork.interceptor.SecureActionException; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; /** * Test the repositories action returns the correct data. */ public class RepositoriesActionTest - extends PlexusTestCase + extends PlexusInSpringTestCase { private RepositoriesAction action; diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/AbstractRepositoryServletTestCase.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/AbstractRepositoryServletTestCase.java index 7b79e7978..bfd363edc 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/AbstractRepositoryServletTestCase.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/AbstractRepositoryServletTestCase.java @@ -22,22 +22,18 @@ package org.apache.maven.archiva.web.repository; import com.meterware.httpunit.WebResponse; import com.meterware.servletunit.ServletRunner; import com.meterware.servletunit.ServletUnitClient; - +import net.sf.ehcache.CacheManager; import org.apache.commons.io.FileUtils; import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; import org.apache.maven.archiva.configuration.RemoteRepositoryConfiguration; -import org.codehaus.plexus.PlexusConstants; -import org.codehaus.plexus.PlexusTestCase; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; +import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; -import javax.servlet.ServletContext; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - /** * AbstractRepositoryServletTestCase * @@ -45,18 +41,24 @@ import javax.servlet.http.HttpSession; * @version $Id$ */ public abstract class AbstractRepositoryServletTestCase - extends PlexusTestCase + extends PlexusInSpringTestCase { protected static final String REPOID_INTERNAL = "internal"; protected ServletUnitClient sc; - protected ArchivaConfiguration archivaConfiguration; - protected File repoRootInternal; private ServletRunner sr; + protected ArchivaConfiguration archivaConfiguration; + + protected void saveConfiguration() + throws Exception + { + saveConfiguration( archivaConfiguration ); + } + protected void assertFileContents( String expectedContents, File repoRoot, String path ) throws IOException { @@ -133,7 +135,7 @@ public abstract class AbstractRepositoryServletTestCase } } - protected void saveConfiguration() + protected void saveConfiguration( ArchivaConfiguration archivaConfiguration ) throws Exception { archivaConfiguration.save( archivaConfiguration.getConfiguration() ); @@ -143,7 +145,7 @@ public abstract class AbstractRepositoryServletTestCase throws Exception { super.setUp(); - + String appserverBase = getTestFile( "target/appserver-base" ).getAbsolutePath(); System.setProperty( "appserver.base", appserverBase ); @@ -156,29 +158,26 @@ public abstract class AbstractRepositoryServletTestCase Configuration config = archivaConfiguration.getConfiguration(); config.addManagedRepository( createManagedRepository( REPOID_INTERNAL, "Internal Test Repo", repoRootInternal ) ); - saveConfiguration(); + saveConfiguration( archivaConfiguration ); + + CacheManager.getInstance().removeCache( "url-failures-cache" ); - sr = new ServletRunner(); + sr = new ServletRunner( getTestFile( "src/test/webapp/WEB-INF/web.xml" ) ); sr.registerServlet( "/repository/*", UnauthenticatedRepositoryServlet.class.getName() ); sc = sr.newClient(); - HttpSession session = sc.getSession( true ); - ServletContext servletContext = session.getServletContext(); - servletContext.setAttribute( PlexusConstants.PLEXUS_KEY, getContainer() ); } - + @Override - protected String getConfigurationName( String subname ) + protected String getPlexusConfigLocation() throws Exception { return "org/apache/maven/archiva/web/repository/RepositoryServletTest.xml"; } - + @Override protected void tearDown() throws Exception { - release( archivaConfiguration ); - if ( sc != null ) { sc.clearContents(); diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoaderTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoaderTest.java index 5d634e990..848e8cf73 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoaderTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/ArchivaMimeTypeLoaderTest.java @@ -19,8 +19,8 @@ package org.apache.maven.archiva.web.repository; * under the License. */ +import org.apache.maven.archiva.webdav.util.MimeTypes; import org.codehaus.plexus.PlexusTestCase; -import org.codehaus.plexus.webdav.util.MimeTypes; /** * ArchivaMimeTypesTest diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletProxiedMetadataRemoteOnlyTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletProxiedMetadataRemoteOnlyTest.java index 63eaf2416..d649740eb 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletProxiedMetadataRemoteOnlyTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletProxiedMetadataRemoteOnlyTest.java @@ -58,7 +58,7 @@ public class RepositoryServletProxiedMetadataRemoteOnlyTest // --- Verification assertExpectedMetadata( expectedMetadata, actualMetadata ); } - + public void testGetProxiedPluginSnapshotVersionMetadataRemoteOnly() throws Exception { diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletTest.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletTest.java index 71ff228b0..1c5159849 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletTest.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/RepositoryServletTest.java @@ -19,6 +19,7 @@ package org.apache.maven.archiva.web.repository; * under the License. */ +import org.apache.maven.archiva.configuration.ArchivaConfiguration; import org.apache.maven.archiva.configuration.Configuration; import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration; @@ -54,9 +55,10 @@ public class RepositoryServletTest RepositoryServlet servlet = (RepositoryServlet) sc.newInvocation( REQUEST_PATH ).getServlet(); assertNotNull( servlet ); + ArchivaConfiguration archivaConfiguration = servlet.getConfiguration(); Configuration c = archivaConfiguration.getConfiguration(); c.removeManagedRepository( c.findManagedRepositoryById( REPOID_INTERNAL ) ); - saveConfiguration(); + saveConfiguration( archivaConfiguration ); ManagedRepositoryConfiguration repository = servlet.getRepository( REPOID_INTERNAL ); assertNull( repository ); @@ -68,6 +70,7 @@ public class RepositoryServletTest RepositoryServlet servlet = (RepositoryServlet) sc.newInvocation( REQUEST_PATH ).getServlet(); assertNotNull( servlet ); + ArchivaConfiguration archivaConfiguration = servlet.getConfiguration(); Configuration c = archivaConfiguration.getConfiguration(); ManagedRepositoryConfiguration repo = new ManagedRepositoryConfiguration(); repo.setId( NEW_REPOSITORY_ID ); @@ -79,7 +82,7 @@ public class RepositoryServletTest } repo.setLocation( repoRoot.getAbsolutePath() ); c.addManagedRepository( repo ); - saveConfiguration(); + saveConfiguration( archivaConfiguration ); ManagedRepositoryConfiguration repository = servlet.getRepository( NEW_REPOSITORY_ID ); assertNotNull( repository ); diff --git a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/UnauthenticatedRepositoryServlet.java b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/UnauthenticatedRepositoryServlet.java index 7e5f063cf..3d630aaa6 100644 --- a/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/UnauthenticatedRepositoryServlet.java +++ b/archiva-web/archiva-webapp/src/test/java/org/apache/maven/archiva/web/repository/UnauthenticatedRepositoryServlet.java @@ -19,7 +19,7 @@ package org.apache.maven.archiva.web.repository; * under the License. */ -import org.codehaus.plexus.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; diff --git a/archiva-web/archiva-webapp/src/test/resources/META-INF/plexus/components.xml b/archiva-web/archiva-webapp/src/test/resources/META-INF/plexus/components.xml index 1cb21906f..921c1cca1 100644 --- a/archiva-web/archiva-webapp/src/test/resources/META-INF/plexus/components.xml +++ b/archiva-web/archiva-webapp/src/test/resources/META-INF/plexus/components.xml @@ -22,8 +22,8 @@ <components> <!-- Components that are common for all test cases --> <component> - <role>org.codehaus.plexus.webdav.util.MimeTypes</role> - <implementation>org.codehaus.plexus.webdav.util.MimeTypes</implementation> + <role>org.apache.maven.archiva.webdav.util.MimeTypes</role> + <implementation>org.apache.maven.archiva.webdav.util.MimeTypes</implementation> <description>MimeTypes</description> <configuration> <resource>archiva-mime-types.txt</resource> diff --git a/archiva-web/archiva-webapp/src/test/resources/log4j.xml b/archiva-web/archiva-webapp/src/test/resources/log4j.xml index ad3933785..a2e7ea23d 100644 --- a/archiva-web/archiva-webapp/src/test/resources/log4j.xml +++ b/archiva-web/archiva-webapp/src/test/resources/log4j.xml @@ -23,8 +23,12 @@ <level value="info"/> </logger> - <logger name="org.codehaus.plexus.PlexusContainer"> - <level value="info"/> + <logger name="org.springframework"> + <level value="error"/> + </logger> + + <logger name="org.codehaus.plexus.spring"> + <level value="error"/> </logger> <logger name="JPOX"> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/AddProxyConnectorActionTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/AddProxyConnectorActionTest.xml index 6f4fb4247..924f07f7c 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/AddProxyConnectorActionTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/AddProxyConnectorActionTest.xml @@ -24,7 +24,7 @@ <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/EditProxyConnectorActionTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/EditProxyConnectorActionTest.xml index 6f4fb4247..924f07f7c 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/EditProxyConnectorActionTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/connectors/proxy/EditProxyConnectorActionTest.xml @@ -24,7 +24,7 @@ <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> - + <component> <role>org.codehaus.plexus.cache.Cache</role> <role-hint>url-failures-cache</role-hint> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/AbstractManagedRepositoriesActionTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/AbstractManagedRepositoriesActionTest.xml index 7273543eb..ba213da8e 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/AbstractManagedRepositoriesActionTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/AbstractManagedRepositoriesActionTest.xml @@ -24,7 +24,7 @@ <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> </component> - + <component> <role>com.opensymphony.xwork.Action</role> <role-hint>addManagedRepositoryAction</role-hint> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.xml index a5891aacc..43c1eae54 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/action/admin/repositories/DeleteManagedRepositoryActionTest.xml @@ -19,7 +19,7 @@ <plexus> <components> - <component> + <component> <role>org.codehaus.plexus.logging.LoggerManager</role> <implementation>org.codehaus.plexus.logging.slf4j.Slf4jLoggerManager</implementation> <lifecycle-handler>basic</lifecycle-handler> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletSecurityTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletSecurityTest.xml index 15ea6ed1a..d7087095a 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletSecurityTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletSecurityTest.xml @@ -64,9 +64,9 @@ </component> <component> - <role>org.codehaus.plexus.webdav.DavServerManager</role> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> <role-hint>default</role-hint> - <implementation>org.codehaus.plexus.webdav.DefaultDavServerManager</implementation> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> <description>DefaultDavServerManager</description> <configuration> <provider-hint>proxied</provider-hint> diff --git a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletTest.xml b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletTest.xml index 4f0ff5531..3b3b20396 100644 --- a/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletTest.xml +++ b/archiva-web/archiva-webapp/src/test/resources/org/apache/maven/archiva/web/repository/RepositoryServletTest.xml @@ -64,13 +64,16 @@ </component> <component> - <role>org.codehaus.plexus.webdav.DavServerManager</role> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> <role-hint>default</role-hint> - <implementation>org.codehaus.plexus.webdav.DefaultDavServerManager</implementation> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> <description>DefaultDavServerManager</description> - <configuration> - <provider-hint>proxied</provider-hint> - </configuration> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>proxied</role-hint> + </requirement> + </requirements> </component> <component> diff --git a/archiva-web/archiva-webapp/src/test/webapp/WEB-INF/web.xml b/archiva-web/archiva-webapp/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..f47bd657f --- /dev/null +++ b/archiva-web/archiva-webapp/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<web-app xmlns="http://java.sun.com/xml/ns/j2ee" version="2.4" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> + + <display-name>Apache Archiva</display-name> + + <listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> + </listener> + + <context-param> + <param-name>contextClass</param-name> + <param-value>org.codehaus.plexus.spring.PlexusWebApplicationContext</param-value> + </context-param> + + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value> + classpath*:/META-INF/plexus/components.xml + classpath*:/META-INF/spring-context.xml + target/test-classes/org/apache/maven/archiva/web/repository/RepositoryServletTest.xml + </param-value> + </context-param> + +</web-app> diff --git a/archiva-web/archiva-webdav/README-it.could-webdav.txt b/archiva-web/archiva-webdav/README-it.could-webdav.txt new file mode 100644 index 000000000..5206a3429 --- /dev/null +++ b/archiva-web/archiva-webdav/README-it.could-webdav.txt @@ -0,0 +1,8 @@ +This library contains the patched sources to the it.could simple WebDAV library r280, licensed under the Apache License 2.0. + +http://could.it/main/a-simple-approach-to-webdav.html + +To later return to a released version (after the patches have been incorporated and released): +- remove src/main/java/it and src/main/java/org/betaversion +- remove <build> <resources> from the POM +- replace the servlet-api dependency in the POM with it.could webdav. diff --git a/archiva-web/archiva-webdav/pom.xml b/archiva-web/archiva-webdav/pom.xml new file mode 100644 index 000000000..fa863c017 --- /dev/null +++ b/archiva-web/archiva-webdav/pom.xml @@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.maven.archiva</groupId> + <artifactId>archiva-web</artifactId> + <version>1.1-SNAPSHOT</version> + </parent> + + <artifactId>archiva-webdav</artifactId> + <name>Archiva WebDAV Provider</name> + + <dependencies> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-component-api</artifactId> + </dependency> + <dependency> + <groupId>org.codehaus.plexus</groupId> + <artifactId>plexus-spring</artifactId> + <version>1.0-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>org.springframework</groupId> + <artifactId>spring-web</artifactId> + <version>2.5.1</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> +<!-- We import these classes directly to be able to patch them, since this library hasn't been released in some time + <dependency> + <groupId>it.could</groupId> + <artifactId>webdav</artifactId> + <version>0.4</version> + </dependency> +--> + + <!-- Required by it.could classes --> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>servlet-api</artifactId> + <version>2.3</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>slide</groupId> + <artifactId>slide-webdavlib</artifactId> + <version>2.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mortbay.jetty</groupId> + <artifactId>jetty</artifactId> + <version>6.0.2</version> + <scope>test</scope> + </dependency> + </dependencies> + + <!-- Required by it.could classes --> + <build> + <resources> + <resource> + <directory>src/main/resources</directory> + </resource> + <resource> + <directory>${project.build.sourceDirectory}</directory> + <excludes> + <exclude>**/*.java</exclude> + <exclude>**/package.html</exclude> + <exclude>**/url.gif</exclude> + <exclude>**/url.pdf</exclude> + </excludes> + </resource> + </resources> + </build> +</project> diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/StreamTools.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/StreamTools.java new file mode 100644 index 000000000..17ebb7e9c --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/StreamTools.java @@ -0,0 +1,125 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * <p>An utility class providing various static methods operating on + * {@link InputStream input} and {@link OutputStream output} streams.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public final class StreamTools { + + /** <p>Deny construction.</p> */ + private StreamTools() { }; + + /** + * <p>Copy every byte from the specified {@link InputStream} to the specifed + * {@link OutputStream} and then close both of them.</p> + * + * <p>This method is equivalent to a call to the following method: + * {@link #copy(InputStream,OutputStream,boolean) copy(in, out, true)}.</p> + * + * @param in the {@link InputStream} to read bytes from. + * @param out the {@link OutputStream} to write bytes to. + * @return the number of bytes copied. + * @throws IOException if an I/O error occurred copying the data. + */ + public static long copy(InputStream in, OutputStream out) + throws IOException { + return copy(in, out, true); + } + + /** + * <p>Copy every byte from the specified {@link InputStream} to the specifed + * {@link OutputStream} and then optionally close both of them.</p> + * + * @param in the {@link InputStream} to read bytes from. + * @param out the {@link OutputStream} to write bytes to. + * @param close whether to close the streams or not. + * @return the number of bytes copied. + * @throws IOException if an I/O error occurred copying the data. + */ + public static long copy(InputStream in, OutputStream out, boolean close) + throws IOException { + if (in == null) throw new NullPointerException("Null input"); + if (out == null) throw new NullPointerException("Null output"); + + final byte buffer[] = new byte[4096]; + int length = -1; + long total = 0; + while ((length = in.read(buffer)) >= 0) { + out.write(buffer, 0, length); + total += length; + } + + if (close) { + in.close(); + out.close(); + } + + return total; + } + + /** + * Closes the output stream. The output stream can be null and any IOException's will be swallowed. + * + * @param outputStream The stream to close. + */ + public static void close( OutputStream outputStream ) + { + if ( outputStream == null ) + { + return; + } + + try + { + outputStream.close(); + } + catch( IOException ex ) + { + // ignore + } + } + + /** + * Closes the input stream. The input stream can be null and any IOException's will be swallowed. + * + * @param inputStream The stream to close. + */ + public static void close( InputStream inputStream ) + { + if ( inputStream == null ) + { + return; + } + + try + { + inputStream.close(); + } + catch( IOException ex ) + { + // ignore + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/StringTools.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/StringTools.java new file mode 100644 index 000000000..3b0eb14ea --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/StringTools.java @@ -0,0 +1,214 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util; + +import it.could.util.encoding.Encodable; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +/** + * <p>An utility class providing various static methods operating on + * {@link String}s.</p> + * + * <p>This class implement the {@link Encodable} interface from which it + * inherits its {@link Encodable#DEFAULT_ENCODING default encoding}.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public final class StringTools { + + /** <p>The {@link SimpleDateFormat} RFC-822 date format.</p> */ + private static final String FORMAT_822 = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; + /** <p>The {@link SimpleDateFormat} RFC-822 date format.</p> */ + private static final String FORMAT_ISO = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + /** <p>The {@link TimeZone} to use for dates.</p> */ + private static final TimeZone TIMEZONE = TimeZone.getTimeZone("GMT"); + /** <p>The {@link Locale} to use for dates.</p> */ + private static final Locale LOCALE = Locale.US; + + /** <p>Deny construction.</p> */ + private StringTools() { } + + /* ====================================================================== */ + /* NUMBER AND DATE PARSING AND FORMATTING */ + /* ====================================================================== */ + + /** + * <p>Format a {@link Number} into a {@link String} making sure that + * {@link NullPointerException}s are not thrown.</p> + * + * @param number the {@link Number} to format. + * @return a {@link String} instance or <b>null</b> if the object was null. + */ + public static String formatNumber(Number number) { + if (number == null) return null; + return (number.toString()); + } + + /** + * <p>Parse a {@link String} into a {@link Long}.</p> + * + * @param string the {@link String} to parse. + * @return a {@link Long} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Long parseNumber(String string) { + if (string == null) return null; + try { + return new Long(string); + } catch (NumberFormatException exception) { + return null; + } + } + + /** + * <p>Format a {@link Date} according to the HTTP/1.1 RFC.</p> + * + * @param date the {@link Date} to format. + * @return a {@link String} instance or <b>null</b> if the date was null. + */ + public static String formatHttpDate(Date date) { + if (date == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_822, LOCALE); + formatter.setTimeZone(TIMEZONE); + return formatter.format(date); + } + + /** + * <p>Format a {@link Date} according to the ISO 8601 specification.</p> + * + * @param date the {@link Date} to format. + * @return a {@link String} instance or <b>null</b> if the date was null. + */ + public static String formatIsoDate(Date date) { + if (date == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_ISO, LOCALE); + formatter.setTimeZone(TIMEZONE); + return formatter.format(date); + } + + /** + * <p>Parse a {@link String} into a {@link Date} according to the + * HTTP/1.1 RFC (<code>Mon, 31 Jan 2000 11:59:00 GMT</code>).</p> + * + * @param string the {@link String} to parse. + * @return a {@link Date} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Date parseHttpDate(String string) { + if (string == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_822, LOCALE); + formatter.setTimeZone(TIMEZONE); + try { + return formatter.parse(string); + } catch (ParseException exception) { + return null; + } + } + + /** + * <p>Parse a {@link String} into a {@link Date} according to the ISO 8601 + * specification (<code>2000-12-31T11:59:00Z</code>).</p> + * + * @param string the {@link String} to parse. + * @return a {@link Date} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Date parseIsoDate(String string) { + if (string == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_ISO, LOCALE); + formatter.setTimeZone(TIMEZONE); + try { + return formatter.parse(string); + } catch (ParseException exception) { + return null; + } + } + + /* ====================================================================== */ + /* STRING SPLITTING */ + /* ====================================================================== */ + + /** + * <p>Split the specified string in two parts according to the specified + * delimiter, and any resulting path of zero length will be converted to + * <b>null</b>.</p> + */ + public static String[] splitOnce(String source, char delimiter, + boolean noDelimReturnSecond) { + if (source == null) return new String[] { null, null }; + final int position = source.indexOf(delimiter); + if (position < 0) { // --> first + if (noDelimReturnSecond) return new String[] { null, source }; + else return new String[] { source, null }; + } else if (position == 0) { + if (source.length() == 1) { // --> | + return new String[] { null, null }; + } else { // --> |second + return new String[] { null, source.substring(1) }; + } + } else { + final String first = source.substring(0, position); + if (source.length() -1 == position) { // --> first| + return new String[] { first, null }; + } else { // --> first|second + return new String[] { first, source.substring(position + 1) }; + } + } + } + + /** + * <p>Split the specified string according to the specified delimiter, and + * any resulting path of zero length will be converted to <b>null</b>.</p> + */ + public static String[] splitAll(String source, char delimiter) { + final List strings = new ArrayList(); + String current = source; + while (current != null) { + String split[] = splitOnce(current, delimiter, false); + strings.add(split[0]); + current = split[1]; + } + if (current != null) strings.add(current); + final int length = source.length(); + if ((length > 0) && (source.charAt(length - 1) == delimiter)) { + strings.add(null); + } + return (String []) strings.toArray(new String[strings.size()]); + } + + /** + * <p>Find the first occurrence of one of the specified delimiter characters + * in the specified source string.</p> + */ + public static int findFirst(String source, String delimiters) { + final char array[] = source.toCharArray(); + final char delim[] = delimiters.toCharArray(); + for (int x = 0; x < array.length; x ++) { + for (int y = 0; y < delim.length; y ++) { + if (array[x] == delim[y]) return x; + } + } + return -1; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/Encodable.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/Encodable.java new file mode 100644 index 000000000..2f644f9a0 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/Encodable.java @@ -0,0 +1,48 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.encoding; + +import java.io.UnsupportedEncodingException; + +/** + * <p>The {@link Encodable} interface describes an {@link Object} whose + * {@link String} representation can vary depending on the encoding used.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public interface Encodable extends EncodingAware { + + /** + * <p>Return the {@link String} representation of this instance.</p> + * + * <p>This method is equivalent to a call to + * {@link #toString(String) toString}({@link EncodingAware#DEFAULT_ENCODING + * DEFAULT_ENCODING})</p> + */ + public String toString(); + + /** + * <p>Return the {@link String} representation of this instance given + * a specific character encoding.</p> + * + * @throws UnsupportedEncodingException if the specified encoding is not + * supported by the platform. + */ + public String toString(String encoding) + throws UnsupportedEncodingException; + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingAware.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingAware.java new file mode 100644 index 000000000..0690a175b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingAware.java @@ -0,0 +1,37 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.encoding; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; + +/** + * <p>The {@link EncodingAware} interface describes an {@link Object} aware + * of multiple encodings existing withing the platform.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public interface EncodingAware { + + /** <p>The default encoding is specified as being <code>UTF-8</code>.</p> */ + public static final String DEFAULT_ENCODING = "UTF-8"; + + /** <p>The platform encoding is evaluated at runtime from the JVM.</p> */ + public static final String PLATFORM_ENCODING = + new OutputStreamWriter(new ByteArrayOutputStream()).getEncoding(); + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingTools.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingTools.java new file mode 100644 index 000000000..c0c2e7adb --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/EncodingTools.java @@ -0,0 +1,274 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.encoding; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +/** + * <p>An utility class providing various static methods dealing with + * encodings and {@link Encodable} objects..</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public final class EncodingTools implements EncodingAware { + + /** <p>The Base-64 alphabet.</p> */ + private static final char ALPHABET[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '=' }; + + /** <p>Deny construction of this class.</p> */ + private EncodingTools() { } + + /* ====================================================================== */ + /* URL ENCODING / DECODING */ + /* ====================================================================== */ + + /** + * <p>Return the {@link String} representation of the specified + * {@link Encodable} object using the {@link EncodingAware#DEFAULT_ENCODING + * default encoding}.</p> + * + * throws NullPointerException if the {@link Encodable} was <b>null</b>. + */ + public static String toString(Encodable encodable) { + try { + return encodable.toString(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Default encoding \"" + DEFAULT_ENCODING + + "\" not supported by the platform"; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /* ====================================================================== */ + /* URL ENCODING / DECODING */ + /* ====================================================================== */ + + /** + * <p>URL-encode the specified string.</p> + */ + public static String urlEncode(String source, String encoding) + throws UnsupportedEncodingException { + if (source == null) return null; + if (encoding == null) encoding = DEFAULT_ENCODING; + return URLEncoder.encode(source, encoding); + } + + /** + * <p>URL-encode the specified string.</p> + */ + public static String urlEncode(String source) { + if (source == null) return null; + try { + return URLEncoder.encode(source, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>URL-decode the specified string.</p> + */ + public static String urlDecode(String source, String encoding) + throws UnsupportedEncodingException { + if (source == null) return null; + if (encoding == null) encoding = DEFAULT_ENCODING; + return URLDecoder.decode(source, encoding); + } + + /** + * <p>URL-decode the specified string.</p> + */ + public static String urlDecode(String source) { + if (source == null) return null; + try { + return URLDecoder.decode(source, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /* ====================================================================== */ + /* BASE 64 ENCODING / DECODING */ + /* ====================================================================== */ + + /** + * <p>Encode the specified string in base 64 using the specified + * encoding.</p> + */ + public static final String base64Encode(String string, String encoding) + throws UnsupportedEncodingException { + /* Check the source string for null or the empty string. */ + if (string == null) return (null); + if (string.length() == 0) return ""; + + /* Check the encoding */ + if (encoding == null) encoding = DEFAULT_ENCODING; + + /* Prepare the buffers that we'll use to encode in Base 64 */ + final byte bsrc[] = string.getBytes(encoding); + final char bdst[] = new char[(bsrc.length + 2) / 3 * 4]; + + /* Iterate into the source in chunks of three bytes */ + int psrc = -1; + int pdst = 0; + int temp = 0; + while ((psrc = psrc + 3) < bsrc.length) { + /* For every three bytes processed ... */ + temp = ((bsrc[psrc - 2] << 16) & 0xFF0000) | + ((bsrc[psrc - 1] << 8) & 0x00FF00) | + ((bsrc[psrc ] ) & 0x0000FF); + /* ... we append four bytes to the buffer */ + bdst[pdst ++] = ALPHABET[(temp >> 18) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp >> 12) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp >> 6) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp ) & 0x3f]; + } + + /* Let's check whether we still have some bytes to encode */ + switch (psrc - bsrc.length) { + case 0: /* Two bytes left to encode */ + temp = ((bsrc[psrc - 2] & 0xFF) << 8) | (bsrc[psrc - 1] & 0xFF); + bdst[pdst ++] = ALPHABET[(temp >> 10) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp >> 4) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp << 2) & 0x3c]; + bdst[pdst ++] = ALPHABET[64]; + break; + case 1: /* One byte left to encode */ + temp = (bsrc[psrc - 2] & 0xFF); + bdst[pdst ++] = ALPHABET[(temp >> 2) & 0x3f]; + bdst[pdst ++] = ALPHABET[(temp << 4) & 0x30]; + bdst[pdst ++] = ALPHABET[64]; + bdst[pdst ++] = ALPHABET[64]; + } + + /* Convert the character array into a proper string */ + return new String(bdst); + } + + /** + * <p>Encode the specified string in base 64 using the default encoding.</p> + */ + public static final String base64Encode(String string) { + try { + return (base64Encode(string, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Decode the specified base 64 string using the specified encoding.</p> + */ + public static final String base64Decode(String string, String encoding) + throws UnsupportedEncodingException { + /* Check the source string for null or the empty string. */ + if (string == null) return (null); + if (string.length() == 0) return ""; + + /* Check the encoding */ + if (encoding == null) encoding = DEFAULT_ENCODING; + + /* Retrieve the array of characters of the source string. */ + final char characters[] = string.toCharArray(); + + /* Check the length, which must be dividible by 4. */ + if ((characters.length & 0x03) != 0) + throw new IllegalArgumentException("Invalid length for the "+ + "encoded string (" + characters.length + ")"); + + /* The bytes array length is 3/4th of the characters array length */ + byte bytes[] = new byte[characters.length - (characters.length >> 2)]; + + /* + * Since this might take a while check now for the last 4 characters + * token: it must contain at most two == and those need to be in the + * last two positions in the array (the only valid sequences are: + * "????", "???=" and "??=="). + */ + if (((characters[characters.length - 4] == '=') || + (characters[characters.length - 3] == '=')) || + ((characters[characters.length - 2] == '=') && + (characters[characters.length - 1] != '='))) { + throw new IllegalArgumentException("Invalid pattern for last " + + "Base64 token in string to decode: " + + characters[characters.length - 4] + + characters[characters.length - 3] + + characters[characters.length - 2] + + characters[characters.length - 1]); + } + + /* Translate the Base64-encoded String in chunks of 4 characters. */ + int coff = 0; + int boff = 0; + while (coff < characters.length) { + boolean last = (coff == (characters.length - 4)); + int curr = ((value(characters[coff ], last) << 0x12) | + (value(characters[coff + 1], last) << 0x0c) | + (value(characters[coff + 2], last) << 0x06) | + (value(characters[coff + 3], last) )); + bytes[boff + 2] = (byte)((curr ) & 0xff); + bytes[boff + 1] = (byte)((curr >> 0x08) & 0xff); + bytes[boff ] = (byte)((curr >> 0x10) & 0xff); + coff += 4; + boff += 3; + } + + /* Get the real decoded string length, checking out the trailing '=' */ + if (characters[coff - 1] == '=') boff--; + if (characters[coff - 2] == '=') boff--; + + /* All done */ + return (new String(bytes, 0, boff, encoding)); + } + + /** + * <p>Decode the specified base 64 string using the default encoding.</p> + */ + public static final String base64Decode(String string) { + try { + return (base64Decode(string, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /* ====================================================================== */ + + /** <p>Retrieve the offset of a character in the base 64 alphabet.</p> */ + private static final int value(char character, boolean last) { + for (int x = 0; x < 64; x++) if (ALPHABET[x] == character) return (x); + if (last && (character == ALPHABET[65])) return(0); + final String message = "Character \"" + character + "\" invalid"; + throw new IllegalArgumentException(message); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/package.html new file mode 100644 index 000000000..675ba3f58 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/encoding/package.html @@ -0,0 +1,61 @@ +<html> + <head> + <title>Encoding Utilities</title> + </head> + <body> + <p> + This package contains a number of utility classes dealing with generic + encoding of {@link java.lang.String}s. + </p> + <p> + Although this might sound useless at first (as {@link java.lang.String}s + do support encoding internally already), this class deals with a very + subtle problem encountered when merging Java {@link java.lang.String}s + and old byte-based (non internationalized) transports, such as + Base 64 and URL encoding. + </p> + <p> + Let's consider (as an example) the URL encoded {@link java.lang.String} + <code>%C2%A3 100</code> can be easily decomposed in a byte array using + URL decoding techniques: we would end up with the following byte array: + <code>0x0C2 0x0A3 0x20 0x31 0x30 0x30</code>. + </p> + <p> + This byte-array, though, doesn't tell us anything about how to represent + this as a readable and usable {@link java.lang.String} in Java. To be + able to convert this we have to decode it again using a charset (or an + encoding). + </p> + <p> + So, for example, if we were to decode the above mentioned byte array using + the <b>ISO-8859-1</b> encoding, we would obtain the string + "<code>£ 100</code>", or in details: + </p> + <ul> + <li>a latin capital letter "A" with a circumflex accent</li> + <li>the pound sign</li> + <li>a space</li> + <li>the number 1</li> + <li>the number 0</li> + <li>the number 0</li> + </ul> + <p> + If we were to decode the same byte sequence using <b>UTF-8</b>, on the + other hand, we would obtain the (quite different) string + "<code>£ 100</code>", or in details: + </p> + <ul> + <li>the pound sign</li> + <li>a space</li> + <li>the number 1</li> + <li>the number 0</li> + <li>the number 0</li> + </ul> + <p> + Therefore, as a conclusion, when Java {@link java.lang.String}s are + encoded using Base 64, URL encoding, or similar techiques, one always + have to remember that encoding (or decoding) must be done twice, and + this package provides a way to deal with this mechanism. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/http/HttpClient.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/HttpClient.java new file mode 100644 index 000000000..75884e0a2 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/HttpClient.java @@ -0,0 +1,1070 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.http; + +import it.could.util.encoding.EncodingTools; +import it.could.util.location.Location; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * <p>A class implementing an extremely simple HTTP 1.0 connector with + * basic authentication support.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class HttpClient { + + /** <p>The default HTTP method to use.</p> */ + public static final String DEFAULT_METHOD = "GET"; + + /* ====================================================================== */ + + /** <p>The byte sequence CR LF (the end of the request).</p> */ + private static final byte CRLF[] = { 0x0d, 0x0a }; + /** <p>The byte sequence for " HTTP/1.0\r\n" (the request signature).</p> */ + private static final byte HTTP[] = { 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, + 0x31, 0x2e, 0x30, 0x0d, 0x0a }; + + /* ====================================================================== */ + + /** <p>The buffer used to parse lines in the response.</p> */ + private final byte buffer[] = new byte[4096]; + /** <p>The map of the current request headers.</p> */ + private final Map requestHeaders = new HashMap(); + /** <p>The map of the current response headers.</p> */ + private final Map responseHeaders = new HashMap(); + + /* ====================================================================== */ + + /** <p>The {@link Location} pointing to the current request.</p> */ + private Location location; + /** <p>The status of the current request.</p> */ + private Status status = null; + /** <p>An array of acceptable statuses to verify upon connection.</p> */ + private int acceptable[] = null; + + /* ====================================================================== */ + + /** <p>The limited input stream associated with this request.</p> */ + private Input xinput = null; + /** <p>The limited output stream associated with this request.</p> */ + private Output xoutput = null; + /** <p>The socket associated with this request.</p> */ + private Socket xsocket = null; + + /* ====================================================================== */ + + /** + * <p>Create a new {@link HttpClient} instance associated with the + * specified location in string format.</p> + * + * @throws MalformedURLException if the location couldn't be parsed. + */ + public HttpClient(String location) + throws MalformedURLException { + this.location = Location.parse(location); + } + + /** + * <p>Create a new {@link HttpClient} instance associated with the + * specified location in string format.</p> + * + * @throws MalformedURLException if the location couldn't be parsed. + */ + public HttpClient(String location, String encoding) + throws MalformedURLException, UnsupportedEncodingException { + this.location = Location.parse(location, encoding); + } + + /** + * <p>Create a new {@link HttpClient} instance associated with the + * specified {@link Location}.</p> + */ + public HttpClient(Location location) { + if (location == null) throw new NullPointerException("Null location"); + if (! location.isAbsolute()) + throw new IllegalArgumentException("Relative location supplied"); + if (! "http".equals(location.getSchemes().toString())) { + throw new IllegalArgumentException("Scheme is not HTTP"); + } + this.location = location; + } + + /* ====================================================================== */ + /* CONNECTION VERIFICATION METHODS */ + /* ====================================================================== */ + + /** + * <p>Set an HTTP response status code considered to be acceptable when + * verifying the connection.</p> + */ + public HttpClient setAcceptableStatus(int status) { + return this.setAcceptableStatuses(new int[] { status }); + } + + /** + * <p>Set an array of HTTP response status codes considered to be acceptable + * when verifying the connection.</p> + * + * <p>If the array is <b>null</b> status code checking is disabled.</p> + */ + public HttpClient setAcceptableStatuses(int statuses[]) { + if (statuses == null) { + this.acceptable = null; + return this; + } + for (int x = 0; x < statuses.length; x ++) { + final int status = statuses[x]; + if ((status < 100) || (status > 599)) + throw new IllegalArgumentException("Wrong status " + status); + } + this.acceptable = statuses; + return this; + } + + /* ====================================================================== */ + /* CONNECTION METHODS */ + /* ====================================================================== */ + + /** + * <p>Connect to the {@link Location} specified at construction using the + * default method <code>GET</code>.</p> + * + * <p>This is equivalent to {@link #connect(boolean) connect(true)}.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect() + throws IOException { + return this.connect(DEFAULT_METHOD, true, 0); + } + + /** + * <p>Connect to the {@link Location} specified at construction using the + * default method <code>GET</code> allowing for a specified amount of + * content to be written into the request.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect(long contentLength) + throws IOException { + return this.connect(DEFAULT_METHOD, false, contentLength); + } + + /** + * <p>Connect to the {@link Location} specified at construction using the + * default method <code>GET</code> and optionally following redirects.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect(boolean followRedirects) + throws IOException { + return this.connect(DEFAULT_METHOD, followRedirects, 0); + } + + /** + * <p>Connect to the {@link Location} specified at construction with the + * specified method.</p> + * + * <p>This is equivalent to {@link #connect(String,boolean) + * connect(method, true)}.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect(String method) + throws IOException { + return this.connect(method, true, 0); + } + + /** + * <p>Connect to the {@link Location} specified at construction with the + * specified method allowing for a specified amount of content to be + * written into the request.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect(String method, long contentLength) + throws IOException { + return this.connect(method, false, contentLength); + } + + /** + * <p>Connect to the {@link Location} specified at construction with the + * specified method and optionally following redirects.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient connect(String method, boolean followRedirects) + throws IOException { + return this.connect(method, followRedirects, 0); + } + + /** + * <p>Disconnect from the remote endpoint and terminate the request.</p> + * + * <p>Note that request and response headers, the resultin status and + * acceptable statuses are <b>not</b> cleared by this method.</p> + * + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient disconnect() + throws IOException { + return this.disconnect(false); + } + + /** + * <p>Disconnect from the remote endpoint and terminate the request.</p> + * + * @param reset whether to reset all headers, status and acceptable response + * status codes or not. + * @return this {@link HttpClient} instance. + * @throws IOException if an I/O or a network error occurred. + */ + public HttpClient disconnect(boolean reset) + throws IOException { + final Socket socket = this.xsocket; + if (socket != null) try { + /* Make sure that we mark this instance as being closed */ + this.xsocket = null; + + /* Close the input stream if necessary */ + if (this.xinput != null) { + if (! this.xinput.closed) this.xinput.close(); + this.xinput = null; + } + + /* Close the output stream if necessary */ + if (this.xoutput != null) { + if (! this.xoutput.closed) this.xoutput.close(); + this.xoutput = null; + } + + } finally { + /* Ensure that the socket is closed */ + socket.close(); + } + + if (reset) { + this.requestHeaders.clear(); + this.responseHeaders.clear(); + this.status = null; + this.acceptable = null; + } + return this; + } + + /* ====================================================================== */ + /* INTERNAL CONNECTION HANDLER */ + /* ====================================================================== */ + + /** + * <p>Internal method actually connecting to the remote HTTP server.</p> + */ + private HttpClient connect(String method, boolean redirect, long length) + throws IOException { + /* Check if (by any chance) we have been connected already */ + if (this.xsocket != null) + throw new IllegalStateException("Already connected"); + + /* Check for both follow redirects and content length */ + if (length < 0) throw new IOException("Negative length"); + if ((length > 0) && redirect) + throw new InternalError("Can't follow redirects and write request"); + + /* Verify any authentication token */ + final String userinfo = this.location.getAuthority().getUserInfo(); + if (userinfo != null) { + final String encoded = EncodingTools.base64Encode(userinfo); + this.addRequestHeader("Authorization", "Basic " + encoded); + } + + /* All methods in HTTP are upper case */ + method = method.toUpperCase(); + + /* Make sure we close the connection at the end of the request */ + this.addRequestHeader("Connection", "close", false); + + /* The content length of the request is forced to be valid */ + this.addRequestHeader("Content-Length", Long.toString(length), false); + + /* Enter in a loop for redirections */ + int redirs = 20; + while (true) { + /* If we have been redirected too many times, fail */ + if ((--redirs) < 0) throw new IOException("Too many redirections"); + + /* Get the authority, once and for all */ + final Location.Authority auth = this.location.getAuthority(); + + /* Prepare a normalized host header */ + final String host = auth.getHost(); + final int port = auth.getPort() < 0 ? 80 : auth.getPort(); + this.addRequestHeader("Host", host + ":" + port, false); + + /* Connect to the remote endpoint */ + final Socket sock = new Socket(auth.getHost(), port); + final InputStream in = sock.getInputStream(); + final OutputStream out = sock.getOutputStream(); + + /* Write the request line */ + out.write((method + " ").getBytes("US-ASCII")); + out.write(this.location.getPath().toString().getBytes("US-ASCII")); + out.write(HTTP); /* SPACE HTTP/1.0 CR LF */ + + /* Write all the headers */ + final Iterator headers = this.requestHeaders.values().iterator(); + while (headers.hasNext()) { + final RequestHeader header = (RequestHeader) headers.next(); + final Iterator values = header.values.iterator(); + while (values.hasNext()) { + out.write(header.name); + out.write((byte []) values.next()); + } + } + + /* Write the final CRLF, read the status and the headers */ + out.write(CRLF); + out.flush(); + + /* Return now if we have to write content */ + if (length > 0) { + this.xsocket = sock; + this.xoutput = new Output(this, in, out, length); + this.xinput = null; + return this; + } + + this.readStatusLine(in); + this.readHeaders(in); + + /* If we have to follow redirects, let's inspect the response */ + final int code = this.status.status; + if (redirect && ((code == 301) || (code == 302) || (code == 307))) { + final String location = this.getResponseHeader("Location"); + if (location != null) { + in.close(); + out.close(); + sock.close(); + this.location = this.location.resolve(location); + continue; + } + } + + /* No further redirections, so verify if the status code is ok */ + this.verify(); + + /* Evaluate the content length specified by the server */ + final String len = this.getResponseHeader("Content-Length"); + long bytesLength = -1; + if (len != null) try { + bytesLength = Long.parseLong(len); + } catch (NumberFormatException exception) { + /* Swallow this, be liberal in what we accept */ + } + + /* Return an output stream if the content length was not zero */ + this.xsocket = sock; + this.xoutput = null; + this.xinput = new Input(this, in, bytesLength); + return this; + } + } + + private void verify() + throws IOException { + /* No further redirections, sov erify if the status code is ok */ + if (this.acceptable != null) { + boolean accepted = false; + for (int x = 0; x < this.acceptable.length; x ++) { + if (this.status.status != this.acceptable[x]) continue; + accepted = true; + break; + } + if (! accepted) { + this.disconnect(); + throw new IOException("Connection to " + this.location + + " returned unacceptable status " + + this.status.status + " (" + + this.status.message + ")"); + } + } + } + + /* ====================================================================== */ + /* INPUT / OUTPUT METHODS */ + /* ====================================================================== */ + + /** + * <p>Return an {@link InputStream} where the content of the HTTP response + * can be read from.</p> + * + * @throws IllegalStateException if this instance is not connected yet, or + * the request body was not fully written yet. + */ + public InputStream getResponseStream() + throws IllegalStateException { + if (this.xsocket == null) + throw new IllegalStateException("Connection not available"); + if ((this.xoutput != null) && (this.xoutput.remaining != 0)) + throw new IllegalStateException("Request body not fully written"); + return this.xinput; + } + + /** + * <p>Return an {@link OutputStream} where the content of the HTTP request + * can be written to.</p> + * + * @throws IllegalStateException if this instance is not connected yet or if + * upon connection the size of the request was + * not specifed or <b>zero</b>. + */ + public OutputStream getRequestStream() + throws IllegalStateException { + if (this.xsocket == null) + throw new IllegalStateException("Connection not available"); + if (this.xoutput == null) + throw new IllegalStateException("No request body to write to"); + return this.xoutput; + } + + /* ====================================================================== */ + /* REQUEST AND RESPONSE METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the {@link Location} of this connection.</p> + * + * <p>This might be different from the {@link Location} specified at + * construction time if upon connecting HTTP redirections were followed.</p> + */ + public Location getLocation() { + return this.location; + } + + /** + * <p>Add a new header that will be sent with the HTTP request.</p> + * + * <p>This method will remove any header value previously associated with + * the specified name, in other words this method is equivalent to + * {@link #addRequestHeader(String, String, boolean) + * addRequestHeader(name, value, false)}.</p> + * + * @param name the name of the request header to add. + * @param value the value of the request header to add. + * @return this {@link HttpClient} instance. + * @throws NullPointerException the name or value were <b>null</b>. + */ + public HttpClient addRequestHeader(String name, String value) { + return this.addRequestHeader(name, value, false); + } + + /** + * <p>Add a new header that will be sent with the HTTP request.</p> + * + * @param name the name of the request header to add. + * @param value the value of the request header to add. + * @param appendValue if the current value should be appended, or in other + * words, that two headers with the same can coexist. + * @return this {@link HttpClient} instance. + * @throws NullPointerException the name or value were <b>null</b>. + */ + public HttpClient addRequestHeader(String name, String value, + boolean appendValue) { + final String key = name.toLowerCase(); + try { + RequestHeader header; + if (appendValue) { + header = (RequestHeader) this.requestHeaders.get(key); + if (header == null) { + header = new RequestHeader(name); + this.requestHeaders.put(key, header); + } + } else { + header = new RequestHeader(name); + this.requestHeaders.put(key, header); + } + header.values.add((value + "\r\n").getBytes("ISO-8859-1")); + return this; + } catch (UnsupportedEncodingException exception) { + Error error = new InternalError("Standard encoding not supported"); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Remove the named header from the current HTTP request.</p> + * + * @param name the name of the request header to add. + * @return this {@link HttpClient} instance. + * @throws NullPointerException the name was <b>null</b>. + */ + public HttpClient removeRequestHeader(String name) { + final String key = name.toLowerCase(); + this.requestHeaders.remove(key); + return this; + } + + /** + * <p>Remove all headers from the current HTTP request.</p> + * + * @return this {@link HttpClient} instance. + */ + public HttpClient removeRequestHeaders() { + this.requestHeaders.clear(); + return this; + } + + /** + * <p>Return the first value for the specified response header.</p> + * + * @param name the name of the header whose value needs to be returned. + * @return a {@link String} or <b>null</b> if no such header exists. + */ + public String getResponseHeader(String name) { + final String key = name.toLowerCase(); + ResponseHeader header = (ResponseHeader) this.responseHeaders.get(key); + if (header == null) return null; + return (String) header.values.get(0); + } + + /** + * <p>Return all the values for the specified response header.</p> + * + * @param name the name of the header whose values needs to be returned. + * @return a {@link List} or <b>null</b> if no such header exists. + */ + public List getResponseHeaderValues(String name) { + final String key = name.toLowerCase(); + ResponseHeader header = (ResponseHeader) this.responseHeaders.get(key); + if (header == null) return null; + return Collections.unmodifiableList(header.values); + } + + /** + * <p>Return an {@link Iterator} over all response header names.</p> + * + * @return a <b>non-null</b> {@link Iterator}. + */ + public Iterator getResponseHeaderNames() { + final Iterator iterator = this.responseHeaders.values().iterator(); + return new Iterator() { + public boolean hasNext() { + return iterator.hasNext(); + } + public Object next() { + return ((ResponseHeader) iterator.next()).name; + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * <p>Return the protocol returned by the remote HTTP server.</p> + * + * @return a <b>non-null</b> {@link String} like <code>HTTP/1.0</code>. + * @throws IllegalStateException if the connection was never connected. + */ + public String getResponseProtocol() { + if (this.status == null) throw new IllegalStateException(); + return this.status.protocol; + } + + /** + * <p>Return the status returned by the remote HTTP server.</p> + * + * @return a number representing the HTTP status of the response. + * @throws IllegalStateException if the connection was never connected. + */ + public int getResponseStatus() { + if (this.status == null) throw new IllegalStateException(); + return this.status.status; + } + + /** + * <p>Return the status message returned by the remote HTTP server.</p> + * + * @return a <b>non-null</b> {@link String} like <code>OK</code>. + * @throws IllegalStateException if the connection was never connected. + */ + public String getResponseMessage() { + if (this.status == null) throw new IllegalStateException(); + return this.status.message; + } + + /* ====================================================================== */ + /* PRIVATE METHODS TO USE WHEN CONNECTING */ + /* ====================================================================== */ + + /** + * <p>Read a single line of the HTTP response from the specified + * {@link InputStream} into a byte array (trailing CRLF are removed).</p> + */ + private byte[] readLine(InputStream input) + throws IOException { + int x = 0; + while (true) { + int b = input.read(); + if (b == -1) break; + if (b == 0x0A) break; + if (x == this.buffer.length) break; + this.buffer[x ++] = (byte) b; + } + if ((x > 0) && (this.buffer[x - 1] == 0x0D)) x--; + final byte array[] = new byte[x]; + System.arraycopy(this.buffer, 0, array, 0, x); + return array; + } + + /** + * <p>Read the status line from the specified {@link InputStream} and + * setup the {@link #status} field.</p> + */ + private void readStatusLine(InputStream input) + throws IOException { + /* Prepare the different buffers required for parsing */ + final byte line[] = this.readLine(input); + final byte buff[] = new byte[line.length]; + final String comp[] = new String[3]; + int lpos = 0; + int bpos = 0; + int cpos = 0; + boolean spc = true; + + /* Iterate every single byte in the line, splitting up components */ + while (lpos < line.length) { + final byte b = line[lpos ++]; + if (spc) { + if ((b == 0x09) || (b == 0x20)) continue; + buff[bpos ++] = b; + if (cpos == 2) break; + else spc = false; + } else { + if ((b == 0x09) || (b == 0x20)) { + comp[cpos ++] = new String(buff, 0, bpos, "US-ASCII"); + bpos = 0; + spc = true; + continue; + } + buff[bpos ++] = b; + } + + } + /* + * Copy remaining bytes out of the line buffer and ensure all + * components in the status line are not null; + */ + while (lpos < line.length) buff[bpos ++] = line[lpos++]; + if (bpos > 0) comp[cpos++] = new String(buff, 0, bpos, "US-ASCII"); + for (int x = cpos; x < 3; x++) comp[x] = ""; + + /* Create the status object */ + this.status = new Status(comp[0], comp[1], comp[2]); + } + + /** + * <p>Read all the response headers from the specified {@link InputStream} + * and setup the {@link #responseHeaders} field.</p> + */ + private void readHeaders(InputStream input) + throws IOException { + /* Clear out any previous header */ + this.responseHeaders.clear(); + + /* Process the input stream until we find an empty line */ + while (true) { + final byte array[] = this.readLine(input); + if (array.length == 0) break; + + /* Identify where the colon is in the header */ + int pos = -1; + while (pos < array.length) if (array[++ pos] == 0x03A) break; + if (pos == 0) continue; + if (pos == array.length - 1) continue; + + /* Prepare strings for name and value */ + final int o = pos + 1; + final int l = array.length - o; + final String name = new String(array, 0, pos, "US-ASCII").trim(); + final String value = new String(array, o, l, "ISO-8859-1").trim(); + if ((name.length() == 0) || (value.length() == 0)) continue; + + /* Store the header value in a list for now */ + final String key = name.toLowerCase(); + ResponseHeader hdr = (ResponseHeader) this.responseHeaders.get(key); + if (hdr == null) { + hdr = new ResponseHeader(name); + this.responseHeaders.put(key, hdr); + } + hdr.values.add(value); + } + } + + /* ====================================================================== */ + /* INTERNAL CLASS REPRESENTNG THE STATUS LINE AND AN ENCODED HEADER */ + /* ====================================================================== */ + + /** + * <p>A simple internal class representing a response status line.</p> + */ + private static final class Status { + + /** <p>The response protocol, like <code>HTTP/1.0</code> */ + private final String protocol; + /** <p>The response status code, like <code>302</code> */ + private final int status; + /** <p>The response message, like <code>Moved permanently</code> */ + private final String message; + + /** + * <p>Create a new {@link Status} verifying the supplied parameters.</p> + * + * @throws IOException if an error occurred verifying the parameters. + */ + private Status(String protocol, String status, String message) + throws IOException { + + /* Verify the protocol */ + if ("HTTP/1.0".equals(protocol) || "HTTP/1.1".equals(protocol)) { + this.protocol = protocol; + } else { + throw new IOException("Unknown protocol \"" + protocol + "\""); + } + + /* Verify the status */ + try { + this.status = Integer.parseInt(status); + if ((this.status < 100) || (this.status > 599)) { + throw new IOException("Invalid status \"" + status + "\""); + } + } catch (RuntimeException exception) { + final String error = "Can't parse status \"" + status + "\""; + IOException throwable = new IOException(error); + throw (IOException) throwable.initCause(exception); + } + + /* Decode the message */ + if ("".equals(message)) this.message = "No message"; + else this.message = EncodingTools.urlDecode(message, "ISO-8859-1"); + } + } + + /** + * <p>A simple internal class representing a request header.</p> + */ + private static final class RequestHeader { + + /** <p>The byte array of the header's name.</p> */ + private final byte name[]; + /** <p>A {@link List} of all the header's values.</p> */ + private final List values; + + /** <p>Create a new {@link RequestHeader} instance.</p> */ + private RequestHeader(String name) + throws UnsupportedEncodingException { + this.name = (name + ": ").getBytes("US-ASCII"); + this.values = new ArrayList(); + } + } + + /** + * <p>A simple internal class representing a response header.</p> + */ + private static final class ResponseHeader { + + /** <p>The real name of the response header.</p> */ + private final String name; + /** <p>A {@link List} of all the header's values.</p> */ + private final List values; + + /** <p>Create a new {@link ResponseHeader} instance.</p> */ + private ResponseHeader(String name) + throws UnsupportedEncodingException { + this.name = name; + this.values = new ArrayList(); + } + } + + /* ====================================================================== */ + /* LIMITED STREAMS */ + /* ====================================================================== */ + + /** + * <p>A simple {@link OutputStream} writing at most the number of bytes + * specified at construction.</p> + */ + private static final class Output extends OutputStream { + + /** <p>The {@link OutputStream} wrapped by this instance.</p> */ + private final OutputStream output; + /** <p>The {@link InputStream} wrapped by this instance.</p> */ + private final InputStream input; + /** <p>The {@link HttpClient} wrapped by this instance.</p> */ + private final HttpClient client; + /** <p>The number of bytes yet to write.</p> */ + private long remaining; + /** <p>A flag indicating whether this instance was closed.</p> */ + private boolean closed; + + /** + * <p>Create a new {@link Output} instance with the specified limit + * of bytes to write.</p> + * + * @param output the {@link OutputStream} to wrap. + * @param remainig the maximum number of bytes to write. + */ + private Output(HttpClient client, InputStream input, + OutputStream output, long remaining) { + if (input == null) throw new NullPointerException(); + if (output == null) throw new NullPointerException(); + if (client == null) throw new NullPointerException(); + this.remaining = remaining; + this.client = client; + this.output = output; + this.input = input; + } + + public void write(byte buf[]) + throws IOException { + this.write(buf, 0, buf.length); + } + + public void write(byte buf[], int off, int len) + throws IOException { + if (len > this.remaining) { + throw new IOException("Too much data to write"); + } else try { + this.output.write(buf, off, len); + } finally { + this.remaining -= len; + if (this.remaining < 1) this.close(); + } + } + + public void write(int b) + throws IOException { + if (this.remaining < 1) { + throw new IOException("Too much data to write"); + } else try { + this.output.write(b); + } finally { + this.remaining -= 1; + if (this.remaining < 1) this.close(); + } + } + + public void flush() + throws IOException { + this.output.flush(); + } + + public void close() + throws IOException { + if (this.closed) return; + if (this.remaining > 0) + throw new IOException(this.remaining + " bytes left to write"); + this.closed = true; + this.output.flush(); + + /* Read the status and headers from the connection and verify */ + this.client.readStatusLine(this.input); + this.client.readHeaders(this.input); + this.client.verify(); + + /* Evaluate the content length specified by the server */ + final String slen = this.client.getResponseHeader("Content-Length"); + long blen = -1; + if (slen != null) try { + blen = Long.parseLong(slen); + } catch (NumberFormatException exception) { + /* Swallow this, be liberal in what we accept */ + } + + /* Return an output stream if the content length was not zero */ + this.client.xoutput = null; + this.client.xinput = new Input(this.client, this.input, blen); + } + + protected void finalize() + throws Throwable { + try { + this.close(); + } finally { + super.finalize(); + } + } + } + + /** + * <p>A simple {@link InputStream} reading at most the number of bytes + * specified at construction.</p> + */ + private static final class Input extends InputStream { + + /** <p>The {@link InputStream} wrapped by this instance.</p> */ + private final InputStream input; + /** <p>The {@link HttpClient} wrapped by this instance.</p> */ + private final HttpClient client; + /** <p>The number of bytes yet to write or -1 if unknown.</p> */ + private long remaining; + /** <p>A flag indicating whether this instance was closed.</p> */ + private boolean closed; + + /** + * <p>Create a new {@link Input} instance with the specified limit + * of bytes to read.</p> + * + * @param input the {@link InputStream} to wrap. + * @param remainig the maximum number of bytes to read or -1 if unknown. + */ + private Input(HttpClient client, InputStream input, long remaining) { + if (input == null) throw new NullPointerException(); + if (client == null) throw new NullPointerException(); + this.remaining = remaining < 0 ? Long.MAX_VALUE : remaining; + this.client = client; + this.input = input; + } + + public int read() + throws IOException { + if (this.remaining < 1) { + return -1; + } else try { + return this.input.read(); + } finally { + this.remaining -= 1; + if (this.remaining < 1) this.close(); + } + } + + public int read(byte buf[]) + throws IOException { + return read(buf, 0, buf.length); + } + + public int read(byte buf[], int off, int len) + throws IOException { + if (this.remaining <= 0) return -1; + if (len > this.remaining) len = (int) this.remaining; + int count = 0; + try { + count = this.input.read(buf, off, len); + } finally { + this.remaining -= count; + if (this.remaining < 1) this.close(); + } + return count; + } + + public long skip(long n) + throws IOException { + if (this.remaining <= 0) return -1; + if (n > this.remaining) n = this.remaining; + long count = 0; + try { + count = this.input.skip(n); + } finally { + this.remaining -= count; + if (this.remaining < 1) this.close(); + } + return count; + } + + public int available() + throws IOException { + int count = this.input.available(); + if (count < this.remaining) return count; + return (int) this.remaining; + } + + public void close() + throws IOException { + if (this.closed) return; + this.closed = true; + try { + this.input.close(); + } finally { + this.client.disconnect(); + } + } + + public void mark(int readlimit) { + this.input.mark(readlimit); + } + + public void reset() + throws IOException { + this.input.reset(); + } + + public boolean markSupported() { + return this.input.markSupported(); + } + + protected void finalize() + throws Throwable { + try { + this.close(); + } finally { + super.finalize(); + } + } + } + + /* ====================================================================== */ + /* UTILITY FETCHER */ + /* ====================================================================== */ + + /** + * <p><b>Utility method:</b> fetch the location specified on the command + * line following redirects if necessary.</p> + * + * <p>The final location fetched (in case of redirections it might change) + * will be reported on the {@link System#err system error stream} alongside + * with any errors encountered while processing.</p> + */ + public static final void main(String args[]) { + try { + final HttpClient c = new HttpClient(args[0]).connect(); + final InputStream i = c.getResponseStream(); + for (int b = i.read(); b >= 0; b = i.read()) System.out.write(b); + c.disconnect(); + } catch (Throwable throwable) { + throwable.printStackTrace(System.err); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java new file mode 100644 index 000000000..fe0eba41b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java @@ -0,0 +1,901 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.http; + +import it.could.util.StreamTools; +import it.could.util.StringTools; +import it.could.util.location.Location; +import it.could.util.location.Path; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.MalformedURLException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.StringTokenizer; + +/** + * <p>A class implementing an extremely simple WebDAV Level 1 client based on + * the {@link HttpClient}.</p> + * + * <p>Once opened this class will represent a WebDAV collection. Users of this + * class can then from an instance of this, deal with relative parent and + * children resources.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class WebDavClient { + + /** <p>The WebDAV resource asociated with this instance.</p> */ + private Resource resource; + /** <p>A map of children resources of this instance.</p> */ + private Map children; + + /** + * <p>Create a new {@link WebDavClient} instance opening the collection + * identified by the specified {@link Location}.</p> + * + * @param location the {@link Location} of the WebDAV collection to open. + * @throws IOException if an I/O or network error occurred, or if the + * {@link Location} specified does not point to a + * WebDAV collection. + * @throws NullPointerException if the {@link Location} was <b>null</b>. + */ + public WebDavClient(Location location) + throws NullPointerException, IOException { + if (location == null) throw new NullPointerException("Null location"); + this.reload(location); + } + + /* ====================================================================== */ + /* ACTIONS */ + /* ====================================================================== */ + + /** + * <p>Refresh this {@link WebDavClient} instance re-connecting to the remote + * collection and re-reading its properties.</p> + * + * @return this {@link WebDavClient} instance. + */ + public WebDavClient refresh() + throws IOException { + this.reload(this.resource.location); + return this; + } + + /** + * <p>Fetch the contents of the specified child resource of the collection + * represented by this {@link WebDavClient} instance.</p> + * + * @see #isCollection(String) + * @return a <b>non-null</b> {@link InputStream}. + * @throws IOException if an I/O or network error occurred, or if the + * child specified represents a collection. + * @throws NullPointerException if the child was <b>null</b>. + */ + public InputStream get(String child) + throws NullPointerException, IOException { + if (child == null) throw new NullPointerException("Null child"); + if (! this.isCollection(child)) { + final Location location = this.getLocation(child); + final HttpClient client = new HttpClient(location); + client.setAcceptableStatus(200).connect("GET"); + return client.getResponseStream(); + } + throw new IOException("Child \"" + child + "\" is a collection"); + } + + /** + * <p>Delete the child resource (or collection) of the collection + * represented by this {@link WebDavClient} instance.</p> + * + * @return this {@link WebDavClient} instance. + * @throws IOException if an I/O or network error occurred, or if the + * child specified represents a collection. + * @throws NullPointerException if the child was <b>null</b>. + */ + public WebDavClient delete(String child) + throws NullPointerException, IOException { + if (child == null) throw new NullPointerException("Null child"); + final HttpClient client = new HttpClient(this.getLocation(child)); + client.setAcceptableStatus(204).connect("DELETE").disconnect(); + return this.refresh(); + } + + /** + * <p>Create a new collection as a child of the collection represented + * by this {@link WebDavClient} instance.</p> + * + * <p>In comparison to {@link #put(String)} and {@link #put(String, long)} + * this method will fail if the specified child already exist.</p> + * + * @see #hasChild(String) + * @return this {@link WebDavClient} instance. + * @throws IOException if an I/O or network error occurred, or if the + * child specified already exist. + * @throws NullPointerException if the child was <b>null</b>. + */ + public WebDavClient mkcol(String child) + throws NullPointerException, IOException { + if (child == null) throw new NullPointerException("Null child"); + if (this.hasChild(child)) + throw new IOException("Child \"" + child + "\" already exists"); + final Location location = this.resource.location.resolve(child); + final HttpClient client = new HttpClient(location); + client.setAcceptableStatus(201).connect("MKCOL").disconnect(); + return this.refresh(); + } + + /** + * <p>Create a new (or update the contents of a) child of of the collection + * represented by this {@link WebDavClient} instance.</p> + * + * <p>This method will behave exactly like the {@link #put(String, long)} + * method, but the data written to the returned {@link OutputStream} will + * be <i>buffered in memory</i> and will be transmitted to the remote + * server only when the {@link OutputStream#close()} method is called.</p> + * + * <p>If the returned {@link OutputStream} is garbage collected before the + * {@link OutputStream#close() close()} method is called, the entire + * transaction will be aborted and no connection to the remote server will + * be established.</p> + * + * <p>Use this method in extreme cases. In normal circumstances always rely + * on the {@link #put(String, long)} method.</p> + * + * @see #put(String, long) + * @return a <b>non-null</b> {@link OutputStream} instance. + * @throws NullPointerException if the child was <b>null</b>. + */ + public OutputStream put(final String child) + throws NullPointerException { + if (child == null) throw new NullPointerException("Null child"); + final WebDavClient client = this; + return new ByteArrayOutputStream() { + private boolean closed = false; + public void close() + throws IOException { + if (this.closed) return; + this.flush(); + OutputStream output = client.put(child, this.buf.length); + output.write(this.buf); + output.flush(); + output.close(); + } + + protected void finalize() + throws Throwable { + this.closed = true; + super.finalize(); + } + }; + } + + /** + * <p>Create a new (or update the contents of a) child of of the collection + * represented by this {@link WebDavClient} instance.</p> + * + * <p>If the specified child {@link #hasChild(String) already exists} on + * the remote server, it will be {@link #delete(String) deleted} before + * writing.</p> + * + * @return a <b>non-null</b> {@link OutputStream} instance. + * @throws NullPointerException if the child was <b>null</b>. + * @throws IOException if an I/O or network error occurred, or if the + * child specified already exist. + */ + public OutputStream put(String child, long length) + throws NullPointerException, IOException { + if (child == null) throw new NullPointerException("Null child"); + if (this.hasChild(child)) this.delete(child); + final Location location = this.resource.location.resolve(child); + final HttpClient client = new HttpClient(location); + client.setAcceptableStatuses(new int[] { 201, 204 }); + client.connect("PUT", length); + + final WebDavClient webdav = this; + return new BufferedOutputStream(client.getRequestStream()) { + boolean closed = false; + public void close() + throws IOException { + if (this.closed) return; + try { + super.close(); + } finally { + this.closed = true; + webdav.refresh(); + } + } + protected void finalize() + throws Throwable { + try { + this.close(); + } finally { + super.finalize(); + } + } + }; + } + + /** + * <p>Open the specified child collection of the collection represented by + * this {@link WebDavClient} as a new {@link WebDavClient} instance.</p> + * + * <p>If the specified child is "<code>.</code>" this method + * will behave exactly like {@link #refresh()} and <i>this instance</i> + * will be returned.</p> + * + * <p>If the specified child is "<code>..</code>" this method + * will behave exactly like {@link #parent()}.</p> + * + * @return a <b>non-null</b> {@link WebDavClient} instance. + * @throws NullPointerException if the child was <b>null</b>. + * @throws IOException if an I/O or network error occurred, or if the + * child specified did not exist. + */ + public WebDavClient open(String child) + throws NullPointerException, IOException { + if (child == null) throw new NullPointerException("Null child"); + if (".".equals(child)) return this.refresh(); + if ("..".equals(child)) return this.parent(); + if (resource.collection) { + Location loc = this.getLocation().resolve(this.getLocation(child)); + return new WebDavClient(loc); + } + throw new IOException("Child \"" + child + "\" is not a collection"); + } + + /** + * <p>Open the parent collection of the collection represented by this + * {@link WebDavClient} as a new {@link WebDavClient} instance.</p> + * + * @return a <b>non-null</b> {@link WebDavClient} instance. + * @throws IOException if an I/O or network error occurred, or if the + * child specified did not exist. + */ + public WebDavClient parent() + throws IOException { + final Location location = this.resource.location.resolve(".."); + return new WebDavClient(location); + } + + /* ====================================================================== */ + /* ACCESSOR METHODS */ + /* ====================================================================== */ + + /** + * <p>Return an {@link Iterator} over {@link String}s for all the children + * of the collection represented by this {@link WebDavClient} instance.</p> + */ + public Iterator iterator() { + return this.children.keySet().iterator(); + } + + /** + * <p>Checks if the collection represented by this {@link WebDavClient} + * contains the specified child.</p> + */ + public boolean hasChild(String child) { + return this.children.containsKey(child); + } + + /** + * <p>Return the {@link Location} associated with the collection + * represented by this {@link WebDavClient}.</p> + * + * <p>The returned {@link Location} can be different from the one specified + * at construction, in case the server redirected us upon connection.</p> + */ + public Location getLocation() { + return this.resource.location; + } + + /** + * <p>Return the content length (in bytes) of the collection represented + * by this {@link WebDavClient} as passed to us by the WebDAV server.</p> + */ + public long getContentLength() { + return this.resource.contentLength; + } + + /** + * <p>Return the content type (mime-type) of the collection represented + * by this {@link WebDavClient} as passed to us by the WebDAV server.</p> + */ + public String getContentType() { + return this.resource.contentType; + } + + /** + * <p>Return the last modified {@link Date} of the collection represented + * by this {@link WebDavClient} as passed to us by the WebDAV server.</p> + */ + public Date getLastModified() { + return this.resource.lastModified; + } + + /** + * <p>Return the creation {@link Date} of the collection represented + * by this {@link WebDavClient} as passed to us by the WebDAV server.</p> + */ + public Date getCreationDate() { + return this.resource.creationDate; + } + + /** + * <p>Return the {@link Location} associated with the specified child of + * the collection represented by this {@link WebDavClient}.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + public Location getLocation(String child) + throws IOException { + Location location = this.getResource(child).location; + return this.resource.location.resolve(location); + } + + /** + * <p>Checks if the specified child of the collection represented by this + * {@link WebDavClient} instance is a collection.</p> + */ + public boolean isCollection(String child) + throws IOException { + return this.getResource(child).collection; + } + + /** + * <p>Return the content length (in bytes) associated with the specified + * child of the collection represented by this {@link WebDavClient}.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + public long getContentLength(String child) + throws IOException { + return this.getResource(child).contentLength; + } + + /** + * <p>Return the content type (mime-type) associated with the specified + * child of the collection represented by this {@link WebDavClient}.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + public String getContentType(String child) + throws IOException { + return this.getResource(child).contentType; + } + + /** + * <p>Return the last modified {@link Date} associated with the specified + * child of the collection represented by this {@link WebDavClient}.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + public Date getLastModified(String child) + throws IOException { + return this.getResource(child).lastModified; + } + + /** + * <p>Return the creation {@link Date} associated with the specified + * child of the collection represented by this {@link WebDavClient}.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + public Date getCreationDate(String child) + throws IOException { + return this.getResource(child).creationDate; + } + + /* ====================================================================== */ + /* INTERNAL METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the resource associated with the specified child.</p> + * + * @throws IOException if the specified child does not exist. + * @throws NullPointerException if the specified child was <b>null</b>. + */ + private Resource getResource(String child) + throws IOException { + if (child == null) throw new NullPointerException(); + final Resource resource = (Resource) this.children.get(child); + if (resource == null) throw new IOException("Not found: " + child); + return resource; + } + + /** + * <p>Contact the remote WebDAV server and fetch all properties.</p> + */ + private void reload(Location location) + throws IOException { + + /* Do an OPTIONS over onto the location */ + location = this.options(location); + + /* Do a PROPFIND to figure out the properties and the children */ + final Iterator iterator = this.propfind(location).iterator(); + final Map children = new HashMap(); + while (iterator.hasNext()) { + final Resource resource = (Resource) iterator.next(); + final Path path = resource.location.getPath(); + if (path.size() == 0) { + resource.location = location.resolve(resource.location); + this.resource = resource; + } else if (path.size() == 1) { + final Path.Element element = (Path.Element) path.get(0); + if ("..".equals(element.getName())) continue; + children.put(element.toString(), resource); + } + } + + /* Check if the current resource was discovered */ + if (this.resource == null) + throw new IOException("Current resource not returned in PROOPFIND"); + + /* Don't actually allow resources to be modified */ + this.children = Collections.unmodifiableMap(children); + } + + /** + * <p>Contact the remote WebDAV server and do an OPTIONS lookup.</p> + */ + private Location options(Location location) + throws IOException { + /* Create the new HttpClient instance associated with the location */ + final HttpClient client = new HttpClient(location); + client.setAcceptableStatus(200).connect("OPTIONS", true).disconnect(); + + /* Check that the remote server returned the "Dav" header */ + final List davHeader = client.getResponseHeaderValues("dav"); + if (davHeader == null) { + throw new IOException("Server did not respond with a DAV header"); + } + + /* Check if the OPTIONS request contained the DAV header */ + final Iterator iterator = davHeader.iterator(); + boolean foundLevel1 = false; + while (iterator.hasNext() && (! foundLevel1)) { + String value = (String) iterator.next(); + StringTokenizer tokenizer = new StringTokenizer(value, ","); + while (tokenizer.hasMoreTokens()) { + if (! "1".equals(tokenizer.nextToken().trim())) continue; + foundLevel1 = true; + break; + } + } + + /* Return the (possibly redirected) location or fail miserably */ + if (foundLevel1) return client.getLocation(); + throw new IOException("Server doesn't support DAV Level 1"); + } + + /** + * <p>Contact the remote WebDAV server and do a PROPFIND lookup, returning + * a {@link List} of all scavenged resources.</p> + */ + private List propfind(Location location) + throws IOException { + /* Create the new HttpClient instance associated with the location */ + final HttpClient client = new HttpClient(location); + client.addRequestHeader("Depth", "1"); + client.setAcceptableStatus(207).connect("PROPFIND", true); + + /* Get the XML SAX Parser and parse the output of the PROPFIND */ + try { + final SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setValidating(false); + factory.setNamespaceAware(true); + final SAXParser parser = factory.newSAXParser(); + final String systemId = location.toString(); + final InputSource source = new InputSource(systemId); + final Handler handler = new Handler(location); + source.setByteStream(client.getResponseStream()); + parser.parse(source, handler); + return handler.list; + + } catch (ParserConfigurationException exception) { + Exception throwable = new IOException("Error creating XML parser"); + throw (IOException) throwable.initCause(exception); + } catch (SAXException exception) { + Exception throwable = new IOException("Error creating XML parser"); + throw (IOException) throwable.initCause(exception); + } finally { + client.disconnect(); + } + } + + /* ====================================================================== */ + /* INTERNAL CLASSES */ + /* ====================================================================== */ + + /** + * <p>An internal XML {@link DefaultHandler} used to parse out the various + * details of a PROPFIND response.</p> + */ + private static final class Handler extends DefaultHandler { + + /* ================================================================== */ + /* PSEUDO-XPATH LOCATIONS FOR QUICK-AND-DIRTY LOCATION LOOKUP */ + /* ================================================================== */ + private static final String RESPONSE_PATH = "/multistatus/response"; + private static final String HREF_PATH = "/multistatus/response/href"; + private static final String COLLECTION_PATH = + "/multistatus/response/propstat/prop/resourcetype/collection"; + private static final String GETCONTENTTYPE_PATH = + "/multistatus/response/propstat/prop/getcontenttype"; + private static final String GETLASTMODIFIED_PATH = + "/multistatus/response/propstat/prop/getlastmodified"; + private static final String GETCONTENTLENGTH_PATH = + "/multistatus/response/propstat/prop/getcontentlength"; + private static final String CREATIONDATE_PATH = + "/multistatus/response/propstat/prop/creationdate"; + + /** <p>The {@link Location} for resolving all other links.</p> */ + private final Location base; + /** <p>The {@link List} of all scavenged resources.</p> */ + private final List list = new ArrayList(); + /** <p>The resource currently being processed.</p> */ + private Resource rsrc = null; + /** <p>A {@link StringBuffer} holding character data.</p> */ + private StringBuffer buff = null; + /** <p>A {@link Stack} for quick-and-dirty pseudo XPath lookups.</p> */ + private Stack stack = new Stack(); + + /** + * <p>Create a new instance specifying the base {@link Location}.</p> + */ + private Handler(Location location) { + this.base = location; + } + + /** + * <p>Push an element name in the stack for pseudo-XPath lookups.</p> + * + * @return a {@link String} like <code>/element/element/element</code>. + */ + private String pushPath(String path) { + this.stack.push(path.toLowerCase()); + final StringBuffer buffer = new StringBuffer(); + for (int x = 0; x < this.stack.size(); x ++) + buffer.append('/').append(this.stack.get(x)); + return buffer.toString(); + } + + /** + * <p>Pop the last element name from the pseudo-XPath lookup stack.</p> + * + * @return a {@link String} like <code>/element/element/element</code>. + */ + private String popPath(String path) + throws SAXException { + final StringBuffer buffer = new StringBuffer(); + final String last = (String) this.stack.pop(); + if (path.toLowerCase().equals(last)) { + for (int x = 0; x < this.stack.size(); x ++) + buffer.append('/').append(this.stack.get(x)); + return buffer.append('/').append(last).toString(); + } + throw new SAXException("Tag <" + path + "/> unbalanced at path \"" + + pushPath(last) + "\""); + } + + /** + * <p>Handle the start-of-element SAX event.</p> + */ + public void startElement(String uri, String l, String q, Attributes a) + throws SAXException { + if (! "DAV:".equals(uri.toUpperCase())) return; + final String path = this.pushPath(l); + + if (RESPONSE_PATH.equals(path)) { + this.rsrc = new Resource(); + + } else if (COLLECTION_PATH.equals(path)) { + if (this.rsrc != null) this.rsrc.collection = true; + + } else if (GETCONTENTTYPE_PATH.equals(path) || + GETLASTMODIFIED_PATH.equals(path) || + GETCONTENTLENGTH_PATH.equals(path) || + CREATIONDATE_PATH.equals(path) || + HREF_PATH.equals(path)) { + this.buff = new StringBuffer(); + } + } + + /** + * <p>Handle the end-of-element SAX event.</p> + */ + public void endElement(String uri, String l, String q) + throws SAXException { + if (! "DAV:".equals(uri.toUpperCase())) return; + final String path = this.popPath(l); + final String data = this.resetBuffer(); + + if (RESPONSE_PATH.equals(path)) { + if (this.rsrc != null) { + if (this.rsrc.location != null) { + if (this.rsrc.location.isAbsolute()) { + final String z = this.rsrc.location.toString(); + throw new SAXException("Unresolved location " + z); + } else { + this.list.add(this.rsrc); + } + } else { + throw new SAXException("Null location for resource"); + } + } + + } else if (HREF_PATH.equals(path)) { + if (this.rsrc != null) try { + final Location resolved = this.base.resolve(data); + this.rsrc.location = this.base.relativize(resolved); + if (! this.rsrc.location.isRelative()) + throw new SAXException("Unable to relativize location " + + this.rsrc.location); + } catch (MalformedURLException exception) { + final String msg = "Unable to resolve URL \"" + data + "\""; + SAXException throwable = new SAXException(msg, exception); + throw (SAXException) throwable.initCause(exception); + } + + } else if (CREATIONDATE_PATH.equals(path)) { + if (this.rsrc != null) + this.rsrc.creationDate = StringTools.parseIsoDate(data); + + } else if (GETCONTENTTYPE_PATH.equals(path)) { + if (this.rsrc != null) this.rsrc.contentType = data; + + } else if (GETLASTMODIFIED_PATH.equals(path)) { + if (this.rsrc != null) + this.rsrc.lastModified = StringTools.parseHttpDate(data); + + } else if (GETCONTENTLENGTH_PATH.equals(path)) { + if (this.rsrc != null) { + Long length = StringTools.parseNumber(data); + if (length != null) { + this.rsrc.contentLength = length.longValue(); + } + } + } + } + + /** + * <p>Handle SAX characters notification.</p> + */ + public void characters(char buffer[], int offset, int length) { + if (this.buff != null) this.buff.append(buffer, offset, length); + } + + /** + * <p>Reset the current characters buffer and return it as a + * {@link String}.</p> + */ + private String resetBuffer() { + if (this.buff == null) return null; + if (this.buff.length() == 0) { + this.buff = null; + return null; + } + final String value = this.buff.toString(); + this.buff = null; + return value; + } + } + + /** + * <p>A simple class holding the core resource properties.</p> + */ + private static class Resource { + private Location location = null; + private boolean collection = false; + private long contentLength = -1; + private String contentType = null; + private Date lastModified = null; + private Date creationDate = null; + } + + /* ====================================================================== */ + /* COMMAND LINE CLIENT */ + /* ====================================================================== */ + + /** + * <p>A command-line interface to a WebDAV repository.</p> + * + * <p>When invoked from the command line, this class requires one only + * argument, the URL location of the WebDAV repository to connect to.</p> + * + * <p>After connection this method will interact with the user using an + * extremely simple console-based interface.</p> + */ + public static void main(String args[]) + throws IOException { + final InputStreamReader r = new InputStreamReader(System.in); + final BufferedReader in = new BufferedReader(r); + WebDavClient client = new WebDavClient(Location.parse(args[0])); + + while (true) try { + System.out.print("[" + client.getLocation() + "] -> "); + args = parse(in.readLine()); + if (args == null) break; + if (args[0].equals("list")) { + if (args[1] == null) list(client, System.out); + else list(client.open(args[1]), System.out); + + } else if (args[0].equals("refresh")) { + client = client.refresh(); + + } else if (args[0].equals("get")) { + if (args[1] != null) { + final InputStream input = client.get(args[1]); + final File file = new File(args[2]).getCanonicalFile(); + final OutputStream output = new FileOutputStream(file); + final long bytes = StreamTools.copy(input, output); + System.out.println("Fetched child \"" + args[1] + + "\" to file \"" + file + "\" (" + + bytes + " bytes)"); + } + else System.out.print("Can't \"get\" null"); + + } else if (args[0].equals("put")) { + if (args[1] != null) { + final File file = new File(args[1]).getCanonicalFile(); + final InputStream input = new FileInputStream(file); + final OutputStream output = client.put(args[2], file.length()); + final long bytes = StreamTools.copy(input, output); + System.out.println("Uploaded file \"" + file + + "\" to child \"" + args[2] + "\" (" + + bytes + " bytes)"); + } + else System.out.print("Can't \"put\" null"); + + } else if (args[0].equals("mkcol")) { + if (args[1] != null) { + client.mkcol(args[1]); + System.out.println("Created \"" + args[1] + "\""); + } + else System.out.print("Can't \"mkcol\" null"); + + } else if (args[0].equals("delete")) { + if (args[1] != null) { + client.delete(args[1]); + System.out.println("Deleted \"" + args[1] + "\""); + } + else System.out.print("Can't \"delete\" null"); + + } else if (args[0].equals("cd")) { + if (args[1] != null) client = client.open(args[1]); + else System.out.print("Can't \"cd\" to null"); + + } else if (args[0].equals("quit")) { + break; + + } else { + System.out.print("Invalid command \"" + args[0] + "\". "); + System.out.println("Valid commands are:"); + System.out.println(" - \"list\" list the children child"); + System.out.println(" - \"get\" fetch the specified child"); + System.out.println(" - \"put\" put the specified child"); + System.out.println(" - \"mkcol\" create a collection"); + System.out.println(" - \"delete\" delete a child"); + System.out.println(" - \"put\" put the specified resource"); + System.out.println(" - \"cd\" change the location"); + System.out.println(" - \"refresh\" refresh this location"); + System.out.println(" - \"quit\" quit this application"); + } + } catch (Exception exception) { + exception.printStackTrace(System.err); + } + System.err.println(); + } + + /** + * <p>Parse a line entered by the user returning a three-tokens argument + * list (command, argument 1, argument 2)</p> + */ + private static String[] parse(String line) { + if (line == null) return null; + final String array[] = new String[3]; + final StringTokenizer tokenizer = new StringTokenizer(line); + int offset = 0; + while (tokenizer.hasMoreTokens() && (offset < 3)) + array[offset ++] = tokenizer.nextToken(); + if (array[0] == null) return null; + if (array[2] == null) array[2] = array[1]; + return array; + } + + /** + * <p>Pseudo-nicely display a list of the children of a collection</p> + */ + private static void list(WebDavClient client, PrintStream out) + throws IOException { + out.print("C | "); + out.print("CONTENT TYPE | "); + out.print("CREATED | "); + out.print("MODIFIED | "); + out.print("SIZE | "); + out.println("NAME "); + for (Iterator iterator = client.iterator(); iterator.hasNext() ; ) { + final StringBuffer buffer = new StringBuffer(); + String child = (String) iterator.next(); + if (client.isCollection(child)) buffer.append("* | "); + else buffer.append(" | "); + format(buffer, client.getContentType(child), 15).append(" | "); + format(buffer, client.getCreationDate(child), 19).append(" | "); + format(buffer, client.getLastModified(child), 19).append(" | "); + format(buffer, client.getContentLength(child), 10).append(" | "); + out.println(buffer.append(child)); + } + } + + /** <p>Format a number aligning it to the right of a string.</p> */ + private static StringBuffer format(StringBuffer buf, long num, int len) { + final String data; + if (num < 0) data = ""; + else data = Long.toString(num); + final int spaces = len - data.length(); + for (int x = 0; x < spaces; x++) buf.append(' '); + buf.append(data); + return buf; + } + + /** <p>Format a string into an exact number of characters.</p> */ + private static StringBuffer format(StringBuffer buf, Object obj, int len) { + final String string; + if (obj == null) { + string = ("[null]"); + } else if (obj instanceof Date) { + SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + string = f.format((Date) obj); + } else { + string = obj.toString(); + } + final StringBuffer buffer = new StringBuffer(string); + for (int x = string.length(); x < len; x ++) buffer.append(' '); + return buf.append(buffer.substring(0, len)); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html new file mode 100644 index 000000000..9ca0cb4fa --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html @@ -0,0 +1,12 @@ +<html> + <head> + <title>HTTP Utilities</title> + </head> + <body> + <p> + This package contains a number of utility classes to access + <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> and + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> servers. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java new file mode 100644 index 000000000..24964795c --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java @@ -0,0 +1,805 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.location; + +import it.could.util.StringTools; +import it.could.util.encoding.Encodable; +import it.could.util.encoding.EncodingTools; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * <p>An utility class representing an HTTP-like URL.</p> + * + * <p>This class can be used to represent any URL that roughly uses the HTTP + * format. Compared to the standard {@link java.net.URL} class, the scheme part + * of the a {@link Location} is never checked, and it's up to the application + * to verify its correctness, while compared to the {@link java.net.URI} class, + * its parsing mechanism is a lot more relaxed (be liberal in what you accept, + * be strict in what you send).</p> + * + * <p>For a bigger picture on how this class works, this is an easy-to-read + * representation of what the different parts of a {@link Location} are:</p> + * + * <div align="center"> + * <a href="url.pdf" target="_new" title="PDF Version"> + * <img src="url.gif" alt="URL components" border="0"> + * </a> + * </div> + * + * <p>One important difference between this implementation and the description + * of <a href="http://www.ietf.org/rfc/rfc1738.txt">URLs</a> and + * <a href="http://www.ietf.org/rfc/rfc2396.txt">URIs</a> is that parameter + * paths are represented <i>only at the end of the entire path structure</i> + * rather than for each path element. This over-simplification allows easy + * relativization of {@link Location}s when used with servlet containers, which + * normally use path parameters to encode the session id.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class Location implements Encodable { + + /** <p>A {@link Map} of schemes and their default port number.</p> */ + private static final Map schemePorts = new HashMap(); + static { + schemePorts.put("acap", new Integer( 674)); + schemePorts.put("dav", new Integer( 80)); + schemePorts.put("ftp", new Integer( 21)); + schemePorts.put("gopher", new Integer( 70)); + schemePorts.put("http", new Integer( 80)); + schemePorts.put("https", new Integer( 443)); + schemePorts.put("imap", new Integer( 143)); + schemePorts.put("ldap", new Integer( 389)); + schemePorts.put("mailto", new Integer( 25)); + schemePorts.put("news", new Integer( 119)); + schemePorts.put("nntp", new Integer( 119)); + schemePorts.put("pop", new Integer( 110)); + schemePorts.put("rtsp", new Integer( 554)); + schemePorts.put("sip", new Integer(5060)); + schemePorts.put("sips", new Integer(5061)); + schemePorts.put("snmp", new Integer( 161)); + schemePorts.put("telnet", new Integer( 23)); + schemePorts.put("tftp", new Integer( 69)); + } + + /** <p>The {@link List} of schemes of this {@link Location}.</p> */ + private final Schemes schemes; + /** <p>The {@link Authority} of this {@link Location}.</p> */ + private final Authority authority; + /** <p>The {@link Path} of this {@link Location}.</p> */ + private final Path path; + /** <p>The {@link Parameters} of this {@link Location}.</p> */ + private final Parameters parameters; + /** <p>The fragment part of this {@link Location}.</p> */ + private final String fragment; + /** <p>The string representation of this {@link Location}.</p> */ + private final String string; + + /** + * <p>Create a new {@link Location} instance.</p> + */ + public Location(Schemes schemes, Authority authority, Path path, + Parameters parameters, String fragment) + throws MalformedURLException { + if ((schemes == null) && (authority != null)) + throw new MalformedURLException("No schemes specified"); + if ((schemes != null) && (authority == null)) + throw new MalformedURLException("No authority specified"); + if (path == null) throw new MalformedURLException("No path specified"); + + this.schemes = schemes; + this.authority = authority; + this.path = path; + this.parameters = parameters; + this.fragment = fragment; + this.string = EncodingTools.toString(this); + } + + /* ====================================================================== */ + /* STATIC CONSTRUCTION METHODS */ + /* ====================================================================== */ + + public static Location parse(String url) + throws MalformedURLException { + try { + return parse(url, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + public static Location parse(String url, String encoding) + throws MalformedURLException, UnsupportedEncodingException { + if (url == null) return null;; + if (encoding == null) encoding = DEFAULT_ENCODING; + final String components[] = parseComponents(url); + final Schemes schemes = parseSchemes(components[0], encoding); + final int port = findPort(schemes, encoding); + final Authority auth = parseAuthority(components[1], port, encoding); + final Path path = Path.parse(components[2], encoding); + final Parameters params = Parameters.parse(components[3], '&', encoding); + final String fragment = components[4]; + return new Location(schemes, auth, path, params, fragment); + } + + /* ====================================================================== */ + /* ACCESSOR METHODS */ + /* ====================================================================== */ + + /** + * <p>Return an unmodifiable {@link Schemes list of all schemes} for this + * {@link Location} instance or <b>null</b>.</p> + */ + public Schemes getSchemes() { + return this.schemes; + } + + /** + * <p>Return the {@link Location.Authority Authority} part for this + * {@link Location} or <b>null</b>.</p> + */ + public Authority getAuthority() { + return this.authority; + } + + /** + * <p>Return the <b>non-null</b> {@link Path Path} structure + * associated with this {@link Location} instance.</p> + */ + public Path getPath() { + return this.path; + } + + /** + * <p>Return an unmodifiable {@link Parameters list of all parameters} + * parsed from this {@link Location}'s query string or <b>null</b>.</p> + */ + public Parameters getParameters() { + return this.parameters; + } + + /** + * <p>Return the fragment of this {@link Location} unencoded.</p> + */ + public String getFragment() { + return this.fragment; + } + + /* ====================================================================== */ + /* OBJECT METHODS */ + /* ====================================================================== */ + + /** + * <p>Check if the specified {@link Object} is equal to this instance.</p> + * + * <p>The specified {@link Object} must be a <b>non-null</b> + * {@link Location} instance whose {@link #toString() string value} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Location)) { + return this.string.equals(((Location)object).string); + } else { + return false; + } + } + + /** + * <p>Return the hash code value for this {@link Location} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Return the {@link String} representation of this {@link Location} + * instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the {@link String} representation of this {@link Location} + * instance using the specified character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + final StringBuffer buffer = new StringBuffer(); + + /* Render the schemes */ + if (this.schemes != null) + buffer.append(this.schemes.toString(encoding)).append("://"); + + /* Render the authority part */ + if (this.authority != null) + buffer.append(this.authority.toString(encoding)); + + /* Render the paths */ + buffer.append(this.path.toString(encoding)); + + /* Render the query string */ + if (this.parameters != null) + buffer.append('?').append(this.parameters.toString(encoding)); + + /* Render the fragment */ + if (this.fragment != null) { + buffer.append('#'); + buffer.append(EncodingTools.urlEncode(this.fragment, encoding)); + } + + /* Return the string */ + return buffer.toString(); + } + + /* ====================================================================== */ + /* PUBLIC METHODS */ + /* ====================================================================== */ + + /** + * <p>Checks whether this {@link Location} is absolute or not.</p> + * + * <p>This method must not be confused with the similarly named + * {@link Path#isAbsolute() Path.isAbsolute()} method. + * This method will check whether the full {@link Location} is absolute (it + * has a scheme), while the one exposed by the {@link Path Path} + * class will check if the path is absolute.</p> + */ + public boolean isAbsolute() { + return this.schemes != null && this.authority != null; + } + + public boolean isRelative() { + return ! (this.isAbsolute() || this.path.isAbsolute()); + } + + public boolean isAuthoritative(Location location) { + if (! this.isAbsolute()) return false; + if (! location.isAbsolute()) return true; + return this.schemes.equals(location.schemes) && + this.authority.equals(location.authority); + } + + /* ====================================================================== */ + /* RESOLUTION METHODS */ + /* ====================================================================== */ + + public Location resolve(String url) + throws MalformedURLException { + try { + return this.resolve(parse(url, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + public Location resolve(String url, String encoding) + throws MalformedURLException, UnsupportedEncodingException { + if (encoding == null) encoding = DEFAULT_ENCODING; + return this.resolve(parse(url, encoding)); + } + + public Location resolve(Location location) { + if (! this.isAuthoritative(location)) return location; + + /* Schemes are the same */ + final Schemes schemes = this.schemes; + + /* Authority needs to be merged (for username and password) */ + final Authority auth; + if (location.authority != null) { + final String username = location.authority.username != null ? + location.authority.username : + this.authority.username; + final String password = location.authority.password != null ? + location.authority.password : + this.authority.password; + final String host = location.authority.host; + final int port = location.authority.port; + auth = new Authority(username, password, host, port); + } else { + auth = this.authority; + } + + /* Path can be resolved */ + final Path path = this.path.resolve(location.path); + + /* Parametrs and fragment are the ones of the target */ + final Parameters params = location.parameters; + final String fragment = location.fragment; + + /* Create a new {@link Location} instance */ + try { + return new Location(schemes, auth, path, params, fragment); + } catch (MalformedURLException exception) { + /* Should really never happen */ + Error error = new InternalError("Can't instantiate Location"); + throw (Error) error.initCause(exception); + } + } + + /* ====================================================================== */ + /* RELATIVIZATION METHODS */ + /* ====================================================================== */ + + public Location relativize(String url) + throws MalformedURLException { + try { + return this.relativize(parse(url, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + public Location relativize(String url, String encoding) + throws MalformedURLException, UnsupportedEncodingException { + if (encoding == null) encoding = DEFAULT_ENCODING; + return this.relativize(parse(url, encoding)); + } + + public Location relativize(Location location) { + final Path path; + if (!location.isAbsolute()) { + /* Target location is not absolute, its path might */ + path = this.path.relativize(location.path); + } else { + if (this.isAuthoritative(location)) { + /* Target location is not on the same authority, process path */ + path = this.path.relativize(location.path); + } else { + /* Not authoritative for a non-relative location, yah! */ + return location; + } + } + try { + return new Location(null, null, path, location.parameters, + location.fragment); + } catch (MalformedURLException exception) { + /* Should really never happen */ + Error error = new InternalError("Can't instantiate Location"); + throw (Error) error.initCause(exception); + } + } + + /* ====================================================================== */ + /* INTERNAL PARSING ROUTINES */ + /* ====================================================================== */ + + /** + * <p>Return the port number associated with the specified schemes.</p> + */ + public static int findPort(List schemes, String encoding) + throws UnsupportedEncodingException { + if (schemes == null) return -1; + if (schemes.size() < 1) return -1; + Integer p = (Integer) schemePorts.get(schemes.get(schemes.size() - 1)); + return p == null ? -1 : p.intValue(); + } + + /** + * <p>Parse <code>scheme://authority/path?query#fragment</code>.</p> + * + * @return an array of five {@link String}s: scheme (0), authority (1), + * path (2), query (3) and fragment (4). + */ + private static String[] parseComponents(String url) + throws MalformedURLException { + /* Scheme, easy and simple */ + final String scheme; + final String afterScheme; + final int schemeEnd = url.indexOf(":/"); + if (schemeEnd > 0) { + scheme = url.substring(0, schemeEnd).toLowerCase(); + afterScheme = url.substring(schemeEnd + 2); + } else if (schemeEnd == 0) { + throw new MalformedURLException("Missing scheme"); + } else { + scheme = null; + afterScheme = url; + } + + /* Authority (can be tricky because it can be emtpy) */ + final String auth; + final String afterAuth; + if (scheme == null) { + // --> /path... or path... + afterAuth = afterScheme; + auth = null; + } else if (afterScheme.length() > 0 && afterScheme.charAt(0) == '/') { + // --> scheme://... + final int pathStart = afterScheme.indexOf('/', 1); + if (pathStart == 1) { + // --> scheme:///path... + afterAuth = afterScheme.substring(pathStart); + auth = null; + } else if (pathStart > 1) { + // --> scheme://authority/path... + afterAuth = afterScheme.substring(pathStart); + auth = afterScheme.substring(1, pathStart); + } else { + // --> scheme://authority (but no slashes for the path) + final int authEnds = StringTools.findFirst(afterScheme, "?#"); + if (authEnds < 0) { + // --> scheme://authority (that's it, return) + auth = afterScheme.substring(1); + return new String[] { scheme, auth, "/", null, null }; + } + // --> scheme://authority?... or scheme://authority#... + auth = afterScheme.substring(1, authEnds); + afterAuth = "/" + afterScheme.substring(authEnds); + } + } else { + // --> scheme:/path... + afterAuth = url.substring(schemeEnd + 1); + auth = null; + } + + /* Path, can be terminated by '?' or '#' whichever is first */ + final int pathEnds = StringTools.findFirst(afterAuth, "?#"); + if (pathEnds < 0) { + // --> ...path... (no fragment or query, return now) + return new String[] { scheme, auth, afterAuth, null, null }; + } + + /* We have either a query, a fragment or both after the path */ + final String path = afterAuth.substring(0, pathEnds); + final String afterPath = afterAuth.substring(pathEnds + 1); + + /* Query? The query can contain a "#" and has an extra fragment */ + if (afterAuth.charAt(pathEnds) == '?') { + final int fragmPos = afterPath.indexOf('#'); + if (fragmPos < 0) { + // --> ...path...?... (no fragment) + return new String[] { scheme, auth, path, afterPath, null }; + } + + // --> ...path...?...#... (has also a fragment) + final String query = afterPath.substring(1, fragmPos); + final String fragm = afterPath.substring(fragmPos + 1); + return new String[] { scheme, auth, path, query, fragm }; + } + + // --> ...path...#... (a path followed by a fragment but no query) + return new String[] { scheme, auth, path, null, afterPath }; + } + + /** + * <p>Parse <code>scheme:scheme:scheme...</code>.</p> + */ + private static Schemes parseSchemes(String scheme, String encoding) + throws MalformedURLException, UnsupportedEncodingException { + if (scheme == null) return null; + final String split[] = StringTools.splitAll(scheme, ':'); + List list = new ArrayList(); + for (int x = 0; x < split.length; x++) { + if (split[x] == null) continue; + list.add(EncodingTools.urlDecode(split[x], encoding)); + } + if (list.size() != 0) return new Schemes(list); + throw new MalformedURLException("Empty scheme detected"); + } + + /** + * <p>Parse <code>username:password@hostname:port</code>.</p> + */ + private static Authority parseAuthority(String auth, int defaultPort, + String encoding) + throws MalformedURLException, UnsupportedEncodingException { + if (auth == null) return null; + final String split[] = StringTools.splitOnce(auth, '@', true); + final String uinfo[] = StringTools.splitOnce(split[0], ':', false); + final String hinfo[] = StringTools.splitOnce(split[1], ':', false); + final int port; + + if ((split[0] != null) && (split[1] == null)) + throw new MalformedURLException("Missing required host info part"); + if ((uinfo[0] == null) && (uinfo[1] != null)) + throw new MalformedURLException("Password specified without user"); + if ((hinfo[0] == null) && (hinfo[1] != null)) + throw new MalformedURLException("Port specified without host"); + try { + if (hinfo[1] != null) { + final int parsedPort = Integer.parseInt(hinfo[1]); + if ((parsedPort < 1) || (parsedPort > 65535)) { + final String message = "Invalid port number " + parsedPort; + throw new MalformedURLException(message); + } + /* If the specified port is the default one, ignore it! */ + if (defaultPort == parsedPort) port = -1; + else port = parsedPort; + } else { + port = -1; + } + } catch (NumberFormatException exception) { + throw new MalformedURLException("Specified port is not a number"); + } + return new Authority(EncodingTools.urlDecode(uinfo[0], encoding), + EncodingTools.urlDecode(uinfo[1], encoding), + EncodingTools.urlDecode(hinfo[0], encoding), + port); + } + + /* ====================================================================== */ + /* PUBLIC INNER CLASSES */ + /* ====================================================================== */ + + /** + * <p>The {@link Location.Schemes Schemes} class represents an unmodifiable + * ordered collection of {@link String} schemes for a {@link Location}.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ + public static class Schemes extends AbstractList implements Encodable { + /** <p>All the {@link String} schemes in order.</p> */ + private final String schemes[]; + /** <p>The {@link String} representation of this instance.</p> */ + private final String string; + + /** + * <p>Create a new {@link Schemes} instance.</p> + */ + private Schemes(List schemes) { + final int size = schemes.size(); + this.schemes = (String []) schemes.toArray(new String[size]); + this.string = EncodingTools.toString(this); + } + + /** + * <p>Return the {@link String} scheme at the specified index.</p> + */ + public Object get(int index) { + return this.schemes[index]; + } + + /** + * <p>Return the number of {@link String} schemes contained by this + * {@link Location.Schemes Schemes} instance.</p> + */ + public int size() { + return this.schemes.length; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Location.Schemes Schemes} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Location.Schemes Schemes} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + final StringBuffer buffer = new StringBuffer(); + for (int x = 0; x < this.schemes.length; x ++) { + buffer.append(':'); + buffer.append(EncodingTools.urlEncode(this.schemes[x], encoding)); + } + return buffer.substring(1); + } + + /** + * <p>Return the hash code value for this + * {@link Location.Schemes Schemes} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Location.Schemes Schemes} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, it is a {@link Location.Schemes Schemes} + * instance, and its {@link #toString() string representation} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Schemes)) { + return this.string.equals(((Schemes) object).string); + } else { + return false; + } + } + } + + /* ====================================================================== */ + + /** + * <p>The {@link Location.Authority Authority} class represents the autority + * and user information for a {@link Location}.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ + public static class Authority implements Encodable { + /** <p>The username of this instance (decoded).</p> */ + private final String username; + /** <p>The password of this instance (decoded).</p> */ + private final String password; + /** <p>The host name of this instance (decoded).</p> */ + private final String host; + /** <p>The port number of this instance.</p> */ + private final int port; + /** <p>The encoded host and port representation.</p> */ + private final String hostinfo; + /** <p>The encoded string representation of this instance.</p> */ + private final String string; + + /** + * <p>Create a new {@link Location.Authority Authority} instance.</p> + */ + private Authority(String user, String pass, String host, int port) { + this.username = user; + this.password = pass; + this.host = host; + this.port = port; + try { + this.hostinfo = this.getHostInfo(DEFAULT_ENCODING); + this.string = this.toString(DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Default encoding \"" + DEFAULT_ENCODING + + "\" not supported by the platform"; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Returns the decoded user name.</p> + */ + public String getUsername() { + return this.username; + } + + /** + * <p>Returns the decoded password.</p> + */ + public String getPassword() { + return this.password; + } + + /** + * <p>Returns the "user info" field.</p> + * + * <p>This method will concatenate the username and password using the + * colon character and return a <b>non-null</b> {@link String} only if + * both of them are <b>non-null</b>.</p> + */ + public String getUserInfo() { + if ((this.username == null) || (this.password == null)) return null; + return this.username + ':' + this.password; + } + + /** + * <p>Returns the decoded host name.</p> + */ + public String getHost() { + return this.host; + } + + /** + * <p>Returns the port number.</p> + */ + public int getPort() { + return this.port; + } + + /** + * <p>Returns the host info part of the + * {@link Location.Authority Authority}.</p> + * + * <p>This is the encoded representation of the + * {@link #getUsername() user name} optionally follwed by the colon (:) + * character and the encoded {@link #getPassword() password}.</p> + */ + public String getHostInfo() { + return this.hostinfo; + } + + /** + * <p>Returns the host info part of the + * {@link Location.Authority Authority} using the specified character + * encoding.</p> + * + * <p>This is the encoded representation of the + * {@link #getUsername() user name} optionally follwed by the colon (:) + * character and the encoded {@link #getPassword() password}.</p> + */ + public String getHostInfo(String encoding) + throws UnsupportedEncodingException { + final StringBuffer hostinfo = new StringBuffer(); + hostinfo.append(EncodingTools.urlEncode(this.host, encoding)); + if (port >= 0) hostinfo.append(':').append(port); + return hostinfo.toString(); + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Location.Authority Authority} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Location.Authority Authority} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + final StringBuffer buffer; + if (this.username != null) { + buffer = new StringBuffer(); + buffer.append(EncodingTools.urlEncode(this.username, encoding)); + if (this.password != null) { + buffer.append(':'); + buffer.append(EncodingTools.urlEncode(this.password, encoding)); + } + } else { + buffer = null; + } + + if (buffer == null) return this.getHostInfo(encoding); + buffer.append('@').append(this.getHostInfo(encoding)); + return buffer.toString(); + } + + /** + * <p>Return the hash code value for this + * {@link Location.Authority Authority} instance.</p> + */ + public int hashCode() { + return this.hostinfo.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Location.Authority Authority} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, it is a {@link Location.Authority Authority} + * instance, and its {@link #getHostInfo() host info} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Authority)) { + return this.hostinfo.equals(((Authority) object).hostinfo); + } else { + return false; + } + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Parameters.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Parameters.java new file mode 100644 index 000000000..3ffa0bac7 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Parameters.java @@ -0,0 +1,474 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.location; + +import it.could.util.StringTools; +import it.could.util.encoding.Encodable; +import it.could.util.encoding.EncodingTools; + +import java.io.UnsupportedEncodingException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * <p>The {@link Parameters Parameters} class represents a never empty and + * immutable {@link List} of {@link Parameters.Parameter Parameter} instances, + * normally created parsing a query string.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class Parameters extends AbstractList implements Encodable { + + /** <p>The default delimiter for a {@link Parameters} instance.</p> */ + public static final char DEFAULT_DELIMITER = '&'; + + /** <p>All the {@link Parameter}s in order.</p> */ + private final Parameter parameters[]; + /** <p>The {@link Map} view over all parameters (names are keys).</p> */ + private final Map map; + /** <p>The {@link Set} of all parameter names.</p> */ + final Set names; + /** <p>The character delimiting different parameters.</p> */ + private final char delimiter; + /** <p>The encoded {@link String} representation of this.</p> */ + private final String string; + + /** + * <p>Create a new {@link Parameters Parameters} instance from + * a {@link List} of {@link Parameters.Parameter Parameter} instances + * using the {@link #DEFAULT_DELIMITER default parameter delimiter}.</p> + * + * @throws NullPointerExceptoin if the {@link List} was <b>null</b>. + * @throws IllegalArgumentException if the {@link List} was empty. + * @throws ClassCastException if any of the elements in the {@link List} was + * not a {@link Parameters.Parameter Parameter}. + */ + public Parameters(List parameters) { + this(parameters, DEFAULT_DELIMITER); + } + + /** + * <p>Create a new {@link Parameters Parameters} instance from + * a {@link List} of {@link Parameters.Parameter Parameter} instances + * using the specified character as the parameters delimiter.</p> + * + * @throws NullPointerExceptoin if the {@link List} was <b>null</b>. + * @throws IllegalArgumentException if the {@link List} was empty. + * @throws ClassCastException if any of the elements in the {@link List} was + * not a {@link Parameters.Parameter Parameter}. + */ + public Parameters(List parameters, char delimiter) { + if (parameters.size() == 0) throw new IllegalArgumentException(); + final Parameter array[] = new Parameter[parameters.size()]; + final Map map = new HashMap(); + for (int x = 0; x < array.length; x ++) { + final Parameter parameter = (Parameter) parameters.get(x); + final String key = parameter.getName(); + List values = (List) map.get(key); + if (values == null) { + values = new ArrayList(); + map.put(key, values); + } + values.add(parameter.getValue()); + array[x] = parameter; + } + + /* Make all parameter value lists unmodifiable */ + for (Iterator iter = map.entrySet().iterator(); iter.hasNext(); ) { + final Map.Entry entry = (Map.Entry) iter.next(); + final List list = (List) entry.getValue(); + entry.setValue(Collections.unmodifiableList(list)); + } + + /* Store the current values */ + this.delimiter = delimiter; + this.map = Collections.unmodifiableMap(map); + this.names = Collections.unmodifiableSet(map.keySet()); + this.parameters = array; + this.string = EncodingTools.toString(this); + } + + /* ====================================================================== */ + /* STATIC CONSTRUCTION METHODS */ + /* ====================================================================== */ + + /** + * <p>Utility method to create a new {@link Parameters} instance from a + * {@link List} of {@link Parameters.Parameter Parameter} instances.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified {@link List} was <b>null</b>, empty + * or did not contain any {@link Parameters.Parameter Parameter}. + * @throws ClassCastException if any of the elements in the {@link List} was + * not a {@link Parameters.Parameter Parameter}. + */ + public static Parameters create(List parameters) { + return create(parameters, DEFAULT_DELIMITER); + } + + /** + * <p>Utility method to create a new {@link Parameters} instance from a + * {@link List} of {@link Parameters.Parameter Parameter} instances.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified {@link List} was <b>null</b>, empty + * or did not contain any {@link Parameters.Parameter Parameter}. + * @throws ClassCastException if any of the elements in the {@link List} was + * not a {@link Parameters.Parameter Parameter}. + */ + public static Parameters create(List parameters, char delimiter) { + if (parameters == null) return null; + final List dedupes = new ArrayList(); + for (Iterator iter = parameters.iterator(); iter.hasNext(); ) { + Object next = iter.next(); + if (dedupes.contains(next)) continue; + dedupes.add(next); + } + if (dedupes.size() == 0) return null; + return new Parameters(dedupes, delimiter); + } + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters} instance using the {@link #DEFAULT_DELIMITER default + * parameter delimiter}.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified string was <b>null</b>, empty or + * did not contain any {@link Parameters.Parameter Parameter}. + */ + public static Parameters parse(String parameters) { + try { + return parse(parameters, DEFAULT_DELIMITER, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters} instance using the specified character as the + * parameters delimiter.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified string was <b>null</b>, empty or + * did not contain any {@link Parameters.Parameter Parameter}. + */ + public static Parameters parse(String parameters, char delimiter) { + try { + return parse(parameters, delimiter, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters} instance using the {@link #DEFAULT_DELIMITER default + * parameter delimiter}.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified string was <b>null</b>, empty or + * did not contain any {@link Parameters.Parameter Parameter}. + */ + public static Parameters parse(String parameters, String encoding) + throws UnsupportedEncodingException { + return parse(parameters, DEFAULT_DELIMITER, encoding); + } + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters} instance using the specified character as the + * parameters delimiter.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters} instance or + * <b>null</b> if the specified string was <b>null</b>, empty or + * did not contain any {@link Parameters.Parameter Parameter}. + */ + public static Parameters parse(String parameters, char delimiter, + String encoding) + throws UnsupportedEncodingException { + if (parameters == null) return null; + if (parameters.length() == 0) return null; + if (encoding == null) encoding = DEFAULT_ENCODING; + final String split[] = StringTools.splitAll(parameters, delimiter); + final List list = new ArrayList(); + for (int x = 0; x < split.length; x ++) { + if (split[x] == null) continue; + if (split[x].length() == 0) continue; + Parameter parameter = Parameter.parse(split[x], encoding); + if (parameter != null) list.add(parameter); + } + if (list.size() == 0) return null; + return new Parameters(list, delimiter); + } + + /* ====================================================================== */ + /* PUBLIC EXPOSED METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the number of {@link Parameters.Parameter Parameter}s + * contained by this instance.</p> + */ + public int size() { + return this.parameters.length; + } + + /** + * <p>Return the {@link Parameters.Parameter Parameter} stored by this\ + * instance at the specified index.</p> + */ + public Object get(int index) { + return this.parameters[index]; + } + + /** + * <p>Return an immutable {@link Set} of {@link String}s containing all + * known {@link Parameters.Parameter Parameter} + * {@link Parameters.Parameter#getName() names}.</p> + */ + public Set getNames() { + return this.names; + } + + /** + * <p>Return the first {@link String} value associated with the + * specified parameter name, or <b>null</b>.</p> + */ + public String getValue(String name) { + final List values = (List) this.map.get(name); + return values == null ? null : (String) values.get(0); + } + + /** + * <p>Return an immutable {@link List} of all {@link String} values + * associated with the specified parameter name, or <b>null</b>.</p> + */ + public List getValues(String name) { + return (List) this.map.get(name); + } + + /* ====================================================================== */ + /* OBJECT METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Parameters Parameters} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Parameters Parameters} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + StringBuffer buffer = new StringBuffer(); + for (int x = 0; x < this.parameters.length; x ++) { + buffer.append(this.delimiter); + buffer.append(this.parameters[x].toString(encoding)); + } + return buffer.substring(1); + } + + /** + * <p>Return the hash code value of this + * {@link Parameters Parameters} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Parameters Parameters} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, it is a {@link Parameters Parameters} + * instance, and its {@link #toString() string representation} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Parameters)) { + return this.string.equals(((Parameters) object).string); + } else { + return false; + } + } + + /* ====================================================================== */ + /* PUBLIC INNER CLASSES */ + /* ====================================================================== */ + + /** + * <p>The {@link Parameters.Parameter Parameter} class represents a single + * parameter either parsed from a query string or a path element.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ + public static class Parameter implements Encodable { + /** <p>The name of the parameter (decoded).</p> */ + private final String name; + /** <p>The value of the parameter (decoded).</p> */ + private final String value; + /** <p>The encoded {@link String} representation of this.</p> */ + private final String string; + + /** + * <p>Create a new {@link Parameters.Parameter Parameter} given an + * encoded parameter name and value.</p> + * + * @throws NullPointerException if the name was <b>null</b>. + * @throws IllegalArgumentException if the name was an empty string. + */ + public Parameter(String name, String value) { + if (name == null) throw new NullPointerException(); + if (name.length() == 0) throw new IllegalArgumentException(); + this.name = name; + this.value = value; + this.string = EncodingTools.toString(this); + } + + /* ================================================================== */ + /* STATIC CONSTRUCTION METHODS */ + /* ================================================================== */ + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters.Parameter} instance.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters.Parameter} + * instance or <b>null</b> if the specified string was + * <b>null</b> or empty. + */ + public static Parameter parse(String parameter) + throws UnsupportedEncodingException { + try { + return parse(parameter, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified parameters {@link String} into a + * {@link Parameters.Parameter} instance.</p> + * + * @return a <b>non-null</b> and not empty {@link Parameters.Parameter} + * instance or <b>null</b> if the specified string was + * <b>null</b> or empty. + */ + public static Parameter parse(String parameter, String encoding) + throws UnsupportedEncodingException { + if (parameter == null) return null; + if (encoding == null) encoding = DEFAULT_ENCODING; + String split[] = StringTools.splitOnce(parameter, '=', false); + if (split[0] == null) return null; + return new Parameter(split[0], split[1]); + } + + /* ================================================================== */ + /* PUBLIC EXPOSED METHODS */ + /* ================================================================== */ + + /** + * <p>Return the URL-decoded name of this + * {@link Parameters.Parameter Parameter} instance.</p> + */ + public String getName() { + return this.name; + } + + /** + * <p>Return the URL-decoded value of this + * {@link Parameters.Parameter Parameter} instance.</p> + */ + public String getValue() { + return this.value; + } + + /* ================================================================== */ + /* OBJECT METHODS */ + /* ================================================================== */ + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Parameters.Parameter Parameter} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Parameters.Parameter Parameter} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + if (this.value != null) { + return EncodingTools.urlEncode(this.name, encoding) + "=" + + EncodingTools.urlEncode(this.value, encoding); + } else { + return EncodingTools.urlEncode(this.name, encoding); + } + } + + /** + * <p>Return the hash code value for this + * {@link Parameters.Parameter Parameter} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Parameters.Parameter Parameter} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, it is a {@link Parameters.Parameter Parameter} + * instance, and its {@link #toString() string representation} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Parameter)) { + return this.string.equals(((Parameter) object).string); + } else { + return false; + } + } + } +}
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Path.java b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Path.java new file mode 100644 index 000000000..722a0d46b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Path.java @@ -0,0 +1,559 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.util.location; + +import it.could.util.StringTools; +import it.could.util.encoding.Encodable; +import it.could.util.encoding.EncodingTools; + +import java.io.UnsupportedEncodingException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Stack; + + +/** + * <p>The {@link Path Path} class is an ordered collection of + * {@link Path.Element Element} instances representing a path + * structure.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class Path extends AbstractList implements Encodable { + + /** <p>The array of {@link Path.Element Element}s.</p> */ + private final Element paths[]; + /** <p>The current {@link Parameters} instance or <b>null</b>.</p> */ + private final Parameters parameters; + /** <p>A flag indicating whether this path is absolute or not.</p> */ + private final boolean absolute; + /** <p>A flag indicating if this path is a collection or not.</p> */ + private final boolean collection; + /** <p>The {@link String} representation of this (encoded).</p> */ + private final String string; + + /** + * <p>Create a new {@link Path Path} instance.</p> + * + * @throws ClassCastException if any of the elements in the {@link List} + * was not a {@link Path.Element Element}. + */ + public Path(List elements, boolean absolute, boolean collection) { + this(elements, absolute, collection, null); + } + + /** + * <p>Create a new {@link Path Path} instance.</p> + * + * @throws ClassCastException if any of the elements in the {@link List} + * was not a {@link Path.Element Element}. + */ + public Path(List elements, boolean absolute, boolean collection, + Parameters parameters) { + final Stack resolved = resolve(null, absolute, elements); + final Element array[] = new Element[resolved.size()]; + this.paths = (Element []) resolved.toArray(array); + this.parameters = parameters; + this.absolute = absolute; + this.collection = collection; + this.string = EncodingTools.toString(this); + } + + /* ====================================================================== */ + /* STATIC CONSTRUCTION METHODS */ + /* ====================================================================== */ + + /** + * <p>Parse the specified {@link String} into a {@link Path} structure.</p> + */ + public static Path parse(String path) { + try { + return parse(path, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified {@link String} into a {@link Path} structure.</p> + */ + public static Path parse(String path, String encoding) + throws UnsupportedEncodingException { + final List params = new ArrayList(); + final List elems = new ArrayList(); + + /* No path, flog it! */ + if ((path == null) || (path.length() == 0)) { + return new Path(elems, false, false, null); + } + + /* Check for a proper encoding */ + if (encoding == null) encoding = DEFAULT_ENCODING; + + /* Split up the path structure into its path element components */ + final String split[] = StringTools.splitAll(path, '/'); + + /* Check if this path is an absolute path */ + final boolean absolute = path.charAt(0) == '/'; + + /* Check every single path element and append it to the current one */ + Element element = null; + for (int x = 0; x < split.length; x++) { + if (split[x] == null) continue; /* Collapse double slashes */ + element = parsePath(split[x], params, encoding); + if (element != null) elems.add(element); + } + + /* Check if this is a collection */ + final boolean collection = ((split[split.length - 1] == null) + || (element == null) + || element.getName().equals(".") + || element.getName().equals("..")); + + /* Setup the last path in our chain and return the first one */ + final Parameters parameters = Parameters.create(params, ';'); + return new Path(elems, absolute, collection, parameters); + } + + /* ====================================================================== */ + + /** + * <p>Parse a single path element like <code>path!extra;param</code>.</p> + */ + private static Element parsePath(String path, List parameters, + String encoding) + throws UnsupportedEncodingException { + final int pathEnds = StringTools.findFirst(path, "!;"); + final Element element; + + if (pathEnds < 0) { + element = new Element(EncodingTools.urlDecode(path, encoding), null); + } else if (path.charAt(pathEnds) == ';') { + // --> pathname;pathparameter + final String name = path.substring(0, pathEnds); + final String param = path.substring(pathEnds + 1); + final Parameters params = Parameters.parse(param, ';', encoding); + if (params != null) parameters.addAll(params); + element = new Element(EncodingTools.urlDecode(name, encoding), null); + } else { + // --> pathname!extra... + final String name = path.substring(0, pathEnds); + final String more = path.substring(pathEnds + 1); + final String split[] = StringTools.splitOnce(more, ';', false); + final Parameters params = Parameters.parse(split[1], ';', encoding); + if (params != null) parameters.addAll(params); + element = new Element(EncodingTools.urlDecode(name, encoding), + EncodingTools.urlDecode(split[0], encoding)); + } + if (element.toString().length() == 0) return null; + return element; + } + + /* ====================================================================== */ + /* RESOLUTION METHODS */ + /* ====================================================================== */ + + /** + * <p>Resolve the specified {@link Path} against this one.</p> + */ + public Path resolve(Path path) { + /* Merge the parameters */ + final List params = new ArrayList(); + if (this.parameters != null) params.addAll(this.parameters); + if (path.parameters != null) params.addAll(path.parameters); + final Parameters parameters = Parameters.create(params, ';'); + + /* No path, return this instance */ + if (path == null) return this; + + /* If the target is absolute, only merge the parameters */ + if (path.absolute) + return new Path(path, true, path.collection, parameters); + + /* Resolve the path */ + final Stack source = new Stack(); + source.addAll(this); + if (! this.collection && (source.size() > 0)) source.pop(); + final List resolved = resolve(source, this.absolute, path); + + /* Figure out if the resolved path is a collection and return it */ + final boolean c = path.size() == 0 ? this.collection : path.collection; + return new Path(resolved, this.absolute, c, parameters); + } + + /** + * <p>Parse the specified {@link String} into a {@link Path} and resolve it + * against this one.</p> + */ + public Path resolve(String path) { + try { + return this.resolve(parse(path, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified {@link String} into a {@link Path} and resolve it + * against this one.</p> + * + * @throws NullPointerException if the path {@link String} was <b>null</b>. + */ + public Path resolve(String path, String encoding) + throws UnsupportedEncodingException { + if (encoding == null) encoding = DEFAULT_ENCODING; + if (path == null) return this; + return this.resolve(parse(path, encoding)); + } + + /* ====================================================================== */ + + private static Stack resolve(Stack stack, boolean absolute, List elements) { + /* If we have no source stack we create a new empty one */ + if (stack == null) stack = new Stack(); + /* A flag indicating whether we are at the "root" path element. */ + boolean atroot = absolute && stack.empty(); + /* Iterate through the current path elements to see what to do. */ + for (Iterator iter = elements.iterator(); iter.hasNext(); ) { + final Element element = (Element) iter.next(); + /* If this is the "." (current) path element, skip it. */ + if (".".equals(element.getName())) continue; + /* If this is the ".." (parent) path element, it gets nasty. */ + if ("..".equals(element.getName())) { + /* The root path's parent is always itself */ + if (atroot) continue; + /* We're not at root and have the stack, relative ".." */ + if (stack.size() == 0) { + stack.push(element); + /* We're not at root, but we have stuff in the stack */ + } else { + /* Get the last element in the stack */ + final Element prev = (Element) stack.peek(); + /* If the last element is "..", add another one */ + if ("..".equals(prev.getName())) stack.push(element); + /* The last element was not "..", pop it out */ + else stack.pop(); + /* If absoulte and stack is empty, we're at root */ + if (absolute) atroot = stack.size() == 0; + } + } else { + /* Normal element processing follows... */ + stack.push(element); + atroot = false; + } + } + return stack; + } + + /* ====================================================================== */ + /* RELATIVIZATION METHODS */ + /* ====================================================================== */ + + /** + * <p>Parse the specified {@link String} into a {@link Path} and relativize + * it against this one.</p> + */ + public Path relativize(String path) { + try { + return this.relativize(parse(path, DEFAULT_ENCODING)); + } catch (UnsupportedEncodingException exception) { + final String message = "Unsupported encoding " + DEFAULT_ENCODING; + final InternalError error = new InternalError(message); + throw (InternalError) error.initCause(exception); + } + } + + /** + * <p>Parse the specified {@link String} into a {@link Path} and relativize + * it against this one.</p> + */ + public Path relativize(String path, String encoding) + throws UnsupportedEncodingException { + if (encoding == null) encoding = DEFAULT_ENCODING; + return this.relativize(parse(path, encoding)); + } + + /** + * <p>Retrieve the relativization path from this {@link Path} to the + * specified {@link Path}.</p> + */ + public Path relativize(Path path) { + /* No matter what, always return the aggregate of all parameters */ + final List parameters = new ArrayList(); + if (this.parameters != null) parameters.addAll(this.parameters); + if (path.parameters != null) parameters.addAll(path.parameters); + final Parameters params = Parameters.create(parameters, ';'); + + /* We are absolute and the specified path is absolute, we process */ + if ((path.absolute) && (this.absolute)) { + /* Find the max number of paths we should examine */ + final int num = this.collection ? this.size() : this.size() - 1; + + /* Process the two absolute paths to check common elements */ + int skip = 0; + for (int x = 0; (x < num) && (x < path.size()); x ++) { + if (path.paths[x].equals(this.paths[x])) skip ++; + else break; + } + + /* Figure out if the resulting path is a collection */ + final boolean collection; + if (path.size() > skip) collection = path.collection; + else if (this.size() > skip) collection = true; + else collection = this.collection; + + /* Recreate the path to return by adding ".." and the paths */ + final List elems = new ArrayList(); + for (int x = skip; x < num; x ++) elems.add(new Element("..", null)); + elems.addAll(path.subList(skip, path.size())); + return new Path(elems, false, collection); + } + + /* + * Here we are in one of the following cases: + * - the specified path is already relative, so why bother? + * - we are relative and the specified path is absolute: in this case + * we can't possibly know how far away we are located from the root + * so, we only have one option, to return the absolute path. + * In all cases, though, before returning the specified path, we just + * merge ours and the path's parameters. + */ + if (this.absolute && (! path.absolute)) { + /* + * Ok, let's bother, we're absolute and the specified is not. This + * means that if we resolve the path, we can find another absolute + * path, and therefore we can do a better job at relativizin it. + */ + return this.relativize(this.resolve(path)); + } + /* We'll never going to be able to do better than this */ + return new Path(path, path.absolute, path.collection, params); + } + + /* ====================================================================== */ + /* PUBLIC EXPOSED METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the {@link Path.Element Element} instance at + * the specified index.</p> + */ + public Object get(int index) { + return this.paths[index]; + } + + /** + * <p>Return the number of {@link Path.Element Element} + * instances contained by this instance.</p> + */ + public int size() { + return this.paths.length; + } + + /** + * <p>Checks if this {@link Path Path} instance represents + * an absolute path.</p> + */ + public boolean isAbsolute() { + return this.absolute; + } + + /** + * <p>Checks if this {@link Path Path} instance represents + * a collection.</p> + */ + public boolean isCollection() { + return this.collection; + } + + /** + * <p>Returns the collection of {@link Parameters Parameters} + * contained by this instance or <b>null</b>.</p> + */ + public Parameters getParameters() { + return this.parameters; + } + + /* ====================================================================== */ + /* OBJECT METHODS */ + /* ====================================================================== */ + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Path Path} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Path Path} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + StringBuffer buffer = new StringBuffer(); + if (this.absolute) buffer.append('/'); + final int last = this.paths.length - 1; + for (int x = 0; x < last; x ++) { + buffer.append(this.paths[x].toString(encoding)).append('/'); + } + if (last >= 0) { + buffer.append(this.paths[last].toString(encoding)); + if (this.collection) buffer.append('/'); + } + if (this.parameters != null) + buffer.append(';').append(this.parameters.toString(encoding)); + return buffer.toString(); + } + + /** + * <p>Return the hash code value of this + * {@link Path Path} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Path Path} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, is a {@link Path Path} + * instance and its {@link #toString() string representation} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Path)) { + return this.string.equals(((Path) object).string); + } + return false; + } + + /* ====================================================================== */ + /* PUBLIC INNER CLASSES */ + /* ====================================================================== */ + + /** + * <p>The {@link Path.Element Element} class represents a path + * element within the {@link Path Path} structure.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ + public static class Element implements Encodable { + + /** <p>The name of this path element (decoded).</p> */ + private final String name; + /** <p>The extra path information of this path element (decoded).</p> */ + private final String extra; + /** <p>The {@link String} representation of this (encoded).</p> */ + private final String string; + + /** + * <p>Create a new {@link Path.Element Element} instance given its + * url-decoded components name and extra.</p> + * + * @throws NullPointerException if the specified name was <b>null</b>. + */ + public Element(String name, String extra) { + if (name == null) throw new NullPointerException("Null path name"); + this.name = name; + this.extra = extra; + this.string = EncodingTools.toString(this); + } + + /* ================================================================== */ + /* PUBLIC EXPOSED METHODS */ + /* ================================================================== */ + + /** + * <p>Return the url-decoded {@link String} name of this + * {@link Path.Element Element}.</p> + */ + public String getName() { + return this.name; + } + + /** + * <p>Return the url-decoded {@link String} extra path of this + * {@link Path.Element Element}.</p> + */ + public String getExtra() { + return this.extra; + } + + /* ================================================================== */ + /* OBJECT METHODS */ + /* ================================================================== */ + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Path.Element Element} instance.</p> + */ + public String toString() { + return this.string; + } + + /** + * <p>Return the URL-encoded {@link String} representation of this + * {@link Path.Element Element} instance using the specified + * character encoding.</p> + */ + public String toString(String encoding) + throws UnsupportedEncodingException { + final StringBuffer buffer = new StringBuffer(); + buffer.append(EncodingTools.urlEncode(this.name, encoding)); + if (this.extra != null) { + buffer.append('!'); + buffer.append(EncodingTools.urlEncode(this.extra, encoding)); + } + return buffer.toString(); + } + + /** + * <p>Return the hash code value of this + * {@link Path.Element Element} instance.</p> + */ + public int hashCode() { + return this.string.hashCode(); + } + + /** + * <p>Check if the specified {@link Object} is equal to this + * {@link Path.Element Element} instance.</p> + * + * <p>The specified {@link Object} is considered equal to this one if + * it is <b>non-null</b>, is a {@link Path.Element Element} + * instance and its {@link #toString() string representation} equals + * this one's.</p> + */ + public boolean equals(Object object) { + if ((object != null) && (object instanceof Element)) { + return this.string.equals(((Element) object).string); + } + return false; + } + } +}
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/package.html new file mode 100644 index 000000000..155907e08 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/package.html @@ -0,0 +1,29 @@ +<html> + <head> + <title>Location Utilities</title> + </head> + <body> + <p> + This package contains a number of utility classes to parse and + work with URLs. + </p> + <p> + The {@link java.net.URL} class already provides most of the functionality + covered by this package, but certain limitations in its implementation + (for example, all schemes <i>must</i> be registered with the + {java.net.URLStreamHandler} class before they can be used), prompted + the re-development of a similar API. + </p> + <p> + For further details on what the different classes in this package mean + and how they interact, see the {@link it.could.util.location.Location} + class documentation, but as a reference, this is a picture outlining + the structure: + </p> + <div align="center"> + <a href="url.pdf" target="_new" title="PDF Version"> + <img src="url.gif" alt="URL components" border="0"> + </a> + </div> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.gif b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.gif Binary files differnew file mode 100644 index 000000000..1c87bd9c6 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.gif diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.pdf b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.pdf new file mode 100644 index 000000000..f970fe310 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/location/url.pdf @@ -0,0 +1,884 @@ +%PDF-1.4
%âãÏÓ
+1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
/Metadata 408 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [ 5 0 R ]
/Count 1
>>
endobj
3 0 obj
<<
/ModDate (D:20060402130123+01'00')
/CreationDate (D:20060402130122+01'00')
/Producer (Adobe PDF library 5.00)
/Creator (Adobe Illustrator 10.0.3)
>>
endobj
5 0 obj
<<
/Type /Page
/MediaBox [ 0 0 416.69336 111.96875 ]
/Parent 2 0 R
/PieceInfo << /Illustrator 7 0 R >>
/LastModified (D:20060402130122+01'00')
/ArtBox [ 0 0 416.69434 111.96875 ]
/Group 397 0 R
/Thumb 398 0 R
/Contents 400 0 R
/Resources << /ColorSpace << /CS0 32 0 R /CS1 33 0 R /CS2 48 0 R >> /Font << /TT0 34 0 R /TT1 37 0 R /TT2 258 0 R >>
/Pattern << /P0 41 0 R /P1 50 0 R /P2 58 0 R /P3 66 0 R /P4 74 0 R /P5 82 0 R
/P6 90 0 R /P7 98 0 R /P8 106 0 R /P9 114 0 R /P10 122 0 R /P11 130 0 R
/P12 138 0 R /P13 146 0 R /P14 154 0 R /P15 162 0 R /P16 170 0 R
/P17 178 0 R /P18 186 0 R /P19 194 0 R /P20 202 0 R /P21 210 0 R
/P22 218 0 R /P23 226 0 R /P24 234 0 R /P25 243 0 R /P26 251 0 R
/P27 262 0 R /P28 270 0 R /P29 278 0 R /P30 286 0 R /P31 294 0 R
/P32 302 0 R /P33 310 0 R /P34 318 0 R /P35 326 0 R /P36 334 0 R
/P37 342 0 R /P38 350 0 R /P39 358 0 R /P40 366 0 R /P41 374 0 R
/P42 382 0 R /P43 390 0 R >>
/ExtGState << /GS0 40 0 R /GS1 241 0 R >> /ProcSet [ /PDF /Text ] >>
>>
endobj
7 0 obj
<<
/Private 8 0 R
/LastModified (D:20060402130122+01'00')
>>
endobj
8 0 obj
<<
/CreatorVersion 10
/ContainerVersion 9
/RoundtripVersion 10
/Options 9 0 R
/AIMetaData 10 0 R
/AIPDFPrivateData1 11 0 R
/AIPDFPrivateData2 12 0 R
/AIPDFPrivateData3 14 0 R
/AIPDFPrivateData4 16 0 R
/AIPDFPrivateData5 18 0 R
/AIPDFPrivateData6 20 0 R
/AIPDFPrivateData7 22 0 R
/AIPDFPrivateData8 24 0 R
/AIPDFPrivateData9 26 0 R
/AIPDFPrivateData10 28 0 R
/AIPDFPrivateData11 30 0 R
/NumBlock 11
>>
endobj
9 0 obj
<<
/OptionSet 4
/Compatibility 5
/EmbedFonts true
/SubsetFontsBelow true
/SubsetFontsRatio 100
/Thumbnail true
/EmbedICCProfile true
/cCompression true
/cCompKind 3
/cCompQuality 2
/cResolution false
/cRes 300
/gCompression true
/gCompKind 3
/gCompQuality 2
/gResolution false
/gRes 300
/mCompression true
/mCompKind 3
/mResolution false
/mRes 1200
/CompressArt true
>>
endobj
10 0 obj
<< /Length 1174 >>
stream
+%!PS-Adobe-3.0
%%Creator: Adobe Illustrator(R) 10.0
%%AI8_CreatorVersion: 10.0
%%For: (Pier Fumagalli) (VNU Business Publications)
%%Title: (url.ai)
%%CreationDate: 2/4/06 13:01
%%BoundingBox: 0 0 417 112
%%HiResBoundingBox: 0 0 416.6943 111.9688
%%DocumentProcessColors: Cyan Magenta Yellow Black
%AI5_FileFormat 6.0
%AI3_ColorUsage: Color
%AI7_ImageSettings: 0
%%CMYKProcessColor: 1 1 1 1 ([Registration])
%%AI6_ColorSeparationSet: 1 1 (AI6 Default Color Separation Set)
%%+ Options: 1 16 0 1 0 1 0 0 0 0 1 1 1 18 0 0 0 0 0 0 0 0 -1 -1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 2 3 4
%%+ PPD: 1 21 0 0 60 45 2 2 1 0 0 1 0 0 0 0 0 0 0 0 -1 -1 ()
%AI3_TemplateBox: 208.5 55.4688 208.5 55.4688
%AI3_TileBox: 30 -698.9199 582 31.0801
%AI3_DocumentPreview: None
%AI5_ArtSize: 416.6929 111.9685
%AI5_RulerUnits: 1
%AI9_ColorModel: 2
%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
%AI5_TargetResolution: 800
%AI5_NumLayers: 1
%AI9_OpenToView: -135 278.9688 2 1422 851 26 1 1 11 42 1 0 1 1 1 0
%AI5_OpenViewLayers: 7
%%PageOrigin:30 -698.9199
%%AI3_PaperRect:-30 761 582 -31
%%AI3_Margin:30 -31 -30 31
%AI7_GridSettings: 28.3465 20 28.3465 20 1 0 0.8 0.8 0.8 0.9 0.9 0.9
%AI9_Flatten: 0
%%EndComments
endstream
endobj
11 0 obj
<< /Length 6360 >>
stream
+%%BoundingBox: 0 0 417 112
%%HiResBoundingBox: 0 0 416.6943 111.9688
%AI7_Thumbnail: 128 36 8
%%BeginData: 6026 Hex Bytes
%0000330000660000990000CC0033000033330033660033990033CC0033FF
%0066000066330066660066990066CC0066FF009900009933009966009999
%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66
%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333
%3333663333993333CC3333FF3366003366333366663366993366CC3366FF
%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99
%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033
%6600666600996600CC6600FF6633006633336633666633996633CC6633FF
%6666006666336666666666996666CC6666FF669900669933669966669999
%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33
%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF
%9933009933339933669933999933CC9933FF996600996633996666996699
%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33
%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF
%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399
%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933
%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF
%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC
%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699
%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33
%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100
%000011111111220000002200000022222222440000004400000044444444
%550000005500000055555555770000007700000077777777880000008800
%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB
%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF
%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF
%524C45FD1CFF7DFD48FFA8FD35FF7D52FD23FF52FD23FF527DFD29FFA8A2
%7DA27DA27DA27DA27DA27DA27DA27DA27DA27DA27DA27DA1FFA87DA77DA7
%7DA77DA77DA77DA77DA77DA77DA77DA77DA77DA77DA77DA77DA77DA77DA7
%7DA77DA77DA77DA77DA8FF7EFD187DA8FD14FFA8FD08FFA89BA29BC49BA1
%52C49BA29BC49BA29BC49BA29B7D77A29BC49BFF83AD82A782AD828358AD
%82A782AD82A782AD82A782AD828358AD82A782AD82A782AD82A782AD8283
%82AD82A783FF7777537D7777527D7777537D7777537D777752777777537D
%77A8FD13FF7D7DFD08FFCBCAA2CBA2CA5277A2CAA2CBA2CAA2CBA2CAA2CB
%52A2A2CBA2CAFFFFADAEA7AEADAE277CADAEA7AEADAEA7AEADAEA7AEADAE
%5252ADAEA7AEADAEA7AEADAEA7AEAD5252AEADAEA7CFFFA87DA27EA22752
%7EA27DA27EA27DA27EA27D7727A27DA27EA2A8FD0EFF7D59595A5959595A
%5959597DFFFFFF7D77777677777776777777767776CA5277777776777777
%767752FF7C7C587C587C587C587C587C587C58837C7C587C587C587C587C
%587C587C58837C7C587C587C587C587C587C52FF52524C534C524C534C52
%4C52A2524C534C524C534C524C534C7DFD0FFF858459AF858B8484598BA8
%FFFFFFA2C4A2779BC4A2C49BC477A19BC4CBA29BC477C49BC4A2C477A2FF
%AE82AD58AD82AD83AD82AD58AD82ADA8AE82AD58AD82AD83AD82AD58AD82
%ADA8AE82AD7CAD82AD83AD825882ADFFA27777527D7777777D527777FF7D
%FD06777D7777527DA8FD06FFA8A8FD06FFA87E272E59A9847D272E59A8FF
%FFFFA877A1277D77A2A27D77274C7D7DFF7D7D7727777DA1A2772777FFA7
%7D7C277C7D83AE7C7D7C277C7D7CCFA87D7C277C7D83AE7C7D7C277C7D7C
%CFA87C5827587C7CAE7C7C27837DFF777D5227527D7D7E5252527DFF7D52
%77F87D527D7D7D522852A8A8A17DA77DA152527DA17DA77DFFA86085608B
%8484608B60857EFFFFFFA1C49BC49BC4A1CB77C49BC49BA2FFA29BC49BC4
%9BA8A1C49BA1FFA782AD828982A7AE8382AD828982ADA7A882AD828982A7
%AE8382AD828982ADA7A882AD82898283AE8382AD8283FF7D53775377537E
%7D77537753FF77775377537753CB537753777DA8A0C3A0C2A0C3A0C2A0C3
%A0C2A1A959595927A8A85959842784A87DA85252A17776527D7D52777D76
%A25227527752A276527DA84C7D527D527D527C7C5152AE527D7C52587C52
%A8525851832D517D5851585852525251A87D527C582D7C58AE7C7D528352
%52527DFD04527D7D5277775227774C52284C4C7DA8774C52277DF852527C
%527C5252767C4B7676CFFF2E597D287DA8527D7D527DA85252A227777D27
%7DA2A8527D2777A252277752767777527D7D52527D5252582758A7277D7D
%2758A7527DAE27FD055258FD04527D7C527CAE525283527CF82D5252277C
%7C27837C525252272777275252524C7D52275252275227525252277D2752
%2752527CA72752F82776524B76A1CFAFAFA8FFA97D53FFA9AFA87E527D52
%7DA8CBA8CBA87D7D7DA8CBA8CB7D7D7DCBA8CBA8CB7D7DA8CB7D7D52CFA8
%CFA8CF7D7D83CFA8CFA8CFA87D52CFA8CFA8CF7D7D83CFA8CFA8CFA87D52
%CFA8CFA8CF7D7D7DCFA8CF7D7D7DA8A8A9A8A8527DA8A8A8A27D7DA8A9A8
%A8A8A27D7EA8A9A87D52CAA8CAA8A17DCAA8CAA8CAA8FD06FF7DA8FD04FF
%A8A87DA8A8FD05FFA1A8CAFD04FFA87DFD06FFA87DFFFFFF7DA8FD05FFA8
%7DFD07FF7DA8FD05FFA87DFD07FF7DA8FD05FFA87DFD05FF7DA8FD05FFA8
%7DFFFFFFA8A8A8FD05FFA8A8A8FFFFFF7DA8FD0EFFF8FFFFFF7D7D7D277D
%7D5252A87DA8FD0AFF27A8FFFFFF7D7DA852527D7D527D27A87DFD08FF52
%FD04FF527D5252277DFD0FFF2727FFFFA8FD047D527D7DFD0DFF27A8FFFF
%7D277D527D7D527D7D7D5252FD0AFFA827A8FFFF52277D7DF87D527D2727
%7DFD0AFF527DFFFF7D2752522752277DA8277D52FD09FF277DFFFFFF5227
%52527D52FD0FFF2752FFFF52272752A8A852A8FD0DFF27A8FFFF7D527D27
%5227FD04527D52FD32FFA8A8FD0EFF7DFD19FF7DFD04FFA8FD12FF7DFFFF
%FF7DA8FD11FFF8A8FFFF7D7D7D527D7D527DA8A87D7D527D7DA8FD05FF27
%27FFFFFF7D7DA87D52FF7D7D527D7DFD09FF27277DFFFF7D7D7D52527DFF
%7D527D527DA87D7D52FD05FFF852FFFF7D7D527D7D7D5252A87D527D52FD
%23FFF87DFFFF7DF8A852F8FD04527DFD04527DA8FD05FF2727FFFF7D2727
%5252A87D7D527D527DFD09FF7DF87DFFFF27272752527D527DF87D525227
%7D5252FD05FF2727FFFF525227527D27522752527D527DFD31FFA8FD17FF
%A8FD10FFA8FD18FF7DFD48FFF87DFFFFFF277D7D7D52527D52277DA8FD09
%FF5227FFFFFF527D525227FD067D5252527D7D527D52A8A827A8FFFF7D7D
%52527D525252A87D525252A8527D5252527D7DFD34FF27A8FFFFA852F87D
%7D52527D7D7D527DFD09FF7D27FFFFFF522752527D527D7D277D7D277D52
%7D527DF87DFFFF27FFFFFF527D277DFD05527D7D527DFF7DF8277D52F87D
%FD41FFA8FD10FF7DFD05FF7DFD12FFA8FD48FF277DFFFFFF527DA87D52FF
%527D527D52A87DFD07FF2752FFFFFF7D7D7D52527D7D52FD057DFD08FF27
%FFFFFF7D7D527D7D7D5252A87D527D52FF7D7D7D5252FF7DFD34FF27FFFF
%FF7D27275252A87D7DF852525227A8FD07FF27FD04FF272727FD0552277D
%5252F8FD08FF27FFFFFF525227525227522752527D527DFF525252F82752
%7DFD52FFA8FD18FFA8FD48FF27A8FFFFFF527D7D7D5252A87D7D7D527DFD
%08FF2727FFFFFF527D5252277DA87D7D52527D7DFD55FF5227FFFFA85252
%F8FD05527D7D27FD09FF2752FFFFFF522752527D52527D7D527D5252FD59
%FF7DFD19FF7DFD61FF2727FFFFFF27A87D7D527D52A87D527DA8A8FD07FF
%27A8FFFFFF7D7D7D527D7D527DA8FD047DA87D7D7D5252A8A8FD4DFF2752
%FFFF7D7DF87D527D52525227525227A8FD07FF7D52FFFFFF2727277D5227
%527D277D277D7DA85227275227527DFD6BFFA8FD61FF27FD04FF527D7D52
%52FD0FFF5252FFFFFF527D52527D7D52FD057D527D7DA87D277DA87DFD4D
%FF52FFFFFFA827527DA852FD0FFF7D7DFFFFFF5227527D52275252527D52
%7DA87D7D5227F85227A8FD51FF7DFD19FF7DFDFCFFFD4AFFFF
%%EndData
endstream
endobj
12 0 obj
<< /Filter [ /FlateDecode ] /Length 13 0 R >>
stream
+H‰ìWÛnÛHý‚þîC +)–N§åSÞÇÕ`Y}„@æ¤Í9ÿ0_m;¦ÍÇ®•‘Ê“2Ç-ï¤;B·6¬å'S”†ª-…@$×ÇsöÄz>(o`:|jÇ!4^H4_\ÇJù\®Rs£Æo–r½™n”Ñ2º]ø›ö}«,•ÅãÒÍ£š*S}_ÑÅjEÄžm:¡w¼#¤ÛÈ¡Ž´:Ñ„~›ëÆ}ÍÆ篑ÏtðŽ<ä²òz?¯z¼lŽïg-ËTÞlZÑ2]ë—íÇi?ò‘žÑ“UêUóðoõÔÅéÆC7rÑüɈ½Ï™nhŽi1HOþåQ½è½~БÍѽ˜ çü(?Z¾¥K£a…Äø—‘Õ¸ñiÕlQb†çEá²’xï µ†fÅ
±BKÔ/ÔÏ¡buYm·ûÞË¶7¨®<â=÷aDo÷|ÇbEE1µÈ°¢€²|×QD!ÍxÚ¶\] +ã%a}ÿ¼jë5UßÇFÒ¨fAæýµx›†íyiìôþñ¸$gGáÉ6hƒüDLÓ´ +ÇqÏñ +·ðL?‚aïyž^D^$@êe@î^!òã["|Äÿöâ +b R 2äA` +Í™âË×\'MRA$C¶dÇÓÌD’•T1¢Ù\x’†PS Ò_ÈÔÛ:íH:ÑOd¶sh[&ÙÓ VÉMUbuR™P_çRå1‹ò¨ ± ÉÛ±}y±.Cä9Ž“8³8œ +±¡VRò“ +íªêuÖ®í%Ú5*WV¯³v=R.óH¹H²Ä \áS”KÍWáä~<yPŽEéZÎxRîÇ”.¨ˆcR¹Ì£5ªÜ+]\]^9§ÑXîG–!´ˆ.µœ³h-÷ƒË]Ä3I´æÐûñ¥0o!ôÿ#„þBñãµD߯ î^üx.„<ˆ!¢³£‰!w‚ˆ3ø§E‘>oHo&fN@J”-Αã÷~±Ô¡®B‡ê2GJiO1m)¤5ùä’M&iˆ¾‚‹É4í ›VD³4½Gó[Æ0(…ܧ˜0$Ã04>: 2˜¤5c²¦-aÊ"¦-`ê<¦Ïb5ÆQa,å6…„1×€±õ_‹1Ög…±–akÂœGÌ{ÀÜ{Ì¿…hè‚.Èuò B!Âá! B¢!( +Â"ýä„&Bp„Ç;' Bj´@•f¨Ód”*B±”ËCÁ,¤LCÒ¤M"&H]„äHŸ‡ZH¡†$*H£4“Ð Z¡™Úé¡¡Zª¡© +Ú*õÝ!¸„w… +?!QìPü
ʿ¼ÞÀX˘vXÆëXa!Vb`)¬eFóN”ÅQ*ÌQÀ‚J½|5t-ˆöžZ{çÖ;6ñí•/ÖªæªUÍßìô÷n§ÔigËüäë7ù¡ˆ‡¹àøõ?ky°Eg…½MÊãg3¼{ÇÓ{¥+vÔ•cµÆ8vÎYçøÔ‚ABÐ-âŒIfÊ)zÔV‰¶D.ºÀ4ØWàÖ†P€ôD1coH-’Ö#Ü.U°èŒ¼fØWàÖ‚P°Ä†½ X^bEÎhÔ¨z-
ºÀØGx[ÈþWäñB¶§«•ì#µúKˆí°í‰‡«à+pµ4~Îøh}ij·<Zd/ðºJ?sý>>ð×5þ_þÿ|̳ôÂC_)Jäÿq¢8'ŽÜûIÙ㛯|ÿ—ŸÞ½ÿùÝûŠ¯?¹<…÷×Ù$üðíwÿþãÓßÿõîçïŸþŠ—¿ýáäêUœ¥“”ÒžbÚRHkòÉ%›LÒiI +þ(±×%ˆ^„øˆ ‡Zˆ¢†8*ˆ¤ÄÖ— šâ ¢>:E5PÖ +;Ci§-Au#Ô7@…=ÔØB–5äYA¦%VÆÙŽï +F#±&O„‘wNÀ•ÜiKÍp«É&8W„ƒ8™‡£YX›†Å)XÄ*›`}`…–ha©`•ÒLB'xg„‡x©‡§Zx«†Ç*xÔÓ’`¾&`ƦlaÎ&`Ör™T‚yG˜x€™{„ t“Ã/púŽ?!«ìH +Q¨J¹Nž£ŸåbQ¹´àœ¨¸j3§É‰k—«9Œ®áÊÕq%-ÇZÍõÌ9£2‚óÎŪ—xåìíJ¡
gõ…Ë8Qãbó*ÑD?m,x*ŽsÑOφhÃÑG? ýˆ”!)3ÒO‰Ý ô£ÒK?.Çá‘©#†¡éǦœ~t6fŒçs +ŽhÔ~&Á2?‹àˆ#}'8ÌXÛ#çÚzÝ5Xím-ØDÑVØŒí„0`à{È];À“àEг똗OÏ]²UV}æäx +Q&5ì±`cÆÊðe\©Å,·5µ ÊôÒüJæ%Qãå9Ù^€ñZ|+°‚&Ÿ,°ºÓü–ZRL’=.uâá¤ñ¤Í#JCšÇ´*FUȶimóʇGN¹†;ÁPyÔì'd +‚³ÎÌyGrÏ&ÆÎÉ'gJ?”rʨ¦ šƒZ…¤!ñáYÆn¸è‹Ì +«àD]OC€Ä"J_JïU£(Ñ +e:-})½wá¼@
EÁŽ=ÒªôÆsKy¹ž•Kß¹h-`¥Ô‰s(\RȤ°I²DS)ˆr!™ìJ´fŠ]£wcbS*™Ò\îdþry—i³LØÚ¸Ú™%Éü(æÇ03™—Àœ€BÆœiÏO¿Õ'秞ùy~\ËZ’1ñtÖÙÔ<•N°a¬<4Œ‰‡Qò. Œ&×"Ñxˆq€Î‘pî6Áu%:@Q-@©A"rHF+¤€¤‘˜:“&`Æ-àÜ WQvõEµÙ=Õ=Ñ=ÍG’Å…að+÷ôÉí©mÄ2¯=«IHí)í =ÐÙs90iEGdOã‘ÄžÁž?”E1¨b¯‰E+Ùõ£L•—c$RcfZšXU«]ˆ«©Î_`„ë)2Vñ;@45ì1Ô¢•#ΉS–E\8J÷¸ýÃíW¿Ûâg/¿š±qLQÒsÍȾ…ßr—–]ù}\ù²ÜÖÅöÑRpg-à³®õœ‚¹ð%nçs…ÉL9ËÜNÑï(¿¢Œ'…®ûç¸:Åeï_ãE—xþ–á
ÇÐäyÕs¼îY^ù¯}šW¿…×?Å+àÌk !—91vFŽH,‚÷ÃÀ;âÊ{¢ç]Ññ¾hyg4¼7jÞÞï3ï‘ži²a1;òÎÞÙ¥7ð+Ã3Öæ2\RÍe]¸´ŠË;s‰¥ :s‡&ÆÎÈQ›c3G ªÿÊ=àKWº.÷–œK࢛Ãmo ÍAÕµ¡,±,X ¥dG–w&
®èf/Frr ú»C…7^hVºêl)Ä¢bDªÂÁh$&›€ŠN‹@ o‡Ê;K—¡º +p¯Ê†—º½¬oyq«[^Ö–²¥ñŽFû9§<÷Œ(Í¢K“äæ˜JSp3”.ð¥ö¦Ô\•JOmy"ó•â#ö›€8íyW2¼)^„&^€òò³–µÇ´•'/<eÝÉ»Žè—^öv.Vh…2H²hkű03ÕDpQrIÖVÝJ1µ2„V*@f_6æ·Êº`Ò3ås£;6ªó¬ÇSã74nMã¬f‘¹¹È•×ìž‹²Ÿ—nwöÄl Wp×V{vdÛÜäŒ+Ÿ×c¨8gլ錓úÞdòbújB‰ž¦¯$ÂýÊáÄT=É'==A«éÞ¿‰?|ó¢{Ãð™Ê8ߘi¡Û_fºž’ïsú¿Ô—_Û¸Å?Áý~ ¼¢(Šb°( Jbצ}Ú‡`¶3h½E’¢_¿‡—ÔÛ%Ûê&ëØYæØÏïœ;.¹~*¯X „â´LÿPá§ñ@)Ó´<!˃ž,»ñü-Ÿ¶”›?-Nõ+‚ø,†w_æÿ-¤ÑXÂ÷lÿ ÿ ¹‰?èøæõ3Ãwqü'ÓÆ¿^i%ýiªŠW(Ÿ|•µ3±žméMýQ}îNϾ>=¿¾œ¾‡öåËë©?ôþðö¯O¯§ß~ÿï;žá‹–ûŒ-ö9ã*P¼t +•È&óº®ëÚ®élWwi »0.‰9f¼,ÅKº˜)^Ò=ÀLñ’îfŠ—t0S¥Ö[áLCMÔBºì…ÀLvÄï’{jÏóínÑâ t§hÏÅÎ\ +‚E{-4¿`õ¸èê +pÔ€¤A»´ fz:P€¦’€U¼j`Ö +‚D0(„FP†Ep´ØËÅQ P$‚E!`4‚Æ ¡[$O‹ýìP7I¢I!¢4¢Ê ²,¢«Å®v(ª‘&m +§u‘g}-v¶C½˜!$²Q!#5²Ò 3-!<[lo‡V,ªáª²šÍW³_üÞw¼asÞcŠ·…¿Âø`® {¾önÓLìÎÚ“.Dè9œSìLë’Âp~P—¦"b“èæ·¤¢µnUX°ºWÌr_ýÔ^öÓ϶ _×GƧ¹é‰Ÿæ¦'zd|š›žè‘ñinz¢GƧ¹é‰Ÿ2sd¤˜ÁQ‹W[œåD†wä¼ÉïjZ´TµîmZ´TµîmZ´TµîmZ´TµîmZ´TµÖ›VhR×1¼–ô«=e½U´zMÿdWÔ,‹â“vEÝŠ†ÉŠG®lE7žå+ºŽ.ñÖ8ßµ–<Pœ.usáKuÅöÑÂwßhæ˜Û¦ËU¤Ek'¬ê²Ä?2ÌNÿ¯Ie#ÒöÎnÙÉN¹ÂI—;XÞ¡^Ñ´_íQ¯hÚ¯ö¨W4íW{Ô+šö«=êMûÕcõ +—ZûíȃïÑ©G§ÿŠ#Y¦«^¨ª(KÅ!;ˆƒÂ]–ùÓ>Ñ›Ïk§Ùo~m]ªÉ)”ý)Õd¡äYaÌø…Xð¿R²?£œ®“:+¬“+Ågüñ;÷gÓuRgñ:áËÆÊ¿`F>ýRg…u„àŸ°Ìª‹3¦Ë$NÂ*¶¥7õGõ¹;=øúôüúrúØ—/¯§þÐûÃÛ¿}ýý߯ÿxúçáÓNß^¾¿£«#XPì³ß @Ò˜k“ÏŒ7éÊžSƒN-:št´é8õN)š5ص7,[v0m°m0n°®7o°o0°·p0±#ø88Ù{Ù»9øÙ;ÚL~hò¶öÆöÖöæ>,`poqoò–îüîoØõÞ÷š½_²ÿ3 ÜdT5s'¡G|÷—pu¢¥³—XÆÛ$·‰xz¸M*%J¼XÞ#š=¬ïñÝåð:¯[Ó~±DÌ놚Ãø&%›ßuUÌoÔŸB+óÌŸ3KîA†§·†,Ç3c +ø·DXVÍáÙ D;áð!‚Uˆ +€Ð\ƒàµyãS¸Ë¾•@4K8V!ª5"Û€yÛ¾E”;üH&–0¶‚Õ5¬o + ©œøé”Ã% “(h¥A.†YlÐ\sؤ“À‰Ò48h€D«ßd:í°Å¸)A’jÕ€ŒmÁZ‡¬ ¯™È¨]>[ º²¦]„˺ð]«m+¾é¿«Æ] œ`5òÁ ',ò¢En¸éHXEéAe”TDɨ|ˆÂ•#Ç3h‘ú[3ÈFÕƒLT…_ŠZ”¶ŒRƒ"ÊQc{åQbP„ŠÛU7ÌýÇo6ªd¢ªA=IF
Ô!Ù¶çññˆ{ +çœåfÔͨ½ÒYZP|bgTÏÈ\©:ñƒžQ9#5£â\Äå÷ZùŒænW”!tèku3jgtÉ–À£KÕ323'6ï2.6cµñ妯7rZq8O+]ÜæӦ㻷âºs^xÎ+ÏyééÎKÏuç¡IåËN_sB¤8‚ºh¶†‡!?Õ<ù¡ÈEšG£’ü‡‰•Iò””Çœñ´ä";ZšüØdytòÓŸ*¡üåÇ(ˆ¸uIž§rž©ÄÄ/½#ÆM?nëqßö;3î¿~·
ûi²aú-1^õñªöa¯x>C„exoŽïUìf4¦Ù>aFcšíf4¦Ù>aFcšÝf\¡s.´¾k¢èUàv
š7`|×8\PÍ&a…ÑHƒÄ±ˆ€éäpÑ6¤„YbO#Í9¢9!jNÏÅ ÷pÏ’sØâF<†]
aØ8ÓW?{õ“Wœ»(Ž]aæ*‡yK³V笊A¤â|f«0VÙ8Py¶(b¨x d’–R364Ó¢`J¦CÇT°LƒŠ) ØüÞø¾%vzÍ×ìì‚
+N„’¡£¢£‚§Ñ²1Ûà-vWðWΆ.ØáeðE«ÕŒ„†ÑEËeŒœ‘RDë…&Y1ƒj&R=ØyfÉŒay4cÁ+™y3°Ž¶l˜‘g&ÓS0Ke´h0©·iEìÔš©Ü0¥»X?3¦xÎT/¢qƒu+Žšc¡‰î|lx;9x9¸¹äÜ£y;úWÊÙ1W¸ÓÕ!;ˆƒÂ]–ùW?ћϯÚoü>â^‘êˆO”>‚TÅtÔi~¥#ÈVà”L˜FHÝÿ3¥Ž¥¨†ÅÖÏäõäBàµøjQM!'-ŸÂ+T|¼ÿÆæèw×øæÙWñ>ÛÒ›ú£úÜž?|}z~}9}ìË—×SèýáíßO§§½<¾ÄCïèúÖ/ö—èrÆUN»—·ÀnÊ‘vY纕ºélWsy×\Ô.å‚ÛwÇMÛr§®¸>+®Ê9ùVì{0W߀F#ÑöDÛh4m ÑH´e q+i¹ÔÜB4·‚K‡à²ÑqÉÀÏIÜ-4wŠÂ$oÕ#~—ÜCQí+ŠTÝM´çbçê}D{-4¿`õ¸èê +&IÂ, P•—%ƃÀ)†Íf†õnŠU·Å¹ù¹ ó—þƒÿÆx˜B0(¹¶È¹Gî.—àBu‰¦S.úZM§\ôµ&šN¹èkM4rÑךh:増Mt_²ÇkÉÄ-ZÐeë»ä¾Måÿ°p§A»/~픾Ž±X\'>4|ã86$±½žkÒnj6WØáC\lãç& +{2õÓ¼^úk1ZZ°ˆƒŽ0Õ$V{Ì…88Û¤–A‹K–›Ü0O±ŽCD¨%ìó
Æ Êç [ŒA”ÏA·ƒ(Ÿƒn1Q>Ýb¢|Z7!ÕMGÇÉfº¬À¤}¥ÃÆUà‚RØ_Q°¢,jüa¬[ö…ž––ÙŸ]ìFÖÙ’RËîÀ*ݯSY´å¥}D¦»›âÖ"Þ´®FËdqqi‘×u\&âý±÷ñþ¥ê—‰<ââұߊý;tû»ßžoßܪ>N—$S+ò03‹Åzz0Ÿë§v»ùôãyóö²ýÕŸ°/¯oÛý©Å»¯Ûíó÷—Mñº;U”ïiê$bËÂn@”{S¸7…{Sø/4TºŠ¯#i§Æ2Žv%"+ÜÁàNwlË +µjºÑµŸÖŽõmI®•gYOôLåÿ°p§A»/~픾Ž±8DžøÐð/ :DÌ)|Ìy ³–ÙóAÜ¥ ™‹þЉWhâ\8}ÞòêÑ7/ÅmòÊÿ¹Öœp²—[ßôÓÿ¥¶z9Ý\U]ã0à™²±VPr,(Ùù’w&ú€êZÄ’=Vçuø§ŠÔn‡-`ß^o0÷äcÝbîÉǺÅÜ“=t‹¹'{èsO>öк¹§o±W«ù ?sáj`;àßp£6;Öèk}ñL›ëÔsnó:‹xb舃GÚÏ : ÑúÃêÈ“€ÊP÷gj:Gßjj[¢ÆjÝ æj¿Å»2h‡&hƒ„F(ˆ…h8ˆG‹÷eQ ‘QSY‚òx(PjÄ¡Jê$¡R +je ZêÕâÍäŒCÖäMBæäÎ@öä¯ÅÛ3È!—AkhdÔÐLíôÐÐÐ0è)‡® +è«„Î*®ð:p‹}`dah ¡Vl£,A½=x4ƒ¢s(»€ÂK(½‚â(¿Chu0-£5´‰V¡Ð2Z‡CiM°-…ÛŠÐ_jô™ýF£ïXôVÇГ8z“@’èU +MË y94±ÖÏÐÔ8š›@““hv +MÏxKè€ô-CWäèŽ]R¢[ªXb6VE‰\Fî‰H—&f\/ËzÔ1hêälÖ®ø‘œÍ_¾û€õõ@nÙ#c•N +´+¤_‚tà [c¶hÊm1[4å¶Ö˜-šr[kÌM¹5f‹¦ÜÖ³E±*›HѹŸHìïj}¡&I¿ø¿!àJÑX#üzÙËü*£wsçøÞìC·õ§&ƒþke© +ˆdñPVMÁŠ²¨Xô…žæÙŸ]”ªE·N˃<3·*ÆѼ®»èªXìî—‰,ØòÒ>"Ó*.SñÖÐØ¢ïý2™G\\ÚGld¼#ä3½LZ¦òˆ‹KûÝÃ-³eq—ú`j¶}³ËÉzz0Ÿë§v»ùôãyóö²ýÕŸ°/¯oÛý©Å»¯Ûíó÷—Mñº;U”ü=MžÅêÂn@È…ˆÂÞ RçóZ‹C‹iá%±ïh;x¢SÁä\@'Gƒ£á Χ¦ƒa> Ñ€0=" ÃŒM ½6Ž_kõ¨CG¯sü2
:4ñW:ñeáç:„ å +è ¥æ¨ë¾!¬wIãÛÑî~Gwƒ4IÝh0MmµÓ’Pè%Ô£2ÂÔFšÆ(£1Ö8ã!¯Á2²¥å¶²ÂÖVÚo¢±Ö:ë¡ÅÁ1W:î*'Ф%öA9팳Î9ÙžùÒs(šðµ—¾ñŠ¼öÆ[ï¼ï|]ôp<ú´:Z1í–Žª®©sHHs—£€wxì€;wV0„yß”ø˜™8™œóž•;^v¾)Q3Yè=7vNò31tÏÑ–&h:53Ö‹Î)òu°Ø‰²‰@‰B‰D{%"%*E2T½Ó!ÊLùôÕN½ŠîZ¥ö–ÿäàÂ:uj&å©yè¢×§TU»”ˆ¡íR#‡ôìí>IƒÁyJwp¹4èFV÷ e{ÙKL™4Û!g´KY^õãº?¬üéÚªŸ2µWÿõ«è¾g÷=»ïÙ¿¸g;mþHsúâ6MÎÅ”Üãº|›á^.I,㘇ZüÜ"PƒÁKLH-ngqcGxG*15¤Î#…©ì> +‰•„wC_3^v¾D…8ªz +Ÿ€wð6¿’ø{„Ü`d“MnŸWU”,Û²»=3È"h}°[nÑ´T¬zõøð¯€ü§üº›™ìåøÂ$Nð‡íÉó´«™fœÝ¾ñ.f>Lð|Ï€Ïáe.ÆáÒ®ß6€¿.Ç×Ã~ìÙõÛcÅÓ=V¤=–“=–žÌÍkož&™lÐfM5Ít«±Ö:ëm€¹ZNÑ&›m±Õ6ÛvX6„Màf +N¼ ¹AòƒËî #ûÂÄÞ0³?,ì…¶ÓoQC4¾BùʯPþoCyhRær|±ˆ$þô*7‡û+.L!:{ñÏ¿|ò¬½Ø0ÙUÇ—-fóCm&œu™ðÎJ“ùUõÞ[¯½ôÜS}Eðf¬¬ï®Ûnºn½µV[i¹¥á¬bh¾¹f›iºöÚj¥æšj¬«ÂºÌ5 Ÿ\µÕT]zi¥Âže´ã´À¤Ø4£f`Õ:ÌZe»–`ØV¶lM›%Û¦RgçV†w[îÍžø7volÞœ7¶m•
YµU±ClÇÈzi$LC2¤WBºH¾©è‘™Yª‘³
Ù[Ç 9½"GgdºGÎ[d¿V(…†’(¨Ž„:Y‘È3jÇ£†,ªIûŽÚª¨²Œj‹¨»µP‰i¼vÝ5jAÁ&îªP3*У¦-j[ÛŽJ¯¨øŒÊP€J Ê`¬6JQ¡Úá”e0Â`1Zi,¤ÆZhØ`r²¨À¥ŽŸÖø¶¦bÑ{± =î<Ðõ?HG#Ÿö«ûçKÕó\
äˆü–¬Zãf-nÚãæg<*”ñ`ØÆÓZ<µÇÓψŠhˆšžj©:ˆéQNïÅt—RVRÑQQÒMG‡Šª!¢fˆ¨Hè²Ëg!ù$ídå´¬šUS3±VÖJÒI£6‰dmÜT1±Švöä©ìx£xó±`3BÛ’ÄEkŽœ`7àÎ Ã`f–¸ý8HL&‡¼pØ…6èÄöKf;,ã~ÀQ2ÜA¯–°í~"“yPuÐ]°ãñ”?j·Ã
üNÌ;ËÎJv×
íÈÐ +-鈆¶8hL€Ö,t„ödhP…uÄÉ@›4*@«hV„vehX…–uDÐ(<Œ…ÆyhÝÍ[¡} X …-wÄÖPNB%=’f†j®POÌ +……¬ð†bE+#Èšˆ“Àú¢ÍX)4½<n˜æ¢£’å”nOÆ+ôLöDäpÀÊ 9¡¯bÉ?/ÝÍÐ;/"„E(Œ<6ÝŸL*¶Íp¾Šäˆèìðˆôˆø°‰cº‘ >ŽŸýhânňåHíÞõèAŽâAŽDŒÌ.Fa").õnýܦ–ô'}ažèf/q¢¸»Õéu±cžlZv³Nz™ãÅ»‰ÒÿàÉ>ȳcÝâ„®ë/‹™Hô®S½%ušÃvý¦õž]z§ÏþþÇ_~ûûÒþù†özø@³, íŽzC¹#ß‘6Ô~XO¸7ï{Ðjœ„SüÜS¬êö)æ%úu=½â)êp^ߦ<¢Îþù„üÔç†H¯Q
x—ÏLßA½7ükÂÿdÂ߶]l"•wíYw- »$ØŽGnõ’÷|€PìfÞêÊf×±q ¡yÓÛ¹ÒèËAæƒlÙ2&dQ¨qÆ+ôfêíboÈèÐvL””~^f¡#3‰AC,Ü7aÿÚð¢xE
_Lƒx½aŒÙ•I–ºÎHæÍr¤AdVBámÌLøÁæUìÎæÊeW Óµ ”jÔXÝ);û
¤ã¬AñŸù†pƒ¿ÁÝaïQÉ< yìbÌI2*þS_R>$_QǤ÷Qãä§êé•õûPßûÅWþ; +H‰ìWÙŠ%Çý‚ú‡z°Á*r_ŒÔêM–Åc0C{¦5KÝ#fAÈ_ï“‘Y[Ó‹º-?Ø7šÛq3##c9bx:jžR)ü¬ù´î²§¦>š§¡F觥U¡zj¸AÞEÍÝKF?ƒÂG~šÇ*ø¿ÂÿI… –e™–qé—¸øÅ.fQ‹œ—yžÇy˜ãf7ÛYÏjÓ2MÓ8õSœüä&;éIMb\ÆyÇaŒcýh›ÑŒz”£–aÆaú!~pƒô 9ˆ~î§~쇾ïCï{×ÛÞôª—½ˆKœãÇØÇõÑEÛDuTQF–0…1¡ +2¿øÙOFøÁ÷>úà½wÞzãµW^zá…[ÜܸÉnp8ØEœwÎYqÚ)'AÂ.v¶¸¡í`až6Xh²ô1V[’„ÔKcfÐdps3ÜÅD³Œ7Ði°Ú£A +$,ë4k8PÃW~Ñ=×ÔAÃØFÃCÒ.> +$Ay#…ˆ(ø_ .Tð“ +D¸±rD0´Qt¸"=*}$QFÔ…h&šˆF¢¨/‰B¦Fz"WÈ2…t¡rŽ\?+æ-•f17é4mh¬4ì¨ßP<P`jʃ?Ðý;õ©•nÙ»åƒ:ß÷•§ˆ§SõŸRøY#Ûg/†w@2 S8‘?;‘=R3›é©É ¦†€¤{šOtþŒLÛŒŽ;pœï7‚c“±±Bã +Œ‹ 3º +…2Á`ÁÀ!ã_Cð—¡`¯@ÞH`—ÎÂiÂ6A˜6šõ„b ¿,.‹¬Yƒ\Ý0Tê‰"Q òDŽšT"Ø•°v"À$„Ըкt¯¤/u°aDWBK,õ2¸bÄ…¬¦ÒJ£4ÓÙ©½¥A©É¡Í5èsv0Ôë¨Û¯êxÜóú>RßãÎg€ýŠ0–û_ê€õÀ˜Þ\8ЛPS([ÈòDÿS'艒!Éútù”¢è„Q S?ÐÔeÇÔv
M@€RG¨?M”ëŸD0ÓzƒÐ¦îæÑç’i=âž5!ÂOiU¥©ÿY¤ŠGÊàHž>P_˜6 +nÞ°)À¯ñ [Ñ ¥m›¥ëʵÊvÁ(ŸVý¹yöâŽUÃûFtQjE6:¬ÉÖŠÖÄÎ ”YÏ«HO@—À +i,¯p²³:ØÖ™pìWew/%Ö)ÎLWžõ¢SÊl´Ý¾,ijÅj³³rÝ~ÓoØ3LͳþwöÅ|ýê7ï.^½¹¼þÃåë7×,úuû‹¿þã͇Ë_µÃw/¿ýe³g¡Îy…p}Ú,Ë2c Ñâû%.añ({»¼°)¼$‰y™gŒgã<Ì=Í™ž&GC£ ¤Én¦Ym ‘$44‡Xš?Ò䑦Ž¹Œ=
ž†Œ<`H.æ2Xôe¨pužÀ4Ñ”a"yÈcD"x„àâÎ1±ylS8‚`óئpì Íc›Â±'4÷h +‚K˜R¿íSr#?¦°¸1yÅiÁ‹/Þ^ùîÍõ‡7ׯ?ùd“ÍÛš/¾O¿èüË—>\¾»Fš÷ÿúøî²}Žï‘å[®U +µG_6”/Øö‘ì“ů~$ö÷xü'„?´¦ýcûõßDûŠä_=Ç¿UÏs¤ðspUóÊÑ‚ÏwûwM +Ÿgв.ž†¬øC‚ÄÎÛö<Á*¯˜3àlpV1"¶$$Íôô2mg£Ì³ÙcX[Z• Dš–wxÇÊè)-ãsH@yÓj\Z÷ž
¬Þ!Ž4oÍÖ-2ÞP²¤acë
ßù„¾xÓºxg+}U[µØÛºîäÃëxÙÚA~{s×|¨.ãsH½)6æVuïwÉuÅG•6ín½:Dœ|U6í-!Žãʛֈ×dà£H@»yÓjßÎâšÄql«ÅÕࣽ¼•wì£@ÚÝ÷×ßy†,àM«q;sk>äÅboîšlu'¾ÚÇ›ö™™Ý-öoâ¯îäMëb¶x
Âs]ŠÊîâkn†“»v–ìK=lR‚–Ô”à£H@_¼i]¼³¸¦qàjq5øh/›[bˆ}µo︿þÎ3doZÛ™[S"/{s×2Z- [ˆØâÊ>9÷6)±¯%>ju;oZíc‹ÿÒ̹3¢•–¾xc§/®Þ~ü滋חh”†:º×øÛuGuÏî˜v_ÑèHŽTñ“¤Yþçø'êŽm©RĸC¯è€ßžÛ3i¾Ú¨©z×£êéßrYéãõÙüO‘r —«ÎÂf…Æu>huàøŒuçz’!]AŠÒI¦“„´>&q¸Tì¢Â™ÎKX;Ìv¦U]R3‡ðb +'v“Ì,ã„W¨ý”I'~S1UVJ +:ðJdOü¦"YTJ–Ã[ê™YI©°–þ +QÉ{*Á8cH o2ì¤?0cEa’êTîð^;óÁ¡²ÓXT-"šø-f°¬x gâWø§«,ÙáL0'–¢™3€e9â.e‹ô(Ú¿Éš*£Ì¢5Þš3¿IÎ**ùk,݉ßÖ@••:1 Èß¿©5–UÌ@+S!žù-ˆ°¬Ô} .ÃBa†Øˆ”°µë0‹ÅFI ™Îz=ìˆ,óÎuNÂfB9–©.JœgmRAyèF.–=,n•®#R3;²¸eÙã,Ã#MºÒ)!׉Ç'E>¥ÊNóÉ€¨¼?²Úw*0b³LcZB² +íd&¤lzö¡}ýÿðý¦¦þËL/þìIëÏ´Ÿ‡lˆ<>Yé@Y?íºŸ'
ìïûîÙ¨g[dîÐ!·l
Ÿ.ðéi#’Û6`"ãÅ6êG÷üÜù×¼Ð=Òt|kzö?ÓÎ^žããåéîìt7ýt~Ñ»†NÍ
ÁÚMôR»‚‰ÐÏ–Y¼Í'Æ{aq»‡Äª*—ÁÑéÆ“ë¶49fúQDÞôT¡R=£©MØÆs076 w¡…g +ÞÏçÝÒC—Òá1w:BZË¡(]åân+hC«ƒùh*C¥(4x$ä¹´Ôm°!ôƒ QO™äF¸ÑÅÚQ\ÚWSü‘áêY2¶õnŽÁôxßêÛ̱ц½‡ýÙÆ#ï£rSèŠmÊ'SööhG&ÙÞC’ËÆ÷Ó•œÚ^k繈nvY¾Ì
{¾í
‘Z1¸âý¸I†ª¾eåªíôs¬y—iÛ¬D°µü<o|Á×؆?÷ŒÆë>ŠeIò· µ4ÿNêÖzµ¢íÍ1'®@kÁ±Wµ…íÛ£› +oh¥LèÕuIâv€Ö(ö;´[X›Ýc_cYŒ5 ]`Zϼž^ýnÿøÃåéöü%µøþÍê&¶>kn’…-ÿOvÚAÕàð?ÊÿCsÞØRZ/qþ3MbYôú7+ƒ”JQüïÅÌçÅ~á…„aeáÚ|íÿeÄúÍþˆ«vïí=U9-k"ܱ}“R–‚¼ÑÜpÆÁ\¨íæMdRA°q¥ñfv»›ï¾†Ð#Ò÷_½Ý=†Ž»‚”»}›áÆšñ‡£G¿Ò/…¹wùî·«ãGgï'ù‚é{ë⟽?zw4KÝ:‡ƒnç +‡Ÿl¢:¼´Ì{ïød[ô‹,û¯#}ë®çGü}½0Cm'GqZ)
,ÖGqY‘“ÜÌ᱓¼½v1qrDÑ^‘ßÇ·-
±\Ó^6ß»w÷Å‹wÛ'o¯Nií ‰aœÚ +R§îMˆ+¾š¨”„®Q8õ[ÕäXÀØÙµ°2g7ÑÑïùèrìቚ”€¶V7€ziWumòY &^‡ñ¿&$e¤ÄÓžá;sLÙ +48í$Ñ 5ž¬š8d+ˆ°}¦Äöâ`8t8ti!ÁžïÕ•,`´NÎ|PÂG0Nq@íLb5s‹ +y“D¡’ðáí2©—‹íûCçMMÒ!à(9¡s¨Ùv…Åp®Q|¹TåT!z+!¯ÅKÁKÛSlVmb程Y¸ˆ +G+'oLöbÂYiÁ¹ËCÇ£X늖O(^Ccb½é´«Yb¸Ô
t!´þr_kQ(I–|]„.Ã*”•ƒ•qÍ^g¼ñ"–D:,I‹Q:&Wõ™Põ=@ÍUu@¯‘´¹ìѹ…¶ÔDZ·ëø¬¬d6hM“ôC°Íª\£ 0ôNí¾‰sJ óª–18%£ŠA³$ºfnßBEHeE»äŒÀZ…¸ƒÍVÆ$× ‹4xiô‰†“4£’ùÑCKe& +±à¸
%º¸>bän~.,$×8}@碫ý9& ¯yX*ì²*—|©©sƇ*\ˆA²N¤ÓÛµgçêy;0^“„ ¯—j¼Õ„J”©¸”ýîcŒ eaÄ@Œ>«í¨ýÆKÿ‡¼x×_²®º~7ÂXí'G³Ã‘n,¥£&ëâ ]€*2zmÍÙ/RZIþ/ïÕŽdGîâ¸Dý;«ÌýÓE€ý<žuôvÃFIl‰"A@¼|ÀŸ‚[Ýz¥z_iû°Y|Õ±1f}™¦÷‘ + +OõDÝGw…ßzáÂ!Ž¾uDÐNU +°Ø’;`pÊù2¶¾´–"ðªÌxí²DæÛE-3{åñ9Þ’›—d€ùŠÁnÛ]€(¯l³gĤ¾Ù,,wD2™Öãô¥K¹*Є™[¥müW)¿o¥hDY7 ÉW@¦Å‡¿é°g¨0È
>8ǽÛ3"ÏRËãК±'cÊñþ©2Þ)aþ¸Ü'aMEÔˆ—ð¡ì|±Ýœ·óœS +ÏTž¥¤2`§þëËüân›ÞmUÓ%y6 &›ÀÆĸäšm†Zw°g1p€þ§ïî=}dÍ0[wÜÎåTÕpÓRcñ-Â5üMxm<±¨u¶!åö¨uùPg›Òˆ0Ùü~°·ÞºuÓ쵺òÖv4’G+ƒM¸ä’Ö_9~7µ}ÅS‰÷&Ú•þãÛ¼óMk +lœ®æ›V4ÕäA°¶ ¬aG|aË7iÆå’DKbÞE@Õ¿*œÅoð^¦z¸ÑYá“VcˆMr‡Û‡L¾(FÖ!ü5ÆkëÔ¹z:¸êð@n†¡,ˆ<Î +^c+×47ÛãO‘WµŸ½¶h²YrûYú?ÔòÔ~“å’€Ün‰Äˆ¥¶Öí¿B„AM²pQƒÌª˜¹½Â^n\T“,„k–>=–Ç—¯
A†í[ChÛÐ~ƒÑï%©<ÓïN'“úh"•*ìaŠS+Á]䬮úQX¸G©º2U+u¥:K8Ó¬!àµ263xç×áGXg
ΗƒÍ`}`ÖYVuÙùs¤»ãëƒpï"Ë«{†²^š&»Í8îB
W±Q-ñ3hèe1=ÛýšU0èï|m +¥Õ¬ôc¼¼‚´–eèÏ`m½+*µ‹¶wy¿x• +e³]×Ëjìaá¦x=ÿ°£z']á±Iý$‡Õ)¯1»Ì)`Å9OÛ$ÁZ–še“‰órU·F!ËCmäñƒÜŒ\§Q€½ŸõR…>¨”ÉÊm,Îõ‰©¸ÓDÏr6 +“±2;À¦Èɶ–>Ïr3à ¿…p—¾r?t-3»Wçþfóˆñ•“ö°XÀÿ^%ë\,ñ sr´ãßÿ˜ïT0MÖÀòóïL Bö^Ž·ñä}Ô¤ìï¾ñȧHE]ªí“ðiz9‰S€-Yy³0·zg±ú£ÒÈH»u®}â_;F(|â—ýø?âò™Gü +,g•GÝô+ +íE„
íÇɧ¨e—!°5é½æܤÔ00DOf9*\Ús¦™Š¼õë{r|§us¥’ä±@AŸ„§æh-¨¶¢Ö&)ÓNÂ_b¼÷€‡5x^ÃĈbúgöÛõ{wcQœQ^Š"yèåQÊ*lãò¼ô—¼sðA æøÝÎkŸjüV¥XK&ú‘ˆ.Ç÷œ†àì!F°œ“àFqÇ—ß½ï.°¤Pµ‹Ùtøyá +êé|ªQæ& Ÿ²+“Zš„€%¯TZ²èàÌ阾¶&±¬~Ñ
@b¿un‡¨Í!~ÓG°J¦âMAÌ`ñšÏ›_¼À#׸ÖÔÁÞtð,an"µÿTÑm…_úñ3Ç©þÈà;Ÿ§JñÛÚ‹Ï3ÁÚÅsHÍìh ÇVMÚ]ٙ诇]ûÒþU4— ¬©hî¸T€vªì]È„uè +œ&P‘°²Ü±ÅSòLT| +O” P»ñ©’"Ôú%J`cmú7tÖ[lø‚{ÐÙÆE]æÂEÞ˹;K…;!ÛÍWrŽÝ¯,tû(Á ‹ +pÃ(h•¶ +!µVµ—íphé§×JLÐÐ +“–.c@àuí¾ìƒ}r›š4á÷i´æAYžÍŠnQ"ôsîõï÷€ûf\J
Œ,P¯T¼ªãkDS5žê%µÒ ÞîíÍŸ`É—"ãë V¥ùα>Á áZø|Œ\åfçaPðå+lÞ²
pím|¾}rÿ½[¥JÞÆŽÃ Òw¸",6NÚ‰¶{\‰=ÿÊ@|ÆxÀ'è|›Xu$ˆ÷¯ªd*™ž'Û5½ÚtzãSië‰õ2u׎Ç÷NQƒvd2ÖÌù4ÃM~©YL›cÝLÔõ`ýÁñ^F±fêÁlžj÷c“lŽÔ=ß·N±T«¾œ±Õ +Ñ#Ç"ÌÉí6ª;P$%Ú
Ö +šÇéE€à›Õ>ö(ovWGQrö`ù¹dµÍ>õwšÈ{¸†îÎïµ8f¨õ
LFŒÙdÕïníÃR¥œâ÷)áí§ÊÎç¤ê!7‘¥p=Þ¥¾»iÀA`™-rg•œG:/_ÜÁ¾©ïv”Ä8²<ý$“tYü5O}3š ëóý$ø +ÔÒKù8ÃJ™``"ÊK ‡ªP÷.œµebuFo¦ˆ;ÖùŽ#¬ @ô×ot‡ç]mr¬vê’n㬖Ê.aÃ9‹„îØö|ºoÇ €ƒ +(£skèu®uâïØÃMmB•åç†÷Α÷‚ëg±Q¦I:¿Dí£HçrU(è + puMÖ¢áô‘åí¶´]xðB½+vÕHà§óL£Ú¨Û~œòÄÊrÑzMXÛ£íÞs +1z¶ƒ×¨‡µ¼‚{¾çÅØ}fÝ+uá¤
ò¥–Ù7‹"˜t̓E€§^kfºõÚ\=·b·GÉ6šÆ¦óÑ2ÈÝîM²eN’¼xóDËõÎõ1?É +Ô•”?. +ØT0-`+©,Ÿ ‚…†îÁ
#}Þéþè¼H~¡©Åkç…åŠçÞ7Ê؆¾ë“Š#a]º±<´2{ÉMxmLÅÉ5/ã:¬Œƒ¸u‚ !<•!ÊÔB«£W&E¦WÊœ@í‡c¨´þ@"Áï6è¢çվ̵Ò7)Ì`´“‚µS„]kº6+[)á±}̤ؗ×kÿìÀ“d
™Æí*7{ +:¯¦fe]£’¿¢%Tm–¡ñüWh)s‡¦èÔY%S$„C•OåÌ~1ßcRÃ6Bdè’æ+8µ†äôÐ9Þ!ì½ZÙ^$å¨otY.àj ŽU«@Ò¯Eϵ›&K¢‰'Rÿ+›ª'—ìb£Ôg3M~Hm3”Fƒ±›~ЮgÉŒoÊjïœ~®§ªU(!J¾ºeÁ‘ÒÔN8-„(ä3äs©û k¦ +ö`(Pl¢w‡’úñÚPdiÔŠe¾ÿYV†0]g•hŒ²´Ö _òÅ)8'8Ú+ƒšüîõS·ª‚ã4XĪ•`d‘ñD„@hŽ’¤éjl/œ¿í
›m +ÝQÿ—cØagE‡¨ü\#ºõyâóÓÜÍñÝ~˜ÖŽÿUü@þýpI@8Á_|¤#bÒ)Ôƒ˜4ãKÅ¢ +ê€ÅêÕÀÖÔ9V*a…´oȲî7jIßÀñÞÖ¬Çmu)sGßÙé*Ëde`éxñõ)>ò|!^‘=‚ÚZavC’ý‘ŽK!êòèA‡\ÏKP¯ ï!IyÄKø +®WÑcÇ~„S–S+Þ*K}srÕRÓx#- |ótk¶G^£³bƒ‚Ò@"ec@Î{öÐ[´¼E e˜ë@¨0vÀOpŒŸ«‘«Q„õÐto8À\¤b_jx+à +8»T(H¯³‚·û„ôKG`šÖJ4cPXÀê†!Av }g‡qOƒ°–äkU'[‹w9y,ŠÐÙǪ%À§O‘Qø]Ë
é¤\v`ôPB}Á©²úÜSe—|ÏÊRQv<ׂieúq~Bpý\Ûù\""KÆ’˜lõZ’ˆSPé9skµ¥IŽµŠÇÚF·Ö¶äšU¡d•§P¸ +Çk•¥4'naǵ1^>‚½I0äÁOÇ|Èà€wÃa}dp u¨Ñx<v}—ÄÞ —4±s©a#z6¨hÎØtqó0t!TÅ#g¶¯õVh$Ì05’
4F®ÎИLØræ†_[.nY{!ø…ö3[lmQxƒØ-º·<Øåð.ß»Â\kx-íÄüN8¶ +³S£½ž÷·¸®b
_àž“ÿ5îh´%Ü–œ;"o)Q‡“f s åŸÂ™Z.{ýTãµìØ!6*ã={„[bvŽÙP.0&NN«`Eƒ'X¬k
S&åå'\´p'S]–^ÊmŒÇ†+ãË *N'd,šµÂÏMAÑo›»Þ=ëóDóÝϧêÓš}×7ûRÀE
Á<´Ë°Ö™ÀÌ€ ËØóM +:©£´Àä«>`^¿àG4¯¾pIžpf°t3ž&ÈVkJÛ¸ðõƥVÅnŸÂ÷YŽiÿì³E•æHŸ•Ššp–AÄæþ!†)ðþ"Ÿ‚‚ÆâË5Óè5ÅF ’˸ +zrÛFÝ_…ŠG]ÝÕŠÍl‰`Ř|h¥ +:—UrµcÁ.·É^õÜ7NÕ¡„¡¶x®,=7‡¾©¬Ï=HOé>ó·4…×jý\£&¸w›A&²™Š_´ÔZœõʪ³å¯ÒäÔ¦…'ÂçW +®9)7á´œÁa€À¹ á:î@ÎÑÖZ£uªæÔ"&½š-5DR »äð3z§*É9ÝSt¶ò´W²èmåñ¢¤§tÇWJ%,B’CP°‰]Eë„CÔ‚£Šâ…Zx_šä¶káY9ºø¸ƒÖ&ƒwÉFsöÞGþVÎYRô9q°4½ +ÈœiHùižE¸}¢ç)‹FãÜ\¼h´Y …ì¦%¨~ôl§µÐÂÔ¤ŸTâü¾õÜñ8`¸Ä³‘K*àìÄ%œÀrÕxhÀÂþéK'°Üœx&°\4\„°˜Ý#°\gKÇÀj3ª&¥z½”Ÿ=«Å•®T;!wm³½µCnQܤ}à°Ÿ¦æ»w,Ú?…ð—x„…>à}*Žœ«áƒâ X$[Sq4xTœui³j-þó\í{Źæ€=Ý._»ÄÞÔ`[¯]a/¸ +á¶m„ìNó¶â¸×ÑæîÕù¢äWŸÀÚãx‡ø;¶}òÔDÞ03ÿ¦4|âzªVD Ü +–EŸi†ÙÏ&Å“CôV1¿H«³O
ÅUµÈÅJæw…®Ã‚¹ž;¿~êÿ”WÛŠe·ý‚óç%0óÒè~y7yHè\˜B{:è!8†ü}–TUÚ{K¥ÎcÓ®Vk«ªÖZµj†ä;&®Ü†”³¨^Û]©³.&ŽeOƒ§µÛˆ»Ö{Ï6×Eϱ‚ +ÝU°¼1÷ÓÔÁU¬Æ†¢KAp…~2°|‰o KÁ ŠUí©Û´–ìß(SwØí¤¸fÿŒóÌ>û»–üvŠâ$HëjýÊ +„ñ?èzgôî ÁÒFã€j<{T3òO$øWÁƒ+™ŒØÄ JË1ȉ‘pæ˜f‰r°"
‡“i<óšÀ ,½ê gÕ±i¾ ÁnO&‹„ŸmNgå‚U883ˆó}º]I˜AX~ƒ÷–3µ§Ží‡ö’ÃA‚9_½2zÕŠ«½YÚxm.NúÃýØqëéSõpÇ³Ê s§ +¹ÏK²ò±Ka<ò:5ê€Émîé‚£‹œ›{†ŠI +ê^nª ¿^
ÔIüÕ1±)êøQÕ2Ó&žžÑ¬]•„|¨R£Š’F‡uT’„œ[yžáê°TǪ6‚_nßßêýÝûûÇðÓ¯>ü¦üåןúîç/ÿþûÿúË/Ÿ~þLÁï>ýíŸ/áw¿ÿôõÎÿs·ñ=ÿÿ~üzûr³÷ÿÂÏ°UýŸÿÁÿü?üóŽvÿz÷ßÝü³¹ÿÔþàO,ïñλDäÑHn#O#âCþ;-vüåg<ÉГþ ‡·µ6úûב¹ÔÐ[ÝvQS²“ë×[\b¯d)ÏibW˜Ü @o¯à5RKä‘\ŠaÿŠ¿ÇØäáÝ~߆ ™ÿT ®‚à +aAÞµ\G-D'ÅX¦¡šûá}Žù
‹ILK$ê—sK•Ù +båaÇÙXr¥0~Ô‚y¦ðçªs" GŽþ–xJpªìél{>ë”O¨…oÅÒ8WV›*{þA‘s–‚í·Ý•zRÜ•¥*ê?ÞÔ’?ݶíQ©ä°Ë÷%áùªø?Di§«öÔÝ÷a!¢µ§0]k`+%è%hs÷=ÈÛže£éElþ#s1bµ…ÂÆ'
õì:á#‡k
yÙ`=ÇC%(&É•ºç–|÷²¸Ddqå“J¼…¤7`B]¥¶j”vÙªuYk8—–)=—ö ¿*š¨è>+ã×–Á] +ì~ÁÇÔ“…-0Ù
ÈGÃbkæ¼\î
q.LyUJvÎP2àP1–¡h›”3…\$-o}Ü +Ò +úW=]Ilù˜ò¦õùkV’ÕÊ%uËcwÆðV×<~Î)/àÛ»LM¨x?HÙsØÑ|GЧì$È3ÛŠ$øÌ9pj§³ør²ý
øutv û0צž½“¬L¥{cöŽßÀ«‚F|é’ðãN$оê“Ò(;žÕd½¤)3£ÏW^z„£P›rßg~î£NÑþ¡ö·}í¼ õhÿs ?]_…û‚%á̯â04Õ„øD/tÆüåÞBHOÉ–ËǶÌ[ øzÓî¥j¡öÆWz—7þØHÇcK˜»ÀÁ©»'Þt#mº
ÁŇûÌ;àoH¢ÒiÊwÓG˜™B*cÚOIVZd:MÄõ¤ß ï¡ÕÅdˆ@îËØsCŸV8yL›u©ÅÓ½p(ý©9c7°‰‚ +¯·]Å´Ò.M Ö £®›Ó1:c]äš8¼§ã¯[n÷ñ$t “~nŒ}ðxêÔV³¹1%¯ÆOú%Ê·öVG‚˜Í¼ËÃò ¹½«—5Cøõ¤É$©ŒÄ±Í¦Œb•ÊÁ*›Ej›IËz¿«¸ÄƤb»jÄŒÇ?—6ÝŸeµØ0</ЈòÃœFëéìÜ¢Ó"ôàãu÷Æ(˜a]ªç¨÷[ÏI…I™¨¹ë$ÂCgEìÆÁ#—FìÍúR1:`h”Ü8òCèC`€G´Ô¾Ù3ø²Iíl4‰ßeKLÖVÊ£9üØNÁ.¾BÄsh@0ÚÔomå‚þs]<oRiBöùhNl%pCtÔ4,¦ÙQ½Òà3…Gþå]µ5¯¿«—Å'mRš"íÔKÕ¹&jú©*íªÊ«X_}Ç‚gù;–hŒR¹§ñôåÿž»Z'6]Ó;¬aaÅ
‰û2Ž…“ÊèV‡¼nTóðÍpê5[ø*@]VÕbM.¶Ò¢‰Ð*X¢cb +Wëlß(ƒ¦"ªÞhÚ¤Ú½€ÙHͱ_Bïv°ÚÖgš¾Å{ö.æB±¸~1[áeqH ·]Ým|°A<QBV¹;yÛá„¥~@qEVMîNö!-ž tc‹k{Î4Ï=Û‡Ø3y5µ5ßJ®ž×šþg¼¹‰Áqa°òrÐûX¥Z†û‚7$j·mí–°4ÿÕc¥x2¡Kµß°jàiŠ<V:Ã!JÀEnïxµm8zSÄ@•"óxm™¸µ¹¹´¶É*Uy£€‘®õ-±)dË>Ë÷§¢l=Ž²TÛDòKõ³ÆæDЪ.ˆÕu®kðH¾¡"†ôÉÆÓ€°ó!p8ºÊgƒ—£¨¡XŠÑZZNÄ
dúÚh›{qË[”-> +-3—lѱ±"Ê x$vê®2yt÷tÁÑF ÎÝåÒNëÔa*”ؽÈòyû×Ûn0¨#d77•€O·=WUR« h…**œ·Ð×H²jî·™gÇ T'¦2\_nßßêýÝûûÇn_6;n½Ê-ƒµ¦7ˆ"Kø‰ÃiaGaš=™‚>¢eY¶ >é
¹"ñ`¶v~9°6DÙùB |2j_+]ljAÁ-¦·Ì¹ìº]Ãôʾ2^.);ºaxÃv¶·Ô_.:zú>š/‡säÎiØH|…ßjªfÖ‚Iìïzæ2ºÐýžƒq³É±9Mt[‡e?¿Ô{cáÀ Ìj&V´–7‡„›\“צof¯ƒ(Ú(\¼%VYd],å6D†l¸(G_ d•Ãy˜XÈ&› +øDïGà¼<Ÿk¸$bÓËÚ/ˆÌuº +H‰¬WÙŠÉý‚ûõb˜yp‘‘kä£æâ›ë…Á‚ÁÓ’¼u3Ièï}bɺKe5ƘAšVtVd,'Nœ¤Z——Ó/[¯aåÌ´pŒyM1¤ÅŒæhÆPû0â§,Æ´¶Ër…×Àjkbâå¢Ö¶ÖÄâ!ÑŠvñPÊZU3Ô¸©1¯±Éý¹¬½v¯}m¹ÖóÅ£
¹Ém©¯µªƒ×œ[’ÚZÙÁîÆlƧӇSXÎ&üÍø“–ªOZsñæè‹›S…‰)”ÅŒ‘²×€‘)6³¥Ôºe°/×È`ZÚiv)¼’î®.ûŽ¸îë=Â*
Í{(l÷ÂÜÜ´«Ö(l)I}£•ÍNˆIö•*>Ѳ¢Ý9“Fš×wjZu‰)"æ2l1†¶Æ#\.§CdM1ø×ó<Ø‹g•:þjø³ç…nR‘ú'ü±jZ¹¯pæiÕܼÛ0[‰Ñ̪}ͼ 4%Äî–”Ë>UZ¹Ñ>WZ+7ó’a¥Ä•‹MH+¥eèùhŽZõèÀ®Ÿ]5iýù4mëåt„€)Tv±žçøˈNÇ¥=²÷©¬Üƒ.tªaLU}ælgŘúïâG¯(†1Áì°D[KjÞVT«ÚLQìÖëŽßëTJR¸ÌÝ>F;ð—{ÅßÂT=±‡—Óámó°2‰=€p$öˆÖ¦wð?ÏC=+”»´-|æî,s¦HÖ/ü.êmLꈳº‘¸—]ZyºHðŠÜå”ËÀAM +x\–:™ßºR0jE… +’<Ïã½×ÞÀâä6¸í=Ä•ÇÖ¹O<f<R+ÚŠ[0Oaÿˆ¹ó<ÚË|ÀðëÜùævË +mh6ÉwA%Ô¥°çE<úXÛ˜ú”»‘umf,Ð4zwÙ+óñè`Ä5¹+®±´Ç¨dñÇ´«¶à+ïØX>¬F7Xœ¢v‡˜ó4܃r·> ++›‹Š£¨µmÄY ËΆH‚ +È«F7V›ûP§Ê¦'Äܵ¡¬)Õ϶RºcÈäFAObÈ«Ïx+Îý!s8YY`Þ$,j&ÝÂÂѺ +ôTÛ€bË\eNÞp3Ø™ +2ÓÃH.7!{œ8Vìµïj‹ëæ»ÚúÙÛÚ +ÕÝÜ—ür:lÏë¼v°u¶—Pš*X~q3†-Ä»5¥Ój¼‡î¦Z»JØc<|$QZ˜“í=j¯œî†CÕzÍV@à…“Ž Ñ™4'³š²¹ÔìÁ Æ”‡†¡Dî@Ζj$«Ikñº‘tǺuãh˺q¢ïÁ=¬„Ú®§lƒ/¯‹¯æ µÁŒcÏÇmFØBîË´‡©›‚M²/^q.ÍC –ѯJºë"áV+ØaÇ +ë€Jo{Í)†BÂ(‹º0£¿š+ÛÔz²Ê’H«:8Ž +ðP¬ó@á÷]N¯Ðä”PÈwBÔ tSÎOIÁñîï./ì +o(’%®‘RÂ3/Uƒ}í#„¦8¨<¶%²ÊTÜæcƒû3ð¢ùW`³7«MøEpÕŒ¸‡,üûµŠÓdŠ0¬ÍÐEÕÀ)-T=
ÅRbâJvàñÑçÖ€ûÑØL.Ô-SÓ°)€6µ ï=ô:¥Úý$®&A8¥eZêó±^‚Ϩûg›ðË˼[—Óñm³¨â†Jaô2JŵEsÙJåeÕùH• ƒAg“ÐÿŠ_küµÚ$_”7£û.“®‰ +i•Š?rj†+¨8ˆøG`Ô:–Ú0€7ßË·ØÚŒH— æ·¨ßà•Çl#ÜNì'ñŒC\Öe–ìùhý#Ðœuà1l:¹öýúB¸ÿâf£B”¥§hwaȃnz”Eº/Öð:-,'•F‚
§±}´gcØÝx¿œŽˆ`Æ{v9O1s9|Þ˜Y§Ýžâb‡¡Ô
ΙÐÖ„ß&Løáôé/ß|»¼ý#~úÅ›_óŸõñÝw?}ú÷ßþð—Ÿ~ÿÓG3~÷þ¯ÿxgþæwï¿,þ%Æo²7øóöËéþ^×ÿÞ~Å?~ƒþÓ—%/¿]~üSXÞÉÉï%xlÖ¥Òԅا¯ª«å2,xœ ïîÜ>=0ß|ÿQƒúýÉ‚" +&rb[Ô5„L“¥\Å©@úáëÐÊ*P¶»žçžŸ
¬+U}`è±]Y S‹î]˜S2Ò‘oê¸BŠ¢…lê–Zô+÷žýÊk•0 J©/"#ˆ¼Çñ,*Ôª-upãWWqÜÙõ|M@øóÜï5G& ;Áñ<È°„æÖÒ©œ“ñCÝ2,QÏ‚kZ.´exçwä×±1•¯ +ôBŒBdV™‡IHÅÄ”c¨†T¶»›P†Ÿ†…û<wúüÿ•þ×QáU=IVB–Ë°y’nã _í-7_Él|:Ñòæ_“©&.9T²%ià)P’Ч0C+{^@Ÿ&ù"°^»‚'‰EmcO@%t—¯ð(51c{ko +ìâU”»ìý|<,ðú¡ž²]•JѹÅÉ$££A¥^\gQ¦Ø,l‹ ã61ÆM|ì+àÊ>‹˜@Éé&ËÂp—MΨïÍ-c8ƒ(ÀP$· ×á¤Á\B«h?¹_}éÀ\6.Ìö(—9—W€±‰›¬.I8Ë~7Ànq+ØËîB?ê]Ñ%ŒÈ§ZTº†í{¨ülùŽ%%C ÁÆ"Û¦ù{aŠ<c„*C•Ý¹
$¶oÔˆÂð'Œ©kpRK×’£ÕXðdÍI
%Ô¸ÊÖ¨$Ž5±‚"sÐlQ"¶•ŒåÒ=YŠãûl:QÍúc1Y#W…h™ çž!·íû`ýÕÂɯÏlIÑÁ\îò…A)¡b_‰ô¤¨°'Œ¹˜eîÆ^E†&$kCËÉû3¤ÉÚM.á®nq…!TÐd"lŒþ¹*Qâ%gŸ§¦iÁÖ²"7å˜Ù$¶¼Xv=öQÕâW@‹‘
'š+úÓ–Yþ^TŠHš ª˜½0‡X$£˜“Äë‘sÌf£œŒÅðÙÍB÷"² +d©‡fÍö}±WÙ“kÖÀ6 +媫Š¯H¹+û$ÊI£3Dkdé1[ì5'#dÁÊ»ÎGq_‚'•íàdá yá_‰G¤“0UèAÇ—|v„=ªu£¢ðˆé³Ï4ëšÓ + A¬KnÚvš?Ç‚ŽŒTP“
±‚pÝÃ#N;Ö +9#Cù‡[AЮyD‘Ûz5—°CÀÇbL4äš Ø¾‡.À +)Åì¡`ÎÞS€Ñ“ñúRƒN©:iÊ=æeòÆy±Åèô’òµ¦)*Ø׎3'ç’gÜ#›ŸñSœÜ-„HÒpm¾~PÛˆû°NçS:6J4U_¢ü`Auß‚Bó¸cFõÒ"[-…ƒXpg< +åz®û8¡ti+|¿Q6¸³[Òäml +š,BS¸Êòü¢ü±|Ù9ÓdD†9v³r}…Õ5,•6¯úU1SC=Ô³ä,xH×|éq¿µ`n€FMåŸruàR¸z¤fƒÝ6mÇóÑ²b›pWמwýÏÀàËÅC¾(£´› Ï»ç`‡F¼æÅM41VaÏD`NìdǺ*tGy"¢@e°ãµHù +ÏX‘S€%ѪŽb–ðèæšÙçˆoAyÉà`-+ýJ5Ñ°p +þå·ÏÏßþôñfö+Ö)þ}÷3äÂOQ´åf·âÿ}÷?üòüðo@?ßúí·¿þÜ>òÂ_~ÝÍ'dBÝcÖ ðÿŸÑFwøé5ŒÐ
Ç^Xx»_ÖûþôO³Ûx”,EÂêXtqi:õ +t„k>Å÷ pìxí=74ºPôyÖÊ™yۃőIÞ}½Ü"ïôâß÷`(ÔºWog€hÌÙ–@¬(}ut°ƒ”´s©£E3e-d»ƒ³˜?—3¼F\ðç¹\¥:<¶—¥úyvyqñ( ;0Ô¶lî5®ëgnY5Â1øÆÔ[—í6Y&áÕéY‡LGœE¡–’gQÇ2àÏ{7„[ôæÿ + Ïb—ëž +'Û’U¯HYµÝNdò²ºG¢—ÿ½Æ—ЈcÏ4jñTtÇl÷û¥·@ëÉî”›n3–Å`‡UuJo©çДÍdÕ+P`Ý¡›“fôÑPódÏÛ$`Êí +Òj€u˜0U*¤Ø‰ê1jòúmܨ “êRTŽ¦9¿ÇÑi-‰e(¯ÃÅ~¤-¾F¾uÕ¸+¥c±¸8¶÷P©ù +õ¨ûm©Xì”®£à–U=ZÞKUíM›÷A;:[¡ÁtÂe8LãànêŸZ†]4·ØäH
îè`*þ™SÒTlë±å\=€-p®üÖ>ñ +¥ŽëºoôBîW–À +Ðc©/]§Œ¾®§z–X)‡3nIä¯Z—@ë¡V0Av1ݧN1«Á@£êLà Ðw¢—aIPyX¦U¬šJPOpu%k +[%µ!…ýc^ߦ,јmõ¢¹E
Wesµ2t"g'œ£³s
-„³Ÿ¬iÕL"ßíF¿R{Îü–…¢«sìÛ{îgX…ŒXx ì?mD³£‡_m:ª"vТ²Õs:F³91læÑ5cŒë<ºpÌ,Wdãxø€¯>=äé ‚ñH»¦ÇŽ!ÎùÚ¯{TÛI¯LnÖø!¥wU1+ý:=ë +1‹&LŽº×uN°Ý³hS!smƒ$U_ûÍþE \ÁÔªA\¥í»åÉý(9µB±N’³:Õ +¥;ic²o/½`ú•8õÓ0_Ÿð&}ÝÛFí=¥,vç€47ŽaèÞ¬.öMËÄ•òëÎv´&×zõ*qÐ@kEàFyjÈ +×âœî%œÖáÀLl|*‚MJÏln:õWôÿøâxÐtc`J«M¢©p.EŠ3³x^¥pUŠç™ Ï}s{XÀ<ò‰ÌÕ¾)‡ÝS½$ý¶—@}‹ŠáôXfÑäŒ5°v†(°S§Ï…T"Ú#‰ß< +#%HmmY„€-ç/@o4sPŽ€8ÖIþu@!ìNX^à¶Î‘PÚ8k5Õ³(=Žµ¡ŠN³y‚›·5»5@/_>ç¢X¶„K/WTæʱ0Ž]i‹%K,DóšZ‰=ÿ˜ÚeWpTë\Ûn`C¨ü+pY*H¬×f‘‡âc•´yåÎÛÛêÑý¦ô3ë¾
ÜÓ +ÎýW… +pQazÐÜ)¡ª#íÚ–m„CŒ=xÖ<ÑÝ›ØW–¬_j!øY›
íw³‚BÝ ;Æé=u§ýlTÎg-;Æj¶†>u¾ÿÍÇ.¥ñXü—Uj¹11è8¸ +úrx]HGç#&ZÌô[°Õ";`Ç4y¼¯·Vhˆ»lUi*À}lù¥Ù½íŽ°@ Š.ÌÅ“¾ŠîÂü´–”v
Ù|u©sïÇN•Ùª#ïÑR¾±2ªS;•]b÷_ÙëvŒƒ«É´öÖ°•¤jÀæ.šv1GY€25Ãg¶O}wptï>´TúÅï‡g°Õªï¯ºL`ûÚtwœ†¢t[6²de´´¨^÷j,j{õ.²‘z g¥Ý] F»^ãFuW‰¬º[M€£¶ +W?½ÜŸü´Î[àÙ
Äì´‡CZj«Ü‹åÚµ¥¥pË3À~$ÚàcpóÆ®nZ¿Õ{7Ê¿ŒmÇÖ>Hqµ|<À¾}f¢tÉè¨÷u-)}Ü +‹P˜ŽVïw`Áˆ‡G°¡ \G&ù$ä¥qsÞÓ(´KÓ(‚g±+«#??ÞƒøšÕä+«WuÛ)äF¼¶ ¼~Íì‹€V'À]ƒ°çØF¨Ho?û'¹Úç/ÅEE‹Þwu"8wŸoàÚG„WW}ƒŸQ%õZfÜkJ^P6³µŒÙOÀe +.ñ„uÄÚUŸwMʽ.Æà‘¤ðÊ[”K©Sü¾dª +ÕEqL'AçÒÑÒhžÛ@%þéó1…Žg§n%–öüüWö<Vùðÿ»š+lò +5*RÙN™mÿ~b´<}¼ó>eê;àåKe>òMÑÊ<›•Ìúõwè÷ë¡ÅqfX¨¡¦Žšqdòöž#ù¢–-•s¬‡F~ÆЃ¬ÊÖöãµ!Ñ*Ã…F Œc‹¼é˜˜Ÿ>j?ô„¥³õX<0e´Þ6ÿ|’ê.ë׊EV†M\%Ï÷úkñËÑΡu;+ÏVg‘T`óì0ú#µõœ +•öñ +aý^Á9#îýyÁ« Ûi³½úŽÏŸ¯ ƒéTªw”Œ¯Í¡¤5aJšJ +/õ£(ã¹Âê…Ú“*KÒÀæÈ<ç¹S +ÃçÙIXÙ>ñÈÆCA¸ùý)NÔÅk÷:².
+ZM[Õ¥šy¡òôcM®o¼Ò-ú§ïWîM`€·Ð +2‹†i}Õð%t~ÈèÂXŸ/_‡KèÎg„뎅µYؽ°?wÿÌwï KGŸŸï8«ä·Ñs|ý½ú©5çõÌ—r%Éú¥÷ù”ÚÆjÝÌKÂËå’î +êyXh!IW^‹h‹7i¼0Àý× ™-eÿžì®Ðó}yÿË–IŸ§õ•]¨mORÍ wEŽúV›}M+Ç+›]PæY'}ž|Y ^'v×l €eùÎ]‹µJ6¿õaGO]»iƒ‡šäbyY›}Ýs${+ðÐFÜ®!™i€¯ž¹HÆ“üº4±›nY:+»åûô^‚ÙçVZKîa÷y0qëm“`M}ÜkÛ<×y@Å,]”4/JÛå1Œê<a86}¸k˜M}Ëû_¶i´êx=>I'?YC…|¥m®c¡ÖD\ìR÷ +»
Ï]7J€%úTÊZîݦ5 +¬m´ðzçeȇuË™à£
. ïnãþÕ×÷ïÿŠŸÂSëäÏ÷ÿ¹É?nuûý~ò7ßü¾ÿýwŸ?|ûÓ/ÿþÇŸüùç?}VðÛoÿü|€¿úîóç?Ü#}
›ßàï÷¿Þ~Á?ñù®?à‡úõžï¼ÿð·pÿÀ'ÿrûmîh
šuHCoƒ°£rëÛá×;š'¯¯þýŒ<}úYüùþ¾ñoº$¡ÑŸ.á×ÿþ$¿€t .¿ÀðÊ9™uŒBª†¢Ð‘ÝK¡ÎcËÌÆö¾·Û +åùlXWžm•(ûå/ÿGhS:kÆ2ðö¥ùhD(µ^v{(}Rx +gbKùÌEú¶$ÎéL¹—‹l~óÒ!‰ñÕ-ÔbhK–ZbÕˉL àL±@N0\;œ“=æ“=vNÝä«Á˜ò‹qbu?»ûyN†£‹¼-3|Q
ËÊYÖØ™U¥½4Äõ¶bRXÇ +Ùä=%í.rì„b:+ +2Fuw$±…
㽈4BR²ÕëP«€Xèʨ§°
|o¼¥&Eô|¶CL´ÍjU«ýš[¾*Ü›;ÛªÕÉ#aÕ4€~?%Ëò\(‹‚ZÞ¢@—…|J”&yLOãñÀ™7÷¿«L\d
0Ú…TvÐ†Ì $¡€cIÉÀ[…LM¼JxÚ‘Ò³£¦ai§õ.QåÇô¬Ç‚
öx¯,F·IÃ*¡¦Ø§û‡›_É\†fQE{‚U³¬°e-.ê–³„~§,Åm£¹ cÅÛ5Ç)qê û«¬RE!Jâ +EùdµnQ±£mý£ œíHM짰.²¼*ˆeå,ªìÓmJÇá*pAÅ’´+~5g`„¢KëÖ +˜Ng¡ýB£“Õv—™Xæl‘ßO›Ç©³lǺØÚØØI!ö³š‡)L|z:í²²öEàJ› +¯§=æÁ{©‚1•SÔzصÓQ2¡F£¿ïìXNT·ú;±áªtÉÜÄ𜊓žÂ~¸Î)(BÝRÚDgÔhF¼ð²am¤q¼×A'#ñ«>&dŦ‹y¶z?†ëËmÅ‘“± sÅûEŠ–é\&~bkÒÂqÎgì¡)œkô46¨Á.„ÀrLiW™…‡Jðbï~-–® |uäÙÀ÷ÆSïÜLg™Ó4ŽV1•µèÄ´M¥L©OþNiÙäûœÂU®/ÊbUAËR›ØDñÄèÄ +-·U—7“rz}-¦ð +Iz†Þã¬A¤Q·ž5¿,á|Rå6ô¢†¥íÞú¤’î8•¿ƒÉ°Ås¸<@÷ÕvðŒç˜«”ú—²»ª„eͬêkÅSpÛº.ˆÍïF×£I‡‰*éÖðÞC‰.”E]n S«Óâp6¾«¼ËŒ^p»JÃ*]‹Ìn‚˜Ê,c‹$eÚ;÷˜îãÒ¡¨#¿ÑÖÐØ`¸-,ÈË9u]gHƒA^mÍÃ{ +ú‚ØQP#±ð’$íVœÆ;¿+˜ó0W°ëf{©æãy¾Õƒ^x¸å"êA+"Oœ»W}l{Z– +øm}BßhU + Äs²-tŸ©:âÙ¾“iã‚ðx,¼<ɵèv£‰ç‰t»—JÁ˜íwÛR”ðDj3·«uì”u–‹¹†a`‹R™’ߦԹ[Ë,/êáÌÆh› žÉ:ëá)ìÇí’¡%—ïöÈÒš +KAñœ»)¾s¸
1œPS:+Á'¥‘ÔBâ6£àÈa¨%dÓ§ °’£À9!½‚ý]¨^È•Lk°ÙX,u!x’¦Œ:¼Èþ²Næ’:Šá9œÇí2ôG/FÅÕ¹Ìm$2buP6ŠPGv°ö\¬É0´7Y{ŠP=£jAàmÆÄKˆÕaC‹MÉ<yŠ Üj6.dôòee'“‚Xešž"—B3ÖU +拓縷KŽ–l.y¯HeªŠÆ¬2WÙCŒø)4²ÊÆ”¯&—‘€± ñýM¾?5BZ7)í4Cµ8 +VšæSD9jX`ÈSíly—rP¥]P% +–îõÞçíÁÝhÜÈ]–Nø<p¯«¢ Y«'÷ÙÐœŠm¢ ¼(ÈB,”|ñÈ:ãåxe¶!K[
ô.Åé¸ +†š, •z¼Š*zdýÃXæé9Y¾ùí5'{kþÜLû¢põs¢´½Ok¼2rÝNÙ;áù“'£4™ë¼ât3‚Ìioj³„ÍPæ/Ñø‹f` e?–Jyµ:(C†ž0‹r“Õ +ˆÝÐT7ÀÙ¨ë"R‚íJeɱ^iŠúóú%ý{X«T<Qy¼÷·ë»¯ÛiŒ.'̵}c:; +ÆPê~ +4‹{K±‚Õ,à˜t| ¢kÊ“Ê‘æcS«øusá%ÍÉïî|ëzŽë༮mŠýdØ=ù¸–ÿͪ‘ÕtÎdÜuSLA= @êM±nE ¬îŠ–áÅ~_r5iÌ‘Pv& ®ÂÀ”Æ!çþɧâ'ðO~hÏ_~|†HZ³@k›.° ’GG +9³S†-ee„bòtm?£\»rf£5‡×–TLL¸”7ۧͥùy{7y^ž]IøÚ9u4ç+o“N4»13¶?¼½k¥XzÆ´Àïê@Ѥ‹.JÙ^>ŠND€9W38Üî^ì2<Ízse›¨ çKºh7ÿÌó-YFÀ“e©t™äø6Œ¹ +F<ƒ¸Þ¦/ÀLjðzÊѾL¹g#ÍŸ›}ì¾ÛÔq¤â˜ÁUÃÔ°(j‡ñ9*]}oõW:4ÉuÇ~2»dÛ0P¦úeF« "”ú/V4ûOÁáûïsÕgî#ÛíTCÑXj.Ê昦L$sŠ +Ì㽟?xÝΨðI;!˜7ŠfÄÖ›}Û™€´¬ª1š+Me–p†ÔšÁÓ÷ãþ +’ì¥ubì%æ64?Ü¢ÞÜÍR¯š +%éˆßÏè1nc¬»>PM¤0¼h´2z›¥‚›Ñïb8“ÍA`»“_‚»ng4¸|pËUýyC@öiK…ôªJ¹˜)Ûò•k²a8]ê°¿µ‡…g‚ÊOÁ6úˆ\7nÊÖìî>¾ÃBèáéí +¸d!p6§ÅàHMÁHdj€ËÖv´9Jå l÷*6˜g;1Ä¡hìÓ +¢
Q/n}nA°¦R4}€Ó°°CÕά4ÑDBçé1HY¬Sâ%µXöVÞ&Ån.>pç‘|ÏÖѯÄ)ŸèH9Ù½-À5(Ÿ•—ÊI]jÒÔ +×LúkØûn<Uk&;ÊGÂë4±x:Q=`° Ô¢¼ÁH
5 èv&¶‚Z†‚ûõ”*"¡ï¿-m:µ7§¾ß¾à
¼?övè-¨%|5úß5#6¥°á¿F
š:ÜŒI€TÌYâ¼iZ6—.EmÄa[L°‡Bnò\¶®:öq?. +æ‚ZQnZ24‘ÞŠ[‡VMz×@˜E^ˆ4€•ôU¨öÓšš +mõG"•SàîÉïáÙ¸‘î +_zäÄtß‘„hêÉÚ
(éYœû˜žŽ©ƒpŒq¦ó‰l”•×pŸ{þ¤r/n½¦;°`p’åá½?‡ÄS4m:2kKÅæë5Dhå?b·c° +Ô[N.³ž-Ã0ƒ»†8êµHð!‘š«0l¹zªAò‚xýÄói +副#zõ¢TrLšÄ k3û‰•¢LgDù°t—N¥ÔÂ'õËI¸Ý‰@YÓKS‘¦#M'ã‹NØ;st8ÈÊ$í£-Yû¢®+ʉà ÷1œµ9+*•*5cÚÏŠ>4Öš– aèNA¦øçúCý(‚D³DP[§×ÈRžª,ÊÔ=ÀCðÝù€¢ù‡P"¢m¬õå¤ESNrC8Tÿ‹f±+%qb¥¸ì^rVZ€ªùßÕ3ª +÷£áLŽ™ô<F˜’D &Qõ«’$-C–¶©ìÜûû™¢Ñ +uãûØ×PMJ‹#ȯV?Ü3bæã¦ÁF¹wbgÎv +ÁbÓ½/¬sKX¡öºÉ—ÇöH#`°ó¹«ÓCHÀr‹òæ÷ûlvpÜ»õ¬êåë<z@½0à8 +•b¸hJqöÉ-Qý‰b…XwTÀ ÿ;P×±CÊ°S¦‡8f´›äØã|ÅüBƒå&ÄŽÁÙ÷βnyË@od•9Ò‚“§ÀÆ͇6Š†‚ŸùèBðøŠÝñ;Ý_jß•¹Ú©!Wµáb¦ŒDIЄF¶mo£=ó>Ë#áµ-Þ>;@ùíõsBX!Ð&úËÜØŠán¢‚ÿµ_Úo,sb_Ý}Z]·È$Å#ôð±âO„}õ~@“¶$.i'ˆ•5ÑÙ“ðÄ9ÊY=óÉÓâA8ßSºïq`q5x“Ü`¢” + +30ªaÁúþ,•tËK‘£–Æ9A.Î/P,Uâ#'4wSÈýÞGÇ“>¶xôü¦Tœ¸œç¡P—·Èùe>iU%ŽÄ÷(™Otø˜:¿/¿z²oOŸÝ~ü~J7Œ’[Š??þö$þùùɸ~ÿÉò›]?ýåáç—ï?þ÷ßÿxýáÛ÷_¾ùåíÃðÓW¯ß½ùùVŸáÈøûãÿž|ÄŸtËŸ_ö7üðŸÿ^ö:RÅ0~‚}‡[#ò爛v-¢£CC‡x}ŽíãìüÜ…ö_Çã8Ç6П£ŸoßÓñCm¿¾|”¥«*x–Ö}»ÞÑÚ|ç}UZŠwÙwqøx=õüúòËbûòâ±åã§}Ž{Ó†Ã䧃Èö)JKG +)gcoËSX9B ¤;@?çíèb—ÃÓBuè‹ßÙùLw—jÒÐtÆÍuf]Áq*}O[]ŸjšÖ±”õÍ–'âÙ-Ïà +…‡O¡_{ðQ\Vt–Z¬ :ÉÙ%¤áͲïæΖp÷u”ˆ4ÙÁ2dó8=^ãúðÛ¼`ê™o3<Û°UŒ7 +ƒo„’ñò )Ì™o
8¹jjqaõT7Lˆ´–[œfqëª[ià%VmZŠâí+ê6ÝÎÙf„V8±(†.…m¸ÅT—Wh™.±“«Œm +lµypÉ
=ú zÑ"\t +Øcf’æk›ÿ³•8º©õ˜3Îò)Hôñ³À¤÷-¼]e¦íËß ÃpÌAQ´£-J¿«°7Ju…ÑGRBs¢¢m4³5B)3M-‡|n +ýiŽÞêvГµÅ*ø/]hkÉÛêTA8e†–ÍòÁÒtý +H‰dWË™¬¹ +‹àæp¸õ󰽞mçÏ#\=3»næ·y1u}¦.ýóÿuÖú˜œógOÏ9!×Ö™àúÄÄÏý/áý&…N“IÓËÜahRØ.ïøÔ£u^δ??„íL~LcOz˜§ ³µÆ&¶·m:=1/ÔÌ”–¢¢õ¦¥Ñà_úÞ矪Vp¸—íù„
>àèŒ÷ø¨+º×ªó@OìMSqZŽy.vð«‚{SàÊ8öyUOØF½þ‡°ˆ‚Ãë]i»wFÊä#!Bpyì²<†Ç¶ß½´Pw[ק†Ðé0Ü°@ƒçÇø^ËÇ
P\¤"ПvU¾îщ,ãYù¸Ú$,HÄOÃļʈ¡á´¾ŽCŸ:}>ŸòBÞ.#}]8Äg¿Rnö®qú‘!׫~Ì_ðáõÜê†Ð´Ûå÷Z†òíõXÔ>¦
V<
ÉõoI¨;ÒÖ7¥¸XÁ¡²™½ÐXª– û$8–|S*j¼ÂZn´"F¯{,Öï–MpŠ{åT§¥ƒ8ŒbÂq+ðÑãtq†o‚è™0\Ìe/6okVý2i¬cèTºð}9ßx
>×^mdzßuö÷]7~f|ßÅ~Ë<¸öL5*·gh7¶Î®ƒ¥Ã¿¼ý¢ƒ„a«¢ÛAfµxô]mù£64±•k—<ÈñÐÅ,F%cNÖœžåÄôlž V}’ó-Xd4cÆ«EEòf‘ég‡úhlfšËϤÛ@‚׺©o·2Ø¢{hU’W¡\Цo‚î+ú¼´[e‹×ÚiSqáùM*örE§ÊQ<ã2ô}-Av@ßÕϬ |úN±ú8J"‚}?÷Zî³*(ºOÐ¥TÀI}<æ$9NÞXo/Kûã8Ég^®ó¨²]dú`ÅÃÖj:XRÕiÐY®’å¨Ï)¬”‰(¬š3WOBpWhØ|§s"E…•Sc€ÖeǨQ“°^ÐÿW«âËú”ß™
wì²4F¿r’n +“ªFxf8¹¿N¿–¡Ÿ‡¦gwŠGvA’®´Ž¾ó1NC9дÙm:ßùa›Õ¸ßÄ,/÷—In@#ÉòͼSˆuŸçg rÌÏÓ‰Ö\EîÑ×rƒr¼t09Î/u|2V?5û¹À±ùmóyk$âWFntê|ü*DZj^â¼pˆŽ'¶Ò©\ gIî +9·Þ½À£É¬z êa"‡°6SC©ðcÑ30‰±™}¯ìŸ +Œ9¤¥KÆR]´ƒÎØxz +`J¨ÎíuÖ2p§Þ*xOô¼CJmÁN-f©`ÙJ +¿áòU¹«Ü¬ÍÐþ5µr9+ðÛ8G·t˜Úäé`Ÿ&ä…À@#Âá¬5 +óVA‰5æÒw|aå(éwUK‚è;k>GÁ¯Ûs´ƒì±bøÝH?„¸ŒW8Õäî5,K¥ñ®0ä‘CY¸—Àôž*öÏwý¾±«ÈX
¥t±ÙL^k™µîhá +„Hí)=sîëjŒ¾ÀðÉË.êÙkØuðÝ +Öè}3ÃPàíÄò*G )qÖàhƒ+_Ë {òÄ%ÑnÖ[1ûvo¢7Î-±¦U^±±Éà¾<]3ç´L¾Ì§ô{Eâa÷–©ûŒ¾ƒúêåêÌö$Ík/´€-š™Ó€G0
øËjÃÌý-žÎ~â÷î ÒVpŒá,9α[WS™ÄxI4Á2 Óiô K ú¯Až#yö"9£¶-€º[7ÍÝé‚Ó¥“}³RƒòK™å + +j7þÿºeáV¢}~ÍÁ +÷k^_ö «†æÊŽÄi¸Í¥êVQeÏTKJªƒï¬\öz¦i²Ç²ç(xĶ0]„1Ðï÷RA¨Ðs¶‹)ã]–'ãw´·>ÇIxÆtÉWÁ +`-§Á©3ïò+A +‹ÀÜ£Šf®ª}/µPRGøêJöèÅï³êž®Êæ`399ã9«âþ®´ÔØ€d #Ý,Xˆ]ÅõÀ˜µ(ÛgDf·˜ÔR¤«ßÚŽõöEøq»›º„g‹7UæÓàÉv¹aô£ŽŽF20*ýÂ[/¶Ž² Ýr(4ÇØ“fOs$PE„ÐvIb mÒ +l¾Ê¯b´CG&O}M¢j¯«Üa1Õâáà$Sùƒ@{.cñè½Ø„s@ >¡ïFW1•ýAë…!”¸÷`4ø‘,ñ^²y¨—¥x¯Ý´¤yË“=íþ¥E±(a½-ícÔñ¶è¹Õ/pU|÷±ÉÚ†Ýc› ò¬Ë¨qJqÈ«fiÀß2ÂvºxÔ-Ç|Œ«¬í8X}Þ$ÞÏg/|ìÉÍ‹°¸„/áCõ=²‘<>ô¶dtÜŒl)Z=j]oÈÝë@ƒ'Ý—>w視-MxS•ÄýIqM5ÁP2‚½ËhÓ÷‡-i¶×á<*ýý•¥øokàiPóâÙ'*gUKB@‰qŸ×ÀÃü¥VVß]+®Kèo[SЬçÒA’_·êÑa#É´£¸kÊÊžÂ*@ӤĦw¾¬Ü¯ +Ö}-ÝF#+üž2 Ç\+ÎMÙ,=Çöu u¦zÎ\l +žÚ4°|µH“2SþZÀÕÏj&ܶ·F×õ@ëùÀ4ÐPƒý¼Jæ +8Dè1Û…é\Úw}௶_O.ÐœQÉÔs3Ï~oNŸ +‚¼Ã¦õ+A+òw
2Z»Ò6µÇÅPΘc©U™à‘..g*z7«_$\(mÊÀº¹W“W_ðö°RÆ[Q¾¬_¼“©–IðŒÌ[Í9ŠTºqmð©iõð'Oîʇù¶Jp¹3âko¿ŸC걂ãyÖG_ºŠ¯ýæÔÇÏïz› +ah5òƉ
Þà„5“–׺õ΄Ďãtp='›o'?þß®(mÞ¢ôX\Äs·ã¾ù«ì™h˜h•‡Ä 14^:Ýffëé³Å¬’òœ9‰¾Ûý ¿EtÄ¡ìžÌ±çXÆ¡h¡Õmÿeöo†ÂL%kûV!Za +®`QÞ‡fpW!©mP ñ_*þïv߆¼Áaü×ÚãQÿõšÖ+tòdS—å¸A«M¥[{¯¦"ÈÍ!Œšj_ûÈwŽYís:å“p5•:Øfe8*6Ù¾¡-j¥Fûn„(– F +Ë#†:‡|‘õ³2¾¯a„½~6òÖ_G_ã7 #ÞñØFV]ëÝM`<YC…ÐÀ°&_^j5&>ÀË÷eçûÖ›àq×Rø¬µõÿ¸•*g“ÂNiÙfàÆX¶5yµë5`]c~Õ¦³mª¸0¦&A‘Ýʦºªzm
¾’jžQæ(‘™L’hŒÎ½öw¬³AØßÄÇòºø½nÚÚÓhÖœ!0»á®^«BpÁU^Ðÿá»êV,=nàì;œ›€Hè_u÷¥=ä"aBBÀÄ!„`¼Kþ˜½plLÞ>¥VUŸ3sÎ.ËÂL>µZ-•J]™YhÏó¼À~/¾VäÊÁ¢Mw?âùÞ×ÛG„²¨Ÿ +àÚýEÄs†g÷ÐaW*)>ÜÕLâØÈUÑÍkóÏzîVÅ«<ʬœ¡‚ŒU[ç{
vŽšJ9Z}}ö|k[{¨s(ünÄŠ™]^_T|èôT¡)˜—*Ñ{ü †»%¹Ùbç™Ö5Hp+6¯pÒ~˜Z×E¨ +GèÞ§®Tc¯Í³$W"Ålžî¹3ƒÕ‡n€yue;ˆP K9 +š^-1$iw4&
¯YÛû僘nøwàMJçýZ¯ìbÀs
ÑÒdº1ýä8æù±Jü3ŠÝÓr?¾Ë.1¹Xë+„X{Lžáz{)=©°tï¼ÞHž!]&Ÿ{øÜx!ÜJ´f ƒgY»R Øæù±ÝnÒ Gf(—ã½Ö=¢ƒñvÝ9˜éd`YüÞ Qx¡ YT£#Â¥scKеZF’<Uô¥}Üωýg$3Æi1ÎÇkŠßGˆ +hm“ý&)½@ÛÑcl8È+`€Ô“XÀ5uõ!^ðY~ÙžFétq½Eª|÷qÉíÞ!”[
À>47ÒJ +ÖÔª³DŸ«SQB«:~d¾MöÂgü±†x“¹T=ç7;osBíKñoÿNð«À̉`›W»Ã¶m=ÀÚ›²b£2‚ÚìšØHô‘9-P`µö¥‘^•ƒµTäîOåÅúz™eYYs©ƒ-¦eÔ€¥kT=i§z&˦¸8Ÿ»¾}×+ûŠ1$×Ö8ôbkL Ç4XÏFÄЬ§I‚G\ ¦e§IæšõÖ:7ʽ¡]+ËÁD"(ÞÆZ—GQÝL;›êë½¢ ^Ç€õKŠA<Q%òã½E„ÏøÜX$€Ûв|ýz<5 !¯]¬%ý‡ +»<¯ÎˆØ"p¬ŒLÐ==ñ{ŽCß@sè9:Q#˜wK6Ó»ƒÉŽƒº +aUÀ^Ë
9Íz æ;å %Å•Z} +¯Ah
ùßûÃNZ,¬ +9*™JìfÄ5Ñ^¬éê«Rˆõ\Á¡¯M,ä–CË]««ŸÌemq>ŸÏ”tø]sÃíE0yÁ›´M×™ÚæSì½’`6L*•ïXÕ~ºªõËÃóšxÄ讨9dÿE°í[Ž•ÐÁÞó-ÁÛŠ[nV×@5TÁèL]”ŒbuÐÔn÷‘ÍJ¿Ý™,‘Í +s™ë‹§A‰N‚@}ë´¬À¦Iâ–US½,ENQ B£mÑâ‡ÉQi+z‚ƒy¼¹‚î6%½A
Ùý…°ê°gm=ÖÈC{ºLâ–£Ó]ôx ¯üL-@.„œ_ËC1„ü¿‹{ZW$8£«2IÖk©NU_ß™ð¤X)¯ÇÊ6l$·ýôOú>7Ò†Bçþ42§/ˆ9IUØRM`ßËÔ%Ë•¾£m +†ö¼œ¡!ì,"î¶T +A¼šJøî4†áQµí…0J/5¬Q_¹5i +²"Ž
ŽCQϽ_¥#¯Œ3¢Ø£>2ËdŒ6øÚû鋆œo¸Ï}è2˜øsPEÌ5H®€UÕ¢zs¸Õ‹÷&ø·–*ð—ÿpÕƨ–*~K+jÏÐØ=‘àÚ¨Fðú•â蜓%Vƒ,ßpÓ´`»ãŸÎ>±Ê¢JílÅõcÔèI§/³ò0HMãB@}ºmµ$¼rêg}ón]¾øòòíŸ_ýô‹¯~;ÿþ›ï¿þá§ÿþóßýøã‡>øõ‡üëã+ø‹o>~üîåÃûKÿñ+üÿöçw?á_ºäKÚÿ¾ý~ù~ø7 Ÿ/íòûË_ÿ–.ïÝöOï~ÕrñÎe—V};ÊA7-eˆƒ]z…Ÿ_Ã.RÞõsëäðÕÉGø‡w`¾üÇM“/£hýmZºðeÃPø(u ®!V9
äpƒ½¦J°;ËÌø~m>Žª›ÚŠ.uÓ» +°TB–ÏËA/^klR{öÛDUî ùSÃŒÝÏAµ3ùضþQ¿3-5yÞ(,b%[A8m•½Ö$:„d;°„ÜÜLž3e
VÌbJÐת +^ÿ‚jÙµ#R?³\L
Ž +×|ˆ• ÛE¸Ü%à¯`Rl©¬rɺÅæéù¯ÖY¶<š-û½/ÕêY]e1h½•?öÕvHŒX€Ìš ð¤íU[ãyˆ9@Ò¢—ZDû*àO¼}wv±Z³uE›Ê÷þfOzïˆy´C ËXqøUÞ&Ú"!¦¼öì×{@9J9öÝëwWø`»¾u/q=ñÎ(IrW)€[¿ôïÑ´¯÷ØI ÌËsÕOi|gëßzÀó.gKM'‰•L°>.K
2[yíŸ}HMèíj9ÛØ—Rtn?ÀWP“ï#xÚí\!u‡ÕvM|t„SäÆÛðL“o'5‚ªC]åÊs<Ÿ÷0Ê–¶xâÓ¡îxƒûHXeÌüv7Jvvò'p:ybÉ}—Àï4¶’Ã<v+§¬°1;É·KSc‡G¬ _=Ì9êíáמ:ý ‚´ð/2Ö!;‰ç=†F0æ,Fß·¾°^»Õ!ã/Öè'BMÖ ¼ï]†jOÊ ÊAzŒG€]jNªxþî0½TšN,T\gÆ_24A¶éßÄÏá `ƒð>,ØÒEEá@fé RÔ×Ä˱:Ö ¢`Ä:7V¨Ûæ$[ +$®‚ + +ÙQ¯“ + º3ÌÔ±f{òœFº¯_aŠÞŽ4tžš@nWt¤pô6w¥
¯Zƒp@^P5ààH·‰Ðº,QXº9°Ozj~*ÖƒB–9Ø#M0Xe;=vÂýu¤-^/AèƲx`"ÜàÓA®ŒÌ3T*-A.Î, +¦ï3øÙÖrŠ +ѱn¶°¤A»ƒ¼…•¿§ ,pK¡}ªÍ½^²40+ÆèË–š'³Bu¬t‹•™ƒÍdÆrк³6`4ø²-µƒí±âÌp¬^ŠðÛ"‘½ÑÀ +q¡h¾X©×‚'™«dw[/‰¤«Çfí\Æ +6]H£ª¥„µÿå*[ÿÚtBaùª—¼›XMBë`EøEâŸBmÂAãÏ!ªOßf³€"°%Jö=¦$ŽŒv9êÐsŽ+%4eà£VàÔ»hÍ5i¨ªÅUZˆ¤¹¿mwýÆCªÛÛÀe/œ‡Û2ÂÌPŽ7i3k–•Äa½Ð‘ñƒÞ¹NKa¨¶ú‘$„d+US2õQd(›˜gùšÀ,º ûÑ\H”—ÚìUšÔ¶³Îοîj%K —È©L\`ìy=Å +Z{KJaÅbaU3~ˆœf’ê¡®òÍ«zØT·ïH@ìUÝföº<ÔÖ¶‡l3Õ¼k‘ÌÛ«bX¼Ltr̳ j˜›J‘Ñïp_†C%ß6Ÿ{¾RôÓNÜfxÑ=Ûb£¥‚f“òA1ѼÝrr@däNçl¥×„qÅáøxl®ønËÁ6¦5ñ¢S·*q±,Ü;AÛ†Ë7ª=ÇQ¥Z{²¨Ý²”S;¾æ´–>¹RÇK5Í*q'';@ÓÌ\ͧV³LRßonƒßݾËt¶ÛGQf»ãƒ*Yh§*Š w"H|}:Ya¶nl÷ô'—O1팞jN³Å?º¼Å_ŠÜ `$[#¹
W7—£ù…ÑÄ-°ÍÖ%Í $¸ùmŽHIƒjñ¶[z)õh]nis1N©³ÕJ ÕS·)1¥âµ÷‹}ßRŸµ¡Öëæ@ইì^Êc˜”‚¾L™ˆÚÞP‰jšM;¼_åÑèßgz›ähÁ#}ÏÝPWøq€SiÀ›“ŸÕÉUZ±²îˆø4{›Šè͘Ҥ +.n•š‹ß%g.ß©ü×ã/¬¶{u&ÚÕJJ‡ÐLÒ`¤Û¤œûL_¤Ó$=¶¶^ÀÍãT •ÂZ PhÞiu(’Ûið—¶áNXšÜQø±ƒ!5+O°3M{ +'ïѶ)”<©áç®›5ĽªrG¥äÑwyëÛSÛ>o4ýxˆàÂçš5Øt;ˆwŠÓ–Ú5´“BZuÙrã¤%a»ã߃“´óôjÖ¸[Kõ›<¬ÍƒŸ¥ÜãÔÇe:Å;MçÕwiú÷ÙamïiJÔCaOõœ\*¤ðÀ“î·vàõusSèVr†sVâ4'Ôša³ìÛкŽL+ÆþøëzÐgäYä<GtÜSá\C^¥?AæPÒ%¡”Ø%"•<³€¾$]«ÈÝuÉ ö‰ ‰ªº¬¼‚Q”$Ž/™Ãø³z·á$»5³ +QsSÛãõ‰[\]l,Tl +D@ïÇVÈY߶‚ ’,›[qÔv®Ç¶,¸š€T8bí‹éD‹¸X§Š-NÍ +28‘X5CmO•w¥ÀýG/ªFp|‚Ÿ×ÚÒj÷¹ì–ˆêë˜ã«ñ…ìd’1¶"šØ]{¨Á°§Æï 'ðŠ)Jë3*Þ+É +;§l +´Ëál[ -wE&2:Öâ" +4±ßš‚žáº–œ–·}Ï‘ÅŽŒÏ씎ö}Ðãá=£ÖÖµ4uÚ_× Ü"øel?öŽìvÖ¦y(»h4iñV4Wµ¤‡Ãþú«K〦ò~z&[Tη‡Šdìi;‚ý8÷q¡6"ØŽ|šÚÍ´o5îù;ÂöUi=R½mÓ)ìÕ[…*¼k;t…µ¢5‚ÖÆ\ +?^öÖšøýi§×†{é.ÖÆ¡R™³›ö°2.j÷ +Û¬Ù½Ž
‹–1ˆUzÍO¶M¶¥™÷n¯‹?d#Ü-äm˜¢[À ï)å üð²‡ËHþâ")é/1¤Èù +#½ÌÓ‘CØ„ªÛ†«Œ]ä8²‚[›Ý^F{M§” u™îTQ¸O…-K CjªÆRî•ñ‚YÔ¦º8õ{U§t—0:=É2P©ÃØ
²!À;/ì?Ev€Ñ§,`ÌYÇSBš)t>ªm +À˜ÛÝÖZóÊ'Ȧ´Á +*ì`)…u‘Њ¥;ªR<ÈéÙH™w–êu…Q ©¨‰ç3DW–§k.ZèF{Ýû¼aºUª’*׃=ßó\² t¿ÓiÀ†¦”• @ÿÛ¿Á£lWéÔµôîX“hÃhèÂ@EóÎ[meЫeÄLª„0šbJ ?Ðy©¤¨µÌŠõé¥ÑULVS"˜\/W÷ÇûÎ Öc2ÛÊÍi‹O‘4Šc(ætŒD¿p3îìF C%Fšæå4>bá™s£úcŒDAçbîvÀý`ÒÚ²‰„ûfÉægR•µWMÆmz¿È +,2¸)<a\°ñÚ” +E‹Œä`îôxßÔá`-ÒyouçÙ£ñ>½kºŽç%ŒE +F°;XË
Ám +->]NÝýQC™Ä§Ü0Q1A [¹Ê™\lVk[µGlѦÏÑFB”£XÛ7ÝÛ*ÀÖóÛOœñ~n¡“w‰kFp—©ì‚²•bæKX’’ËåÔúØÀ«ï¼_ë +°4´MÒ(Y5o×öBPu'hjû¼·z{¨Æ;sŸ^=×@̉Té.ô‘Ìã)*ÐÖCTÑuÖDl¶‚Ö§ü8ª¾¥¢Þ© +$m<¯’EAç(ÚN-P§Q×bJÉRéбPÜŽ š?ô|•6¢kÍP¿5 Ž’ ö<"ûæ€9â|ÎR9 +«dG×-QXYƒV›hŽ¤JF¬Ž`_çï·í©4·§Q¤ý^>ž +W7{Zɼ il"í3•Õ]3¯{+hÈ=‚"µÄ!6ÊÞiAO®Î‚ÅIâìP¦7‚uDd|À>´?w¡ç°5ië°Ú,Ÿð
p7´E×Âd»I›|ºªp§\uÌ áŸQ—çá ŒT—ÃYX—=0&’ͳUÉ~œ/‚οlׂ"\䨶=šš)µ’ªH«\[!;†gÉ-šùÙÏ×MmšD^«,z>p +@Ù\h×z•óäå€ä[Uµ:¸8×1‡£Ü?
[ë¥g[çyxZy'rd““§QŠÁíF]©½ž½¤“´ž© }µÎƒm¥65%ÑP‘`VÚß¾4ýÚ×€PIû^aÆ\Ô +uïwÑlçfu‹úq¬7Œc
þN›¬”Ýöžö·Ç3_ÞÌË“§—¯ÿúÞ·Ožÿa|ûû»ï^¼ýéßÿøóËwï¾{çà‹ïx}÷üäË»»—o¾ÿî²Ðá§7ryŽÏ×?ßü„ÿÒEÖ¿¯ÁÄ—úùR/ºüíïrùÎ$¿ØÔ-˜Ï²I¶¢Ÿ½‡‚/,>ñÙ>ý9NÞAÓçó4¨JµþuúÌ›“ã§Oœ«r¢ö«›tùáýßÐ=¥çë·ˆ\)Ð[>Ü»õݧ_½¯YsÍŽw¹² µokÐ=›ÎÑ}úÕ5ß6âO`Î'o£Ø,ÄW0kdn\©õÕ»M'Mî| +¶PþžµjUÛ¼ëaɯrîÞ»ºÕ8éÓ±àv=a=kÖ°bP¦O¦50°¡céžÂà<{”\—ÞDpP>Y°Oi¦åƒ7(¶ÿh3±zÀ£èbmȪ‘Tò€T¤‘S“d§Ì ü bcšêt9ªôîà0É°"+òÃoÌFí5"+€¹Wj§™c©Iê^i—ƒòPý¶ÌZ fj;&úƒ÷ÃåÔäõÇ0uã'[ÒcÖëZ~h««U…¸^ +ë*ùk`ìVRg: +{)Ä«·çú°¦ÃEгV Tkus‰£jó/‘Ï82 +¥B´êCðk_6[ÈDIEˆa$)2ôs¼[ÁpúÀÿ%ÁM}»ó$`#Ó¶ O÷ïjDøéœ=L+À`Š÷/"ÕÐ Û0H!7«‚W«* +¬jmËJÒÆëâë?‹¸£1ëbâÓÕB™"\‡9¹RLŸDéͱb†@Ù‚¶é6¾/Ú´lù{@Föo
"í®æ
hYŠÁ¤` h.¦ÊòÆvù·ÔiÍ*Ó1
ì1GkW.¥ÀŽ£˜¢Û™•AUÒ’E‹aê×IäO^Ö¢Ç"ܦ€•µNÛƒ©Õøýmž¶å7.6í‚~O>¦¨_¹ÉD"¼d +´hN2 ]Ë©g2'‚à,JàÛøƒˆõ²%*ì©‘0(Ñ
©
úpljÈÇÀPw&Rd‘‰l2·f¹!,§8`ÇÄ Ú‹ñ +y-ÁXdÝ^«(D7¸˜kkIÌWR˜Ž¢& /êÚ +¿‚ +Š_«Ô“, +RQ¬³æ$±I¯Ö•-G;0fHé0T«®•?Hò°v‰1ˆÔÜlLƒlÑ +H‰´WËŽX·
ý‚ù‡»)à +cê6†ÇN`8æï{øÐÇv³é{¯$>Îá!èµÓHèx~óµ.^ç“ÝL§Ì9Ž4öÕ×Of[eœCÜ&g7“ãÖ¿ídbr³â$#wãä¾}—ª„qžl3<çIŠ›â +rr¼‰ÔéÉ+³¤gÇnCn%3/'7Öã>ª$§ôUB˜#ê1èl}V@H—¤ë@ņ§¹/œÊ¢ÇÝM¥”–‡dŠD
Ú1ùŸq„öãCyöDE¦©ïì/¢êñk?Ü|wó‡~óôÝûo_ݾõÓÛïî?Áôäk|f<e~u|óýûw¯ÞþçxòìÙÓÛÛ_Þüý§÷/Ü÷«ãðü3þyöd.äKq§Í¦'±y#{„—/Ž—#Ü4³×Ѹ¤þvëHI€È‹`açêè¦È ÑÙ{˜ÑÖtF?wôkÙPü•©Fu{«Þe]™ë~šeNò{åZÖn´iHZäí?Eª§Æ:Ò,gC‹ºy»Ue†!Ëa6ï@É>hûq(Woè’…ÖÙ£¸ª'º9:“Äsç6CiÆLWbt®”uH„©ì•J!’Ǩâ“gy|ÿã‹Ÿ_>»{ùö_{ñþÇ¿|[.ýŸ.?ÜÿüòWÝ`d†úÛh"AÞ
ܯnXjN¤-šu·e#›ay÷¦+¼ª¸/.#Í•¶Ù‰“F`UàÕÍÀe*×Õ²™€Ô9ò{BµP£¨–tÒ:@²Ái‹Ã«X˨-É m§¦=» ‰hñð8Çà¢çÕUL=©âQD0¨@kèº< ̲ùmNº|µo#Hÿ~ûê&½ýZð‰y’’5[£2jç±bU¸0OÓ´ +þ(â&ñàÙsi³‘ »ë{¾ˆGÁÕ´ÊWŠÐ*#ÆÓljݰ”uJåª]8š”·Q´G¨Êzø>FL_ +õ½æ³ ¶rÂq: íáû˜ŸaÔ6õ,3C½Pƒ$j`ôø:`§LK—ïL0!«ÒhWηÒ%,w\ê.ý2×@3ûj¡½jM«¾g&]«¥°ƒŽ²`ôÏPË3QS«‰Â'?ï×çàEJ:%H\I.ðIZalâFX©_ +ÞÒbc<…$åˆ#ëúÚ ¢b¡fóšž®[ÚÈRq—T%óâM»ÅÐ-¬B
÷‹Žô"NÑã¸^VÄÙló‚-Ö¾·ìd¢)L6¾&ý%‹Ô¯ªï7—.f·UªÁI暸ô Dµ(¨LŠ0ï‡"‡,f‘‹m¦+«¶k—®ë¶§ Ç«VÿhœÁâЬÀتh«ªÕil](\h…,_ØhZÍybÙ¾mŽU£sÐ^í>iÛ/wÆÝÍ¿ÿoeøô1•a´ñV’Ó(9ׇò›ê£ÉzñóÚ˜ƒîàð×ÖÇÙ +«»’kj9Óœ¥èÑKÙß#EL»íi
sà.ój\“#îKãH-é3²2Ñ5…J¶Ã°hüÎç²\LÐnØNËfiÕ:µQÎÞ._çfª®“/£%o8‰¾X2Sñxç¥'tÄÌ×~t*P¨¡²W;Û—VÈ£•D—¶±V¡ü:ÕÇo_•Ýïü¤K¥^úy%¡ñ¢a‚S“ïÁ×>Å7ß¹òOéT£
®DTÌ\«QN°tÉ,(éju#¸I§7Œ•!hh¬gÝï»+Õ )‡ýÃÀé6³èU.ÖbA‘zø4F½ƒvÿ¨·ÁØ«„ÅàTWdÕRÐà^N«ù#Ó“Fˆ» ‘ m— ¨ïäÉŸËUØ¡<4®ŠJë\´Gîº0«/é;dÛ¬ñu,S&,½Ò•¼?B;„/Ø~'xIî©^ŒÔ±0 +§~DdÔ÷>+çcõ +&4aZúÄÄkÔHžqºu™€Ì#éÅ3ÎÌ⊃ÄÁl¿fVjÀýj^¹ïéu:ÄŸ€¯‡iŽWSËDZîojµ™Þš¾H–¶Í +›ïœ:¯|r“0ƒžŒ÷ÔøäÔ•dD€ýÐÃ8|±½4Ir
5gܦFMœ 0ó©2Œ³\AŽ…
—Ê$\¿öÎì w]ò~¯}±Œ¹o¬Cw{úiz£Å„ù7nƒ™ê½XòÆܾÀ¥ñ0ïë ¢„¶àÇ¬Ø öüoß@UÁ}ís?IÎc]î-‚ÿ{¶”®I§.!³¥tï•®ûVÏ#ƒ$}´HUÂHRLBIL»ÊÑòÜ™¬v¿e‹åî*s韾ʩr5¸¯7`wœ9}fÏQùù{ïgÌážèyÌ9Ùbš@fH=qÞgn`ÝNäG ì[ß³µtñçÇþ¾ ë’–œ>Ãðµ’KyLW4é‹ÑÑÓ÷¡_Åy"{‚R4ø{1»ö¹¿ùb*vÉRëcÒÐüë`FákK\k•kÃÜ,QÕ·LAcŠä–ËÉÓ™a«2°ýPÏÙöBª²#þ$;Y¾®j̪F±oæÁYi£Nf«L4‹:ª¬?hC¸bÁ Ž-|$“‘8’f¥|<D… ØY•bÔ\ˆ®ÄŠ“ùu¶ê2s¡´Wfrκ+³rÜçÒ¹Í~L½WpŠ5ïøbÄ•á_jQp¼ÖHŸ$‰`ÿkYYEïu½æyφA{Ùd¶V=»…1ûþ@‡¤Ñãy©³ÞZ_AoiœXûò + çž“Ó"K‰p
”äg6³¨Ù˜òXšØ÷Ó^4£Ûî³½£Š6‹½ƒ¤Ph›ÚWÝ’ñü¶û)EäíxçS‰È/¿s/ŽDlä +úNU¥ôsÁXêÕãA\0}ô{u
ƒŽåT“½-ËÝ +g”%ÇÄÃÝ£É ›ðsÆ0ÃF‚fWç~DÅ2 +Cl`~¦QWxwási‘3Ff[—¿äêt}ë(³1¼-p‘Ž +½1±ÝçrÛZ ˆÔy‘KYbÔ•ÉÅ<Ê!õGŠG/|·qX®ñ®—È4KùnäQ Ͻñ´0zL†¾ØÀ݉ëø•Ã—Ï{mÕv–U/1Æ&‡ªD(Ó6ºú¹[R]±‡/m\ò
•&*b_eM”O¦c••8ѱžšðBoGØÌn+lwÑk#£]UéX]FWmJInÿ}ñí>"˜…Ò{²P)Ða%ˆÂüœ˜PM/Î4|
°¯î®XzWÂÁ”ZJæýK6 2Õ/U¯ât
Mùœp›7Æ[ia6jéxÛkË¡-ú˜®SË…¥+É.çºÄ…ØM_5ñÎ}2ã
ø›¢¤AǾDóO¹§zqbê£GË^MK”Ç8¢Óòk‚¤§— Ô>3^k}Þ8¹1l_¢;¿iïäCï¼o÷@"]öìRûÚ#ñå•Rô~ãõ¦›*ÕŒÕqÑ°ÕHB…Ðj=àᕪ‡Ù¦2ÑÝš°´¿©}eóAºï1ÖzÝÕý†F@‚ás-îîuSéJzc’¥–¢³/òæ²MvzAÙm2¨bÄgŽf§FpªI«pfå’Ð`;XMß]½ðîPâ °Ë®°ß¡ž;_$p9úeHÞ öÕ%²‘žû]‡ð:ý¨Ö†ÌëiƒÜÝ|ûaBÒ½þ^AA—R/3>aÂj´äÝä¼@¶áÙ‚—1/Q¥R¨Ïyè£uZÛ%ä¢[Ÿ__\+åä`ÏÝwˆ^Z>ݪ&âEAb{(ƒ¯üãAìSÁêxëUNÙâã|òïªßŠ©Ôtºúú[¾ü,:9Ö”ØÑ’š· `,ý +¼·qg¶«8OJ¤2àÅZºyŒÍ¦kcž -xë4Õ¸Ñjõ›À
Ø°Öº‡¬ŠXln•dº‹WµÒ
æ…øÂl Ÿk^ÝNo« aB¢—¾NàÄ›×Æm©vÞ¾»jâî SèôÇÙÈ?É/ÒÚHV:PÙ¦JiÍw$À•Š›rÆ3N–¶¶˜ÐÇYÊÕƒ^—ÅÁžÖ ù{ñ}”)ÀRí(&ì;Ø›&
[Òš4ñLyj†*†Ýo¢˜*ÆØ2ìM¨ó^×ÏãªqN”æUTü*mf›Å¥+‘àóàÆf€óð£˜WÝdá¡‚ƒ™ŠF~¼uÁê=µ=šÎ²xÙÙ<+™|Æ¢æ D×¾Úm1œnö~PG!½?ÍîºåuúFèFÖ›£-úF#I-d˜÷¹ÉpãxøÉÒÙ¼º×“ÉÔê¯yG
+9YL¸%}S£Fu÷#ðÃÏ"SîP§œìÝïO977 +æÌ]§,ËHA½.g7jK¨Û¸ÇE\ŒØ"ãȤK1ºñ€ÐsôÅÁ!y @n:zV<št>æ²a/LäBÄ×¾4ŸÝáìLexb1™ÓþξrÖyâÁ‡#z
íuñ‚©>®YCÄAn5‚åÕêÇë¥Íê –ü¨½´EcÔ _Q܃¬3—ÆÉz°ûÌêŠ
Æz'Ñ%¤gÎÁQ@¸C2¡”:Ҽ݋DµPŸ"¼ÏÒ¬àõù8©S6ør‘x.Dë +/¾ä뻇œISc¹©<>‚ÊædfíÛÚkëªf™h‹…Öcy¯«7¯Â‰êi‹4XZŽÕ?WÝ«nótÊN +Öä×Z‹;Ucôy¡aœ ç–¯{?}Š¯~IgÇyU,¾ ²ÄyÚñ
I[æý¯iŠ–x6ª#nMÞ +SI]ãú!O¬rc©ÂÃæ<;
1`¤Ó¬¾|´w ¿R!ÎGï¢qÄ]ª1øÜ%¦ãæš°‘pµÙW™‰´ öïæ‘L™ÃBP?êÂÊ4‹8J—`‰ƒ +U•ë +y<ÓãFyC’]æôñ'›G͸jfh—zAcEª‘Ï;ðN½?Å–Ë:x¢•¤È +¬9rŠDªÈb{`@{±Ï;,wÈ +'
0»nàËÔòæ«0iv@fVeÛ\0áeúŽ8Jb×'d\\2ÍãËŒ{òôØŸ(wüX°"$&¬dùd̃ŽÁUúh¤sûZô|V`KÞʬÃÓGj:hQ5.TPëêiŠ×ÓfNÿIQ¥ðhìFBê™àxrëTâÄUT${ +!_êñ›»a¸²®½@àtâF’²
pÓø+|ñ$–ä«NRƒ¬ã0îYCÄ1ÀZÍ”ÔabÖHÄfŒ'‰ÐWSÃÓj©U×"À”œû±ã1=
šRâÌÕÝ®lúX+ñ(’£OŸZpZ›Z²[qöÆL$,VNKŸ»œÙ5@¾þ²'.Ð(˜
/T_òÐs< pÚ‹99i‰Ñ®VVÁ‰Ey@4W¸z“/G“‰ +‚ý`r†Ix;œéÀnû”VË‹¤D·ìôz‡<;¼Ñ·])JR&‹KB;¤Ù´>l‡K-ào×}Ûa¯Ùr•¤´’¼HjJÃ4«òŸæhµ ‰¹ i·¬H:TùW
¶?[á•™i´§kË.(¼†Vظ ïœý“!¸ïæÐ<m,AÞ›/wdVѬDï:¶àçpÛ>,0¹YÕ‹=N¼›™<8útuDWtåÚáCüö‘%ákJJãˆÏ®ÿÀIyœUégCÍ¿eŸZ¬%¸Ã<½ß-L?|ašç$=ÏH±[™ìZ™,!ž´1vx³Û˜vÓ?9¥wÓnczéì6¦—¿ۘØÆdɨßmLnL§¨* +‚ÒcZmãñLuÔ•nGMô5ÇÓÖ¥ëáX±.åìßÄîx%ÛEòFZüb>/°)g–¶„=•ôÜ^Èêè¯!%+:bfr–ôÁ¬'äfFõlôû
wºÂIEQþ™©&@¿õ‘ú%#iùù óEØØ÷ƒÐq;r¿ùât +`Ÿ‹jè˜S«((6ùA9{$ÍòŸï¤ú7[×t5üŽOU>G ž&ÈìZh‰-Èwøûå~•¥85Vk iôq?îׇ,-Jô¿ìrKÁÞH‘|È–«¬n Ë· «(íÿ˜—I꙼˜fUžà#ºZ ;<©1·ÝÈV8Ge64Ç¥/ƒ…·eÞSS7•tš£Õ‚$v$â¹MIù‘ÁaòZzÛ*†ú}rR.–¸´¥¬ôþÖýù30yb‰¨b@;Їt‘uÐÝtÿ•h>´Ýض@÷6œeg8¿ÙÆ{äUö××9A³ù¼Àå1†ñEÆç¦Y½ñ·kì?!ú÷'Þ GñÞÄÏùÉŽ +KX_Þ¸ûØRþÏ¥[´Û£ôs +ü1Çß*(uc7;¶èÐê¶Kiµ¼€Ê¿ÅF]Úá¸íJQ’2Y\jö%ͦõa;\jßxàYRe‹¼R¬(Jð§åZÙ89þ|ëÛ@î¥CÅm_ù3¨÷®%Ñï‹Õ‡”¦˜N1ÅI½{®÷äÈGú°ÛnÝoèÖ¯v¸Õ‡Ý™| ¢F‰kIq¬ŠÍò3‡îmOY0–ëпŽÕÂ’zö¨ìÀ?0êÌÃÂüýaQ0jng$þŒz?ÝÖO;ü^mÄT§¨* +‚ÒcZ™†Ë *ýq” ½AÎ ¥¦ò˜çB¿åÔ7xÛ¹•‡ÎkÄë)ÊÙ ¾%ˆ=g‡KøºÍ³¥É%Kj~™™À‡v€g@ÛÐgFšØ‡˜uËàk¨mô å’3ømr’£!G·ì†ÂÜéï$©–53è~š¥–𔎹ãÍlFJrkô%Ç+Š[Ü‘ {ú®D¹Qà!z‡,i ¹íÉ5¾1mÇ8XƒþÙX<aÓ°Eé>wÓ°dÜ<{Ó°Óx‚®²M°´!o´L\Ìç6UÛ‹ú'ô°Ñn'–TÏs[Øüýž=˜°Äv`gH7ê×Ë*¿®(NõÔ‹ölsÃúÐÏs””ˆžg¤°DÊ7bkÊÿ¹ôãÙÿqú¹Fþ˜ão”º±›wŽ%r¢»íSZ-/ ôoñøVºÂ±ÃqW%‘2Y\jv&ͦõa;\jßxäYÒ3ÍÈÛ`lœbEQ‚—±_õº Ã`ð x–J +CF‡GºpAhQdvn›·0ðŸÝæ øÓm*¯KgŒÄÏëX€‹’:¹OåD—ê—há«Æ—5ñžµÛÅûÈÖ=—º“”ý¯kœø»åÓÕ*wŒúÍ÷£n¡…Þ¬`_ìcÿbžòµù\GÁ¿›äàS6ñµ£^f¶xM‡½öË°#w$¥ßY˜ûÜ7f\Qù0¥=ã‰Ñu•#¢À[Þäˆ,¸2§ƒ£FYíÒqù±T¼÷žWíOh«/¯? +9õ1å’“’x,Òj£¡…-Ø[èˆHº´'ÉÔg.•uìïÒS*‰:V<Ɖ1œ_Ì—aŒý·»*ÅDk÷µ•‡AÝcHÀ”5¨¢NkPv‚²UªP¹¼Â ‘³?=Å•°ó“µ'„²ödí)eO Ú,ª=õH†Œ®±¸±Båu錑øW&šRHBÎ¥5vwuRÛ!ùwȆÐ:¤,ì +H‰ì—_sÚºÅ?A¾/g&w¦=@RÚNy +á†éL“vJÛ¹oaÉX=²ä+K ôÓÙ$Ä2˜
$qv?´Mƒõ“öŸµ>ÉcbØZí±Ñ\N[‡³XHEÙÛì/Ib÷Ò›ƒõ¯^aó—»ögiüÍ<É_kŸhMæOý»×ƒˆª™Ì_òÀp%‰ÎjÿïüË…{Äýxr! +ûV¼´|ì"—¿™ê皉JÚ}0jÁPÑí ɵúò"‚sØÆ +.©"ãNs©Æ‚ÀÆVÀƒéP*‰¤ý‹šK4”rÃ+ÇTa˜%‚Xp–¢}ÍRC´Xˆ¸&s$Ͳ”ì“Lض×XþFlù.FÌž[\Rrg¸WAøN qDlšr"dF×ÆÊꀈ$"8®b]U¥V€
>©¾‚úÛ;×ûfǸ‡‡`5䥆Ù'Ùû!Ž8 |É;µõ‡¡MÙ>ŒÃh"Ðòh^Mš3!Ôuª“}×™¬ïvŸªþW‚™¾f´¯4‘S$¤ÄM¹¿
”PH&ѽR‰_jRš€Ô¤W†Üݤ4¹“Âhh˜pß°©}ßÁqÁ¥™õ‡ÇÖDàBüˆd©¨§Gȼˆ‚Öža—. +ˆ}ó.*ˆ5‘×êÐ-÷±³< èá€ð%—Ìòâ´O•Ì'28'p¨^×-[] O±³Uþ*TPeå;Y„qÂçÂõ—›©Á&x{SòŽ-ObNB.AbUTj.ƒ…^ÿ~s U–¼hwùTõ§š1Ù_TvßmiÖ¿âJ0Ó׌ö•&rŠ„ô–ªdÁn»È¹6#oÕíu{2 ´@àÔÒí! _tTŠòÝÄ
ÎÝÏð½HFÜÇæR¡ÝÌÇÖ¡TIË5—h(å†W±Â¨K„s7Hp–¢W‰!´WD\“9’fYJöI&l +G©†±å»H1Cxn¹õÃB.ye?Õ~#Ká;Y©!Ï]rî¤08Š
ÒŸ<2á4do†(Þwp —Fî[ûü#¥^ v¯Hð.
EâÖ
¸,LBéMÀúj‘tÖl&µ:të}ìLhº=$¾æÏͱÿw–8&ÁkBô¢{=ש’ù¶¹ÌUÝ>×V¬`F«vYÍXeÙ;†S'*ö=OëÀÙŒn²½ðÍåD2VVìD$ÁA’kö1TÂ41pW`Íwz_Ss/*i¤Iñ +©Éo˜²’º‡j†ƒÉ#X÷h½N«‡d³4o¬úö…þA{ükt–¿¥<†ò_Ö>ûFĦ)'r l_þão¬¬Ø‰H"‚£"öÍ“ê+@RC‡ìŠ“ì3 $óÓ—¼“ù†)ƒîiK੤}{A姟
%Í(¢Þ^ßtñTE¼šågBKÒ«\:é³&K<þäÀÚw[„ŠuS3Ò$‰xðl® +œ|¾G„ªëË# ™äÒÀæÂ0ýSr“‚+jò›f ¬¤î¡ªš +L‡‚KFª@kƸÓ\jò °±ð=”J"VEÍ%J¹á•Cµ0zA*÷UÝ8KÑ>Œf©!´™D\“9’fYJ.“$Œ˜!Üô\Rrg…\MAøC$Xo~çb7G¨I`ˆ¸P<Er ËO,æ q]ê¹\ÓcV€- ‰?·j}†m¬>OSRí#Iu•ÅÀP×íàÀ)*öYRÃMýà†‘jœ?ŒÉ^jyú\Z&S +Œn?Ížª8QiõzÒX¹Å¨Årî+£–Ë*iKñceuÀND,Ge•0MŒÚÂÊà@¸ÓûÐ'Ÿ/džà8ŠÅ,ðÚÃôOg8ÁÙ¬&¿Y`ÊJê¨&`ã
·‡,lõÚã_£³üM¥ë\|R
SÏŸ·?¬žXáL +5qR\(ž6© Wjz‡ƒåBš&45¡© MUMhjBSššÐtïÐÔÃÓ„&„¡iDlšr"ÂÖñå`<›{¤Iñ +F,Òg; 3¡TWO2¶ëà@ …díÛ´ˆÖmã¹`A˜ÕÐÛœãÓDàŸ~kñ+•€›ù§ÎßHÚãg§9Uq¢Rn c@º¥1•×ëXÓòè¹åÒ}•0MLe-‘*±f„;½»Îá4"T]Ÿ"jºã¤ò0´)û§‘qâk˜WåïO
‘&ÿ~”ì‘I¯jß'_ÆL8
‹¹ã}BA°OBþðØšâè÷p€,õýøû{ⶎCØw}ÓÅX±¯KGñ/û峜6ñWɱ2”¤¦
§”Naz²k*K®þ¤éÛBØ›Œ»&—$ã±wýi¿ý¹‚ÓN eSJ⸒©Prhgõ¶ï•,%º'0í¨®JËA#1(͈dö}_¬ýüÉê¿Ÿ:ö´-¼aMF ú‚XÇ!ÆQ¸Ö…°N‚°Ìj-MH²ª)ëÄ åõ})”dP +ÈÎØÕ]í«gôµ+ûB,éFóB™z/ÿ—¢»n³øŠærs2UNSv'ŠŒ`8)5‡6TÁ4±ñ”×ÀŽ-¼é=§p/OÒX“"ãcmã-¸ýNxmWö%Û OSg@zïˆ\Ím–3‹r¬6ó½éç×mÌ 3z¡[Òë°½Ð-é7}yë7;Ò£F‘>:¯.Ã*€3¯18ؾTÐfjQe(Ò[TJ·¨2éM«ì‘é§.j¬â©þÏ~¨JSÃìgá´fÉ(2‚1ýe•6~瀅 +H‰ìWioTÙýþïHÃóÝê.Ã'ÛDH4$¢(BM»1=cw£¦=àŸSË}^4#¦GŠ-Sï.µœ:§îúxqþfyz¼Y¬î
ßíí?\ηËõj¶¹¾ß{0ÜýpvºZcÑl»Ý,_oïx>lg¯N?ÎNÏ'˧–âÔOÿòŸpäÝ׋Gç«ùã{Ãþs,[\îã_«ÙÙ‚w}üë/|öÃþ“ÕöúÇíÅ[ù¶°ÙÌnxÿ鯺¦'|Ö/
û‹&÷ÂoæÞg+ÐO¼ÂÕª|>’±nØ‘H~–ÿ¥ßK~p;ÈW@é«‹º\Ý–ˆÇãÝ|¶+e]®nu³xw~º½-Œ£õÙÛ¯›GËÍŽDÒýþ*và0Öï–ÛÛ:ñwaˆÛÁ´{Uø5xz¾>ßÌ»Ë%¢®W%ÜŒ‚Nûf9ßXÄëë¬ß.6³ízs[4Ÿ„á7áÒ߯jñ§‹ÍÉ·hïÜÊÿýãË®µ£RËo«×_FNR̃ÓÓÝÈà +ÝWçâÃmi¸ïÝÝHÁ‡›\vñÇñýâ¦ïï—ÇÛ7·ùïiWü7w¯Çðf±<yskíNÝßÒ~|[O^þs½>>ÙÌvEÙç’·<Ý.6ÿX-··¾ÊÖ¯~ZÌ·‡ëóÕ1®?Eß8¦k|–æö.^öî<ñîåŸVÇÏ|üH6³‰ØòòÙzõ7D´EP÷ï«ùpq²\]ý°÷ì!Ÿž_œ½ZŸîÝ=DÔÃßg«“{{n8ÀÏ‹÷{çöÏ
ÅþûyûûÅþógüñLï‡4<þý7óîöî—ÖòbJC
äFO!gb¦1·ÈæÔÆ@ƒ®ôÎy6ÕÑÅĘ°©]—½KÃ\¶Ç1ÅBvj+qP£ÏEmD%{zöÊR?–èô®2f×Ä+“oµŒøaÌ84x¯wU¸ce³‡3Uü*mô5z5:D(FxE)ó©ˆ ¹:ñ¹`šiløÆk3î-Ž£0–Šã˜ƒËBH!¨¸¢I<Ì^—º±y9Æêr6cò•Ô˜š³ýÔpOjö%‰bbÌ#¥hûc‰¤¶Ôª]ïF‚Y Q«O“ #5ç`FçRScòyÚ_J㼦4¶¬×#T—ti°`IñD²{pP¶8ë˜ðK’íá¿ãbcEáÿÅkÊM«‘ìÔįêÆŒØU¢$«âÔ”9ƒ1ÁAĢЊ(ÖŒÂú—泺`C[õr¢ +¾Ö›È„™¨f[]šG©LÐLÞYbC
ñ”51Z—_li }iu©ØR¤!X]ƒ¢å‘1]<ó±rÓHŽšÅXûQ‚ê TW¨p&)Ì@fÑS±Àqˆ±RIºÖwjóÕ=3¦Ú÷\%*œä}¿«QÕÞŽìˆîO.3'…Úl?¢²Î ti_³¯fK¤Æ0æ©Ù$’~¿sÅÖ—Ã
hªMéuuø(_í½ÞsÜã4 )¹©àvF`0rH?÷ó[ÍÊ2§0͉¹DíqT«{
§ƒ^Z9Alôh1©Œ1ÌZ´¸i„q,^;•D¾‹Q' ~ì&Yõn˜jmb£ìµ“±¢ +†—À™¾\1Ñd2#ª>b©*Jù@cÔÍ §Z‚Ô‹R67vh¢³%ï™óžhËNyà4$_uz%7SH?q~72‰¸8‘n(”Œ‡úe0D«QRà›ŒÇÀþP:‹ +87E/†±€ŒY£!¯Àe\n(èª[šrs¢žÓ¦:È,Üìö +ê’é +^¶‘¾‘ÜÖÁåÉ ë‹N*`†š-',Cyar”Eh +ÅhíÜ‚h÷mT·0W•J²é¥Ø+È÷êpí'!àb
Ý‘JgÍ]B(TÍ5V‡8–¦.è)Z˜r«6ÿÄPý4—Vë8È#”™µwÐE-uXQõ†5ªÓdÊcGëI ®;ºÜ÷ûd…–®é÷c„¶¢ÒÍ|õ%4í+¤½sétL¡^Ö¹™ê¦6ÝÕ'ã©l=‰ú¸Ž&š*HtBžAÞŠe”F š¬Ï…9K:U•Ií +¹NBá)Ú\×YPxA¾E²b’Œ Ï€zošš“Ó.,®O«|@¶¶EgÕ×ÄÈIÇÂ1x¨”É|ãÖ+Jjêkð¼ë»Öe$>YsvajÂj ¤ÖºÒß‚9¤k2†Û„HÏ:=Mct¥«Ð•ÈjƒÒfoEºT±¨sÙ *¦TH޽ȼƅöY¥¢9IóRxÒçW¥wš^GþÌÓž=cOäø’Ú$ÂJY˜‹Z¥.·(½
Ô¹haÿ7zžï¼Ü?Øl.çÛåz5Û\ßÃt÷>‹CE'ÞöŸo7ËÕÉp÷ðð`>??ûa½ñÚ{ÃwXù +.§QëçŒ +ié[vW¬§×®W8±€©ô¹PzF¿JÖsZoõ9cÕQÝŒÎkõXƒˆ¦¤3p³À":{8]¤‰šÂÖHŒa–ïÅ
ôáëÔü¯€m¡+c¥ä«xáè·a¤íÔxÄ8+¼qEšU“Óm`‚kj¸Í*G›ÊE_6úwúÒË~qŽË÷:)œL +þ8„"=O³Û˜#!c: +†ècÕ8S”Œf<çl<ëIÞºrŠ»ÅÔÛí*ÜJiÇf”‹’s²Ýo ñ,ž®GGH«Ñ³ð¯û,}ií·ŠÖ@#u±n®½™k÷Ò—…æV¬¹JubÖüδŸÄnSÚ-NL/·Ï'\©Æc³‰Ð°7Äýzõt=»F:¬ž…}2ÃÝÙs‚pÔÇk€äbi|«0ƧÂY…IB± +CLØ[‹¹»’À¢VGÖ ¼´Ç +q¾káq´“Ù¶Ù\A»ßˆS1Æœ81 +.B”¸çýÞ’ßΘ›çñ+s.Ö¹ùs}ëè_ì{ƒ9…!„]j¶mù–¸€|•Å®Cü×VP¨ƒÒüz|ܬyµº‚æp7`Ä¢î†ÕÌMR‚Ÿ8Èo—³3®Åê9Â^]MÉ‹.’×È“|X+æÇí#̔ȆTµ“á^†ë%ôÊ2ÐÈ#,ëY‚Ù™7£f÷u†"FOјS“™W,wr½ÆœººœA-VÏ Pæ ovêf +uAÖ.|‰fšsTêŒÎÅ`ÌUR’Õ¦®Çâ +s„)зÊålkhU~³9 »]¾6!ºª‹ Ãâž·8ˆO—³3¤ÅêyøJþÛ‰£\N‰ÃÔÈò¥mð¦(bL!y%.âƒî"gÕ¬$ëê8‡³Ÿm•Únu‚ê€|?Ð¥œ½q§Fsº¨¡´Ý;uv9;ÃZ¬ž1 Ôñ„Ù¨›ÍoP‡‰¥¤Ç@yp§phPèÙ¦D‰ +o}Z¥å˜$œ3 Öº‘j9ª +,.€7;ÜrÐÁ$TUK1°é*•hW9&õbžF+îµ°”‚ª`«, +Úº1–¦>-´¥X¼BQd.…Û +Z³:ëÏi(zTšÆ))tâ3ïG{º9Q:hÓŒµ€º)¡b5Öf‡ó³zNÐW½9À%Æ«»Tÿºÿo‘füúY§:wϦ‹ ²¨dDQØ£´Y”É¥ÉxN:î‘!³Ù-zN–™Dté¿9(ÕIÀb`:Ýš×T)ÅayÔŠ2©Ã˜ì´
â$›×’›Ùá-ƒë¶VåQ¢Íš3ç÷™1ÊP»¹Ñ:½§Ï)IEsX¦AÌ3GAžoQ8í(ІñÈftjuY•Äyg3iC€{êfNQ‘NNê– Ub©´¨žÅ°ç¨`®Ñ6¨ÅqåÈ]ó¨];‚$ºOÅBpjÕ`a}ìLÓÚ*ã=ɼ^Åišþ•M±áYó°e ¦:}HpèøŽ£6Õ©¡¢»™2M];×fÍ™ó'8î¿ŸÎp¤€þÂá“9ôX½™Ÿý×E_§ cTÌŒ·µÎÑiÔMT]–#Ým-ŸLT{tª‚µ™ˆ ɲ'Ó—=¯$¬ac±JEÆ!¤L´¥6£,˜§àI¸-v)8î3CʺèOgÿB¥
Âzà³$TçótjßÄníc}íJy§ÍŽ''÷®c·„ k¡}ç8³ˆ++Ã*›9|Ǫ4MªÊUkïÒ]‰+X3ÃI×Uãh® +`ä +vùM7¦´§f¼Ö 1HwçµÂ:wFY7È–8;Ï®‚JcZŠc§qFks±ÎV•}¬JÊxc*Å´KßqŸÕ…î±ßpØ#rk®ZS<jÅ+ãܘ´áÖ((S¹Tj[Á”2oàd9
Ø}™*jc£)ð[<ßJÍ¡¶È>aͽð–:_¼ëCýÙ›!vIÃÓI'QÐùI˜ËꃆçÒQ]ƒ,Zö3ûÄZo®sXÃu\-“ñŒ–K–™¢…•‡¦•è´@ÛA–²3‘7f Y&ÒIˆ Euè_-Têc5£Y¥¨(q£ÝX§ªœe0¨E+c÷y<‡R1BfÊœ‘™ùñü°–öZÃÞ8–bd!J®ºKÞqŸS#Ý/Ö¬f5ûR¦÷Ý|mdÿŒêÆÛ(k‹7;Ýu#*|k]Ùë3„u~/Ù5¦Ý_™Ê!$ΰf¢ºéá]KKæ«S¦ñ-ur«&p¯4”«èZq#ÇÃn¬Ç} +“˜K& MœÁ=s!&ÍüªU¬à0Ãà EJsëcgD×Uf¿®œ%’Ö‹€
†R…-]!WAOÊ“Ö£e£†ù£«ØÌÜ -6ÕY6gxžÚù1K‹¯J\[jfŽôL¿N88îSc.mˆ‚:dåXg´š“ð´º=Âg
:ã¢Òµ)\·ì/¹
&6¹ +*} +ŽûÌe¢ë‰N+8XQ3¶Q:¤úȲp:gÁz£1Lûƒ²BÙÛFtäl„qÞ^ûA`+lŒSŸÁ£•»è`µŠæØöª˜åÑ Ð‹†=2®È¸²æž‚öi°Ù¾?!à¸Ï‹&âPHM… `ŽUëSH§àé[§!k¼êwh1Â]†]©)U¬H!SУo•®sÀÙ ðZèˆ;1.H§êNŸˆÙïQŸÐIߜ١ã [mý + +"(₈è$ˆšÄ‘ï©:§úÙeIv¦öí¯êêSÏq¾– t›&´{/l*y;-i¨4"ZÒ f-)öÓ¾}>¯ïº ×½«ï74¾ +Œ^Â,¤¬V +=¯r÷ªwÏñU7¼;öðÎŒë‘c0 + +Si—éOïÆuêL¨wK»ã`SïèäÌY¹ý`þ.ÖÏÞcßñgêVo§…YWý¡¼{ödš+ØJæ:IàžÕ#h;é +rÕjA¾Ù}ºÁæhú’8`Á¯—YW/Úºì5ç·H¯1+> +‹ÙÛV„U‘Â×ôu³êI «rÆ°KŒépÐ=w^ŸëR‘Öì-[7Y5q-Pý¥í÷¥fg½œ{ZÝ[Øšyÿ§†!›n!WZ§^Lm¥z‹Ìü%šá!·š³B?¶\2½² Æ^•¿l¢ˆji[NáÖO>«^y§ö¾ï˜'æpÓk¾ ’¾ITvÞ“Û£Ñì& 0)<ÞûãšB?=8Žb;ÃÑÌŸü©Wb¥($ÿtÃÃqütœ’xW>ØÚ$Ceô “—÷WµÑMXÈôuîï*×GQtî½ÔŒ•t%cM»õ³ôãRÏJ-×E™•–EeM®(Üß-j§U¸âßvSþÊé[ÁA؃i„ŸL +A⦃õ ¾„‘¸øáa ‡'‘’“ΰOÚ>–1 ã[™¹¾%'w»þÅApSb}ØRMLê<‚A÷oº?âŸ`“4ýN)SÌv¦"‚($9P¿WËÃzr5Æ7±ÿžõGüÇV{w}6• +z¤àTvw‚ÝÄ Q-Þë_¦‚â2Ä]ÖZ7gb +¹_¨…îo£„4^t©UÛFtÎ"ÂêPÊu9öÎIíZÙŸ\n‰rƒÜàgE·×·ke²@”;'(EÁÐᜠp#²ÝÄÍ\gÿ,ϾR¸[{×gm +g oã9/™'OÂ{Í¥9^íéiEÆÿ¸X˃‚”—š<¤&%¿± ¦waütÊÀBì½r¸Þ凥æNlCøüîjÙ¹3'‚Owø7HƒÄdܾVÙ 6HŠB`" Œ¢lòöTwUûœDB£h¢Ê±?ÛÝ]—m<,Ô³ê%øxzËVË[ñžÙú¡£p·¯‚GßÑë-q_Ü^˜[³[ú‘ipL1Á¹‹ú¹œ±žš»J³ûý²W¶ Ü…Äò¢Þ&%ØM
bÒîú=C7=1•–Ó¨'hèð.<À>ì}VÞ +÷×V +:@³, Ó¸ë
×£ÈÒA4ùä´ +òÊÛ€Öwµ> #EÜ>#ùùL8UWõ¡õöM|ÀWZ×iýñü³ +µ¥J—l™ÜÍaÄ;µåópݼ<BC¾S[T9`íêÞÚ(ê2cVO.ʉÖkBö£yMgä,¤€v×wctÃØú“jSÊ,À¦œ³ëÍž Ÿen€)"qÌÊ®p°Ðñ8iqQdÌÝ1“,D°˜â$F¢ê·ª§¯6ÊÙ¶ûK÷åéó3C愸é·`Æ»BÃí†×G67ÓØ¥E»–Åg]G—Â Ô +;ø#ÿº~ô-õ fZÙò´ªèÀYëÝÿh´0ô²ÕÅ;øˆÓm|UôîHW„¬¦/gË\‹Ù"¹5Ø"8OÓÀÞ¶ësM@mŸ¡AvoLcò +3fÇTÑ¿C^&Ä&º.݃$–ßÿû[‰à‰Kþüù«j›ò;ÿÜ‚Ôî%Ô̆$Ÿb°Ó„]gÎÁ’c +¨+àíŸ5LÉäÊÝÖb¤7}I´a$ç$§yÊ*(°†¡®0 jÿ +ÊQTkv×/^pMÕz:%tíC’GPp”÷ëZÁQ×o]†ÚïÑZ}í»9yy{iÕ/0¦H¤É^âÐ÷ydº¿ö…ûÕ·÷ki +=ÁV|è|0—)À\ÌG{:2ñ*£ûÔ«OaÎ +æsÁ¸Ÿ¶´ÖâxBž‡ÀUÞ4 EaJa€çèZÏ}4ª¹ÑŒÇíõÔwÀtËÏt<žqƒ°C¯xÀy fn³ã +umPíú½i-¯?f;Qß4´'[Ó+yú„w}½¨¢¨$È°©8b>cq€Ÿ Jbèþ’Ä6u
ÖºY¹–2‹’ê·9 ›@:2D‚-g‚8?í2EL ð
t,T.O"؈õ'‘1«îIlb·ÃÇØ%ç–·"¸7AHÐ&&˜åß ZüK`lÚ„k¸}ž£.Bë%°´>¬Ûw½òåeòh\"«Ï#$¬« „/p?çþÎÿeÝg(}™a@*ÃìßóéM\¬Úi"‘µ†Ï«ód2á|0 ¼Z¢hTFØÝ©ð¼µß%Pð¹ˆÓÞÒEŠ%XC{\zvH lFåCùpo˜‹6Gkõ©¥a4Á±Ž¾á®/ +ß‘ + ßbGð’ÚoÒLèÔð…¿ú”šu‘f€’ùo.ØÉS!±Ë°÷ž±Á'уðÈÛÆ~
“<è + ©‡Á²¥;*9#€5†ŠÜ¿÷Ö½r±ÒíÓW0^€Çuß·Ú¦å†o¨rü £y(bë4S¶Ûáò›l‘½÷ZŠh]rœX1z¥ÓÓ/m/Òvè¥Ü*ÀlѪ~Ñ7Í°Ðã %‡’”7é}_ƒHÆödU 'u
4U«îëìMu"8ËVµ™ƒ¤¡ÁÙ6+2<¥.~+€.×ñª"ÀµÝm Þw€úºµê/
‰Pã9zg‘}
i¿¯|`|³€QjE‚Þˆ¼†Jºþú*lÕ(Ùó…o +æô©+Ë›ìËŠ¢Úñ*£¸~p|'_bPv`v¢?_{2Péç¡ÞL‡ÆÎdUôý£#{Ü¿¾—:V».àúVƒÞ4ÀxHPYÙ]Âà +ܧkí«Àm ð¼ +D4¬•ˆ ÒF£ÜE·Eq†û¢¬äE€é™ÃEæL¸µ3µ¶q&íàÛªÄ +óºý9©8•ö¨Cñfs&q$XJ{û;
nf뾆€]ƒ +Š¦¡i"€uØWb̬!Qßé3©TM‹gøæjÀIlƒëXÑéTñÐËUíOq/0!}Óf›Ícᨸ€55{ÉE-à“gZ·Kš»vŠÄµm@óÑô¦Í–çig:“ЬÿÖ—¡4ßæêb Ö´EÅ SI0nkV‰U÷$hŒG·WÌ_ª® ³ÜG´D^íÐÎC +Êxú£2Šôp:&ÏrfßsÑ·5òSÌ7€ìï +–‘°íuAzÚ:Áøu‡f"ÜdYuÁ"·ùä±Z´3û9é@+xêCC7ó›=°œhædÿpý6 +æ•ZNø…ú>øÊ«¦ +_w¥ W´w~ÊcF°Ñ ‚ãÒAìõ¬A‘üƒç`‹àr®ŠG¶9B¬¸8ÙØÈ šoªÕÑ©0. +¯E)JSk纓Q:Q„¬_šÍ[JLoØ]&þkÔEkþš‡fËaQ5–! +kì/…YkºpD?ºA{(2bìªþ fgV)µ€ú0-8Zt?Öv=€çÊš(P§Øb³£š=@ëSõ +/¡K2ð¯R‘1ÀW@W`gy)Ò?1^^óx"ÆOµ¸à‡³T¨êzóá1äÖÇÖ—Ä@_Š-ÜË–½¦Øȱ9$K‚³µ Îçá‘V8ÎÑ:ùíE×¾~ÃEDD{îâÏ€I«€7K X¤ØkuˆÚüæ—Üf«´ê5¼1›jF €öD6m-0Ã|°ÏöαÞR‚ +pùáü;åÀ[oÔà1Ë1yÒÿAVs+â(©÷·Àœ8l¹|Zë|:&Kp’‘~4s˦;.ðPçÂò9Ùc”Øã{º‰kr™|#àîµ´ó[¿¯ÒWÐàåÅ•w¹Ö%›t®ñkjºé[¶æÃIuù¿¶Šü +–ÿáceø,«‹³’e°À[2]Aàç?3ëfŸG[¸†ÞU~ßg1e:7cK}0öìb +²mDG1‡›2LeátIhÛ|íÍ +¤+AÔ]/X±Ñë^éšg»#k¼ +‹Á†#©Ã +“p´dã¸ÊØZ“FêÊj¡âCek«·W„JuæuÐdY$¢çæ*Õ<±µäÖb6£$€M›bàãTAoQøwOðXËùLãvc=«¬FÂ`ûŸBkˆ“Všz€ì^ßçê—=6C,®!Ƀå‰fèBÀÎõ(”„Áà|'m“ߊÌÈEçÒ})z@€väõ¢ÑëWÿ~Uè®þ¨òø€øŸþë_‚YûÍk¸ÄºøY°žÖï‘yX;º™IêP^+l²]¶nóhÍôÜ»9o*f†Ú•"Ap§\튪ʀMÉå¸CðÝ26(teõ\¹ôªM[¦`jl–åÊË¡»õ‚£(?úgèWÆìÖGîrûµ +\EïÑ7Ív"ÞwìJUm¥*V¦\®K™°¥YñP8”÷hC¡,_ºSÝä‹uÉî×˹vw£ª0‰üÝã_Ùíbñ!¯íc7-ÍMAÔÁÖ™¬j§‘Qä¶y 7!™âíWâ¯Éíºõ*®³÷QÓ5ŠóÑP¦j²ÙM#Om²Äµé< +a.d5·ò”¿¤HÇßÒèi_aNÓf¹+`+»a{ÀÐÀ1’§è:ã“ÓAκBu¿M)«ÈŠ¬Y—B»"g &‘D”(rèYKâqßxtÍæœ$7%x”ds{ôFŸé±Œ&µûdÛÂÁL˜hr“ÐÜÏYË¢X2iàÖ>$<Å‚ç8öÉ;Çèr7Ú‚¥ÞÛ¼—
.KdòZ¬w2ÖÖÎÀPI/veÝöã²æ[íþùíwŸ>ÿðþíç÷¿||óéË〾úó×oüüéýÇÿ<¾úñ§7¿¾ûþ¿ï>þëoo>ÿô—¾~|ãCþú‡üý˯ï8èOø *GÙáøþ[)P¯¯xÍ2r1}“ÁâqÑM”ím¦»#zÆTnô«§?B]6´Ûk=HÝ o:Í?CùÏkž¼=Ç0žë·ýy:cµIT`=lHîƒø°!LR~ë¥lþ°d¤’I¿±]…·x¶–ÓG~ß ®|ÉýTÅFrPœåmÑì.:Ke;< +¹KŠ®õ“=OûJ^”>Å9ÚYôG"ëLS¼gÆ“-ç¦f9éÉýS¨C;3UÓÉŠQ-e{_3áQ”DOKrÊ(I„s…º:Ó5,…Œ€[ª®è“šÕn(Th´-™¿ÒŒxë]mèsï´bWugwΘ÷—ô¶ ä…í§í€žŠÐzÉÙº¶J»óüKª¶éä• ùGÅyý9_G…Œu9LÈóªC*–M +H‰¬WÛŽ]Gý‚ù‡óÉFpÒ—ªênxr&…‹@„%„"36‰qìD–£ÈϪ®U},LÞå‘]³wïîêu+é—_Œ5êuŠ´ËlM¯}Ú¸<ÜyY®Eæôr¿Ž>ãÙq]e¶(J¯æÅY®6—²Ø˜õZË’ÊQÔ6z. Æâ’šOj¯\À®u(w`¢ºŸ]שµr·¥íUW¿b¿æE»–^{,°ì*VV”Åp/®k·ÞcÕ
«ÎR°-5.°ÐŽËý—N6Píå*µ4¾?Ç®µ«ZÝ›ZhK]þ¡.WC³Î÷WWßkW´pí.½ší¶ôÃJÝE¹¶9ýû}^Kéüë®\ʵ~˜ø¿ªŒË«»X¢ù…ài´È°Äë»X¤÷5£lÚ:[Sãã:ÚŒí¶<—-?í.cÿÜÙ¶ïfK”;3ì$ŠÚÛÈí¢±±@¹b‘åÚ{ô›ìAÕÑXëÍZÛªñ:®VGá±Z[qߧoÜkím¯9±íZ¹Óa¸ùX` ùuE™õÚy¿
‘,Vmñ%œ¦÷«N–MJ€Óðl¶Vî +}Ÿ…OnŒÄú£tðÙ2UòYY칸—X@¯Rn;˜±[¹ŽQã4|Èε®|-™Eⱞdñâ0Ö·Ä[|,^íà~ë¼Ò5òý¢•Å¦ºv±a;]¢(Ø$hqŽ]¶iÊgÁ¨Ýý +OšÁ€ÞS(5 •È臿#É´Úc®6êc·˜&Žr +'Þj.ŒJ¿O––`›Ñú +˜‘sF°Ç³³–&)R'=¼0QZÃñö%Ï‘ï&Êqñøʉ827×™Iµ…H"þš<·ÔPnÝ®}ÏkSkši(¶éÅVyÌÕÎûm‰â@Óo…ÆŒ•Ž¬‡Žü?©ã,ûä«OŸ¼}÷ùˇw/¿{óìíûË/QzôäñåÓ/ß½}ùæëË£/¿yöý‹Ï¾}ñæùŸž½ûæ‹Ï_~îüî'ùËûï_ÄC¿Â_ò:ÿabus«BdîY`óNØpsQÞšEhÚŒâA‚0³rcCjf +ž0¦…<’%‰æž±å9Ë¢Âi[<ö½ˆfÜã6aî¸qhxÔ<sžLÍèe%“qÍ~¹›Omž)GSK€Ô«c[³*óŒ.îÅ+‰§vø09åà%[•¾³Zeh1e9Ì-§Í¦vh³¼Á8ÙÁ÷í{Z–£[)¹ÏÚõPB:7UfÆóÁ%±#é™ù3 +AKaf˜à3_€ÔézÅàç¡cÐ ,.x¯º§Ø{FîÜ*$$ùDƒw’uÆxa¼Û|¾yþè;u MCj:Qس{Ž–’p‰Î2ÓËgšæd<ô±tPZ%þ]ÞhCÖ&¥ŽCÏmëÞÃÕ¢°káëñïf¹Í'Ì}ÞWÜÒþ8@j.¾¯éW4×æ“•¬$ê¢;èÜy6.‰«YbÌs”í¦¹·¶Lçlžszr´\wŸPó¼ò1`±ˆC^'ÒSÐQRÉ›é¨jáƒhdZeÑ°e¸¢ŽkÓ*GHr¤å\êÑRc´ZnÑ•i)“áªÁµM[s4?Ð4’ŒõÛZÁpøQ‰Ž:—,¯mÖ¥×gÞÇå÷˜éqK’#]I‘Ê´Ø×2ŽÆžd`ªö©–À…IÖµ²ÈÊ‹zÆyÉ¥dP4Þ +ÚÖÈ:>½†Æ¢(€ÿa˜4eÚïW¥‘fBŒˆrsEXªg´b +ó‘|̼ '.qƒ®òSxÖºr¶¨0Ô´Ý·(ý $Ôp“Ú•j†¹¥üºà…tx’!wy«pÕA¢ÑѼYÇ%†k[LL œÄ |8“Iå—Ááì¿èwã)´ÏyŠÓ½â–0ûI +*ŸÃz›ŸúÌLÞyr§D@DzÍ\s*[´z|GöHŒ€´ìæ&4ç7ÍQ4)íxÕ›çá µÑ‡sRA seŒš‡åŒÀžôóÙ-·!áEË¢V4%è4òzYt¿n;pIY=i2G‚Ô§‹{^ÜÔ!¹ÑòQ‘tmÓSì%O€gÇá9A9¤ñÙ}[AÈÙR½Jͱjz? +y¦èwƹ•é#KÍiq€ì÷ 7óàM¶>xíÌp“C¨JìK×”j0¥r2¬GP\ö.ZêGü»OªâÏ«Ü
CTÄô +q•9>(S¢çÞ¦Êæ{?ZúûÍÛ™5’L±g²IE)gÈÊô¾éY$9žQÔŽ£¹òÔyü|åèJáªåˆÄ¤%õÔÎ>ùá¹>êÁO!÷ÿÇ1䯸Šz‰¿Oßï{yúüÎ._žþ
¿ýäɵ|õë7Ï¿|ÿúŸß}Ëÿöâë—oXyô›ÏÞ>Æ›Oð÷éw?àÏÏ~À¿ÿÈ¥K.ý[üãß(ýx‘Ë.ÿG¹<÷WþŒ_ï˜V?Òp_`öƹÇo¡Ìè‹ÚÚð“zr–õP×]mßl3K«ByÃú÷,¯}aÂ4ã5HcEcXBQûeQlÖ)C(²£¢ ¶8£µ.au6söê @éT”Ûدº+#øûç&Žrì™é™ž™äŠ)²´¹Ib)wFH^ˆVDŠß>ÕÝÕßÁá cßåbY(曟þ©®ª»<ZX[ëG‰uMŸlƒj¼Q2Ç‚ â« ÉWHr=ÈQûaRFŒ¢X™Â<;;°»³PA+„>bu¡¡Ý¸âB?B…¹àÅðÈ>g(¿ØD@V43Nè;È‘£ÀZ4”ƒspqƒÑ™E þr`Çà +<YÊÒB2ÜS(®›K÷aÿæ*B£+u¤#“€€]‹…£ØÜ8V6i§ö`–‹M¼šè¦<ØÉE&Ivƒ[Êð^âa[Áã• H”á¶öéÉf6><àÛà$mtgi€½f(ñý¢†–è¨'¼€”E–öf¾Ü«žà Á(dø§)ö9ý—FÁå®0Ä_®
柡…ãR”v +ª==a\Pò~Yˆ™Îh‹m¥ñÇ +¥yeD°G‚¢¾y§ëxÄ®3ŠŒ»./÷ŽŒ»¾÷óG_ëãY;à>‘ù;Â>€Çf¼¡ÞÚ"ˆöÍxïæÅÝñ
œÇ®5ˆÎÝs)”žÝk”à-&føDˆáy©jÕÄã±Aõaú˜=¿M•¬ +‡Ø¡•$а!€D1ì¢vý +D¼ãý˜’¶¶dT´ +vÀÈ5ŸÖ.N¢JvCYh”9œKÎÃw¯Û*\jÉ<»=Z1§5;ýÍ!© +eE]k§Å§°Øwp“¹Nwg€ú†^ôÒÃãÐÀ³ºõƒ\ï¶Íþyôú1úgKn÷ˆ[¡×&Ü…Û’‘ÀŽûÐÓŒkà±£ˆr_!¿ïC×Ú¬úôýÔ*ä\¤µ
¸…‘ +‰ûœƒÙ6ZáYØ@™Ä¸ +’!N±•QWç´b +›aÚæ)z¡,üh8OxOÆÏŠÁj&5¤p7‘‚à¿H—»ÝNõâŠáìn²øQ‚1d]}ú/ð[Å’ÞPõKèMJ”&¸§›"·ÿ4zn¸ZņÀ´b™&ÄØ-«T|ˆú©*W¿DÌpè°Äi_7hnpÌžyûygBc DŽâp•ýbû±‰P›u(|ò<g<x
’¯&¯-!…MZ7ã5‘ ;¢äµ¶:yfÇÍðÎkP„› VN›•Ä©›ÌVBx³Il€äcì%µqƒ&)©¬šÜ6vhÖ~øÌ"ð[R›µºky¼êƒ1b¥úŸP€õî>ÏÔé¤MÛéi-³imxÙ¬3fHikŒÊ¶Ý3]`ÿÎÄjË9Ð}Î8ª”_pYÙªK” ôŒˆ¸ÁxXÐ.F—°^Š¦pÐå}_d8¸ˆrªb½ß¿„âÎÁ0>¨xÿ†5ÄoØ=ïµ®ä^õÑU´P'Æpgs¨K1¥Àæ,ÜüãOgûûÕ¶#ÕuD¿ ÿá¼ ”Œ÷ý’<Á8Rl·åbɲÆ0ƒ…!þ>«nûìÓÓ“Øî\^"{è™ÕçìKÕZ«ªŽ‰q«vìé(þp›ß·ç£_¹gï R-9gáýßáŽ:j/7a¨@…‚5‡òÝ•ï(óê|õ.IOQJ:µQú•ÐMs oa% +`-Œ 3†Î™Á^QöêXéÔa€DV’p¡u@EÒpc/I—}mL±Ûò[GCð/,cY‡!øÓé¿d_‘Æ9ØŠ}¢,ÊŸ}s:íêÑãæÌ
Ú=^ +µ¬žÂá#÷g¿oÇG¿fÇæТÚ%Q —áÿ½uý†%¿ßõåþƒåéøíÞï¼{ö—ë—O>_½xÿNÿ~ôêõåµ"÷ÿúüæâÍåõëåÉÍû·¯àšñóôÓîãŽ|êá/jVwN³þÍÎrç>ã¯ñËÏðQDêÓ’–o—rËKZå±hŽ\(ŠË\
=vˆø»Ý“ßzæýåë77/>¾{qp\:ðwãœoŸÓ=d¥yŒÊUÈ¢Õ«-Š9ÀÉ`„A/²¯C‰Ú%bšŠx±»ëѱ$Ïbe‹a*#‡¿Ø ì4›Ñ¢’.·›+x®–¢:7x¯ÃbávtZ|‚½cÚiÆÖ3èzúuÅõ–Óæs@Ž=º.¹ÆxÅ4›Ã]ŽÛÍãYÍÚE®›+x°ùúèº$?—òC[ãb=ÜÜÓ<·ÝÛ¡;rå`oÏwÛ)¼ßd]aMûØh…æèzöuÁõŽÓÞs8Ž>:–\#|Kd†^ŒAþ{»“~Þy.¨ZW‡(Ê“I±Iˆ0¨¥1´È#Bbæ4£{E1þ®Ð¬ŸŸ÷šA>–iã +›aúÒÓv8Tj<ÁL{ôÂ
;—¬›ÎzÎ~ºÅ~O7F¦úhê-6 +ï†mÈ9¯
q–œ‚ɮʦ‰”ÖOÓ¾ßÂD‘õ`¹ñÝ&2nÅó“F<3îJp„'+(M /TÝE7)ÓÅð-
ò>†¹¸š@1cä@@Bœ/’Ýp–NØ…9xg~L‚KQHƒýcrm9ªª4M%7š:µdtôR7CëÚµb“¤G’rÎ:vN=Æ¥Û椆ÉIAðÆŸÅõcãò7‰HÅ8o~üT{d?âGãÞ³;ãÞ3‰K÷Ž³°güv¥_`,õLp*¦ ËFZt×Mš± sP#vÉ +Hy×¹³‹ž¯…šë(DÄç±2vM”<LE9Å>&ÜUù"Ñ™Eé.;’N‚RÍ[0R +{¸¬€FK¡ÀúC1oä>›ž³X¥æM Už0gݾ@ò|Vs¤tS¤6[‹¼Ô~×{8Hñài°Ñ%î¬ +nå"¨u‰í›ÁÙÓXÍ@#Í£$O!N"–#éøg´yøo¥Í÷øéËýËÓ¤íÄ?øyúi÷qt Ëwww¡T'¥^>ýŒ?¾Æ/?ú´¤åÛåÇŸÜò’V{¼»£àœZnŽ›KÍ©…æŽ2sr‘¹£Äœ\`Ž–—S‹ËÉ¥åŽÂrrY9¨œ\RN.(ÿrÒ¦Ù³‰ê©”´ +¡éìI
p]ˆä}Q5ƒ·×R88ooUŸ-UfO€1sg+RWÒPN,1,ÖMº‰<ñÄ~–|ë¬EAŒ´c\ظA[„•èT”µ˜FêÀø¨™ù‡ôÁÅ(¬-¡šKô蔵`BP;q=jÛ’ëaÀ)šõ@e8K¸
zïÇ>%¡wÏÑëa"uÉÐ\TzÁ~ðOùÕðlóB¥Ð¤Šƒt˜@•µ¥Èl†1¤;ŸD\eGW´êà~–¤`ê*üvæ³- +ëFbâ`•½P™5Ç
DáøªÏ„‡ôç…f¢¨—Íy‡%ô1›Fë+"¥Õl#±‚Å
yŽMš½cQ±é횬rDMÝ%HH‰.™z§®×©½+uâš÷Ym×û ^Ú‰^ +¶.ü9åaÅàb38Úû±š(|1+×–ÁÜÆûY
Ø×µ ð¢ÀZb5ÏsS€ ecd¥Ö® Ù‡nSÈ•j
œ+½Mà(Q¾V…«ãý܃‚MDuÛBÎánd:*™‚ÓI8âj:ÚjEÊ
ni¦ƒB—Î%iÍ-+Øz( +¶Ä„C¡ƒv5ægá=%ó)ÉÁï³YŽÉ@\sXŽ«lO‰¬Ý‹bQG:‡ö©6Ø7; +˜W°®ÌkRôÁà\ÕlBp +ú껚ËYm¥êhÉíRñÃmªº’r›ÒšÜ ×`|ÕJ± t³†‡ß´Ñ¦@gBr“a>ª‹ç*©"°0%(-aø
Z½ep‡&$Ó¥Èlƒ#&Ç6 +E²ê£¥Š‹Uš¼‚=™•Ì“’Žôê$ß–Íœr1Mé‘€ádžáÀfš¡Ê@8Jê‚JëÛî1ü&‹ßÐ,;¿É&×H¥4ºRÊäw÷`»k>‹o{0UÁÈÔHT+\ùÛå’·
Ãà-IâCÒ¸Ké8ûŸ$Aù&é 9§¸¶›"?7p3Òò}¼uí¬”@qæt@ôŸ Û4ÕºdË8ƒ]°Y‡óJˆh*+8´óÈ5k!G‹QÓvIzÝ1éM,lšœÜ—tT’«ÇƒÛã<Ñ[´õ=À +1hÙQÈÆ[áqÜûÓLhŒïšUoî ƒn–¾A»¼VÑVFƌчƒÚ4ÜY5à!/®i„¹ +'«3DÝgÌUxÆïãÿ>““ߊ‘<±D;Ìø +>ÿòUÑÞ +nJ2FD$Â0?î¸öמµÚÛX= - 7ÔœàIF>nh…±±j—2ŒéçxêS}”{íû
_PÜAºé`’‚}6Ý䎕áBá¿ó¸ã¹ÝcÑÑvOe×FÌ}Qé‚?úºt²+CµŽFsŽÊZÖ†ž6EñÇœU+³†Nl¾ôòŽLKâœæ¼=ªãs«×”|w4`VñǼ=WW¦xäA,æoᩧÍö[òÒÐÑbþŬÚäàO‹Ä˜øû/ÊWB•Óf +cΊ%ÚiºNù0P?à`#µê·£Z<D9Hþ5;ïö4u8í –O¶…u)À³Ä³ÛžðzÕW…m•æ¤ +óÎè·÷`üõ‡+”Ž»ó|¼éW{|q³åñ*nwâF¹câ]ËÅâ¯TˆEoaÝážþåç[.žhz©vm€ À·5mt6,~°Fí‰ígÉ´d§¬PyKLGùP³±r®}§Pq-ƒ5[È_û¡f³¦^E©IGÉ–ì@‰Ç¤¨à釬/[¹søgqÜO®èËfieó¤ñN¬ÀÒþ^úG}ÁÈêŸ8ÒFÍžN‹ƒwróæ +T9-©T`AÂÏâÐ@‡¼ŸGVA + +kGÞÌyZ´õñFÇャb•KܼE·E`%ïžO=‡S
gídËÜ‹ó‹N¾-VO‰QØ•–oá:¬6k è{† ¿ +ï9Çn¨¥†&»HvËvÃdËzàY§¾ò½
6€5…Íl˜)“#DñýàV›)öu8ìx¶‡Ô Í3ºBËäY÷¼@â$œu_ÕºC{"'MŠðolzÑv;)q’Ã\âù²s©|«ó³BÜÝuñ€2,¼fÄÛêjF´À3ºbn/ç6*ãçÔÜS~àY—†' P€¹s76¶?ìÔi…SÔY¹Aƒ Ö~GüÐØìϯD 0BÏê僎² K[‰—‡
àíøM¿4vNÓmäËgàä FW.–ÿúüìJBî_‚Æ2‚hÒïl®Ó¥Ï/dF«@ÝY¼¶ð„¦ØKÛ\0èÇ™¾Ûïȵ‘&H;Z
©^‹úÕ. è¬,?Z®CthøBFŽäµX;‹€’Ù$«µï~{s‰D4ø[€Î,ÀE0¶KÀÕŽ¦ŽÕšp/Æ4×Ç]»E™‡WÚæX_¸Go½£ÇùK¬ßß +•tÒ¥w!
±²ì.y¥¸þ·t>^È‚W&r¦ï£â– ¢å{-p6‰ãq`¬Ý·+Àex›UÄ:~}ï§=õªs.ð@›Ä‘]Œ|9iu +xåOѧýE"ÎJâÄ? Ûù8`:\Ð$s°NdÎ,Ÿì-A[¹å“³K:çÁÁŒ*è «,ËÓ®ž…;š©vº 3Û¡t¼”¨ÞH¡¸üÀè’©b!ŠÖ¤^Ö˼,‚€/A§õ·°ášãðg2÷ç”ωre©QÔjéc1°a]ų‰"ïÛ¾R}Öc[66õÿÙ $ìi‡ÔA”û¨3Òa+Æé¤ÎJþHuf\wr£aUQÇ“ÜQƵ‘00Er0F¥Coã= +8ø@,ÚuÕØ(wÿžMé€Ä¶å`h‹Þ8ëýÙ;¹³V“³òÕ¨Gø#ßc¶o¶Ñ³%¿ä×B_x¦íMð`Êw*ât½Q<Wd!ÇOã ¬ +=XõuÂü—Cs"¿¦iÆLæœmÆô0,·²ßô˜¥}jîMn¾¯â™Á²ý)¶QΡ±PXQ±ÎñèV±s:Øú½Ÿ–1ÀsÓ.„³9“£¸n?ÿÈÚ=]ÄØÂ8 <Xc{ãR7ïÑ”ˆhòí÷7Ú.‹Øï2ÚÈÚìïYE´ 씳 +ASÛÎó¬h oñÙ +Ñîoœ´ÐZg¹}§¡ð£Y©F(I`§B9ÝwËa¬[2ÚÀRJ/‰Õ´”ÈaÛÐnR +Tí3à2Iœ~IBEÞ¡¯×ýùÆçaòŒb¢z`]±Ð–U{ô¦ýhÄš»éa“cãb™.|Ç"Æ;àᔳ/ܹxÇ}ª{ïÄ¢´‰ŒÔµ>‹ažÇ(Éž‡›Ô +ç`ªõÎää£3¿“ZæÊh¬mýð±8@/\*¬SÈ: ïEÖÑIÀäÔ–ÛAFëZÚ‚1LƒÑÔb5æØ×Ê{©1nçÞàRz?¦PXÑ°ÐéBcnëù̘GÜË|Þ[cL5@¼¥rµMЫã¶Ñ„¯ +¹ò#y¢_kŽVßMyk˜‹j4FÕes6\PõÔÌ-ñPœÄÞ‰Êg €œ-趢™=D.™,¯ëËÞ@güB_ ÌwrF6z`‹tbŸ›’ ÚW€å’sv29'+çÈ\xšœƒ6ö=¨¥ˆbÅo·Ö®¶¹újøhGƒŽOÔÏMiÆ8Ç7e
1ó\‰§¹r&Up›KÝzÚÜÕÔø¤://g¶ª'8sn|²¬9ÃÕ2ã Ã4µL§Q´dB$bçyRIðæëJêyKž%šzã—•2³Ú*ѯ<½žW&Âõæ´X[ŠÞ,~‘84èx[õÄlq¬t¶,Æ?m4ÊñÓ(IþÐ6ªzún4[
öxX›ý׌BÉz»T32N¡{Ãk¦É8È,I_¿t´`N\ÇL?h¨Dš—g>+âi5Ç‚Á²QNs· +íÍ‹1’YXï›qÞH–ªEa *9ÃƼ=€ÒÌxpöEóìéu|,ÔçšÊò:¨ g±.«2¨Rµ8Í
²†Ñ®ˆUÚ0Ë@S£+Eá4ÌÉ¥w£¡ž +õ=¡WçªMãÅ4‘2mªÑžµÖ\Ÿ”WaŸžØªmæK¼RáÇßcÉ9M9'çÈl/ΡQ1%Æ94 +€‹'#¢ÎXõÕ¦ +´yO‰\ã +L“3ÑOÉqßâçZH1×Â=›ÏZþ KfWèí`ñç`îÇ™o°µ<Wæ´h'Ù¹"ÛµêmÒ8$›á‡ëøÃÅûA==êñÂ@¾£zpÑrÎt³ºO^Aaž8»_6 ½ví°]Š!‚’e\³qjQ¹Ÿ$ê`£W(¢H0¡Xó~Ä64I¬sž
ŒBŒyšƒóRvð)»Täš³+¾Ò§
8/b·v;{#pö0³úQˆVKðSjW†ÆäÃqaÓò¸»+¸Ä½=úáÉŇÏÞ^~|{wûæÃçÃo_<><yùñÃÛÛŸç/~óþúé»ëÛ«ïÞ|üùųLJßÈ’?>¸äÏŸß_ë¢ßáûìÕ™;HÈ÷ëÏø?®ÎÊáüñáõ_ÏÎ/®î~¼>Ès‡±Ã/ÇJ´ÑëïÎ!€DRrúgÔÆ/õÙ×ãkÂüJÑͯ³Gîá¯üéxίùt˜{‡ã•OŸ·íŸÅž´Ïöiÿ²nÉáOwc2ðÿ¯ÿBupK]æ3ÐW«£ÒÓPÖѨ Ñ“á®[÷Æè'ØÔ©Âs93%“cƒº‚ɈtÜ Ââ7èâÜÀ{S‹~üÅø|‰mLOUÄaë+Ư6r|—81Ì6àµD&¦®´Ùº=Ná‚î@Îøñ +ÎF?>Û“Í™Äî×ïá>ÿGqH1~ÓÌOŸ^\^~ºùþîãY»édÝî42œÒ
ad[:˜ñÎZ°ž¼±oÑÌÐ<Û0}׆ž·¨éCÉÀ̤9+'Ë5ÔÆô ÷½6ˆµŽƒ[y’•k%¸‹R8šÖàŠõÇ°.”tsãøf/y§Îè~
.0ª¶(﫳ÎAÙþöèâ…w?üþöêåç›ïÞñï§×?½½%rþí›w×_>¿¾ÁÇ2dø~ýëÙ§ñߟ MÏtêàèD•<gî_d&gÌôüòO@¿ÒáÛÃßÿáW²Õ÷¬ÏÈzq¢zóq +®Å«¡Û€QÁýXåµ6ª
Ró—ƒÕ}êõÔZdؤ‚q@šÙQAÝ`Á~§Þë^
¼ywxòâöãá\.ëpywóþîÓíÕáŠÃÍÝÕõñX5˜:x(U
£¸
/Õyô$ùH×f¨³¬UÒá9êƒt +*ˬ F‡!LÐ);ô[ÈÇ‹áǶ‰PÛÒ{ßÀ¼®`r€ž\¸†[œNåÞ–l?Ý™µ
dÂ2ð”Ô`r¹…5GÒê–XÈ~KM^†³br‹ 2Û1Ì&oNÀ4)>c¬¾7¶$âÌÁØ°}¨8.¡€ÑÕ"R+±ÌÀ®jÈ^ÁK @qkز
}Ò‡4!L/Þø5zA +D¯[í‚TL»v†¨ÉòN=Î;ÆþîŠÀjÖ9¾ÄžËÕDfÖëí°_6žOU9ð" µÎ44ììX6)HW°×o…3c†(¢ze§ä¤VÁž¿ë™m©˜$^}’;<`axy˜°§d v¶…>1&—,§TÆRL3†æZß0®jظS¶þ”Wzã’§%Z·Ñvÿ~UÖI±h +²EÕñ™Žv†½Ù½-×½¦Ê’ê3emQ‚½c,Ðx+wØ—¼ÈBÔ©¢C0Ù,¹ørPkDSW€á,º©ï}0Û
‚¢£%§£T À +iüXkxg(vy÷x ýœú«|(–ËJa/½áìlG˃heѼoƒ¿ ô÷C]=^^üp0ËSü|w --8x‹ðaùú`—¯·|h¢ºf+Ú¨´·›™:Kš'`ˆ{®\c™´”—¹}õüáæÇw×òýÉÕOoo…<úöâ-ÖüôâÇÇXÆ~^|8ü‚?fù~ÚŸc\~ÆgËß–øò>üèÖo–üÓ,¯èÙïºþA;a=³‹¼ÜL‡ÙÆÀŽ B!ù.(Œ"óríE+5sGÅù‘¾˜²´ÎʽCMpʼn®*5®³Nñ90ŽP2¥˜3«5ôÃ*:nacOüJR>fÖ¡±à°¾(ÆàB»b€(^¾Á€ûâíì +qÐadˆFÑâ…VK7{¶Üøe¸¥k8_¿,’Ðt…þh¬ïF»Í®ÛÆàëBâ$Ï3Fù2tQõ²èR¶!NÇ ÃË9T@Jn,nHòR?Ю«ÀÒŽÌÆ(Ï[\#ƺZç¢Ñ4ï|0¢]:JæŒÑ +d'•Ü±‚¶t¾tp"[¢A«0,2¥fÒj6l)¥"€!©DkçE)K}ˆ“m?/@ÜúºƒÆ;¼ä¿µ[Ûˆ«ÜïrONŒiÅ8§àª±ƒw\¤’åAM'qéUÐH¬“ +U½‘3¤ž#%¦9@ÔÆÊ%&¡®ÅCšy}ôêMì@㶈¼Ï]¹™ÐŠ|‡¼„ÞtÜJ2koÉ—^›LOÈcFH4Ïðã™;™*ó
Dti“GK9T)rï1jVÔ¶hC»çwNûöYf’Q®I‘>ÈPg’‹Œ}]YË벜哸 +þèpëX90JhÞØ"cM:Ÿ=uØ¢ðõÉWg¹…†* +n l׋ ¼’Ö5q£ŒôU¦eϦ¨•¾ÄùÊaXseÍNè¥Ôú_ÇS´4ìa™ŸW´’»ž·ÕñöU(`vÞk·¼œ«Æª' +_À¦•›äôÞÚu_ݯàÙënÖ¬Y7~YßNûÆywËu¼åïbZç&˜)Ì}ØWQ#µašpY§;ÓþT3"5ÓËfíS!RÄÚèNOT_œšÓί?}~|u·<»¸½¿º»ÝÌ2E¢ú¢m#¬ÿ@ñ´9×}âL‰ +Ù‹µ:jðwÓµm[°¸˜E(ØܮŧXh8kSVXë®tÚ‘õÅ/ÏÆëòi¬Wqˆýáå/øY·i‹ mc?ƒRžát›– tô&aå3‡éK§¦åš.ÆZ/£ýèï—ü0 èÜW†èpÄZ¤{T¬¿½9Pœ2Š—Éi¢ŒäÔioïßåíc™ÊpÝò¸Û;9 ˆ}yvwÿôíåýÛw·wËŸ€=y¼|ùüþîííOË£çoWO®¯n_}{qÿæ¯O/$“¯?jòýÃû+1:{öì¤Ù7÷—o¾÷äêõ»;±ý3~ð×.gïÕ.TèÆâžXßú%FS{7»_@‘Z¹Uh_¦P
u
{¦ ºÞr;”HB¿z_Ël]ý*]†_e=Ö_ ÈÙšºL~Ѭãn<VÑ¡¸kVÖcwÊï>¿u¢gÿŸ={â3(J×Ë^Æ®ªKSMR—õªÑ‰+ýú3¯óg\æϼʿ·‹<N5QAíd9õÑÙ©QÐXQá7²{\§<e„œpN%ÍúºI3–++ÆM5&õ3g§òšäÔôþû¹´~m‹ÓÄ*'Rwª¡Z€Æj±Ã«ÚÖ>t€#d!'jÏ©Z3žU mð’æäÅ45Ö1ËQŸšNcþk¨-«tLû+"´W¯ˆÆM»L+裆œõ\b»m1â·XíT1õ~EÕR‡Kµ©} +Ú«†j«^Õ¾ö1˜ƒ–"•ülÑQsYz_E“¨y$ªS”1åV©2–Á"AAºëÎKÐVLÛ«†ƒÖºtô¦ôD“÷wÈ1C¦bµ³m¨4ëIˆÓ§(<;¼ªmí#0Ç,TÊ44Ë b–JåvRê“)²8˜¤0+×Zp@=u%NÌ£^†vF%ÍnÚ—ˆB‘ûº¾½3Þ¯=:ʌɲÀ²rª ~ÿÀj©Ã«ÚÓnûs¼<MRÇ@ナW4{‰q¨Ž0Oo¼Â¢ +Bm•‹”÷ê±d[R/ûÓl;ãáÕýÊ{ÕôÆvÛÐ_0ÿa6’,I”DiéL•v÷eÛ$ݤ Òf‘ßCòðÎÛÙv€÷ì9Öå¥øqxÕa~ð Á¸3Ö>mûñ,†\¥=Z=gîðÉÛ»ÕÓ½ÞÇà1j!`ŦØ)jÕ‡-¨ÒÉ £¶ëî'8†]ßÕýÀ£\¸ƒØÜ´¥=Á¨|9¢F«ˆÄÒrTZx X4w?WiûÑê <{p‡OÞÞžîõ>µêûC‡¨µƒÍÞ/«ŸßÀ˜*µyFÔÆJhÊõ$»d¡õ“ Æ•ñEöz<.UôÑê ¤9c™Rë«àõÚÕNŒ¬ÞžÎÞïu²ú>½·=ÿö¶çý÷çßOkÛ÷—rµ¥Ãþ}ú_ðã§Ë¼~ùÕõÓ?._>ÿôë?¾Ú+®nå?_ùIðð§¿_¾è„^â‚\þ‹~{óñgþ„>+ÇçòEùë”ÊÇøŽŸãéÃbo'¿ùn¿J>y¼ûùíe¿þʹ^¿Å/CYWãŸÒ¾¤A"ýÂp•W^ŸîÈ1º²’[G}Z£ûÔÜÇÔKÖ1Ž.–ëkªš1}{ê@ûõÅ·Ž‰±[†@éÞÚ&%ª—©`”ö» +>D° Ûõž]Zy×jµêw]xPb›³j¬È¢Q[£ÌÖš‹_Ùλx]5½n >M¼%À]qòÆsÝ·ÄÃêÔ‰ÝÓ°ÛÖp
£ÃJ(Þ*(•êÛ‚^g~ÕKÜEÖ7o7½Ä êD¥œY4ºÑó·ÜÏ@¨ë®ÂÍ4´¿Í]#¸7AkÛ”ã»/®mHÊ ÝñTw´""È¿Û¨7ßñÐi³4÷˜ÌNpù¬ƒ1è`_ê«@A=;šrÙ^ÞîìÀEr
°Ö©ô€›«UxÈ8‹>œ%Œ Šoû0«cP8ûoæ*³'Ö-c$8iQ:[¡–U¨»MÕ¼FüVÀÝ(9.V§Ôð XìÁñ~ë-.î ·¦‹ª†dÁ¢˜‚ö¬>
09@R±Œ‚
PÔ.,o«Ï±j\zˆÎAÕ¤S”Å´k*©Y}ÄøÚ +5“šrµÅíã®)KŸ;˜bLÚzÐk6ZJMðYRжʓ8ŒªÊöë³p/’ "}Máv¡p›, +ôŽ•ÏAçêö–]úÆæ/Þ6ÅÐJÀ,óQeJF +þüÏÖ±{ÕÇÐÿ"ܽ+gFßf1KÎŒ n\‹‹VÓ-Q;4µÑGL +P_¾
×Jl…ïDЫTR{dÇgbšä3cßÅonp)‹±¯Üͬºc¢#‰Pªpª€ÓID¨Dbü¤A||d9è¢éÆ°¡pZã>'@O¨©9±xÈ4N‚›œ|o¹½?C +ÐS‡,Ú°PoqU†Ç@㘦’ðT7°f8à +Ðè8‚µ®v´…ÎNÇPé5Kݨ÷jQ/æà†3tWa¡-±Ü8cÕhnÕ¾”+$%-Vœ^b¯Äâ„,Ì +¢ÙŽÇåh']ؾ¹1ÁØç:¸5D#CÜh€"¦™0Õ¤lW…pmhlHrçp0ϲæ¶É`÷@zÓHŒ¶ÏKgÉ!¬ÕùJÀ\sdÅ ¿[°8êÌ&s’;Î()D¢©‘‚ÖH½µ¼ìc\/¼NÂ/.ڀ멹 +y‰Ë–!ʪ-þ&õÉDÒ‹1õ™½Û"÷.lS)Á4 +273“†ºÇžpÏ͈ªAé¤brp€)4cQ0ªO!´™ˆÜw°ñ%ŒÏ}ÇyBlyŒõÒcS„àD:2·¥{5¼t0¹\†>#âÓˆ}ŽHC«Åãº*–ƒ°Jè°'ÀTFú4Ї.™Óo`ŒÐ¨aÀjŸUsd'païHo#Õ!¥§kµ.íóúa,;_©×çß.%¢ŽY+øo™ŠÅpj×_.U2}O3 ‘>¸YFl›ê +‘í¹b‚šÙQГSÅñø—D^&:oÅ‚Äwy½‡Ï.üÛ”ÏGN£*§cÕ‚!oRxý¿ +H‰\WÝŽ¥Å
|‚y‡s .øÔnÛm÷%Yå"Q¤ (H(Š’"B’„xû”ú;3ËŠ™9uú×mW•m].jŸk^ÛEŸÙ¶uÙÜѵöâÀ<¾ÆŸOðýKÀûš²ÖÛ±[®1¶ÈþXz
V¼‚×µ'ûãíª~IôÑ Ìž‡}}u«uù°õ(—菱ÿÝËx|þß—3˜?Æ%Œî3~=~x©}Ýbšá¨sóãC¢z-ÑWh`v98/š¾ß‚k_6´ý +¶këª+®k9S[\ó5<µ¨‡þx÷ò¶K‡ìÇ
ï©^ñÈ[ÕºÊÔ¡Ëmß‚ovÃù®±ˆ˜šÔÍDXoÓXÍG +˜KÇ9@l îºuwP¦FP¼·¼§£<hæù‘¾¢É*ÈäÁÓ¬–“
¹›ä£4¦< “$Τ‘æÚ#™gAºä`+Š8A·.|”¶®|fe¤ÄÌkZ658Ý‹žwO&è2ûð¦×™}*ÈìQcñ Ã"¢ªQÕ~¬*ìΨÌZ +²žÏâ‰9nW|pÇ”aó>€zÞ5Øb{ká¦Ñ E¨ŠrT ™sÇoUz€<!eî½6h£@óD@Žd“dË}¯LJ»ÅL“gß 0ƒyÞ`É¥¼©½ªg´2“« +(©æP؉%Ôbå´4°Aq¨#[´vBmŒu +„J
ÒÒÆtvu¸pWÐî«r]·³ÄXcír}¤qÞé(TíNUùÉÐØcé}Ž¶ô6êþáÝ+Q@Bk!ØÐû™Ò+‹5ñu›ÉyÒá
¹=ªÙý”ÁÎì¢ØÖsùµ+J¸†“ß¡“}èz—D8g¾=0™öš{²gƒ¥;óÇ`i¸Ò,0/ ÂÓÁÚœ§»Å÷Ý;OCn˜Îíw©…áiäÍHj(ªÔèì¶q–‚å“Ôƒ‚Ã[ûíÎIPy½?^׎ãõX(C‹¸µb§UщÝp[…2Æ÷`µv¹ +Zv•šû#ðG"r¡$A]>Y‡{YR ícV¼.Ž_°Kl/8‡ôV08ÉÝ"ÅWµýÌ×ÇtÐÌ©¼u;5‹ ç +ôÝe wã™”3{Ö}?áDeŒTõ~íUÀU"–'Q² +…ûá+© 0¤-OàÑfǃUqƒráz²¬b¤ØÓÁqiŇFo4ë +Á·¥,ð¸:30rGßV]ØŠ(ײX‰O;úVXRk(ëÙB®Ì +*}3ÀcÜêm‰ŽÑÓ8·ÂäØ2¸³®²£‡5£ŸXFët(Ñþ%ÛîF´¿ÎÙ7¼ï*ÂW ×° ìlxpXbm²›™7h¢çŽðì»h8 5 +(¤…´"Š'mªÑ±ëÿ¼—a«_9Æ?A¿Ã}ShAÜ$“L}Õí"¨ˆ²«X)¥[ܪ¥tYúí}fž™œ[ÄWd¹Ûvî99Édæy~cã‡w´,6wÊ
zÌYÃWcf¬întÿ2›u‰ëG=y6‘ŽÚ˜>dsî”Ы‚
@¨t„´¡j©w’ÇBYCý¨â#E}QZš>œP¶Ûs·ºo;è;PºOal£&6ŸsÇzßG;:Í£VËGÑ7Þ×Vª¬_<©Áý“€´m{0¸ŒÁ;ij»sB*t>‰®ˆ± š˜ÍÐ6´EçÖeF䞇aMAeÜÙÃÜ +g‘i(æ*ŽÞÈW|.@2ÒYÔ ©qƒºÊ!†v·à´Á ž×M5D¦:ïzÍluw1Và—6øòÁ¯<|þ#~¬$á™îœ0͇ϣ1I™H€mÁ6ù[u‚Ú•ø¢ï/8¥Ü‚œIq3»o+® +#3:%D©°Ô\ªÉaxrŽ²fÎ~p&ʶÚAjÅ{Ô×A“¸OˆÅ6 ÉDâovö̯¡J*Û}ÇèÃãº\‚ÿ!;ý&
lý)5Ù +Gp8[ËqDZ°ÅnT“{@֊׶)J +öUãö +\¿ìÈ4ˆìk7Ž °ÃH.åô;š +æXéhfÆI +êÂuÐ¥ã³Ä¾p³„ý‚¾+bÝάÄèß,;ÂN|í¢ŽS%Á±‹€X\øgoVÀ,5Ù¬S8à¹0{¯ö£e¾êv‚Î× +£ÍÐ0ÑJÍ!òÖš*‚X¡-"Àš÷‰¼„äHxîH¹¯ å yµ›vÎ~3‚e׸ÁLZ[dÓ¶neóŒÅ×ÑÈPÈʳç4´Ð,ˆoÁôr…²óx=¦7“P1§¶ÆL»BÌaêÚCT R¬“àjÁÉe;ju'x,n¢–o©É¾‘†]Ôkä&»0¡¹ò1«Î›V-¡½³’´ì.8ˆ)•dzc¦èÙ¹––dì–¹GX£²ž—!ÝÁý¹ä“Ë8*›ó@n¹m—$´ýZ%wFW;ÐÜ7%…Ù`܉PL%¸*v‚£¤¹MåçHµ ¯¥mÊš¨1GÌ•œkX„³ÞÈO1Í"ªœÏɤÔKb 2Vh!Æ‘ÿEÿŸüOõÿOøÙwß=ûóƒr÷äý?Ï~zð#þøýƒÏ@¬øŸöß³øÇoð—¿#ôÓ]¿ûÝÝ_þZî¾·—¿Í9ês\»/¬ÝÕî
j÷Æ´{CÚ}í¾€v_<»'œÝÍî
fÿ,ûÊŠ,òØø‹Ûñ|¾‰‚BÖ„°¦M“:šçnÆèuÀiD’H¼MÖÓ„|7ÎÊϯÅl¢ììÏu¼xCw•ó¬W(´Óí%c„<˜:®ë\Ý1ÈT¹¬¼ù]ö¤ ŒYå,ÐÕ¾õYoêÁ³2ˆÞÒ[tN[`àÙÙwÓBñT5@pc¦qØ…wñîèGÙ+ŠÜQER·]†±× <Gö’ç;éfŽUL»pÀdˆXØ?†0®»æ³\Õ5‰×Š ×ç•E#¹ëãصô|”0a:ÊðÞ˜Á
öF¾[5š˜´wÖ–8%UƒQ€Ó‘<´c¸‘#,½¬ÐŽè3˨\èd[ §Ð™9¿Òð±ê¦šú±£q-Dƒ¯–`”µêL&;¦Jr··'Éךï—(AÃè\)+¦š‘ŠdÁ¤…™èf+eAèëêA»-´Ú§ñ¤â¶"¶9Ùé»FÁGâ3ÛÇ"Ë%¶ÊEàv·^¸‘9äz[ù¨Y@¸É&ÜúB—k…<Œ™îòÓ}Ü +3ûÔj&Ò—VSºdQðÒ›ˆ¢‚Â*R)lZú¬ŸÎmüME˜ÖF}§‰>ŸýB”…ìœÜëÞX +XšnÚ7¹!&–†»ÖEÔré|ón=6%µˆYžåSe´]µ£%«áôPé–Sóüo¤ðÞcß°Â#·š,gÚŒo"f4Ý‹Bºë8t`d¸Öåh÷v(ŸDäºÞG:Õ~V×dvÝ[BŠI_e“RH€n0`Zßç$‚Ièç€.þ*ÆBTé;I&í:Wqи¾úìkJ1+À/5«´Ã–È$•ðÜýâ+T)
ò*n+¢Ëù +ícá|kë?ôs¥DÐ"*3Ž¾G5U“ZHIr.e•G…¨ÏN^1ü¬IÀ¦™©÷G0)Ì?ðÇœ-")ñ>E]4J‰·ÎI÷«4˜Lþ8bŃ= „k6²¤•Ï‚#÷£×IKQ
·#Ä•½$ÃØv”b^¡bMî¬;uzuºÎ⤄ÖhzbŽ½ šÅýº½N»ý¡e0¼1ÑM|ïœjoóy°Ç k$ß)pšäÒdìéØHÅÄç~|uú¥ +ØÝR•£uÙµ&P^/>€’^Á3î +~Ï3piÉ3NêÉ3¤›“gL$zpðê)@F¹3…TX˜bßåÒU8¶Å±~”•1a¸â^Ò¨•ÂÛ™¼£ jáK 9TËÃðJ‘`Ü$”h_ð™Â<<'>ic@{d®é×>'šÍ~S±£ÆIÀØ ohB(øÛ8ýûl
F…Ô© Ø*Lt“ÐO°;sQ£µ‚i€.MêVI ÔïÎîEÓ±Um¨x6|vµ‚¿Ö“>>SšÄK*¢Oøq´û ®¤7.g¿z·jvg´Tã&¤øKƒi<ÑË™q€šô]Пfq4•q2•·š_ØC +^ÓÌb¥±«ªº¶Ôʲ†h¶#ˆÓ˜@™ÇÙ¬¢Ÿ=Š=ÝîÕ¸Sáò–¦*¹ÊB[…-‚G¬ ¼$hÑläÑÕÖý©*2³°’CX¯ýa!ºÁù¸–†á”KV#Ø‚la6N BçØA7¸f¡#¹ØÛ»„[]¡Å†¡öóv <øÂv°m/µ%¤e,8{¢1V¿èš§÷ +Kw,•h†\ؼ3µ…r=ûU’sà]z^–t
8ÊE»;|;éLSÁ¿#QÖ_ \IhÌÃ8ËKB‡i~IFËÏKŸõ +B {«l +ÎÆ©Ú¥»A|ùðèß_<ûðñÅ»ï?¾ûåýëŸnEèñ³'·/¾ùøáÝûÿÞóÃë_ß~ùÓÛ÷o¾~ýñ‡¿¿xrû‹-ùêO—|ûé×·±èoøóðÝC3(÷?¯>áøë̓Ü?¹½ú×Ããgo~ùÏÛ›í»ù ÿ{â+Ñ௾~x +:—+|ALÈ\…v:Ae}äâò)N÷¸P¾ö\9ï9ÒO;E0ù6iÁuAC‘‰ Ýv)®@e’Ü5 )6ddRà Ç 2Ø–}…CV0ÓRÏsK>„ws~0«‰~à>è6ÀÚñVäÔŽÚ{ÁA¦ô)n•&õèuõRŸÀHB9°ïqš‚ð‘QRâZh‡‚d;“°„4q +”Þ<]ÇUBŽ¬W/>`‘{‚:è ™KD'ƒeÊ¡#Oé +N’ô q6ðÞBºB(Î s8 rפÁ¥4°qó¢ÖÂq +“àšæÀz㘚¦+ÞBøUu<˜H[ùÕ6-5´ !]uãªñ£‚ÓCÒXN}æ“P÷a=%< +M=‰8Ž*_
-=I0‹<[]À˜¥S‹ê
+*vqºn +j•‚~'d¿s8EBÕ»¬h»ÉÍ ‚³‘X(2·J^.…œ©
§ºÏ´i~ÅqqĪ5C“U‚Ǹ¥–£¦;mBÛ;ÑèÐÄ´qš\3¶ÉÆc¢O]J[Uõ:´Üp¼ÐR{(XoÒq@"!.u0ßQEíbš{$4a8© ‰8Haë˲+áBi‹˜ì¦Rh2[³0”Ø).š.ù„ãǽ +¢^kSÆPözšj‘«þüÝW +r&?öYøRZ@t&êD߉VךѣxQ%ZìImh’ô[ª®&ÄÀÐ5kReÒ* +q2bíH$ì8én)"gJÏ€Ð×%</Ô°èÚ@,ù‰§ïCpAWaÀ},â8\ZF½IÛ ¥»IæÄ;&xj’PÁÚþlßî†D8ÄØ5Ê +†î„b*È÷ðÎ涆7é£ÌˆÊº“§:ÈÐùŽQbËh¡7¡9óhm”„¶iF5”“*ý/¢ÈLÀRü~L#/O¾]ZwªîͨoŲ̂ڵzÅÜσ)ÿ–Ꙋs)ãN„u)až-Ö¥€y®Xó|ñ.æWx“{^áMîùÁ»˜g9aÕg9aųœ°ây…7¹çEúÚbÙ´Ü0/Þ—…ùÞ¥3Ùx_a]zX_öI†_“eãb`/æÅÄ:Ì‹u.˜çr×rÁ;WÛ’…y>w,u¼/bçBc^¨œz‘û¿xŸ÷v2±.æs¼k±±Æãx…7¹x“‰y!±~ÙñÎs2sr¾v¾Î¹`xÑwæ㟹à˜íYè|[¨ÜZlÌ}–ÖdbNöYNXóË>Ëç³Î’Á‚3KÍHªa¤Ff£NÍÊ"/+5œXõõÜíM.4mß”I 5wÿO~ó駿ýé¡Âô7‡26_|ôøóÏÿyà`nê̘ÃYåßýû‹/þó´9#nöÞ±Çÿ}úôé“Ïn•¯ÄœtÃßž<°íÇŸüóÉ¿fcø¬zè/ÿëG³ðÙŠpz¬ã£GN”&ŽÅŠ¯?pë—'íìYÄ<Q10TûFʬr'«ìêhkAÚltC[:ïunIà/¥›VÐrûZyöka:3¦}dýþë7ªó×e˳6#e#Á¸ë÷Ï×TìÁóÎÛî×f0Â|ýу‘¾^8×.µ²ôt3·^zôɯn$Ö…Ê\óÍöû~¿º"†pêÚ7 +Š«oܬ-X—À¶HznÉ¡êíÕƒ6¬œÙg‘ôªöž¯<Ô{»%Aä"éE5Ýwoœ)-»x¯sëšHxâL½=œ^XÓ=ÖÓüVnqëÏV•§¢'+crÚ…R7Uu]©-ÎZýõýCãçÈ‘]ߎ¾¯ÉÚÖy·§¶ÈˆD²ÊúŽ“ƒ{{ûúúú¯ýà»ñJËoi-6"jüLï`?éöõ_ºuçÞ…mY‘¸kK»îw•fƒ$”²žïÒïïjê8qþÃùcB©yŒ©§d¬UV}uƒ]Ó~ æòxWip1¤x×áÊú +igE2ÅÙËÍåí#qCv
>ìÚš95 !CœûàbE奸‹ÃÙ{ÇÚ¦‹Ê¤Òo–‡”nÿPíÆ•Ó‚¹rcí;½;»Æ»v¬[•¶2ôüò…{[G¯–o(l¹}œççeC†ªò†7œ)å_É.íúðÆ÷Žú–’¸FöåLmL‘œ}ý£-ùi«òÇþôë›çÞZ–å;4Ý8yE?\pa|ìj‡k„”Ó.ïÈž|qÞ–ÐB©Ñ=ÇVðõXQÐy;wGæCœªQÜ:2|,ò&¼úE›r¨¢ óì¹ó,ˆ{Ä(ªí¹ÛIiNY• Óï0Õ¯`þf§¡(Ž¤e×^ë®Ú¤Ò1ÎYBcW[wäd¦LËÌ)(míë®yfƒGå+÷ï\n«ÜZ°±`˾–®‘»Ý5EÏnð¨Üóû?Ž¿ßÕVÛÚ90zo¸³ªðy³ +ú›†.z\éãIºøRØÈ/92µËšðŒÎi4DÄapËî_Þ£·+Bb«2CÞÂgÄÏý»v=EîHl•Y|³ü€NØ·É& +}¼ð‘ŽÍ +‘<µ¹µáe‹×h«L‚uÞruDèØJ)•d0R¡8!hámB7í&VT‹Ðbµ8Eƒ·þä’1¹ƒ¥aUÆú.hfthòåÄ]Š¢P“¶âÐ%£
Ýœx0u›ð kªfi ËÎã€HU4ÈQÔTX +âŸ+,M<
0\µR¢LC…Þfb+ÑMÁX³‰ +7ŸÜcKÓN¬j +ë`MS•¦5¹/ Vø½DE¦V½asI†!M)U4y@Û"Tùý@~ ¦P«^—îçúñH©TÍ|çVÔS%2HI›§bjÒÄ¢D>pE“Q¼½UÉ3e½ è&혃R4Z¥ÿÒúBχ°ò-’>nE ’W‚§?„+9º.¼õ\QÄ>5dú¢ƒªžV ÙÐ'JäiîÔŠlîÊK”#¹˜É¸i»+XeÎmê´yÞÉÖ|-ñ +¬½aX¸`mFÞóâ\cŠ +Zä}ÝÖUµ-‡+pܘòZ´~#Ú†šS~kã$çÙÜŒ@ŸÖÖ¢ò°öu8¸´•å‡ÞZ1rþ¶‡~,íuTÌÛY«Ná’ì¹Õ öàY¶–ôl¢Fó?ë$Pä}u+²iB_Ÿ$fµ¡.ZëB‹-<¶'½yÅ–3ÀŠƒxᱨÖúèw¾ø^æ&¿¨ÎãÈuXšUÐ"ôªÖz;ŠÝŸ,nuƒéŒ_ñ.\÷4¯Eáw}$ähC²e"òªl´=£Û}ípR‡f+àÉÛ¾àÜ|O¡ox
ƒ¸ÏFAè€3a’/Oê^-íµnkKO¡¯øñ<ü€¡;¸~Bß¾cäÀè×¾ûêÀ1ì½A5T_nÙ€@“ ª‰Ø»ŸÌï9~ƒ„ZèM
µ.i— +šO‚ðW#?ð“…1€Žå¬>ô|§ 0ôæ¡À8rhx›¬‘ÁŒŽ’i uÃcOÚ£ÏÓuƒÐga´f‘•Ù›¬JìdÕHðx™GS4wùê¹µK¥À=‰FíQ껂ÅíG8ác9ÿ˜©3T‚*óÙÁ,#
?nsÔ‚òÀúY=çÿ&Œñgà³Å(˜ÑÀõnŸgTq¹“¨ws±]Òx²V~ +s%ŧ8¿Séi’eí¼‰DÖÓ©çN2÷EEþádz'Å( +Ñ|èÛH³$@+U'š`
lØðeëüS3LûÞbñHtZEBTÜÔg´ÑHjQj7þÿ-×G³}¯ +è¨?š\¿ø3J•(óx“MžÉ}(@ÁØ"p'"üCÓøõ·±µ©–éG©e'¬Òx2ë(ÈžF9èq?‹;—¨†_F[׸«±f£Û—Nºú‚áz@3¨¬œª`€ÝºAè³0*+ùÙòâQ؉cÕ%§†ÔqçÑ1éPÂìRÏsÇ!MàžÒ çÑ]AÌÞ(W}]6‡¿2ÞQÈFRÅ +roö©Í€Y< +eˆŸëúcT?w£™Gò'k½ï
ôaר°â>ƒ&†‚ÅŒ{RCZf&àù:k³§ôO=O)¶aÚàKWEc´Û<“4MÛì=ë7óÞÓh/^n›7ÁMÅh0¡NpWqÿðL.'ŒªçܣۦΛsÛ œØ´ÿ±_m½MÃPø¯d“@¼b§iS!!uÝaƒ{©¼Äi³¥Ie;{Ø¿çÒ\Ü‘¢lhÐ=ß9>9¶¿sÎçN»™‚ϯÑ{Ѧ}Ÿ\þÆi(Çæ)£TƒI b&Z³²nTȆC7ÅmØ…äæÌ5äœ0ñ ŠÝ§² +†~(ó«ÅÃÒ[¸Š)â™ +?Sú±nPÓã"Ë›c
L\5dÇ&¡Šm[ÑJ°VÊ)ÖL°VÊ)vßñ¶Ë‰GÖðc¾–·8?» /Z~FÎiGa”7ÞôèvåI±Ü\â,°"Èeð ‰üî((O.§³™mC +§§[ýOØÿI'´£„qѳBP6;Ð"kªø>èTÞ"ê ¿ôèJú7' —ל>¦¸q^y`ÙγڞÜð8L„ÍaýxbÙBÏi°XŠZ„–¾Upã:.ÐiÌ"Ê.ˆ$¼û,Ý]rý±Ly–>Ç„lùÄ’¯2[þÚöìþ{*‰’‘û#÷c¶j»´h'Ä]*™]YÜÝ¥îÅJ‡ø^Äk|[Å÷ô|îˆ6zUAe
DÉj¯*ÁbB¬ƒÈ‰ƒ¨`vöIø¢¯ò¹Qe± ‚ÎéD W™R‚“<AèÛÅàwI9žTLj³Œ†É¡Ì¥¤écQe¸«íá®”áõCh;ßUà“Dw蘴%“¸wð_ÅAÁ†ÜéuéÒ‰omS7c#Oæ~yÒï_hëØÉzÆV:¥Ÿ_rï}_èûBß³¸Ââá#4Fº1áí\F[™ŒŒÝ©Œ¶¹íé˜ü=8”|Ú7û]&æcÝ^·ðæñ³où/ å÷_çÿT‘ò}N…µ¶l]jͦ¸Ïº½Ûe–„Ž}.òÂÌk8V)€óJ½Äz'Š^‹²hí`í:Å;Øغö¨¢òáð¡Hë]ˆÜð8L-%eÉ +üE
b4œ”(Ú´”À[dµÿûmÿ` +H‰ÜWkoÛ¸½@ÿÀ"6Ø’c;Iƒ~°ó覵¯íÞ¦( +ƒ’[JÔ¥(§î¯¿CJÔÃïík½A]‘sfxÎ̈¤*·¥h$” x„W‘ õd€Žúµç˜/Ð+T¿gbHÆ]âJ£tO¼_׃a*o=ò7=.`ðþ„©ÔD}ô鳉\˜}*Ü£Ë't‰TÉBVa`ÒeŒ¢ÊûˆÜÌIðàºrú)ZèÒ@õæBN}¬wqä9©˜WF¥KIà~‡^åŸÞOb®-wߨ¨ß|!N,)(ƒò5êïö¤•÷A€}`tÖªEx”Ü:À{N4¦~ÅüÅ›‹¿ša/Ø.~Õi$°ó¼Ýi)Ç#ÁÙ3ÙêÒPÏô§Ø]iO`5ï +QlB˜
ù_ÖrÅ7öÄ[è{õr¿ÿMZVÕq™MÐ5g!Í°Ë^ªŠ[®ªR²Õ4žzXIÚ¤Kö±'èÎê[¨>$˜ÏÿZ-NÌ éÄ] PŦ[Ø4ær⬥g\ÌŸU:jù±“º%) +±§Ü~Æö9”9;wölÆŸ°y5þ†ÍëÌ☊υ½käù!Íö®<Õ¦Jõ0Kü»½o¥‰Àž' +dŽ:w“›Àíp¡–‹äLKÎLîY0à€ö‚éÉI2Ý%пL‰D‘Ø*²ü«|‚5K½Ÿe-#cevà´T>Ì<A +“²— ³°Ç‘µs9}n¡US[šÚkMMij®55¤©±d²T.Á0„鿶L}”¿ÚS¾#ɵŽP„™¯åÙ“ÂZÉ? j™Ô’ÁZy¬F+ýQL²X
«‘Æݦæô† «×ç%R ß\â¤m³À +ra–YiX³È«Ö@æ-k˜91+
e&ÕŠ Øe¶úy •ªÓ³%™¥H˜ì¿>=«ijY"2n)°eª„hjgåÀF±!#ZZ±iÚ8¦Dç¿XÌÓ•jfXI®\ÒÆRM3¨ªW¡¬ÖJ]3¨$X,®^h5ÿñ¼UZô…NcB²]Îc†U<û¯Ûy.ÕêÍR23´ÊØRhÊ´œÐ¬È‚¼,©ªSdçk̉êÅbæµ½m»/§¤íÍRÛå,´½‘åU—ó#¡T•úU.ÕoÅX(ØŠX¢‚±kÃm,Ù–Žõæ ·&i&&êN!N·k,A“zƒëÎd0e:F +ˆ†Øõ0ÝsGF¡>ë6†Ð Ìk¿-EnuËP네Íq³ ;¢1\u<.£8ˆˆØb
q*}âBF8¼Ph÷¨¨Z +’ü}*DüŒÂ
~2à`Á±ï¹ûEÉÁÒõw…Ã|{/ßZ:¿ƒ)•ßÜ÷Ô°ì"Ã$ïÉèýb”ð2ÀöYüDñ”ìå_„ƒû ›]ô ¿%¯Æ}˜\4”E_5zp-(^’îsåøªÿñ]U®—ÜÆ8$UõíÅQ7æ‘Â}ÄØ6äØáðHè¿Èq–qÈçjzN½aLbÙìq8úêmT¬“P ‡XDž«’F0;˜á@àÈÇSÆ=G2Œ#á9h@`‰å«“Ò³óö´&/–™˜FßfT†ùWJwG‡¤ƒ2É犳(B¿cáÌ`tKáºÁ¹”,ç!,ðOè÷¼éLØ1µá¹:¹%>V’kà5¶Õ3ì¨J W™$ÿ™‡«ro#„:3¸þ£Žã(ª¦tA`‘ì·ˆ>Ÿ$_Co8‹Ã»à‰éÏ©’ÈÜ3ÅuyÍоCª¨HMIi„Kw½u
ÀnŽŸú¦Ø#Gñ[¹lÊ¢¯ñغ¦ԛrÎ ×,³ÆžµÉwë‚Ðh°od1Ô0û{2T—x‘Ÿç 0“?ïê(”üÜŠ²0É0çÔóæzr{0Øn÷¥U³7ÁœPDf3>2?`n ++Ù=Á¾ÅêkF—ù!‹å'êLî_h¹åÅI÷ÊkæÄ> Ä5Ø8šÔõ¾ +å¨ðy+ÇýÞ=s‰T~sS,|WÊSö…ÊŸ +8ãN£øéú㎘Dþ7—9±O‚´“ÛÖi+Í_çîa.l†¹‹FGâú¶µ9åDSÚ‰µ³âµÍÆùfh#'°Y °k¯œ¬«°rwI»à8ˆB©w°œç¢Èûš,ÉK-! £S,k“@NÍìô‘ ‡X„±@C ½¯ +ˆ†$b4Î}¬VɧO¢Ùå~Ð… +º`—«‹\Gà3¹ \ív6ŽŽ`FùK{kÒ…– +zxA¸a¡äÏ„?ù{v,Ø£Zr¶g…EVõ‚äìkÎÒ÷w× <ÁN +ÿ²z{j‘ß+Êá?ïÚcÛ3Ù_Ÿ«ÕÃט@øl¿´°ýP¹¯bë1ç“'‡_ÖÏ®¡ŠC┘zMäXZ¿¬®ŽNOÎm¡Ý~©V¯¡ÅlwŽoí׉˜ó?¯šoY|ys¹VÛ.vÇ•¬WêÝ«4B‰Tˆr¦úp£QRn!CÛXã§pg7ÕÀ<‚:³MÎ3-f§8p3‹¡XŸÎ¦éϳ]©>9;éÆ×™®öË|WûäzØl—{>ßåºÕj¦{ý:£ž_`ªžíb/fºX/ä¼^ÛÎ|¹?Û-æ¯W·´³†ÒÖÕ(!}ö@èºyWNûnÆ0_Q{4‹ïo®O/.Ût&MÏr¯Ø)`<}{1Û½~˜å^§W×óÞëÉ|þš57ŸÎ”›WWŸaK˜íZ?Î'ã¬îz6».î6
&®PÂ~‹‡›Î<F±ÂÀ6Æ2žù +ÙËY<÷ûÍÉå|EìÕ¼i¶{íþÎÝü·¿K7DxÈNÒBsŸ
¡_HAðg<óeÃÿfqâÛËÃwn)˜à½—•éÿtÓ^,@öî⻯#iÓ‹¢ˆjNmÁδ –ˆegoX€‚aó>¸zùÑãqýîòðø=\½fÚï¯M»~öN´¿›wÖÑ_˜P)µœYMâ¯íΫ5FÃ8¶ÖÔpd”E¦ âLˆº9šÈÝLåvïæ·ÀÝ4Õ?¾{xyýøýÑõû‹‡—_ëï-”ýý_õêáñÅ›“ƒ‡ÏÌqýõìä zPÿÓž‚NŒ(S¬ûkå¡ áé M€ÚÈVgX€‚álò0š~-‘Çú‹cZ6`Æ@¶>.OÎÝLåvï\Pž6/¨Œ²‡j…Ñ:JE©âÐÞì!¼Ó§,ó ¥P;˜˜ÂÚ’\)RGSM‘aX•ÎCô•GöÉ3º™Hísm9mÚ¤0.”M¶@¹ÊÆ”¯Y¾²ùd2¬€³1eÈeˆDJð6T±I/+Ã0œ7„—΃2жÖ4Uæêf"uÿ쥅±Õ +#¥5ñIc”‡6²õW—¹ï%
ÇHs-}¤FWJ’ÆЮÎÛ¤!N–4·P7©Ý»ÔFj$03µ6n׳‰ÊÁP{¥`VgW¼彤ќ%hRD:â$iìqBŠÇAM1K•gn&2ûW/§L;•JÌãr¦ì„ŽË™ƒ|ñòvCd9“r†!È@¨“%Ê@´c¢dé<ôqAÅ8Ïrfœº™H½x9aÊ]$L4DƲôˆ×À
€½šL͈†?P[ÙK¾•Œf˜hÑvºÍÏCÚèÌ'çQêfuxårª(ɪU¤J»ñyØ!^g4 +‚"†!Á™‰¼ë/€3 ++5»ôbnâ¡ÌC¾Šy»!2oak‡T“|+‘†*e˜ŸJÇ™*ŽuQ™q3‘xù¢ÖŽ[XFII,•ŽCß!!?œÕÙ›0rÊœÜAܺ§·‰ÇdL`§ËùqØ,•—CO§äqâf +q”Y‹)ÒZ4Ê“nà¹C|N8£°7GÔ= 8å`nÒ»$z@¡L
ŽC»=CF0’ê1NÜL Xpkç,hçVYE"9ä0PMí¶¸ñVC$XÝGpˆ ZÆŽèyAÜ›ïQFúLqâfq䃥‰*VºzÐ|;iëSo4 +Èa4t>iÆò#'n&GXpî" +I¦âý±Ý<ŠC|}òVCdî’eTz›DWNÅð¸±ë€„9&INÜL!æË—¬t+áí$LbÍA› +¹08.,œ X)xÖÙÇ™›)ÌÁ‹õ’6/8º‚CBçpVCdž^«(†ÝÀEht›DÞ„Çu›)Asˆqâf +1ÿ½$D<¸Ý·÷ í`)%<8J‹,ï#„#hÒ‰{Þ´§ÀˆJÏw-^k»IÖSƉ› ıÔE‹nG
]žHjwÕ¸Í{(´uoX€ööz‚'u «€°Hœ^)Ii›2x5?ωÍmÛ½ÖYo§n¦P³oÒðí²K©ŽµÜÕ&q±‡"!œašKÛm™¶ÿÓn¤ñ•mœ'dxÞp«$HS]ZUÊÔÍêÈç®5È^Ó¥tmÔXã’Ä `x/mÐ(,z‡$WJ´áv·rx¾É¤ÝZ²´¹…¹™Ä<±œ4TÓ.ã»j›(Ó!‘0}‰µÍÞÛ•0oy/eW0èB€ú4p=!UÆ@ü*hÙùöÆkhä€j–Ë·p7¸ÛóþåË©ƒë_+Üûç§Êy ×»¯F¢þ\Ù»S"C’R+ŸG|5ƒ”—J©Ò[mªý8Á튦0V~äˆ÷%Ç]@z«˜)ÜÉ#þÒ Î
-!Ñ”è×EÏäogœ™1ùMÏ3%»_WõÛU/!PÄÕ(:vz€‚©›†‡HÌåûQàkQœ¡˜ÍOmdi+hŠØ¨VH›Î-h«v€ºm +æU²çU?ÞùUqfFÏÊÊßy)öº¶¯"6û$ßó¬îò,ßÞ‡õÄÞ…abÏ#>ÖrëM6.(÷¦ˆ§ƒ9TïK¦Ÿî^|ñ5…tr
!ä“?ìª/A¡‘äLN)ƒ°2rϳßïYÖî¹y +º·r<¿ÓÃÜø]xØÿ™¯ÚÞ8n#üô®(\$@µ^¾-IEëEÓ"èµ ü‚8ý"\¬‹-À'§K\ÿû¹äpørÚµäƒóA†ô˜3ËáÌ<óL)Ãw…Š›¢(¬Jz˜›ÐLÌöì>MÒO'q™+VBIQV
«õ‚dLh3×g¯î®%×{Y¬{A„cDÙKëõ±5‡ø !ÉNÚ~Nküt¯üv$LR¹%í’(¬ rǶ5½¢ïçê↑?\)ŠDáJÃdÌ”ƒÈR» ULˉQ½aålp?>087…¤ Áù]JÅÙûÕŽbá|]+v3ê/Î2Ó‰ÁÎÉÄ—ãëWÏÐì_g=,;&_ТÓ}ïªÞq#Ò’Á[H°rË_Ô¾É"hÅH!žPšyqÇÊ9KÉQYµ `è½EªNKÞ#Œ%Þ„ÈÝ0}É[‚ДÞõRÛ&D#æc|4)BÞ”¥Ó@‚™ó…$—^-AøD™Ë6D^MˆÄ°äf Â{`_¶ r7 +UÏŽb^ÀI|¤ÁÖ˜ÎÓó¦tiKN(òÉ…‰=çu'ÁPeÖ~ÖÕ5
^‰úý +}aõœ¨^/n×ï]„ZMƯ>‚jg\+•BÐ+ƒÍª#BDœDÔǟáƒ_½¼¾Þ춗«7ûÍåÕöú°_ûïŸ[ÿÖ!÷ïø.yÎü$¶ðUã41ƒ®Î¾öjwvÞÃü~nŒ.ÕCÏ—nvñ¡Ÿïøz¡ïq‘o'xþ|ãÙ£‹ÇO÷‡¿_½>\Ý\oöWOTüýÍꫧ—7?o/ž~g/àøóÃÇwÛ‹tèëÕŸÝ)ø) 6¼’ÏÜĶéÍåiÐ4-ˆ°G«@°Úmâ#(öACEæ|!”hW#ÉìH‰°~ª‘v‰Ä,‚aš¹¤€ØÌ/D*$Ô€Ûuëóbc@g[=UÈ×ãB×!ìÓ•G®/üû
ížÕM‘…Ìe3Õ^8KHÒ앧ÀY0îã¬ÓU@| oΰ!#dÔì*ÚçÉmNù¨†S$ŒsñïTí•—ðfÀLÚ‚GžëñÅú××W—›ç›ëÛóßo?ì¯Ûý +©“ªîœuƒì‡ÕWi/6g_ݾ~ûÄý<~üëívÿäýæööooonOü~sxû‡íÿû
ùõ×€ùmóî¯ñ—?Å_þøË~“¥ëÍZôþC‡sè7éÌÑÐNÆБ’¾`U#ŸÞ€äȾ…d‡LßCK˜åÅyÃW¦‡†îرiSz—x&1žLÜ‹kC²â‹—êÆ +%e¡ÔáÑ®Ff¥þ§oƒð¶ôcù2ب«Î¨
fy”ú\‹\“(O—>À'zªš¸Úq¿’ !BûÁ®Ffgô²%ˆRr(W'VX~¡l ƒr†™ÊZçám‡¡ÓX'ë—;\]Ÿ~XçTíÀD=Hó-Œ&ÅO› +3C>MH•}+/ +>tVÍらNö¢½56ÿJ_;ÕF€)o½lBŠƒ©N +1ÐÙØÛNj ³1BdFÃ4;?¹<@5eËu‹5²vh熊ɞDžžq˜~ÁÁ÷öyö¹«S0åNè&ï*WL½•”=ò誰]̲ÇBñ4À–©¦¤ˆüB™xb¼Óš©ÖyÈ°ì;iŒ(ÄÓq×ãBבoN'j}¹¦û†e ®É¾¡”†hW#Ÿ+1NJJáþÓØüB41. hò¼õÞ`
UÛv=.q℉‰ôë7TØBb¤“[Z'Èo¦LRÞn@ÉðA©QàFÇ×n,¿YÇô +†ÞݷЃÄR”6.d~t +éÙ%Mô°²’‰·°rñ¾ƒswo\ÿ^¸qµÃ«ÖÉ)i“±KnŒ¥õq&•$©a‰T|3!ýpŸ°ŠêŠjÚ•ù§3ºFî¨J’í¤„1wwHφÔÜpØÐ;ñ†Pê2Ð:A¤Ê£aJ†¢L óÅI_·°üJiJÓY&Yë<Ðæ +sh–\uB‹æÒU;9Ew²RÚ_ÔLŸâ>зª—¡u‚¦ÑI24l@Éð³eDù×ò¤Àk„6MéêÞ*{<1¥ïq©ïç Óãš@[F[EôŒ,A[%B¤5‚]Ì/<lÑÂ#:ãä1<7ù…舰&2exëü¹áJx†É÷ãžÇ…žO¾ïL
¡â&Æ=…96bˆ¬™êJa)Û‚’݃òÂø¦œ8DdÊÒ"Ýlv¶:YÑ +^T“û¸ãq™ãó©³"©¤šZÃRI…PšzhØ€æ%Õ¢¼$!3U©mK*!Üá¬uÞõ‹ì”‘Ç$Uåy\èùw ©‡ùMQ¦#ÝâEAÏšP0t¢*J…ä‹h‡ÙVÌ<ÅK ÔKê:´lAG¤ÞäF‹z±qB/4I$@Ðaé 9í?Aá +†ÎÛË3n~[==ýN]|{}¹Þ|ÜîÏÏÏ=úaófûb¿¹z·ÝŸ½¹Ýü¶]m®¯o›Ãö=ü°õööp³ß®nßÞ|p˜Äã}ûý?Îþ/À +H‰ìWýSSWöð—팿8»û“[@\wm«@Ñ]· ?@XD$|IHHE>(‚|hATŠËâGk5PEªk´Î2»m׎caxË™ãM¸YëòÎ3gÞóä9ï=÷Í=ï}ïä¤ó-77÷DµoÃÃÃy…¼ÜüL«ÕFEE-Øå’’’,'¸ÖB>Z¶vÅò·öª&§Àpô^¼ÚÙЮýßÂÜYœª6/ªq +¯þœ$‹<Nƒg/\äÑ—sº@ž! í¢!U1›öçš°ïKsçƒ|—4¥¦lùÙ0'=Ä +zŒŒ©:;ÉNüø@Ш9ªØ»2µìðõÞAŒŒiihëë8=|DöR\îjûϳ@Uêú&à>[Œí–‹ƒC²TužÑüÅo›ÞÒƒŽH¥RMqŠ4i²)«€Ôæk?éìþ(ZÙ´P©àäîW¯X±"Eª$æÒôO a±ÆV@‚åX‚˜‡4*ms†JÀٲ탕ôÖ]kâÁHL]’E + +jUˆ_¢S—7¥+Ó£0&¤D*eªlÕ‘XI\h`˜2& Χí½Å% 1=–V½;(ÔÇÇçоÌ=~{ ¾[1§U|1pP¦&ÒkõŸŠóK–-[¶'À/-.¼ IŠÑt88€<,ˆÈ§cÃÿzöÜow˜4U]ÜÔºÚëºþd[×'!áÉYÕgÚ¼¼××7›{zz¢¦íò¹^w7÷’Œ + + +t:F“Îøý½'<À৪¢2ž¼Ðj‰Q ®8Vßn8MjÚGOïð0˜êÁ_èð@f–zŸ<V@êMuÐ÷œ·Õ«3Ó +°)S²†ì'ž[ŽUå%G¿»wᜱàÂóïÿwʇ†iã™®îî†ëÆ|b‚ðc££<9`µ‚Ä(§|,™ +ØÝ5ñ¢øѱQÌìígéS{Ã(d¬Ö‰í§å3fµÚ—Ùò´prâÇ—cÚ–.]Ê:í´á+lÆ
ä£J`ŠÞ ¾ÝŠ7¬â5ø”£UBéû£¿¿ÿl-ÅO±„–#ÖÂ߉m4î…ÝÛ¤ƒ¡P…˜‘= +BÑøtÄT<”|@,A@Lá dÑýÂÇæ!{é]óù§?‘^XK‘ioàé/Å_bÁ˜4câ;%ûÁNÀÐnéfÏÿòåËÑŠ°{$£;EL–.zÒæºÏ³áis\ïx~æmÞÁù…xZætGÌðœój¶úCgŠX"¡è¡"›¡gbÅ?Mˆƒ'
2šÚÖŠF¾àÔÓµ÷È6Æî‚Ť-±í3¿dÎð7– +‘úCeœþ–XA(>ÿˆIS>9¬þƒÏ?4|þmë…lr¶SÏoŒ¿(“þSâ)»îkeÎ*óˆãÄúãÜRöZžXþ©¦SÃ×1:àTÜÄó€’olë–É×z“²g˜ÎÎ)žvZ¯?TiC_Qêœb|+ÅŠ«É”¾ÿ
ßÛÖŠ’u ,íTŽXùäßñúCõE¦Z÷ªr·€æ”ú㔦å
3¾þØíXïDÇAäŒÿbš´WX'Æ·dvCÍ»þ°žWd7âÔ„9ÙøŒÙö?T™QUØ‘ç¿×DBMÚ«?”%V¨à8²1Çë]#}¯½¶}ÑØbÿ㈠žgª0tÞéóßqô«H(ñúCÕ€âü"š“0>iÈ-j´
þÓ†ŸXDÀÁP“³ñt°W{{¿(sî1ƒ‹†mÕ´±©àû‹ºbø—²ÝP‚RÆ÷*üÿ6×ëÍ3Aý|±œ Q¡¢-ÒÿPIaSVˆç#°ff®-Ö¢9n‹9tįTÖÿÐc?§P‚÷©ƒõ#k±p +ºfÌ9·÷ÚŸÖÿPNœ9…¢Þ†M¬?¬G…€ªÐÿmýYìmÑMÜþ+À +H‰ÌWYsÓJþúz„‡‘{_ª¨T‘.sÉLŠør—"ȉAv[a¹¿~Z-©û´ºÈ!TMQ(ö'Ÿýô×çÌŽo.oWÕº9¯šÛÏù³g³³òªú÷fyµ\çåÿZkMòw³7·uµ^¢åïŽÏ²Ùëò{µi…ç˦®ò'Ýwütöv¹]¾7H³¹òÙÙ¦ú²¬¾ößN>,›¾\®›êCÿíx¹Z™/‹²ÞšoG7õÍ&¿ ˆ–S"…Êç”—uÙtæúßÎ7åzû¹ÜTëËï=vppx|”µþ¶ÿ>™OË<›½©ŒèòKe•/WU³Y^æ›eÆ°(„f”™ß®¬TmžãB%¹ùæ‘F[‰ël‘Í^ÏÊæz^}kÚ䜖ÆÄ·ü[7pN +E™¹ÐcJRÍÛjÓ,/Ë:p\ñ1„˜XyÈÉÖY¤ÎCÃß:ÖÕzºÎfgW‹üðôÈ~xq³Y•ÖãçõòjÝv‡ñ¶«ôÑu¹ñïÏ—W¹ê_ÍË÷@ò¶¹9^n›r}YåT´%¼µjfçÍÍçm~q1{]-šœ‰³KŠuþ®ë&Ô§çSv87Zçíçù"SV>k¤ „š0ç«ìÉó§óÙ“ÿ4í#‚;\H‰…4bóÙ“ýÍ¥}^ÛgeŸ+ðÙþædžô©°9¹Ó
n>Fˆuãð‘ÜÈíó³}–ö¹±Ï¸g]¼»ÁŒÑ‚0*xßæðìh1ÂÚhFÕÊô˜Çœt%TBp®G–Ì*PoËkÔ·y}ÝÆ +®0g}ã$c0éßÂÔ5>Õ“*ë}°µ|8PÛfûzvµwÔ|Uxª›Œ‚Tý«K•¡HÎw¦ +!Šñ$Ÿã,#Ïðyó +.Éã >ÝúÛ8Qá%èÌ…÷u?Ÿ,k>Lð feòñˆ>Y&|zñÃyú‘³ë“<½ÜóìnÁókÔUö÷ÆrßàÍ/?\µGÉ zÕŠ£‚Ò»&ÆÆ“ +ÌÊžC^bLÛATÆ©BpFÁŒç 0Í%ç¾Ô8é›ÂS@;œð~»£”äòß1™÷ ›NHÞx0Úý>ÁøÏ„¼SÁ ÷ÇÏrêZß»ÌnvÍ/”àdgó#Å‘úÿ¹æã°ãô0lBåŠÓã pPR'*qP"mSÎŽ×
Î_}Öãt÷rD¤Ëú"Ê÷Uû$æD
*î^Mº)›¦Ú¬óËm>;CùöÒøÿòåWÛ̬…”©\’‚(†-Ù Q`ë¥k +;ÙßQÒš6·Š +‡Õ +:ó"0o²@”‰—¬À’K$Þ Ç^< F‰—qnè”0†ÛÂæ"0=`Àˆ“Ma‘]Ø•¼ÀØ°<´kùNË@ÖaÀ†“Ma‘]ØÕ¦˜(
íjZJԬÀ
'›Â"»8ä3LU¡´bp +6w!šiÀüá…“à˜UpÈfؽÖhšË~æ¦8á$™;0RJ˜o¢ˆ™{uÐÚ¹õÂI0ÎøˆÐÚ–E\ƒ¨ )„TAЂt=H¦°(âÉÄ0×€€»CôÈ +‹ƒ
9¬ëWÖs4àÍ:È›µ4‰ƒ×^2…ÑÞ¬ˆÍ-AŽG'‘D¶Ë§“LaQŽCÒÂX‚†Me"¥AS9dÔɦ°8ËjtˆƒÛ]И˜¡iÜ°wñèlÁx(Ñ)ë°Ä6BFƒB9tM@Q‰É˜¸œÓöã°³zÄ7Vw2Å‚ÈÒ¨—î
hKñB¥9¤Žx‚îòd('œ£9,¤Ž ‰Lwt«i@Ö8á$™¦qÔ
u½eÌ(á°`]Iµ‹¯•Ma^¶·ËFLÝ°2Ó{WDBËM÷e¥Âx+ŸF‚Ö>ê—§_ÍÞÁó¯9ËOÍ÷æÿ?ó‹w(ÿ·žaÇã™KAˆù°˜O±erÞdëì)´ åZ:\œ•$6ÈáxÔùrüƒ>zÎôwò(Õ;0¨Ñ³¡×2dߪ–
•;@¨³£9…ƒÀ§•žnP Ñ) q÷9+´zŽ +L¸NQ4L˜nLbƒlС@£Ç&Ýf>ß6@! +bŒõM[µ£2sW÷Ëý⣙wÄÝ]Ózp¾½¾V +ê”ÖÉ0W¥àŠtÄÏ6¿?<íÍÈîþªàˆ+Ñ_ÒœÚ +äõìñ8òÖm€Y<jÅC%`±jÝÂÜyR[K&‰V.Q óÞl€,‰èœ&i%bQ^²ØAV¹ \O–Ð¥%¤Ú +I1JÝP¸é»³·áŹz‘4d%”z¯²£ië€c–9–"»Yù²"æÅzA²ƒQº‡áèØ‘
õpŒ_æUGš‰%”f +BÙj¬ŠL¬Æþ~ Ü ÞR¬8Ý|]UÛúþ¹‡å,¥pvFúKÝ÷Á,<ÐÅròJ/¶Èåo¤"+O6DRßÑW5üøîýÃþ:Úýr÷îö~sÿ6¿xñíåÙÅ7w×åÿ«wßœûôWJ‚LIðÑQŽ:ˆØâD¢;!ü0óœXðÕëßÎ>}ÓBw-ôYÖ%OÒu1ý1dÄM=ñ¡µþzèÅôÕPTßéz²~n!Ù:Ô‰ê‡ê¸Ý1¢ãÐGCéøkH}'i×ÿ‡Ô×ç-t?äÐ~ÈÔ/ZèÏ!õ»ú²uè¿jê'§FUþk¦~ÜBoOíŽÝ>סNpžEÇù”œ,œ6ŠòD7 X'¡‰¢k=Kö°Š#^Â
ÁèiÐçüL^
kÐhC$3BIh–ëa¼4š®(qAˆž¹Azi‰Ä-µ
ÒÂãx( ; +:a°¶AÆ"·K †ñÏ +é§Ù/!Q@ +°20˜¯k‰¥Q΃ª\Ò!ÀÓYaô6ÈõÁ)E´èi±~ÀqjÎ;.Vâ:–[íÈ-º‘ÜêÅ%œ”ˆÑK¶ÕX0”m<XÆOIˆ›Ÿ\_‹™œ +3óòð|XX§âã^L ižs”Á„vžˆ¦¡RMJ8+)«ŸÁ«¼å×$‚´#P¼Ö—Þ¥šùP´ðRd>i¨½ð“¨Ñ©¢]°šQG*.ê‡=:OðÄ䯧ÁR@:FZÝ$aëè|Æ•iæIKé«!qU‰h€€§îl<F>‚1¤Â UáwÌâ±±YCôy®ã»°û1)ò‚M¡²èdñ.XWæ ? Ó°'U\êƒt ‚oŠû†‘£¾)˜×ÐLaƨ<ì5¸åYù€xÖ“Jo +e6ÀÊ3Í=›Í¤ŒÚ¡b™œ5–vYÁh;¢pìóò´,¾Ø‘I/*R¶Ê®NîTp^,Š—bõF +÷`Í
ŠwÁvèYþz4’×ʼ̓RÞó‰‹ }'K÷°öi×:³÷‚ø¶^pGÀ¸UfXÑtA"Έaq#2<c™ÃÊnY<’’’™¢u;]ó¬ÔHµ³â´¶šÛJˆAQгø +H‰ÜV{|MWþöÙçÞÜM$¤‘œÜH„ j¨”ÐP´Ú (ŠD$‚„ $õ~Õ«f”JJ=2hŽRñ~F=Æ㊡¥¦Á`F¦Ó{Ï|÷D=iÿŸ³~;9{ïµ×ZßZß^çB +PD[ÝDœaë +·J™i6üzÑol$#žø}Ê°Pƒc/û}eåqåCËÓ¿¿¨Œuööûö{ö»ö;ö[ökö+öËösöãö£Ï²‘ÃÉÇã¿ÇHËVË–u–Õ–U–<²sëŽâ}Ú„êVŒÊá)¼o±ð«dßò[§Ù+äÿ›ÑN„dµ9>¦Cvµ5|XßK7ÿ£õ›Ü‡ã?ÇøÕ,ûͼA¯äy^Ë«˜)±æXZ
QKëàöJÍçPÆ¢'ÿnáÈ=œ+¢sàÃS›D3Uˆb5˜YuÃ8 ¯Ç6žK@‰pá|ó^<óHœeZöza7ª²~9Ø%¦sß¡ïÓ÷ñNJޡ̆© ¬Iu/ˆPÑKÀµHF“…m"TÍu„é*,ÔHC±«gë‡Ù!jÑw1³o㉶Š§,ÔÃy¦5-÷€ÓÚÌ%«rÈ—|ê•P¾ÃD™bvì¡Ï`ÔCô¥f*Ù¶‘U.K•eÊ
™¢©;ÔÇ
})£ðAur
O¥dëC<"†.¢È2OÓ;èúT}š¾TÿU·Àž´Þý0€LžËNR ?á/‰CÒ[ž–ÿRýÔ$u´ýšÃSïOcF}Ј±G£-ãïÌèGPrÉó³†?wž~_Ì»)ûOÖµžK"ÒVO5W͵ÿÇ‘¯§°ƒyó|?ÞdZÉdf1Š,g]Kœy{ +ˆæ,nEW±[ £¿³fÊÍòü¯ê›ï9Â1Žõ`½«Þ]/4îÊCÐƈ¯bÑÛ¸Ãy{Fò®dó®Î3ª¾œ7ûszqÊvæuvÑßÞÐÓ¼3·xˆÈM¼BTîì%Þ"€Í(mEŒèM,FQæPVSò‰úkqI\eâ¡Áï +œ›Ùöe±ô<»ƒa¨h,¢ˆ©¿`ýEªg©h·8+®ÏDT¦¨ŠIq¡(‰ë5%†¨&)“•)Dµ–²QÙFÙ£ìWŽQlJ¹¬&)‘r”Ì–“ä2Y(wʽòº|¨ÎRW:Ù«Þ6Á4Þ´ÛTfö4/wéî’鲌ù!Ï÷ë ü%*È=ÞY}º0±/¢Î
ؤ"}E+Äá²(E„x$2·ˆv.™à‡l“éèÆNÙuØ«g°Ö)X)«È:Ò•¹š/á¥ØÙE ›ðà÷¢
¶:ú±—,r©ÉýwÄü[žÁ™I¦f_Î…Ìê~JØkæâ¾hÝ´idDãðF
ÃÔ\7Ȩø×ñ«íëSëUïš5¼<«{¸¿RÍjW‹‹Ùù›] aû qZAH\Ô±c#ç<(žñÏ,Äh\êð¼Ng¨iÏkFS3éÍè +Íè'šÂC‹BT£†Zû àxLV$útý€ïób‚zkeÆ{ã]
1&Õ8 ä }ä@Äií:ŒIþßÕÛÔuÆϹOÛ‰ë·;Ž';M‚qB2+¹%qR;
,fq!Ìy©‘4AGÚµi·¸ª¯+]%þàÑõ:¼:â1u‚Ië6ª¦ˆSƒ¦
Òi`³ïÜ$¨ü3ëžãïœïÜû}ç{ü¾säH²¾—ÎÓµˆ-Cºª +¯Šñl"ÛA“žtࢼ7# þ¤?PìÛУÐ} "ÃèWÚÅV¥}|ÆQÈàcÝ=Š¶%ƒQwÏ4Š=žHG'Z[°R¦#²¼ëéåUŽ®/è#Fözˆ‚]=ªn°;ªA<™#˜ßÊ!3ÉÍE+®‡åÍIpC‘¬ ®Þ©¢˜4ýø‹xäîÑ«4;ÅD_«+mAr×ΓQÉ}šSHÆy¦
D¾þ›ÄОJ©Ë Z/Ä(8_ñx@“Q¡|+H7´É+`ülµ ,“”…FbbÖ'ˆù‹³_==Ó·0Ãù„ˆ$žðiÅïW*+Ið-à4ЬI‡ª;”qLð(`2´¦^J4VƒÉ½^â¿ÉŒ„úa L¬í™{P¿s +IÕþ„B% çâ"Ç'œ‰EΓד"ê)õRcU4åOžÁfŽ7*ØöØCóü6È%Yn=mrRîË<žè=‚(§;:ä±HrQóÌã‹“NEÚ›ÆUˆ"Ÿ,#ˆGi–É`ÇâøO°ƒ +OçÚIKån§rMøR®‰Z‡—ÐÄóŽÇ·™?À YDÕ¨}*íƒö£¥xiYy`y ª‹8Z뵛˶[¶[æ}P~ÝvÝnb
´Éb°˜Ê4e†2SÅÒ!dZQqÐvÚrÙpÙôG“`×U”a†jl/ƒêÜn$ÔhéžYOç©L¤W™bCð¥136GÍn}ƒ¢×»y$+îpeÓiþ[ÂÍYxþô©ÑÔP=ÝŽêÞY‡Ñdo
5Ï6‡›Étõâ”P[ƒzq¯·‚KËËCËëêÕ>¸*ÇÛÌMT¨"ÈQ¼Íî+/¯à8«Åf.£ëêBËËÅRÒ—b‡o¿ûûWå-¶ó»ÇÏËo»]Qj¡~£{¡ãpnõƒ-ÃCÝ»7¼¶·¯cyÃó‰×^ýóÄþºáÀöe?ý8%oýõ๹Æ'Ÿ{^ÉýmdlôÚÉ{»—¿½~ãñ$ÝØ×ÞÖµ±©²¹#w‹Ù
·=+åÓÇÈ‚1d]_—ôàLÑŠ±t’îRz¯Ã_¸J˜îÂ^£2¯aûŒÿ‡ÂH,bf÷£á£ô{œÿ?'øoÃ÷§!æSàW-Úpú]öKÑ<*队°,æ8Vƒ3ø§µ(Nÿçñ!Ä!ÿ”¡ð¡³ 4QÝ9ü&¾9Ÿu½÷!Ýf„°švÍÄàÙ,±t0ä5²!ŸÑkÆ“¹íÔ<™b^OŒOÿ÷Ãhñ„Ø× …½-Êå®)Š8#®ng·k;ètmunué54.Ôèµ.gÐîØbWGOºQ¾;ƒ-’ÀÝü®[bÃïÁ7£%çð«8¹¨Ø +>¡_°ªðy˜@'èTÛ
b¥6òÿè¤'RrT)¸µIKiDÃåÒGÏâè”î}QxßT Ññ|ž&Ï´¿ +ÖÛhª‡d¦p@˜¢÷`ÔÏ*%%…æ|€I_+5šâsÔ{ùß߬X*fdIP/uB
ƒÝ¡7ìùI¬w$IÈ@Äôz¡ÀÐ +Ù,˜NpRl5¤”68©kHpSHR3¡¤u’Ó2ÐL¦iJú +ÿ*ŒsãeBë&Ik'sÃTO-¬K{«D'Î΀:‚Uxp +ï‘™X7PyNo?6ÉsaB³ÊIÑs"XKU•Û¹sÂq{%Á9F¶Œ5±8Uáõy1*NÄZggZ¡ÓUö´¢7¡c +¨#€uòÐñ·‚Z]TQiEœ"TVÃáÈ$£²9™‚X +&òD›™‡©™(ì¦É„ˆ¥'þxµWHcù2bHBÎÌ[EIÍé´¤î +ñEèw +ŸûÀ±eƒ`è“oIn߲؂¡®Þ£«Þ]^ë¦8v†Ã1ߟhZ'7þîÜêÕ³¿»c霗?ô{evÖî¶ú›ŽDÝJb›Rl¨¯x‹a£7ùgh~ìkÍëšPü‚—îü¸±"€õrÕüˆóÑŒ`´Œ´1G뀳ä0†Ù²×m"!Ù">xôÓ-…"Iuí2]a[™×™qNe:tá ‹L”Q[)›T½zÏŠ:INM÷ž8^|·x† xìÄý¯ýòê™Ý¯V6Á$ÄQ%Ó‰ÎÿËcš«zÊ“èͳڛ»Ás²üŒpdž©aÃQרáŒëœùŠY— ‹$n1qéÌ1:KŽdlc¦Î³wÖ#vrcº†FÑQ¾} +R¡´·&Ô¡KÕ
’Ѥ%ìnÊl¦T8œƒµµBdÐ +pó:„Ínt¶B·Þ© 6iÍ>Õ—È7Ôd 6ä… +`XQ +e"›p,þþ¢âÅâ´ßûGÁÅŽxñLÁ‰±;êÚáiQ€Þ„KoÏg~{p1<å:Ðÿ2z"Ä5ØÁV¹ÂOqSÎ +Aà0H"` +§¢o%¯G¯%ÇcÿŒ1L¬‘ã¼GvT‘†<á.÷T3ƒ|uΡóPñ>Þ@äB¦p§ÞÐéÙ¯_[QaòÂmÞ½'¼g½„7Û + }„jlÕ%aDT¯÷N›Ed¦ÓqUçÉ™Îp•Óåþ?ç÷&Ò,—J'Ó5åUi6ž'\U¦«Xfj¿ú¯h೤5y©ù]IiÊó©.ƒbAÍ€x2Î[K«ÞíºTê’°m@~ia•G/°¡F_rýle_´¡áæÕ%ýkØáÀü–7w=¯-Þ1oç[»V®ñÖŸ9œ³Uëî;ßÜ`ú[¾ýõ#‡«Ò«¾}û…á˹•Íx +ÖAîÝóñe´_AµÚˆœ-^‘=ÝúnCŸ¾ÏpÅy%ð¾óýÀGÎ̉ì~'˜Qøžl´÷•ét +•»v›¡yn•Ù!#lz„wRÅö½² +ÕɶË>ŒfUšúÔJØ˦~6ëD¨ìª¤Ÿ)©ÌĤ¢h«Ñ3ÅsfÎÂᔕ6µBŽþWQ¢DR)äI‘²åd#‰Oeû4îÏ¥(ï³ô„”j$°Sž5dâ[‹gáYrD7ÂŒGø3–3Ò«q/¶×€¥isŽÒ±¹ÿ²_õ±QW|f¿÷ö|¾/Ÿï°ñ}`0ÆCøpÌÕ|;à »¹Ç9̱áÎ&|A m
i
D7 +8GÑáÔÅY9ÆrÈ2Dbóžéñ_Žléñ³ulåLf+£¢ìç”áßãìç„»Wà*²ÊÌ;HdÖ²pàU²‹Û%½ªëÉFn£´^d^§‚‘H¤Öôû‡(Уù-¤ž³ÆnúÆ€qÎø”ÍŒúzò‹äuzȨ¿1ßõüénל珞ý•h +a¿Ã±ÖjVÿªýzïà ñ‚Hä&¸UÍöì
"¿o,$`éàààïE‚òG¿ûŠ„‹)Ð~¨ +“›zJÍÈ™|SeuMåâÊŠùsŽëáȪWÄ£M-/ëùú¼–¦5Ñ„^ÙÜP¤/¬o-ÒgGEzÉÔi…¥%ÅEú7ššôE±Æ• }Q4¯¾T¤›7{z~ãÚ¨¾¸¥©5ÖÒœ(´:zÒ[óÛb/Õë‹ë›æK/ÇcÑø¢hc[S}üÉu|:]¯ÈŸ7ÝÔü?7¬‰Æ †>±hÊ4«%k8þQCKxL%ôz=mŒ%@ø/î«5*ªë +çÌÀ\塽™í¢]!MŠ¦•g(0š ‘k|€0(0„”H|4£iì4XMmmìÃ>îÜiÍâÚ¤íJÛWL_bWÓÆ&iº’´«k5tŸ;c„È`û«3뜽ÏÙßÙûœ}ÏÞw__£µºÖ×Tݺը›Ü9âH%¾ê¶öV_pEÀ±¼¾ºÙï{Ï„A3eõV4¡ïí¨Ajiä@õÍ·Tºú°
”ù}D+ˆ +¹À¶R?•µOÐüb,¡ÿbâŠiÞOó´“m4žO”øFÚ•ÏÔQFºj°¸U¤·ÍäܦLp·QfZ‚[ðqâ™3E´¶‘è]¦æzZ4Gb…°ßA}‰¼º“EÈ·§+»';i'
ĉ3Ý2aGÔÖTü#Vû ßhúv*+ï[»Ôôï|òçÒq>ÿo4ÞmÎÞ:"Ÿ´Ž×yEcÞ¤Ç#¿KLÛb'¦%1Ž#…çÄ·’´–FMæ¶Ò\ +1
YļLLM¦lðúi»ã‡øaýCʱ¢~µ!jù!XY¯>’£ÍdÝØbÚ›0¸‡…x]M™uB5g:Hç»,¼¤jˆ®ÐG\JQ:ûAÀK;QlȬ”¸ÓK%ººR1˜SX1˜¢«å4mKÆ?h‰L³‰#°¸ò¹Òºo_¥¯ŒT)/ÆÄF¢~ùÙƒq¥*úH@‰PÓNÆÍâ EeËzêÂMr-|ðTB¶›…,=уòNõ¤9Þ!æu%2,Ïéýrýudþ|ŽãûIYR2x®kF•\¾/¯Wß-ïQ
±rMœ¤¤< 3)ý4ÐÓ + @ÍÂ{ôµ +9¹“·šOq{œ²ê¡}Ê)ö&B<—‚_f—¢B§ÁþŠRÙ®÷DÛãîæë¡Ze•‘€GUyH5,ïF/ÈÝñ
<'÷«ENó +µ[©R[Imï;‘¼FIQR¿ݵÂ>o?jï·…÷ÚÂÛlaŸ-\eo°…×ÙÂkm9Ò<É!}Dš-Ý(Ù%Yš%eIÒ)MJ•$)Y²J\‚dçLâ”áwS_KQÀ°Mô,ØôjC5ðnvhïT8
–zç:-ÉYÌ´,/¼«‹í1í}Hî(´fy+*c˜5vÑu³x̒ͯÞäA’¢·š|=¿2–e.)qOÒUgÕÜköq<ó®ê<C¯^1 +{Àì]'lÊ%›´™oÂ& l¾d' +Hä¨J€ˆzÔÜ;?rU¾Ñ”ÇÒ¬PMjšõÌöI1Aúá}O(¸1¸‘Mòä&ÿ'}Ź¹1³‚HW¯ÇçôT9=>jUZoG½]Û½Ùሡ‹‘C³Ì¯Ú\S/hµÏ`NŸ[ërº‘áÞkåZ¯;ÝôzVWFz]>·>ìö8«ÝjÔ?Ð?:Á\Ïs‘þI”
eý–tñ¨û…QakTØò»ü¦-æi¨(¦›[‘P¬._§Q>-5kyeUö\µXÎhY¦Ñ vÇ\ûÎìA+Ø㘖«jiÎbm:5!Ê+Ê+"Š/!šAÓé ‘}çs³Ùã QMgRèN|$Á6r|û +H‰ÜVyxgÿ÷ý¾ïÞ"Bˆ%Ò¸."¤HŒ¨eT,¥è¤‰$b‹DDšµTj«¥¤‰RBì¤Z”R[(b‰”(b»b¤„AÓ¤ó¸ùæÜ/™¢&Óÿç=ÏûÜûngýs> +³’£4e µ¿ÔVÖB˜´¨BhšP¥ZI*üDâ…áïÙÓ‚å‘мi|Mãh…U*?–eSv.ßqçÿ#+Oíù5rÜxyð¡TTÍd~Rå*NU«Uw©QÓµVí:uÝ깿îQßÒÀÚ°QcÏ&^M›y¿Ñ¼EKßViݦm»ö:vêì×åÍ®oùwëÞã퀞½z÷ùkßwúö0pлƒ‡¼7tØð÷ÿôApF„†…Œ5zÌØq‘㣢'ÄLŒ÷a|ÂäÄ)S?š6}ÆÌg%}2{ÎÜyó?]°pÑâÏ–,Mþ<%uÙËW|¹rUÚê5ék×ß°qÓæ-[3¾Úöõ7Ûwìüv×îïöìý~ßþe>òÃÑcdzNœ<u:ûLÎgÏ!÷üO.^Ê»|åêµë¶ùP„[Ú]Ù÷•ð¶a7tò¥Bñ´Dd‰“âºÌ´Ô´¸Y<,V‹§ÅÇÒÑ’ÑÀÚÀÓ*¬šÕÙZÝêju³zX½=ÁÖðƧ ]w` +¯º¿ó +c^¤ëú²pèëõ }†ÞM¯ ”Vž]ÍO+;ÉOÊOáYýƘ ¶[ºm¾mŽm#`‹µM°}`kió¹RTíìžCô1~Z>¡”þˆ€ÿŒG¯ìd`-f! ‘‚[ø1+±ëðæác,Å}<À¤b®ãX…-(ÂC攎¯pYìéÅbÖç4Âq§ð#²q9(ÀHäâ,ÎákDà>ÃœÇO…Û(Ä\ŒÁhŒE$Æa<û8 +Ø¢LÄ$Ä"âgÄc2ˆ©˜‚=XƒiÙé˜;¸‹ïq ù¸‚«\9n 6Žøw8Šo±Ç0G0?`=#ã"Ûg £-céBt(²Št–ÕeMYOzÉÒW¶—þ²ÇK¨ijà¦ûŸ!§dDÉØ’I%“K¦”L3PôªTˆ$HÍ[ƒå6”Í¥ÏïrŸ#ÌËÀØ«rÿ€²’à’1%цܹ¨uö'öÇöGö"ûC{¡ý¦ýºýšý¢ýŒýô‹häéÀã™?C¤y‡y«y£y9ݼ†ÑÇqÏd¶÷yjT6+äBµˆ³˜Ü+87ÿ¯×\ËèÿÑÕš7êª'И§§Òu9¾WyÞqü–NÒù¥yÜ +òô'j>œ„M¿£^4îÝ¿¡¶ÆÝT-ï?zCì3Ÿ»·4PhLZ sÏ4ZÉ-»ÇºÁ˜ÿ}$³ÞKØ£9lõxök2†òÁ>:Œ\ªƒÍ4…ÆÖ&ói¨£òÍbìÇßÙ!“7qˆ_Œæ÷Œ½8LçDüøv¸¨ ~9„O“±§(.¼?…åy"J颧ê·Q-91Ì' +Öú&’e€ðU¼¸c¼ÇÝ ƒôÕ¨‚X[LƒÍ]Ež‰Sðopa
d},2ª,Gí×7+ÿæ»z`›¸Îø{ïÎwþï³ïü'qç\ì&\þC!WbBÄ +Œ’ÖI6-¬PD´¡HåÐUšX5èT6Mm§Ñ±ÐÖ´2:`ý³µÛ@Û´‰âmš +ën+cvÃçŸäZ‰ ½mºÈÿ•Tã_âs‘cì
v'`3F梻¦2Áþÿ”<Æ>Š¯Ú½Â¬‡îN3± ÌÏÈ.æ +Ár"°%.ë8-'ôö][2‰t~oÜfmSÚ¬µ5hÜjÓ–Þ®lÇíK±aöÄâq‚̈J_©Äz‡§!èL8Ñ»I_½&™ˆC¡TmŽÛú•>)Ët—j¸ 6ã6:צóÆmä44&לËÌ +¨/Ú7)›z»“:Ó›¢÷p«úr%®/ß3¨Éâ7Ö%uK[£uÉÓhåäÈxÇH<žÏ“ÈdFg»×Öt®M† %qP¦®M±;ÔÃíéM ˜Ê€’ 3ém²nQ–)[2ÛÒ + i‡&ËdÚ¹=“Îôf'GúYP2ã™í‰ôtJÙÉscA];˜Ú‚¡¨zÔ¨l F`§=ÛNºAZ¢y9þ‚‰‰ý€AVÎ4Êðì(FY²ìmB˜c–ó§ñ*P…»1!ËÓÔ +ßô=¿!ê¹Ã!wh„Aù‚ +Ètñ^Ë{t†í*ÃvÁÃ3„^ÓÊ%R Ïëuð$2½*a ~[”$$ YÜî,þR³[\‚Ð…`I$‹÷kåq‹"rùm¼‹sÝÈZå>:wÌd“ +ô#-85@Ìáõ9ñú,²¸Îà/‘‰€œÍíñt!' +ù\L0p|¼çq€²ÍaÈ^Ñ0kdÖÜm„'72ñ4Ÿp¸©ráŠ.[Qø|âÈ©ÜGê}¶ðRá>°-‹™|ÏuÌ.]ºïÒO?üä8°kÃ¥BÞU=|øÂeÏÙo?ªîžfGÈ +FÇ}ó€>/ÄbôMÌþ"Vš£ZƒñPŽçC|Çâ!éÐÆg#ªîõ—„’{+x¡Tø¨+<mÚÜËnzÄßm-)9qBë¿w„¢»¯Ðm(V t~¬)’¹Ò¼ÀÌy³ÙRr¥æ gÁÎj(S”8.h¦lKKmsݼ—óY9kUdÌfÃYò™æ@G™ÃW]8.K~£9-ÊgG5ç窆dAæO ÅTO«T¾(Fuª‰²H1C»œ†z¡ñàDSóà +FBx¶„½¿ìÞŸ‡iÅpáãçûwâË cÉ%®$îïWšê[n(öBEáÐý_ì;¾Õ´Ÿ* +Ú¿¯ÚØÎÿQ´›™Ð‹rômŽTÆ•sær–c±{®Ùe–•tÐyQ”¦èƒ-lyyÂ@ÌÑò…ËÊ€B¤›Ò•ŒYyÁÝ]²o’DŒÑü¯¾µ¢Õ‚zµR–P’P}Šñ]FëT'Œz°›öѨY8_¬UÈb˜hS¥ÑN<¨|Hl*¶ÃÜĪ»VYžÅÏê_üîØ®ÖØ(®3zïÝyÏìÎììz_³Æk¯Möâ5¶h-v%L)ŽL“'¶A¬(±Ò`A´ ŠJ’PMK +‚³¼JUÅ´¨NCyÕm%'„ú0%ìÐïά»ÍŸõ{V¾ç|ßwÎù¼æ/vsÑè²»o,bÿÎ1]6»Û¦-Ï¥Òµ¤xêTóžùµù}è`\Gå‘Œv%5¢ã!IìÀZ)ò>,B +ùB]Óº]«fE†ñž/Ètžr*ÅÀ/®ýßùm °5˜Ìap¸rŒ½`²7EüB˜ R)#ì5Œ°³¢"ôF£•LMMþ‚‡9»·äiÓÞwáî#.â‚çþÉ“‘£ú4Ž!•Â§Ž&#Ÿ‡Â‚FM4ZQa€*lTF¡ÿŒä#¿RwRµ†?ú #)ÔÖÑ«3ó½YP1»4‰Ñ¬f…lgg“–íW*Ú£ðhº:'”
ÕXÉÄ*ï@v¿òñôËñŽrz,/û¦Þf\u÷áÇü·×lœ×®ÿx§Ç<´ÃY>yAvoÛàv×Ü}æÙ6(yÚ<„/®z¤ôÉ°cõc±ö\Wšü¥¹´ú¡¯ï™ËáI™¢¹¼0å ŒÊ$‹E—è ¸Fð¨&ǃ:2!Î`ıbi-–Ot©jJ÷xuÝÃF¾8!Z™rŽs ÀV‘Ïs¯uº¬{tà\ÿ¦™·>kGºìÿð8¼ñ~¨ì*pù
Ã_ nÎf³&³&ÿ´yn¡MÔÕ{…Á„§NÍݱ‡¹?yà·À‹þV›Ôn±_ÉDßÅñÊ.$sdHê}uØ<ð3jù·il£Ñ&K‹ËŽ©u‘†*ët[‡@ª‰¾ýÞƒ?0Wünï«–ì¹±*xî +ŠKRÝÎ èž„ýNE*¹œt;E§îTô¯€Ò+Î7X!’^I÷«ç‡U‚U)š8Ìó¼))…DÞχ¤¡¿âô¿¡#c2Ѿ;:B³£ð (—ªò,
¬
¦Çìű¾ªoläã|¹ƒ³WGòÖâ‡{^R6¿æµjhzΗ{Iš9sVB©xáÕMd{ÏàôÛ=¹EuÑx}”"ga{¬ffOüí(«(Ôd1¢Ý B›"Ñåv§$Ù+Ir L!?4ªÿ'd!ÖæXWï[à[ã{ÎwÄÇú|HV:\¿ßÅ…ª‚¼0·R¸J–üH«EgÐrH(ã(KêË,óÂ"üÅ/…¥Ýùp
´Ì·V‡ÎÖ‘x§m&6=¹ê¦DÞ©
³`Ì.pš¶áåqŽbÿ˜›½´9Yî#¨³.žL¶Ò<aاë5oóóæGù‰ÛwóÑè)Ü°hðƒž4n¿’ën«í"¿JßûgIxê4"âÝ¿¨Ïþñþ(+Ehý)ä“ õäBÒ#ë¢à(b:œW$ XîÕN’+4™3éLBÖÔ˜c9E^Y¸ˆ\Û‰aCôfŸï +¸*ZýkkL{äìï„<(I=ÚR)¶h-"ÀöËf¢èi.Bžù ^{Ä${I}ô&q'Ç?Öê°@k@ù6½©m7ûÎpÎuäŽÚ:bH.RÉaÏÛ&çAZ%¹~ZuœÂ13Œtc7ÊÍ5hvtA¹QOšˆrGÛ=Q®-C¹^ÖöžYÁþ9ì?Lj +Ç`4Ï“›c8vfQïIs›È1kEgžð›È1ö‰$s +ÓX†]ŽÅ”„ +ƽŒEžÅY.IÆ_4IÎ’:+›¤ïé§,ÒpÒݤJ£^O"?`Ô‹ý7–¼†í‡d¬Êx‘ñ bþIl•¶*›Ó´m i}6ÔŸ4ÆÙ&}åd¥-sPúŠiœ÷?ò¬Çz‘ ^`M÷˜ž¡¯0Õ3Âìh.5¿rLrtq¾åêåÚ“óilmì¾ÿ?Wþ¨;üJàD!Þ÷× }yw?§ñ¥F³µ¢s7}8ê{‹ûm¬Á96Öq·±$¼ncqx×Æ®²ÕÆrÝÏ#˜¯8fUĈÎ6æWHdØX'îmcƒ¸¯Mâ6vßnc'-–V ÃA;ãD™
tåÿ:‰ªÿ!Ëþ +ËyÍâM¤hÏÚ˜ò¼s#XG_m9W;fcôd;`é=mìDH®p,õ$èml ›¾SávìOÑÙØ@ÃT8NžŽÑÛÆ2ŒÛ–ÿg¯©Ç*œ¬úÙXö/U8EÚol·1m6ö(ÜYõŸ±1ûM]bWªÔcv·1õ˜yÇ)ûÍÑ6–ýʆ8e§9×Æ´Ó|¶xDyqYñÀ’!+¬·;Çè«òVû§[Ö0u7h×VdY¥žP–•Û×YP—e
¨®¶Fúª&‡‚ÖHoИæ̲Ôüì^Jɨ™S¼V™¿:òùkƒ™›U®ðUz¬"ÏŒâ§ÚWq¾mE: +à +•iß+÷‚Ôoådå»#ƒëh¹åZ+àòCÞ€·Ò +<•ÞOà>Ë?éâ{•fy=¡pÀá·MöÔVyÏyéJ +£¼vùyåÊŽ m¿œ™…|Ξ™×ë’â<ŸÚ™\%@UªRºå-â +ñ#ôÃ-¸·¡?3~ ÷:C¸‹¡ŒãbÜNï–àŒÀ¼—îbö—ñù13o4ÆànŒÅ8ŒÇ܃‰ø ÷ûSz¢R²¼1|¸—¹"s¡–žžBOTf…éÇéôÒLÌÂÏðsÌfô +."hR[j!q +•(Û'õEðAfg‰Ì.ÅâgH4>ùlC´Ä&‰Ï½³|pÓ¹çžóÿsïܹw»#:¾èÁQ¾˜ƒÿ#“â^]³7oM5‹ž¥ÃØ+ƒ?ÜÔ1݃ßÝÔú'0 +Å÷N¬w7Âá.agû]XùbÎøåœê+d,¸…N ³¥õ”zrƉ•hW¸©•zó\XNÒ¥Ëñn!fx/ö8,#í0‰‘¥ À5Ü›ÄÞaÜÇîÇ2ïÌáîù§^8g|Å¡T|]œÀ3s”Òî÷üG”œ¥ñKh£Uˆ^4N=οM=ª:+É/A9€76Y>r’Ã@w5w
Ö¹ü¤¨ðtíéÑ»ùQÓÏ;ˆáSõü
!cÝ×'q!Dü5ô1ý弘ÛÂwówE‰CŽ°”3˜Ú›jÑ7ìIw%ïÉ‹|WïÄa?sñž_çy´îÉ:¼Å¿\+€ë~͵¡ùÕµZõs0M,8Ž§™Ã•œõs#b?¸™Ô‡]k>š!×ÒÑúfHò`·kЛ+`±êæÈÅÞI¬’ïp»sØ^,³ÍÍô"ºÕ¯ÖçfŽé“°…ô‹{„‡ý:¹V/Îa &ßK¦0y±L.ÀBbЉ¨¦ÿ)òOV~«|ƒS¸€y_Y†Zùç<k…ô^áõ^¿'Và]Cš·2ÒÁ3ú›¢ðˆìÀÇú +kƒµ…'$]‰áÙEiIó’f!áNR†EI\®-À‰¢Äp5‚Ûn´¤³¨Í(ÐC$L%¬8.å&ßïöýlÊô°×ItÅ“Î}B;ŒìaŸÝáûݾŸM·{Ø“,qh¢Ï£G}NÔêr"qG;Ñ×å,@o=Äöü-À +H‰t• P”GÇß›oºé#D¾ï›aÄÛ¨b<ñ6!FÆ3*È©€€‚âGðD%ZºV*‡…qk׸ÙcÖÍF£IÁDãÁ„A`ĸñ*:Àl3¢)+kõ¾×ïß]}üªû5 + +ú(è‘ÚA
TǨԩêtu¦:KÍV¿VO¨%êUõ¶Z¯6k“µ©Z”–®åiµ +íºŽº¢sôŽznÖCôžz½¿>P¬ÖÃõ}‡¾Kÿ@߯Òÿ©×èý¦Ù`ö0·3û™ÌAæžæñæ(sœE±0‹¿%ÄÒÓ2Ô’mÙhÙl) +¶ÿhÝcý»õkëaë·Ö£ÖãÖ¬'gEÖÚïv!Óºßo06D7¬jp9NÕÙÛêâæáíov¹š\O“ä¡A¾!B¬ÌR2Ü<ÖJyÊ^å‚rÇÆ\ãc¶íeGØvǽ‡Bå« ú«š:^hå1G]VÔrµB½§>ÐÀÍ#MËÐòÝ<@ò`ºÐý$M·êÝÝ<^Ñé#$H}››Çgn¶VÜìcî`îôŒGl+àVžñÈ·îwóøFò8&yXÏ<ÇÃÐåæ’Gg?ç@É#Ì9Ê9¶•‡<$®û®ëOÏžá¤á?†ï›E³<W”Xã•ÆÝòô7n¨/¿Ð¢VoqT8®:.;.9Ê¥ŽÇEG±£ÈQà8+í”ã$ÀÍÇ‘›ÁîÞ5Õ×êÒ-7ªrê&µøÚ£u—j×Ê[\×½.Sêmª¼ª<íí÷ìwí§í§ìµÚÓ5Ø2jœ5U5ßÛ^®<PùEåÇ•Û+ó*³+à +VÊe
Ü„[ð-‘!GHà„FôD/4ah‚f™!}Ð\ø¶ÅvØ; vDì„Ø»Àxˆ„*j¨£-ŒVìŠ!Ø
»c쉽 Ê°7öÁ¾ø2öÃþ8 +‘Mƒh°XMCi;EÃÅJQ$V±Ób¹8HaðWA#Å{â‚(¦Q4Z\¤14–ƉQˆi˜.N‰Ó4ž^'Ùv–£7(œÞ¤ ô.ÄEì<M¤š$ÖŠub©X/JèmŠ¤ÉôMDoÑGld…¬HfÑbv‘•°RVÆÊÙ%ö»Ì®°«ì«`6ö3«dvVÅ®³jVÃj™ƒÝ`uò%ú…Ýbÿe¿ŠM¢”ÝfwØ]výÆêÙ}ö@äŠÍì!k`ØcæÄQ&–±FÖÄš™‹Gnà +7rÆ9÷·™¸'÷â&Þ†{sîË_âmy;Þžwà~¼#÷çx +$UÄ‹š&‰¯h:Íå4“Þ¥YD³EŒ¸+î‰K"V$ÒŠ¢hš+æ‰ùC±'þ,²(ž(Q$‰dñP¤Èü2æ‹<ñ>%Q2¥ÐJD&¥Q:-©"M¤Ó"Ê Lñ-Å"‘AKÄ.Ê¢¥´Œ4Z.‹%´‚²i%¢Õ´†Þ£ZKëh=m ´‰ri3)¢‘ŒÄ„S<‰“‡g†g&†ñ^Ⲹ"šD3!„‹þÇq]xwy&Q +ZM«j9-/Ë1s0ó0°‹°K°¿a–cV—&ÑdšBSišœ¦é4ƒ~¥™4‹fÓ9+縩œçfÜœ[pK¹ åöbö{§â á0Žà +®Ê=\ÃuÜÀMÜÂm”àîâîãâë=½¯ô¡>ÒÇx‚§x†çx—x…×xƒ·>ŽùZE«Éü)€õQ0…P(…Q8EÐZKëh=m IQM1KqN8ÅS%Rm¢dJ¡TJ£tÊ Lʢʹ…²i+msü)‡¶S®Ly´ƒvÒ.ʧÚM{h/í£ýt€Ò!]~ê¯H‡5Hƒ5DC5LÃ5B×èZ]§ëuƒnÔHîÀ5J£5Fc5NWú°•Oé¥ctœNÐI*¤"*¦StšÎÈgt–ÎÑyº@é]¦+t•~§ktnÐMºE·©„îÐ]ºG÷é=¤Gô˜žÐSzFÏ齤WôšÞÐ[O‘ÇÈ®ñ2Þ†5bÔÀø˜R¦´)cÊšr¦¼©`*šJ¦²©bªšj¦ºyǼkj˜÷Ìûò¹ùÀ|hjšZ¦¶©cêšz¦¾i`šFÒÁ46Mt“&kŠ¦jš¦k†fj–nÖ-š[u›æèvÍÕ<Ý¡;u—ækîÖ=ºW÷é~= õÖ#;k¢&齫·µD%Ikj-ÓÔ4 ÐúÚ@%Hrd»Kˆ¤Jš¤K†l•m*a²E²µ¡6Ò–ÚJr%OvÈN —Y#ke¬—]’/d£dJ–DÊfu´®Ö“(é(´©6óøå¸6×ËœÔÆÚDµH:Ké*Ýd¼LîÒC¢%F[ké)½¤·ô‘xI¾ÒOúË +£1c1ã1Õ—;sîÊݸ;÷àžÜ‹{sîËý¸?à<ˆóÊÃx8à‘<ÊãÃ1<–ÇñxžÀùþ’¿â¯ùþ–¿ãïùþ3ÿ…ÿÊãùïüÿƒÿÉ?ó¿øßüþ/ÿ“x2Oá©<§ó4gò,žÍsx.Ïãù¼€ò"^ÌKx)ÿÆËx9¯à•ìË«x5û±?p q0‡p(‡q8Gð^Ëëx=oàÉQÍ1ËqÏ œÈI¼‰“9…S9Ó9ƒ39‹7óÎæ¼sx;çrï་ó¹€wóÞËûx?àƒ|ˆó>ÊÇø8Ÿð˜¸‹¸˜Oñi]Âg¬¯]eW[?ëol
²Á6ĆÚ0aÃm¤²Ñ6ÆÆÚxgl¢M²›l²M±©6ͦۛi³¤¬”sçI©(•¤²T‘ªRMªKs›ïfìnw¾»Àîq’»W|eåíääÙ}Nª“æì°ûYÎvg¾»Ð°X{ȶGìQ{Ì·'ìI[èì´E¶Øž²§í{Öž³çmN²ì¤Ê +ä"ù@¦“4¤#Å^´—t•½ŒìF6cüàìt!AFB†pD`
ÖbÖc6"QˆFb‡x$ IØ„d|ë.v—8Ý¥ž½¹Ì]îõÒ]éúº«ÜÕ®Ÿëïà;½‰Ÿð³+-Ý`|ï,ññrCìüˆ0S1
Ó1¿b&¾À—ø +_ã{ÃÞ´·ìm[bïØ»öž½o؇ö‘}lŸØ§ö™}n_Ø—ö•}mßØ·â‰W¼Ä[X¬ˆ(~Á$L8QâãI)©ã”Hiy_š8¹ÒÀ+Ï+×ÍwÜ]N–×3©áf{§8lðÇH ÿŸ›àÆ{2ÑMvÓ=¿q3Üuÿã¾êƒ£ª®øï¼· ,¸ù +&!è.~íÆÅ ©Eþ°3VÛí´¢/ÑŽ§t¦Åѱ-ÊxSÝX§Æ¯–Î@¬H[¿¨µÓuŠÖVû!Ûß}û6 !Š3Nÿñî¾ûî»÷ÜûÎ=çw~ïó~æ,æCæ£æ!s?zOCæÍ=æƒæÏÌ‚ž©»@*1 ALÆLEÓ0§!Œ*T£µ˜™˜…:ÌÆX˜‹Ó1gÀFóáàLD± +4ã<¬Ä*´à|\€ÑŠÕ¸q$DÖàb´c-R¸ë°—¢èÂtc#.ÃåH#ƒM¸Wâ*\kEŽúëÜ&ϼæ:f0½ÌWú˜±\Ïœe+ó•mÌXt¾r#3¯ÜÄ\åff+_a¾ò5æ*:Sù³(cÝŠ¸9Ëk +|Ö…¥—Q¼¤¸‡í!úÅ5Åû>^\Öà=ú $ËiŸUø‡Ô³oö Dïù#^O²g*ùµT†h»’s?±È'<æªCäêðÁI¢{ýk´|ý”{Ôe7¿U¥roéf6Õ¦‹§ˆ”»Éó'–!¾y'¿MOÙïJÔˆ¡ŸYg
rˆHî77o%ê´ÜÉå5þnãìÑý,²qw„ñµˆ%›±ŸòÞâN_Á[Œ‡§dîˆÍüôïö–õ+·ÐÖÏó;ÿ©#Ód¼~LadDYûœ8ºÿî[[ÇD9>¼Öª‘Õ·+øŽ¹2“XŠ‰ñ6„¨â©á›<ilóå–PΦMÃä$ÿ¢U1Îw•¥d5Œy(?\Ü#Qr&¸×OU*®Óv/ÇsÔãÐè>*ûq^Šð÷˳Ž¿ëÝãïß8Š¿áWÆc2@Íž'ë<B¦«ç hÞƒ¿?Œæ3ÜWŸ¤ËÜŠÕ'uނܽŽ–¨Ëkr€oš¨ìäoÆ+;ø5Ý;NîiÆñnÓcÊCgÞ„÷ùÝ[Þ¤6‡ŒÇÉäwÏ[½¾»C|Ëbýkmíj¿¸eÕÊóšWœ»üœeg/=«iIcClñ¢ú…¢g:ó#öóNŸkÍ™]7kæŒÚšêªðiÓ§…¦N™œTY0
AƒÔ©ºx:Ù§fdz*ä$œ°BëkR¨¶"N•½¬)ÓèK©Š˜BMJÕv¤ÑÚœQ•±ñ"ë•
¿áäu–T(ÿÎÚ\^Õw¥#Nø%kd<Ã9jN<‰XʈòßÎ!þ×æì¼ +w°?b•zÚ:Òú*ßhf'š#Ö]i5¯ü˜ÉL¤$¹¤8<NÍõâ†C³ã …ÚA„ÞP˜¡ÅŽ5óäÛ¢êcT$Ì–·š”Ô¾§¤FÉŒuTùÄWèiG›'°A2ßç$ó[hÑ|vÔ¦ÇJØ®ív¥«–±é)Rû:ÓƒS§Äøæ)ì€×Á)SÙ3Uwp‰mƒº@¼†J®4œFóUku“úêSýY6œíÆ‘šÑ‘Bqx`ì8ܪ)µJJ¨Ê¸šTRÂÞ¢Zs +ýö`ð;P£'å|îÊ´2s„Möv«¹©ŽMìâ«xe{míî„WiçÙÉ^Û峖ͲvÚé'ôç{7g5L$ë$869žÞ¶T5ïIUSÓ(6íæ7-ÓMÖm±õ£ëî°Õnª;f4¢k‚ Žª»I‡oãbɾ‹´KšFÜ桱=ï9§µ?g«í=}%ìåÊø¸aúg„Þ¡8Ó›è›2ŸíÓ*÷åô6“}¶Û¿ÙÛꀷ5âÕNö%ô¥'ýØÈÙ›ÒÉ^'9úBnœ
3:~n$¢fÇôD×Mjsyj_R™£ú똰bB}⪵ۻ¡ÛóßØšKdü._`“ž¦G²‰L&Rò;Eդ莊%Žíê'EUm,yŽcÃ
©®t2ay»WF<}þ;uÖ;l§:Fº¥Ž2nÓ;VÉF©
Nª³„‚Þr•í.°1âyŠúòÞªê¬l·9mY×msì67ëæ +Åí=ŽvÜÁPÈÝ–ÌÚ^äûŸè·TÛ@F…³½²’NÖxkëJ©šÎ+´{ÚìÞ\‰,.t"ÍV¤*S–éø¸a?Έxâ^Ç™~›º…ÈH–ݦé¥@V°T¸Y‡)5Ù˜f\ëaÖ«¸¸¥#ÅÌD“[6ø"}ÀhÞëô{¹H$¢c¨¿ÐŠ>¨íéÒ³ë'hmŠÑwY=2\™±Ql/ŒLÏ:ôU]jÃ)0=Ïn•SmŸ×äÙߣۼîæ?lVÁfßÝ5ñ´i~Ë°LÝš#}µ¨Y1o¢¶ YÒ
;öAG…cª"ž¶Z2v¸Šô&#`ðWÔ0
t~)šDQVÒ¢d¦îIÕãvsV3G&ÚI7ëÃlìþü/A¾wâMR&ìpŸVI¾ªÚÑ[Ýïq›OÙÑ6TV¤$±6£¦kbVÓßö*nΊ§mÒöÓkØI»W{]ÙÙ„Çklw¡x4›ÐüG•µˆåã›uf¼RÝ#®ôW›3lhH0™_R‘;3)~«€ÄéCÌ]Ík®æpwƒm'·$øB>ll`Çâ[—5›úi'£¿$íùÿñ^±QWxÎÌܹ/¯÷îz¶±WŽÝ´µYÓÆ´({KƒbRRY¢Ôrm'`J ØDEEÀ(%rT…Å)I I‰øÑH•"J±Q+VP7”"×é#)ŠÒB€®u“Tà½î™»^³Dñ#êîÞ¹¯3ç|ß7çÌôWKu¯Â˜¼otõ·ÕWKqb¾$8ÛmwMw»ÚÚ¾†vZ¥îÙéoCk§,¬õ, >´lÖ·0SÕµ,Ãd›Â„n? CÄp38«22bHÛ´§Øn{¼|ÊçåèsÛ—°³"o¹L¡‰¶þþüY
ʼ¿ÿ®~Œcê|Èg/ØS‰g +·˜*1HÚ^^®Ã°þ‘N+((ºÁ…f¨œ2ºÃà!Ãà êN!Ü,êŠHÛš*gŒ©Œo`)v…1¹·0,c‘1®ŠQ>K&q z'QYñ¾“H¼í$Hy²±1mΫ¯Ì&w+V|÷¶ã»ËóÍqù›Ó +Âõi•’´†pYš–iÓ—Ö¸I¬]ŠÝ‡6‰EÈÂuF¢å ‡r뤌¤R†{œ$s£Y<œ¬G C0Ä||µ"VM‰%¢aÜŸRì6&îS:ßuGÝçܵð"lý÷˜{õK.Æÿú÷¯wÂ~èïÃÀ¼«î»î˜ûw¤[òL%ÏÊò¬‘2²Ön~Ì„Zøžâ{8_¡Âw¬@Åñ¦`ëýà3gšÔgÍ´¨Îõ4)+Cj}»)+=4”'nŠ4©)†0„Þi†òÔ6&‚¹_ø2 -t,ƒn÷I÷{öüøXç‚£GŽ*Ãî€ûš»×]w|ùÑ£¡ßàéó)ôÛ$í:- Ò”tEºäèŽçŒ`Ĩ6¨Î_‰ô áE7FO"¾¹L±„æ4 CáØÔ1Ä^Î=×\A_P†O¹+OºMgc>‚cêd]{cÌ¢UÂZX'c:c¦Q4äo1à;”KSâ9Øý§si2¥·VÔ[”ìµWE9”ø(ŸÞ7¸ßÃ~¦{ú;Q +kÔ-*]ܤKJÁƒEŠåé‹JFíêšæ¨*oŽV”Ä{ÒËžœÒžTcA|ñv§ð©å5Åú#yýbJ«{é-w‡ÛàÉñ1÷ò‘Ë“‚W¿ò Ìi†Ø›a¯áˆ›u?qÿè‡y\•<—óíj#ÍW†¸bnQ‰¢õ©ÔDh}j7-¹¡¬\ÆB@ë3ÉL1¢Óß!Ìo«`®{:÷’2œû)m»ÖDåRÐdq<FfØ¥¸Üfís…ô%¦dK’ɼ=´#sãô{Ê +ÇTÛ§À1U—Rh©¢LµÔÁ¤øLOpypÖhr—8‰¤õ&H2ƒ@eX#œÞv,X&¼ò É2¡ló*ÎÌ^ùGd+'h +U†®©!MSu• N‚åRRªŸU±zÆf3fcl»°MÑM)®’´j
/k…à4Î( +NÆVÜ÷⊠L»CÚv©$,½OH~úú§î¿rÊð„_kbã¾é:Äxs+H6Ù÷-,]ZJT[UzÑo!=¸ãùaÎu¡§MÌf:„™7ˆ™×¿Ë› 3EúZZlVB¦ +,+šÖ¨ôÒÉÞ”„eUêð$/R$"±šŠü‚!V3D9”^pO¹—Ró_yý°EþÕÈ÷ãÜo˜`ûû7êç:oTF&í#©2;Ï‹}J@™úMu¥ºIå3TøãÕÅq†çŸ™ÙÝ»Ãwö_˜˜GBLÜÆÅÆÅ „˜â`ƒâBI0¸RÉŠ¡ÒR (5C£B"^%
)R -0A… ‚"IhܤX€ªò¹€ïÜoöîX•Ú“?ÿ·s»³ÿ|óýqõcúZ¸n¹¡ÜГ¡‘¡B–ÿMw§Ë-—‚~—ž>àÒ³.å¹\D¦Ø,öˆ£È³”X§‡SíˆédwU9™c´»šIGû’½–ó
Ûaé¦ §é|]„>ÌÕÁWy¼›țζE–°#?ø ?ÀÙijß^).ÆÎ¥CÅIž{ZhöÔp8ù%S æ¼d67ùçQèqˆŽK#i{†<£Ö-¥Öøòxã¶J?ºâË¿«æÑØ&Q@iGùµ¶ã¼†š×‡½W>íYéA:L{mÚì§umý!ÄŸÏ¥¬¼†<^GSÓhz/š.iUw²s{ä.öÙmÛ—ÛÖ½ƒ4öÎHoì™A‘Æ ôÆêåüìú@F¹›V•‘`ù uªFå×çóüG®w»*ö0Šaðh´0x$x¤0ˆ’˜žõóš1SC¸jèÚäòÍÁ;°43ÑhÑãT:àáɾ}z«H83Ÿ¢HXöê=õÚ¶yËkæ×û=¾øcË…ŸO]X3¯îìïâ§âÿúS þýº‰UµgŸ\¾·¥äÊ؉#Ÿš4lÐüã§þáñ” ÍÍòÎýÊ3Yº¢F!¬š_ÐØSìÿ½:v1xNB1C)5Í/ˆ]µ&Ä®Ê؉wE‚÷íè5b¾W^V“N©Ïߢu!Z-hJ˜œ@N€¬Æ`†Ë„½ÈÇë#åi>ÊY¾yfdÏÏ¢( +ó½VÇdøTËÑ+öXA6ƒ@TŸÞèøJ·Ó€¿]Ó@þoOûåÕŽßÌ‹´0÷ÜÁ=ÇÅ[í?:ynÔ<úüøaL÷ójÂìògðÞ4e²ñ<‡ +°i‹Mk%)¦¿¯¸_–úŸñõ‹'ü#ü|¹ßTá2‡²µÎ>çCG®vö dìë¸^þ¾Ój³»Çõ +H?w„+¸TcÑTë'ÖRKTeó>ž‹eœD%ŸÇ¸¨Òã5Ïf´–meŸ¢bSzúצÐ(VJ»–²’…Fú}þdùa&1? +)<–¶-BŒšÊS¤f©Õß”zÓ3B)ÛçÇQ,(z +ô”T„,ˆöÏnµ¤¯•çQQ´$çfqqk±©ë¦ª' +|I ΄÷çâÔaðÃ&›Î®Ð(vrªöF;»€¤¢qd¤Œ^„ãhi|%jQ·¢”wúç×Pˆšå »'d)Òy[»ƒýê}öóú¹åuàW+Ÿ’¶ î EÚ¶‹ïÜò)ßf…q·#l±˜;aŽ2¥vIJ,ß±Á’°º íJ"éjÉP–MÛhŸ¶¥Í}A6›m4§(V4§øhÑœ’£ÅÎ"|©8”¼|Œ`Ñ$ì#¶ùf#”KÓò$þ÷§Ïãƒh}†ßŽ?Igãëãã[ùgüZ¼Ýˆ
o¿ÁC±k^ü ²Æxg¨‹åÓ(ŸŠ°§De´Œ.“øHR¥¤q²Ar[Ó2½UïÓÿÖ²@Ñ^uL]RâQI[ä^Ù&Å\A•‚ƉK‚³.Ym–(³ª,>ÞÚgqK+½DZa)! ºt‘Ä€Û)–$YõŠ˜*W‹Ô!%]¥Cõ‡8ëËÉq©>ŒÌ]”[}å¤ÉöÁ“P¨ÇcªÄä4ÑL’t_hDÁÊ@/“h
K(Äsâiô<M§7cwùL1)¶’Ïiß{Ýã¥5ý]¯yM…®Ñ|Šž«¹Þ×q¹ü›i‘ª œêøO9w9ñl\sÒŠ+½˜x˜¸&»ÑIW¢Q +›+ŸÔA(Þ
žEO<[|ÈKk±Ó
2ü9x$)çÂ9Ñ’R2ÙŽzEêø—±‘2/Ö“ù‰,DÊ;óI"ç%%cå߯ON|‹ºÛf”í~íp»±ûWÜY{{çݯcœp¯Ñ4%žÒâ£ëÆnï\ÔãÞ÷ñ¡LñMI,aÛŬ֪d'T˱n± +ºÃjy[ÔŠ÷XŠ²ŒE<Öl¬\ÊZpÿT«’zÂþ8|ˆÓ€›À +` îŸn`æHAÜaCTjµ²,ËeMÖ.UãY“<¬Åu®‡°&¾…5‰ ûÇw=Œ5©¾Àjøpv
|z¿½Ž5¼Ê21ßvk
ËуXĪaýŸ±«€ÕaÍÆg³øôœd¬ÆÊ£Z¹›±ªñü·p]9ße5â;ð«óÅû×Û:ª`â»>ß0ÌýMæ± Ï/eÄö,~Û.²GÔ%6RîgrˇM𘉓¬šî̺
°n³æÔšŒÿƧÿ +øhü»ð©Hãë;ÎÛé[WÀ¯PÉ>óÙ*Ø·6à´Œ±FØmÖHøÒÄžóÐ +=äÑÇ@µñ[¿Í"ðs¢ú[èíå¤ðÜÓÖkÞ»·a|Æf©-ËcÄyìáyöŠÚÅ*t|ç±ñØÿ2£=3:ª…xþŸÞ<-àkÖ¶‚U@ó—ŒFS\~ì,ªw€3ì~
íF
Œ&
¼ùÌþ‚w³÷ŒÅ~…뙼\5³ +³n¬åª¹×<çTc}˜ÛìAÊ@wkø–Ž/`#æÝÞû[ŸÚ§Œ.0gxȾÜÞþ +,˜{ä.ÄܧÌoôj4c´iôa´ý3ë!Ì
¿=Ý\öôÕÔ3çY3ðð^Ïj€Zà-¬i£‰£Y/sm]§lu"~=í{{EW½5›èj»<kbSÍõb”yýý›ò©«5qmtªö#>Á?xnƒ~¯Ã¶Â¶ÀÞ‚½ŠµÌ·SÏMÖDðb¹ÀÄ£É &’Ö»‡ïYWÞ»ºÚ26 óûj—š®ÖäÛ]쌿<#ïx±¿}Šu÷ô14©“ÿÚœeòFjÿSÚèÔH„Ô>øögøøž¹âÅq´«Œµ£Ãi‡ÒîÞ†MñÛƒ@Kò·ÀJ`fòpÚQxÚ·16˜ý‡ôr®ª8ãøwïyÝ›‡‰„‡<¥ÒÒaÆÑ,šJ±i©ô +b‚ˆC “‚0U^:>:L¡LZ*5-*ÈcŒ¯LØA +åÙea ÒZ¦¤†¢¥¥PBÈéïÛsN¼¹$\fzfþóßݳ»ßî·û=VÎï¢üwä<º8¨÷Âøÿÿœ÷ˆÎÚ/ðÏz`¥ÖêéWG¾ÜN6¤ëÖ³V}ê½4çFŸÈ‡ÛMøIµ«oRÖ»ÞûT>AŠÜ¾”ñßîÍRÄXÓæ•'í ¬¡DæZ=9óc²DÛœÁf¼Ø—ôߢ}œ«dšé·¹=éooÛ®ó÷š³Í—ZêåΫŒ©ÆÖ˜Ëm¤¼É?î¾$üÉø‡ž~³‘AemÃ]%}uŸÆ>пîO×iM”¼Ä@)J¨ÝÎ÷TÓg±®8ô/íp¨#—vË{H†1Ç(Wý.0cK‘ÑS¨?e££zïz›9‡„:šoîfµ÷GiHô¡|‚±·Â}Mÿñ eRd캇L³·3—þ[\ìÔ}@ÛWI™SI°÷Ã"] üOZ¬3qÿÑÃ^Ïÿ¿11NíÓÞ$•ÞxöRK¹UúºoÁZ.“R—=Û‹Ññ»&å©lÚSzgM|]%…ñMŒi•Þ`c“yº•adŽÖ«)Þ“&®ü:èGÛLðË ,#¸©ßK½Ž2w:v$©[aÿÛA4.æÒþx(˜?ö9er~‹<ª}åhÛÃÜ?Sh=öµñ +g’"®N àŒ”û#Ø3™X·ŸVäÝ$)ƒ/¸Ïˆ(ðÉ)…Uå7ç'F“TòNxíÜ'¯$¡²‘›R ³!—C?ãypwq)ìÁ? /Ärק¢<ÀúH¦¢óÁé{É܃®O‘ØÂú4ƾº÷D°Æ.ä8_E¿Òš.—ûZí¢ÿ‹ìþ¸Wsê$væORý÷–c§€{r¦#g%ö;”ö·dž·‘öp¼™s"þa;ùêXÆ".(ÆH½ù³ÁXËKYSÊžÎzt\yÝt™m/¢®sM4ÿkøŒÑ²"æê=m_ª}íEíÄŒ¶Ò +žGŸê·È¡ê¤Ô³®<ë$ãÕ‡4 '!o[_úUÃK6w‘øÒžÔ±”gá[te +÷.ì|å)à1)³pF°×7¨?-e^9üDÊÝå'ØÿôÊú]ÊWšËtÇØÍRü׊x˜t—Kdã(®gãÌœ!ʲqGNÑ
Ûú&œÆ~€]«$ÇÔ¼¹”µ=·€›À˜$ïï¿ØÀÉ31f +û\
+£2üç°nú%g°ó©ŠY¼vyUìÖÿ9sÌ<Ê»ás!šâKe°7ˆq
%-`—'5 +îâT/GÖèžÁï¬1ö:@ò™÷ g¯l³OÊ]Üó:ò•z÷¾X™{¬`Þ·QÙ+!>«à“SÉuL\ Ý#ŒEGÃcˆ‘oŦÐþ¦m21Ä +¡óãCýMb>_ã±®ÝXUû«´Ë|ä·ÿ0øï—jìɉƔìð×kÜÉÿ5{´ÿ‘‰m³ýo°—¿±;6<¬Çõì£û œÜ’öžÑ÷ÃÛòþ5ä‘«Ì›÷Ë=ÈÁƒ{YÙŘ…QÞŽYiÞhÝú ÿ-üËa½„ú)ê‡:ÞŠ]ú¿Ì~×¾Ö÷PßgòÆîý€ëðwf´ßpl?þ´O$SÛìÆXop#xŸìbجÐX?é7[ýæä6dç’Ã>ê7kvšýfE\ +¬3Lyg ›Þ oP?AÜÕ7hÍÀtâß›A,Sný4ˆ…æßõAlkÀ¿AŒ»€M¶m¥¼>DýiñÄvrÍ‹Í´U›øÉ9ZÈ(|mM~•Íûµ‹²»ÔðòŒq5Îÿ¬²¾9â‰ÇGÐÃjYdþáñÓùŠkÉ(§* +æý6å!á*éÓWg–¾o§?| 't”JL7ÿ+Íÿ7ýb··?~ÞecyðÁˆ±>vnì^oU¬\#{PžÅ¶Þ
â^ÛFð ¶·Âz‰užòßËôÍ"çßqž•¬×sþ‚õ{yYÒ>_èN RÜsj~r[/÷ÄŸ—ºÜõÒ{4äJ¹‡ô +èçKCò1iH[¼sܹ0nÄ×ùÂgˆüyfÞÖ³Ö‡3c·Azy…ÔÏ~9Šg™1ßü]³Æ›Yc?Úèßë2æbÌ$ÆLÈ–³:ö±þüL™aò¦KòÓâÎò£y³åÃQ^Ê[Ž|ªmàEÕö h +åL¡þ9fÝyÓõ¶öæ¢Fa©|s£{C†»z_¤ÌtöåùÚLš³?‘H´¡Ü“Q¹hngÎ ×ÑØyO¨ÓÜK®/˜…}êž{–"}Ûs¯{yíãƈ½¼³{ݹ‡9þøàÕHNg¿…4Ó&[Y¥æ¦¯¾;îÒbu…$Ð1,Óɯ¹kmMT†ô¢Ÿøe·Þ*ú›}µÑãîïbۃȒÈYP"RPjíC‰}ƒeºìÛ<+$Úßý½ˆýJeWÞ¥‘hïâ‹öJíɘg/GO“7sì¯M£CN +<îîý6ž§‡°/b’}?E}H{_â{Œ¿ßÏi÷§U +¢¥¬v}ZEh÷A¸çÍÝKµ1rŽÜ\˜rÁlê©£í:'Ñ\Öse(G›(×éã"ES¼—"¬/Èõ%9˜BîW¦(×_0×Ú-kÐBöãû0‘~»F—Ù +eký•WÞ”/0ï\¥Ê×ôi±ÏËÖXCöÛ`m™Ê1>’3¨#Œ±É2F_19¾XˆÙBh…õD²>k…Ñ’ís«H’ÕsÈq²0R=ö0¬5ˆ¶Xl÷@€sÙeJû¢®`ÿTñ>sØsø)¿·á<ÂÒ¼+6£HBØ>ŽRŽ* +è›èÝ\K®=]kCprƒiÈ
=˳~É´i2힬–s]!l’Ä&±S$vb¶ì¯kä +ÈçÙ¾‘Ü`ߤ-òD¶>Ër„³‹ö/Æ+â2×BèJÍš=q¼%ôA…¼ÛsL)Ò9×YH°+‘GúÛ?Dy -í,Êqnâ-ŽitlEÏfœ A>†8{6ŠìiŒÁy,Dà\3E&Š'Qd
ҵϲ½Û¯“4EŒ}·°ÀóÛa—xêœÏ3g÷¤/3l ž¿¸e‘©ÛÝÑÇ`A/ƒ½™wøm.ŠdËUd!¿_`¿1”µ2ÁoóÉö[l÷áýhLYÄñapœ-ÔŸ¡¯}ÖBW×|Ó¯¾O!fFû8ë±’l SÉ2ÇB ÷û™áCSç÷¿Åô©³Œ•œL|ÇÄ'ç&¾ëäðÌç ëqV.2DÖ +¼aB/‡ù¡EFÝ%¤y‰t%3È(²Ûî…\«•®#wÉ'V+8V>R,©OÚ3õ)R)ª@)óß~È~‘Êý4ì§
Öb kÃxÒ>¯«˜Wg²Œä7È2VŒ¢O[ˆ¥¦´F‘þ®½)OŽ’IdÉúoåÕŽˆ_³^CÁÌA”ñLÎÆeÚâmw+°ÅäåÎVd›1ǹÍ|ã|/<€‘¡ú;í_aˆdòug<š3Þ™x}‹Ì¾ÏÝëôE_²®xÿÞîœP{¼©»2ü^w€mwY.#×½¾Ç¨ãŒ÷ÝÜëò’¡ö2õ&Þ|^öô5õô<i«ýÄkŸF~L]YŽ$‰É&Dêµcy”p®u#L»›ÃnÆÕÍMìäD*bûòÍzaï9óÂÿ~€•ÇÛÄ—¦ý½Ü(rˆžl'³æRÌ]œY¤Ò.¡Ýb¦Eˆý_iÿ +ÏR\O’©¸““v*ž„š¤·£2y¬âT%‡—¢&yŸâɨIIQ<“SêOEuÊfÅÓ0/å-ÅÓ1¯×hÅ3„ƒŠg +wk–¡6Šg¡"µVq9–¦nS\ºÔÏF]ÚtÅ•ÂÝë9Miï+®BSïî¹ÏÞ¥xšÒ“ÏÇŠôfÅÕÂÏ(^€¦>#×oP¼Pø]ŵhÊèÎaVdlW\'ü™âz4ev×®Aø€âF4e
U¼Xxâ%X‘Õ]‹¥jÇt}þÙ·D1‘•=AqR²g(NƨìrÅ)ÈÉö+î…´ìÕŠSáÎ^¯8
¥Ù[§cdöÅ–ý¡âLvQqŠ]C÷ÅWb²\»‘⺾‹™·k«âdáÝŠSÐÛµWq/¤»*NE†ë°â4¸\o(îL×{ŠÓÑ×õ±â>Èq}ª8éî,ÅYp¹*î·{Œâlt+v /PìÆ`·OqŽÄ®SÜýÝ›÷ŸŠ Ó½_ñ Ñ?¤x0z»ßT<DòyGñ0Ñü@ñpä¸?Q<CrÒP† ¢Ða"¯C*–q,á0Zä»Z~;¯\øÄÏrü:#C¬>´‹‹héX#ÖVñcUŒêUèDDbtñŠ-b¹ãŽÑ{¡ü«Çè[/3‰8^–ÌÊ”ÙEÄ7>Y«Xsky›ò_¬Ú¶¥]l±zXÇÜ
©^¬¾º£kÅf‹v@æÑ¿Füc_ñ:U5]Í"s^ubH„¥|uøª˜ã—Q›üš1ïäJ'Âμ+{{ߘ¢c8QÁU8'Ì>SÁSÒ)¯°Œí¨€Œ+œ½ÓìœR
‹„ÃNìÉ©Õ+:9µ2©WDÅÚÝ%ó‰it"ÿ»U8WªúÍ=¥¾V¡Fv‡‰uõœUõI(õœSâ”6»¹Îš{Åu´ìÝ‘½ÄädŸû¬tÊØ|jºå¢cß
‚Ιô‰e™ü&V±1æ>ÓseOE±ç˾ôïu,±Ÿ÷ž3Nœ~¼ùœû½1ÞüãÕ7ÿøž°âÍ>>õžsŸýÞ;‰Ó7ÿÄìÄéÇ›"öN¢Ô{ÎýËß,³w§oþ‰Ù;‰Ó7UÝòp¤Ó4QmVPZRªÍ2z0ܡѪÂÁ6ݯÍ
ù<eÁ æ¸Yš©[º¹Z÷{´êvŸá÷j†¥yµ¨éõëm^s•n9NÑÀ€aEuS÷kFH‹¶êZ£§Þ£Õz£z(ªyC~á¨È–ç;Æ°xšÚÊvÓ°ü†/j„CÖÑKG;#z‹WýºeB–æ3u‘ókÍÚ| è¾¾SÞê5ƒÂ*9’˜cÊŸú¬pÐÿÃÈ“ï/Ѧhòó‹rr'ÁA³C”O×]öÜc¤œ•9fÚ'^¤$ÝF&çåuttx¼o[8¶—Àã·Ù¶æ./ lu$l{EX'Í'uöhå^©cc}™VRXœ[\P’›_˜Ÿ¯5Î× +Çæ+(žX¬O**-*õü+åµFÛ‚yö(/(E +YºÇ¶·ž–®Õ9[«Ì'nQk±nF
Ÿ7¨Õ†-Ã.äBÓo„¼A«¾=¢›FØ´ªõ¨¬”¤iésCâ"¾åÞˆ{ËÕáåÙÐ Lo¤Õði³×È[ÕFÀm—Zn“̵£ã*½ÙÔ;´ +Ãë3EÞgÕ·JŽ³VRßã¬]³8š¼¶Øk^™–=Ïj½åè4íq¹€Sre³3>6ÏJ½+!ÛÞÍ'^že†ßoX’“£c?óņ
y¥ôºVnoêÝ Îçóyžîá“ÑG¾(^ÉHA/¤"
½‘Ž>È@&²ÐÙpÁôCÀ@Â`ÁPÃpœ†)ÝfFãtœ31ga,ÆálœƒñÒm<ÈC> +0Qˆ"LB1JPŠÉ˜‚©˜†é˜™Òéfɳo…Ü“*1Uò0ó¥s.@t²Zy7“·Õéu‹±KåÉ}9ÎÅy8+pš¤ë5KŸóK‡k‘þÙ*}o¥Ü¿‚Ò÷ìwš.”>hIm—¾Ú!]°k±a=6àb\‚K±—ár\+q®Æ5¸›°×a“pnÄMØŠm¸·0™)ì…[™Ê4܆íظ»pv³7îd:îÂÜ{p/îÃýx + +¡0þ@E1ü‰â(’(…Ò(ƒ²(‡ò¨€Š¨„ʨ‚ª¨†ê¨š¨…Ú¨ƒº¨‡úh€†h„Æh‚¦h†æh–h…Öhƒ¶h‡ö耎è„Îè‚®è†îèžè…Þ胾è‡þ€„Á‚¡øÃ0#0£0cÆbÆc&b&c +¦b¦cfbfcæbæcbc –b–cVbVc
ÖbÖc6b6c¶b¶cvbvcöböã +£2£3c2c3ã2ã323 “2“3S2S3
Ó2Ó33233³2³3s2s3ó2ó³ +¬ÈJ¬Ì*¬Êj¬Î¬ÉZ¬Í:¬Ëz¬ÏlÈFlÌ&lÊflÎlÉVlÍ6lËvlÏìÈNìÌ.ìÊnìÎìÉ^ìÍ>ìË~ìÏÈAÌ!Ê¿8ŒÃ9‚#9Š£ù7Çp,Çq<'p"'q2§p*§q:gp&gq6çp.çq>p!q1—p)—q9Wp%Wq5×p-×q=7p#7q3·p+·q;wp'wq7÷p/÷q?ð ñ0ð(ñ8Oð$Oñ4Ïðþ˳<Çó¼À‹¼Ä˼«¼Æë¼Á›¼Åۼû¼Çû|À‡|ÄÇ|§|Æç|Á—|Å×|÷|Ç÷üÀüÄÏü¯üÆïüÁŸüÅßüO!¢$+¤B)´Â(¬Â)¼"(¢")²¢(ª¢)ºb(¦b)¶â(®â)¾(¡)±’(©’)¹R(¥R)µÒ(Ò)½2(£2)³²(«²)»r(§r)·ò(¯ò)¿ +¨ +©°þPU1ý©â*¡’*¥Ò*£²*§òª Šª¤Êª¢ªª¦êª¡šª¥Úª£ºª§új †j¤Æj¢¦j¦æj¡–j¥Öj£¶j§öê Žê¤Îꢮê¦îê¡žê¥Þꣾê§þ ¤Á¢¡úKÃ4\#4R£4ZkŒÆjœÆk‚&j’&kŠ¦jš¦k†fj–fkŽæjžækj‘k‰–j™–k…Vj•VkÖjÖkƒ6j“6k‹¶j›¶k‡vj—vköjŸöë€ê눎꘎ë„Nê”NëŒþÑ¿:«s:¯º¨Kº¬+ºªkº®º©[º;º«{º¯z¨Gz¬'zªgz®z©Wz7z«wz¯ú¨Oú¬/úªoú®ú©_úÿÂaZ¶C:”C‡Øâ€Ã8¬Ã9¼#8¢#9²£8ª£9ºc8¦c9¶ã8®ã9¾8¡9±“8©“9¹S8¥S9µÓ8Ó9½38£39³³8«³9»s8§s9·ó8¯ó9¿¸ ¹°ÿpu1ÿéâ.á’.åÒ.ã².çò®àŠ®äʮ⪮æê®áš®åڮ㺮çúnà†näÆnâ¦nææná–nåÖnã¶nçöîàŽîäÎîâ®îæîîážîåÞîã¾îçþàäÁâ¡þËÃ<Ü#<Ò£<Ú{ŒÇzœÇ{‚'z’'{Š§zš§{†gz–g{Žçzžç{z‘{‰—z™—{…Wz•W{×z×{ƒ7z“7{‹·z›·{‡wz—w{÷zŸ÷û€úûˆú˜û„Oú”OûÌÿÁ P +¤D*¤F¤E:¤GdD&dFdE6dGäD.äFäE>äGD!FE1G ”D)”F”E9”GTD%TFTE5TG
ÔD-ÔFÔE=ÔG4D#4F4E34G´D+´F´E;´GtD'tFtE7tGôD/ôFôE?ôÇ +¬Ä*¬Æ¬Å:¬ÇlÄ&lÆlÅ6lÇìÄ.ìÆìÅ>ìÇÄ!ÆÅ1Ç œÄoøàOü…¿ñþÅ)œÆœÅ9œÇ\Ä%\Æ\Å5\Ç
ÜÄ-ÜÆÜÅ=ÜÇ<Ä#<Æ<Å3<ǼÄ+¼Æ¼Å;¼Ç|Ä'|Æ|Å7|ÇøŸøņbh†aXIÑÇðŒÀˆŒÄȌ¨ŒÆèŒÁ˜ŒÅ،øŒÇøLÀ„LÄÄL¤LÆäLÁ”LÅÔLôLÇôÌÀŒÌÄÌ̬ÌÆìÌÁœÌÅÜÌüÌÇü,À‚,ÄÂ,¢,Æâ,Á’,ÅÒ,ò,Çò¬ÀŠ¬Äʬª¬Æê¬Áš¬Åڬú¬ÇúlÀ†lÄÆl¦lÆælÁ–lÅÖlölÇöìÀŽìÄÎì®ìÆîìÁžìÅÞìþìÇþÀÄÁ¡ÆáÁ‘ÅÑñÇñœÀ‰œÄɜ©œÆéœÁ™œÅٜùœÇù\À…\ÄÅ\Â¥\Æå\Á•\ÅÕ\õ\ÇõÜÀÜÄÍÜÂÜÆíÜÁÜÅÝÜýÜÇý<Àƒ<ÄÃ<£<Æã<Á“ü¿óþÉ¿ø7ÿá¿<ÅÓ<ó<Çó¼À‹¼Ä˼«¼Æë¼Á›¼Åۼû¼Çû|À‡|ÄÇ|§|Æç|Á—|Å×|÷|Ç÷üÀüÄÏü¯üÆïü?ø“¿¢P +0 +«@%Yá^Q‘YQUÑ]1S±[qWñ_ ”P‰”XI”TÉ”\)”R©”ZiB&+mÈ¥™«ô!”!d¥2†¬V&eVeU6eWåT.åVžoÊ«|ʯ*¨B*¬"*ªb*®*©R*2*«r*¯ +ª¨Jª¬*ªªjª®ª©Zª:ª«zª¯j¨Fj¬&jªfj®j©Vj6j«vj¯ê¨Nê¬.êªnê®ê©^ê>ê«~ꯨA¬!ªa®©Q1«q¯ š¨Iš¬)šªiš®š©Yš9š«yš¯Z¨EZ¬%ZªeZ®Z©UZ5Z«uZ¯
Ú¨MÚ¬-ÚªmÚ®Ú©]Ú=Ú«}Ú¯:¨C:¬#:ªc:®:©ßô»þПúKëý«S:3:«s:¯º¨Kº¬+ºªkº®º©[º;º«{º¯z¨Gz¬'zªgz®z©Wz7z«wz¯ú¨Oú¬/úªoú®ÿôC?õË!åÐã°Ó²ÎáÁÉ‘ÅQÍÑÃ1˱ÇqÏñÀ ȉÄIÌÉÂ)Ê©ÆiÎéÁÉ™ÅYÍÙÃ9˹ÇyÏù]À]È…]ÄE]ÌÅ]Â%]Ê¥]Æe]Îå]Á]É•]ÅU]ÍÕ]Ã5]˵]Çu]ÏõÝÀ
ÝÈÝÄMÝÌÍÝÂ-ÝÊÝÆmÝÎíÝÁÝÉÝÅ]ÝÍÝÝÃ=Ý˽ÝÇ}ÝÏý=À=ȃ=ÄC=ÌÃ=Â#=Ê£=Æc=Îã=Á=É“=ÅS=ÍÓ=Ã3=˳=Çs=Ïó½À½È‹½ÄK½Ì˽Â+½Ê«½Æk½Îë½Á½É›½Å[½ÍÛ½Ã;½Ë»½Ç{½Ïû}À}ȇ}ÄG}ÌÇ}Â'ý?ÙUØHn…ïâØŽ¾q²Ù½^™™Û”¹ÍmܬwsÉv“ÜÝná*{äìì:…+33333Ã13333T3ÒŒ”ô‡GŸžžÞ{ß'i¬9œAŽ$G‘£É1äXr9žœ@N$'‘“É)äTr9œAÎ$g‘³É9ä\r9Ÿ\@.$‘‹É%äRr¹œ\A®$W‘«É5äZr¹žÜ@n$7‘›É-äVr¹ÜAî$w‘»É=ä^r¹Ÿ<€}°/FPÁ(ª¨¡Ž1 +ÆcðX<ÇðD< OÆSðT<
OÇ3ðL<ÏÆsð\Láyx>^€âEx1^‚—âex9^WâUx5^ƒi€Í˜A¯Å,¶ …؆9ˆy,`;^‡XÄ–qÆ!؉]x=Þ€7âM8oEØ`èÂŽÝèÁƒ +ŸÆgðY|ŸÇðE| _ÆWðU|
_Ç7ðM|ßÆwð]|ßÇðCü?ÆOðSü?Ç/ðKü +¿Æoð[ü¿ÇðGü Æ_ðWü
Ç?ðOüÿÆð_Ž#p$ŽÂÑ8Çâ8p"NÂÉ8§â4œŽ3p&ÎÂÙ8çâ<œp!.Ÿ—â2\Ž+p%®ÂÕ¸×â:\p#nÂ͸·â6ÜŽ;p'îÂݸ÷â>ܬ}¬}«bZU«fÕ1‹X°¬F&lRïÐг+~:µ¡O£„SÏæÝnfSߧ ƒÜ£Ï§<H˜É)VDmÞ‘xí÷£po®áÓN´ÓaABZv˜Ðv¸ÂHÜMÜaßeAÕçÂøܶ=&†ëÓrFʶ6ÝŽØ +«Ñ¼©O‡N°^ÊÖÚÜáQ'õ»Ûku4&›ËLÕf:T´lÚã$ +EöúŒJÖT-SI›2)ËÒ,ƒ±Õ›ª&ÛZ3^c2ǬQ–c”5[Fr +$|C¡n›yážNŽ“4 +˜-G·‘\G·´i4êŠG•pÏf5ž7õ–b“–dÂ¥|-U5—íHkëßmm5rì6ð6]™ÕÓ¸Ò‹hmŽvÒ„Õ¼¼±æOÏÀsZkËÓ˜Ì öwÜ„xªÍˈŒ8oD 4®ÍK©s cVšSaS_P +„J©@(×rÁnËšIX ÚÙD2ó#sddÞ!3G²Y”sb9gÑP-Öxl©Ãlîyt,Q ¶$Otá£KÙR&ÙR.Ë¥LåR.+"©"²,‰¤yS]ŽxàTÓìI–KRiêËj©SuR6JÜcàj\Û% ®Ê¸»Ê»Z ªN\]pÃ(¨†ùs9¦Ù³æÊh5c/8ÑxL¼TVXÇoåÓ×5eæ\ˆ%3åzÐÈ*Ë_4¢cÙœE,æ±Àu7{Y›‰%Ú
n84J}¦™ïX¾&ù¤0ÉËÈó–E‰Ôúe™¥ Yœ8ô¸Æ± œÅ™PýL˜,šš3%p5 ƒ0ž°9•Uå]L{}—æÐì%ì1NÍ~ÌE„Ü<ÖLÔx+T¨±¿1åxcÙpÆ‚Ïé´‰÷µÉªy²ÑXB«³TP««D£»„©"U—\F³LÕmâ=žr¿mÓ‘Ó‘ùtä^W©G¶óÊ7¬.rǧ•%šÖU•í.¯l¿í1o™%L*Ò +M=‘Ôc‰´L®ÒëhBÓàP»˜ÓHxŸ¹*˜%”ä^¥Ñ HÒ§q™Ñ²„¯(œˆ ‘ÄãNöÖt§do“ìùéÀ¦¾Ê-L¢À!÷•Ï䀊[òP|ª¨Ðû‰ÿO±©#.é^‘®OW™Jaù© .ñ„O]Ú§=Õmø,êc›²ŽK{¬›–al9PÛ_—r þaeÛ$öuÛ,†~(M$ô˜Dà^:TÐf./ÔâAÙiŠ›¨¸ô“Äfg+*ºØÞ íË&
}ñPåoÌ
ñù”í |¾Ë#>(4…´E\Æ2"4ÖNbúÅ@ÚN‹†-”ŽÝB[&¾L´«HF´ËÊ1SUˆ#Ëq×*v¡Ñ™Hû}•]qM`*iÓr¸Ï¼riÊ
;æª\d…{lUlµ}‰[8’¤DÃÐV‰{´›ÿ÷«^ÁA|¸ø¬pÊ=|¦!%šÒÀœ-º91»ØgÐÅŒ÷™á?ÖW•O&±¸S#VnPQa©k)=2-Zí† %¾TGž&/¿ÖŠCÓ/ü˜‰;J(î%²?1HÙÿ讲Þ6r$(ŽìÖ-ß¿"X){>Xy0˜`¼˜]?äiA©)5ÓÛdÓŠüë·X<»ÕÄQÕÇbÝM²dÃ;ÎHùaEouJ’¦ßþh„xs$Ç.ñ=mO4ë +b¹
åεhhVgòÙ¥”Ñ‘?(-í:IÌ=™@£øMŽL„ßþA˜{$)u)€fwt…ðªÕxõžòŠÆ‡ÌÓSxÌ)uv÷Înߊ¥rOAÇYWJÔ¨â®0‰:dji +”K%\–NNj¶'>\™Sý\lN¤ì…-’–F1COàÌÓ`s¤råR§-·æaHrI:ZlÜd‚ºRï9¸jIæ ‘ì§S³ó‰²\!6Ì)^ÂI¡Ç=}¡äž>¬„kŒê{¡áÖÀc“q%I•Êζ\sV54•Ú€ù.205ÿ¨ñ
—ÔŽ‚©5ô^ê›ë(=EG ~¾?F·ZòÃw6HTÊÐ
ïž:¸nXèÂ`d¬;S°{àÎàÊüôH0iÝdK„Ïô±Ä<TN\¨¼/‘a}]DÅB`*fy_6³ß×Îì +h€¨ŠFA(%òIÎs û¼9|ºHOV¤ Ì1–‚© +è.tR4;ª4…R»ŒÃ¥&H3#3±È\I.y絜øÉ +S-›¤®¯ËÜЙކڗ@W ¯Ô.¡²üÖ™vÙ¤‚Œ£h’ò”¢‚¤1W'¸I{S£Áù·®‚Œl²å[íŽÉHj^èÞ¤F£VxÏ÷ÆiDºDƒ&m€Â
U[¯J^¢4Ü‹'`"¸Pˆ
Z¼°I9™nâA‰qH¡¤ñ
P£#ÉxÆjô§€„*#zÅm&£.6ZžÖ„HUq«:¥áñÆ ’›–á déÅ™Y –vlHêeUP6_p,`íØÅÞÌ(Eƒ™°S·DPW£T&·×” ó‚äØ£'[òR‡Xr¯®$a¹±™ +U _ƒk*¯ÁK¼|m{©Ú^¾/Uðòµå¥Š¹áwT;<áOòÝû|ò>êOá[ùn¾•þ\ÒJÖdG/³SÑj55¿‡/‘ä38«á,èV0¥NôI*¼ZS¼v +ÞiÕîtµêƒ2Á/ +&ÈEÉŠâsE€¼¬©¤
‘Ÿì£ŸŽ¼J*z„±ä)ä%ø§œÕÃF¼Kÿ)Èæ¥/"ãÃJ•p¾_þK7HAáI-ÞÙNOù§—¬Là‘È074—H©z„¿Bæf1åÇjlµàÌ£(4EVç&,jovÀ3¢§ÏF ³aÖƆ5ª¾Ò?Z©Ñ—™i}cÏ©zæéÈ°fõ¶ r
Ùê]SÇ àg™±}sµ#µ„G¾LÙ;“0âÊöþ—éå^k‚£Zðt×ÀŒ=Uú.7*†¼n`Ã8¥PBj”B2‰óWó#$ººÈhQµFø^KZ©¹íl’-ÿ¹4•ˆ2:Á%^Ó +–ÇÛ‚ìrù¦ˆ ã#\ÞÔЃë‰Âf1 +~à:-×oðvideš‹B:KýüÎZH½M[ˆ¤^´Œ öÖ–Éè;‰‘{x=‘ý™zŸi¼~S[Õví>ã)3îÄðƒ â
ÔT1þˆŠ{;²”É9:ñqÄ£wÑÆë:ÖÀ߆H»`GÓÂDKùxгøÜÒÅ w¡m:Ñ›6„Q¶!t¯
a¬)nz÷àçZoLÜYzŒBl/tàVç’ +MÛ‹´©`Ê Y4¬¦YÌúÎDÄ— +¶þº„y±† I‰8Ä*à>È#þùœîaèý‰ +’miuâ%`ë"\c±e +[¢õÌt`-c +²J¤£®~±ÔÕoŽÈñÍ…#~wDåˆ?!,‘üÇi<õ¯ +|ª\j +^¦É/~í7Oåžúæ©ÂS¿{ªòÔž> G,Ÿ:ÌjIΧÎÔ’1ÈÝSψ³º#}è™Íõ™Íõ™Íõ™Íu¯Íu¯Í~Ù¯Ëîl»Zv‡ÛU,c£§gÑoz£§½Ñwm®Ïl®Ïl®Ïl®{m®{möË~]v§ÙÕ²;ήb=?‹¾oæ]Ýõ
½«3›ë3›ë3›ë3›ë^›ë^›ý²_/49ÑÿíÊí—N€ÁÓj°Y
^Vã’¤)!‚lÙnf×pÙ"›Àgi¨isÔߨ4ܼɥ¿ñ¼~EllA¥ÜR«ì! +«´.%o!}é¡ß¬ºæG•T'ÿNx¦’L#3¬È9ÈTêA¦ROd*Y‘©Tä™ +®u%ÈÐ…²Â}¹„m=ÏauÃ^ªyš”y'_UIž×^Á韉›rÃÃoTüÔ‡ûcÒç_iÒHŽ¨X³Ft¥¨äcpWÉ"®„`‹z4•Øö¡¸YpÄkFå”ûÝmšHÌ´;V1,’}Ã"Ù3P£ôn{U° ˆõ3cÀtqNqåþeðƒÆPQ?Æ +cbq" +";s\hŒ "M + +eqK- +jq¯,jeqO-j"Ššü Ò+I¯þ‰ô5Ò7H/‘¾Eúé)ú ¿áK¤>ÒÒ¡æðj>ãeõš¼¡gx"Å&_ +8"û3ob{eЛèÐcšïO01±]úÌ”ºWzö3ŶßËÀí9öÝZ+Ö5ú j+îC³‰CîðCS¡™Þ½s{±Á_3H4ï\ÓS'!bë“$ÙW?©\jå>é•[(¶óYi¦
ç*£”ÊÝÏÚSªXï³½6©´¾È—rÏ}Ñõ\êz~Qù,Uæ_µf¦5¿*ÍLkêÌ2ÙW;³ÌÞ57¢Î"¯^®yGâM¥œˆG7æ¦ÍMáÆÜÁ¹!´þ’d%kð—žÙJ³·š-ô|oÕ|µR·ÖJÖJÝêzšý¦ý—Úÿ7å¿Tþ¿‰4•Ž[Zò7qM0Æ©|¢|j¨Üƒºwšt>w*ŸJïžD¥×÷Î^ßÊô%LŠ5{÷ÆWkÞ»·ýÖö¾¹7öMy÷^Ö¿ ãÖTný-t[kA:«™®®õ]l¤ÖF’ïr×lä×ý]õ—MÃu¿ëêÊv¾+ÏUôæoùòù[ùüWknÛû¡‹ÐÛi¾óCÅÚ©X?ôzí4ëöS5ÁžløI÷dËÿ;%<æºÃ
Ä¿zù©t€tˆt„ôé+¤"}ô
ÒK¤o‘¾C:F:Az%©ñ}Œïc|ãûßÇø>Æó1žñ|Œçc<ã
Ðÿ +Þœ)8Sp¦àLÁ™‚3g +ÎÔ:Ûð(\GºAü)GÏ¥#±p¥Ï¢eG×d¨deÏÅ"‰¸Ãb·åR³MHæÍXtú+ %WÍÔÅ%Š¹s¦gæ)‘y2è䨳¢Ç™Ã£¦\‡™¡\ºú`R•}””C”£:›Ñ½¼é]gµYí¦¦¼¾<òE ŽI1·é¬ÕXEÂç2+·t=uµm¤&>‡3×ý:±=æÓPŒòî}„{&Ã\]ÂýK¸}y5¬±`Ë(ô`RèÕåÍ´vO“M÷€ ö¸uåøѸÑêÓ0‰PÏh÷ ^L»ò20UKýÓð‹È:ÄÛ†åe-kî¡'ã‚ÉTNPZ(§µ0íI¦®Ékó̵yæZœºo]ã[ׂÌ5™ºÃs®É]×zp×zVKºa3úCÛVýëJÀfÓÖÕ出ú×ñÊKâ†Qº†žsdÿN¾*ªVð@-^µ|]vôœå춚øÙ¢9ßZ©^úº~ò5 l™0)BÓcýѨ}f"2½=Ѩ¾«í¥ÍãïaäüSï²£§§YMƒéMËÏ"¦®böU' mó+aÞŸ¾ ©ŽÎV,’‚yŽ˜Ï!ĉïk8„Ž$§›AúM;bŽ°·]°Õ* +_ò½¶ÓI2&f±Û°=a/®vuS×=&9uÜÎbE¬£Îႈ?ž3šÅÀåB3Úëñ«?½úŒKSk,h§y¨;ÔzóÁiæHߌóÆcÞ9ãdž]þûÌÓSø᣹ +P91å
ì›ÉÉ“Ëgj_Ût‘ðh<'›¡=,š¬`žÓûH¦°]¥s[ì±#Šö9ù°ûÍùf_;{Ú—6{RçÖ\·´¢}z»ç±ØSî÷”pW13h’•“ô +›q“î·½>“]åí77 ,J|%2S“2õ|\2kö¾vñƒG{ ‹×±1Ù?i°m]ðfzÙ2»;¢†dW]™D”´ê‚ÖmT”,{TbÛ]³ú^bŪ¾iÑöÕÕãcV#ÈQA®¥fU¢Ý3Ê5ÊWS6^(ɉX†«ó\=u#¯‰€4J¾mÊÆ…çqÃpY]Â÷e¾†”»)Ÿ¿±6îá÷× ¨<¨ÁTGÔ ½©ÎPNLÙp¸'1•{=A3•ß–¼B_‰¨Ð@ô‘A
ªúdÞU7¦l$õöpM°–Ž0ŒÂèä“iV£f6£¥Uã¨T6ÊûÊ“zÖÿ“úÞF
cÕ¿ñʲj!Ù¤òcuöתڦªj¥b¢Žnzõ¡þ%í«5›Q”Õ~r¹ñ%[Ë‹]åBíjS(¢ãK&nJÄó;¶¯-K4+Ø×ø¾vú<aj’·;mWy—µ)zý.)×Ï?—øãîkã²QÛ%bóÁuYuWÌmw™ -½Ì2+Î[{³&3;,Ú“
g³¥ËbÁ²ðsᔉÜq8—±XlŸD¬Xj1·¸zfûZ[¿ß±tå§ì÷å_¼8¹c;ÂrGxO³Xþí{û¯Þ4ãÜ|¶¬€dfñ¼uzÇftŒXŠí‚Yî*çO%ËtS¢YqÉ’,ÑK¾•ûZ둲ÿÖo™·^£ƒ{qŠÃáÂ=|¥öéÊëö½Êüx)<‡WD¥[ +ÎlѤÕ[3m‡jŸ7ø6àTTŸU jÿÔW8F„¦|C´"AÓɼ¹ŽcÍÔR·˜FZlvêP´x•bÉèfƤ\¤?Nµ`3jKâ»\äÉëm[g)äNBæ™9䨗KR‹NWÏ|Æ)„)‹Ùt“I*ßêSG³uÍÚ—ÎæE~©ØÉ3üÐ95—$u³é°Ëc~ª.ÒÍ)gùoˆHŸ5Í1Á
=&ÔõˆL½èVê¬þí×ûWþš«–¹Êbê¯:pÂ5Sí%ÕëÉ +µš¾¨×‘jŒ\¥ëç9%NV4Óaô[1‰¤r'*6kiR«KµDú$“ܦ¨®]ºß&ªÕIf*¿‚šv5Ù §A
ËM‘õŸY-Êjë¬öš9º`´2ÝËjqV{Ik´è(Mˈ"æ"¡¬†²#©B²Ù€“éßJFšö†ü;F99Së‚6!¡Œc~óy^riŒj– ÁlÁ4¤–/œÌk„rØ¢ÿI«Mýõ–Åv$V²…~¦ŽdÂëè¹Ñ§8ýÓ+Ž>P|ÎV¨¨Ûe›L•úö3Œ]"þ=Ÿ$÷ßÚÿïí[:zÐhiI(ß2uQ¦^|-Weß“G¢<T`ÙejûŽå?m/‹¦L^0é±7":FìR§ÿ½ëA\¦¾Ãq`OËõ£?t9òuѦL¿¢ýðK_ûÖÕGZP±x©JSVFzyŒõS¯¤ÊD?§úùI=›ŸÔ²A~©ÜéKýÂcþÌAn¯þ³õõJíèÊWÝt¥žg™È£›ES®ÔBdžnò¨ŸR=Û…{å¿úùC?_uô£¥s]®Cœ—Õ˜æâ™×$*o±M³¨f2Ÿm'‘Wד¬z:-œvt€,(¶ªS‰¹³©Ž>éÍR‹˜»a4=³Øt>®x¹e[)Å¡GW|½Ó‚tjÚA¡MõÞÒÝž„>_˜
jൕ+´Yæ"Ö1ø4«mï_ÊzC3|†¤þÓXÉ'$Ç¿&³s +xšQyVýЮ´7Åÿ°Qߨ¹W屫‹•ÚÈG#ݼ¡gÎ8}læH¯Ÿ”ûbÒÔ«üÒþçB¨qóýž’úZs6ÌS?õÐ!ò‘£8ñ¡¤AIý?甼øÀÛœ¥— +Èöÿø®¶9Š+ÜÝ{›ûew<{³Y›( h¤Ä2R¤_¼f7`l¼Ž×2R„z¦kgÚ3Ó=îË®gArˆ)O ËÚ%oü4« µH„%!WÉà)"<å™ +,ñЯØzªUð Añ¨²kz–ÏKYèM¯ñˆ9³&Ò€ŒÉDH*œ)G§ŸÍº³t¿‘ÀbÚŒ-Úé—Ĉ‹¼)\ÁU`/‹(YU1ÝäMn“ÖâdÖõ@wÍ¡_t9qå(÷’|ÆDžÙ…|.¢9ÅϼÛDtwûDO_»^MíZ¸Õš\„k¢¹.¶åµÐ²M÷0RJÑfFüôOÅäWDójd©=a©úŽk·~šÞÐi3äÛ3 cÚ"ÿÉñ<WXó’Èlù©\±Å:Wº8ȉò†ºkBÚ5Yæp’²Ü</©²<MÙ³®(5ºº1^Vf_PÔ‹Šº ¨—uQQ—õ²¢.«.?¶Ã¦‡'Á+Šºª¨kŠº®¨We*EÍèX"¾ˆô«£z+òv«‰MEÅS“y¾®8~Í%7—¸ôjf[‰aõtÕ—TÉ£B]µ‹›Ü¥"¯²Ú6q¥Õä¢E)kVÕ=VÂõÇ„Û‰íÝG xö1³q"¦ K¹F¥êE·b:ª)*™œ“aLî(²(¶¸ßW±ÊãÃw“L+Áä¨C™ _•EÏxR]P +F´Ÿ§]ðq3É°Ðs‘d{(+†3dùŽÝάÇPP:¬¸m¶|»o£Ü±ÛNå…Èš¦ƒjó·+—Æ€ÙKêÚ+膪%”•Çôãêíé–é1w;k¶ZH)w»dZ<ΰy“µ‚™–i™HG3Ïî³¾k呹HºÔra/!Ø¢9àsСβŒñžÛ7–U%#¤°J€{2Ð9èØžEu/'ü¯v·ƒAœðóÜ´‚Îm»¡'HÉO¡"ÂP,Ìe»Gf’¬U`á ‰Ð(*[“X
gI0ÇA§Mý}Û²zŒgr° Ô¸$@!Åÿé2[awå±*²dW%²Vµ +ü½GL9©-uî1O¹†¬zK'`uÓ@û]ÕD'M×Én!çèüQs¢íÛUžm'
ïø³ï';;z±³#E”plGM*z¡ò%¥ÎG™˜v+Dt“®'ÉÒGùŽxɵ³±³!emg[ˆ3)êJºÈ¯™ä +tÑbÆÞ‰ç@55‡«¦8®ššDª)†T“L¤Z<T“\¬šDª)Hܨ[!ó©Úë›^7o;8ÏmšN»ÒôÌV—´Ê±¼|ŽÐõ“]ò4Ã^Eaø'F«V•ƒaÈ]é~Vn….¼K³'{f9 +Êíºžu–e^ ºY^8€‡#'’õwC§$¤SnÄìãˆÈ!dPàÐEcøfZ=×gá "™ìÌ +QqlyrdP€×š=¨GÚò4T(rŒƒqr*Ъƒ\×cfòŠµ*4sW&®U>'…ð—™Ùtw(õ-òû/ûkä(pÒ8 –ÂAr +y¤$À¯„dæ;®Û˜‘¸ H%~Œpèé¹Xû¶ZºéÙV›©É8
ÓCˆŠŒ%qr\¸W\^‰¹2w<ÐÄë™CÚ%Ê{©ØˆÑš@qæí¬ûn϶ðLÓcEpzpÉÇé{JβÏÌ6NAòÙÛÒœ.t44òòu„ÀîVÉÁÃiÄD¦U…›€_’›ÔÅS¡µi±ÇT"yù‰Aæê¸}$Ü/ª *pwˆ-|éÀ•“ã8Aª…ƒ¸¢˜Â"¯€äHÇ)p–¢«CWH‚9¼@^Å‘ÔUH+¥^zY ŸHÃQäSKñÞñ]'i¿INÙô#M'Cú‘&-úuèקé4Ðo‡~·ñ›A~D/} +™E‹eÉ7Afe¶`hÊä2~D”®we‘ñ}Ø´¦Ø#Ý8Ò~†o«±ö´¶iÌiºöþ.¾;øÞÆ÷.¾)M3fµ +¾“øNã{ß”1‹¾º¶ÌÛÅGô³ùÕYŸ?þégø½þF}ùõ7?þôÎ.~ý~=¿®S_~
>âŽ3¡uõ®sÇ[ +ÂÚ±ãí›ømÛø±NmÙíÜé¼Ý™ÐX…dï±/ÙÔ¦³Î/o--úõ½çO
ñÆ}ãÀØÿÍÙ•ÿ>2GºqˆæÜ‘®½ŸË~µ?ÓX+gigµã¤±¢Mk
ãýmBk<z`œU««¿3NÇ#àøè§Wïsbq)"Ê9®æ,ææxÏÒ¨XBÏ"zhÕºþÍhº‘[;®éúgú'š¡5ôO£öŸQûqÔ>ŒÚèñöïQû·¨ý+Zì§ÿ%jÿµÒ?M6òkYý8¯<Ðešñè¡þ‡ÑW!×ç VNEDm^‡ÙÒjù·ú›Ú +>CoŒ&`«ïîRóìáݙ컇ÙÜ*Úïæ¼?Ò—Gû§VŽôS£»+hD3ù>Lû-?zôàÜÙÿÌÎþâ^¦ñ«ýÉÆÝ{zC{Kk?׸»?Ѹ·o4~½fåÝwô‡_|u0q°¯7Îí/.¯žÛ‡±gôæTãÈè>œj¥›‡Xúç/j:¸åÃgž…|hëuÞŽ +E~ ÇF'ŸŽˆù…ÕûÆ2ˆ ˆfÌê¼£6ÊÐQÓ¿q™¿>„˜ÔŽpuïë_è:þ‹Qmqu-§ÿ^ÿŸÆÿè(Ÿß¶É0Ž¿fÍêÄI¨YÚ¨uh²®Š³µK¡ÃëK릫&”Y×CB/Câ +:±N€nÅ×^Ðÿý{ùû¨.{5ÞÜÓ4Óg"éÑRö¢QóÁ®lìÖ{}‡â¤ï°“Ëù};ÏšŽåTÙp)ÓºwtÜŒß#ÏÑö§ÜxÂ=ƆÝcÄ·+ÛÍmùC‡´=0`fÚ³íËíöVûn;üþ*žžWUó|0gýä´?g=˜»ž/t˜î0ÛYK½?eÖmÉhÚ!㲬Ú~+}©T1/Ù5oBûðaÓ²u
>†¹÷$Y +I=L¡‡´Gƒ÷i—0ߣNÙ,TdI¥])Á¢R1}`˜ö¤^Pe\Š@aHf¾œîC—éú9×é3úlÒ'ô)x
ü†Eé*ì_ƒW`ÿ +¼†˜«Ð/Z‡šÐË4ÍbXË4qzñã4AÇÁ¥càiðUÄÏÁ>ÎÀn§;Í@ÓÐ8T°Öyú„–šÒ´—´ä‹š:©)E-r\Ohò¸ÆŽi£GbcGÔ¼+êH6–˪Ãz,£«j<¡Dö)ὊêQI +““º.¿&oÉå®ÎªUNÓP4u`0ªÅE“¡þhçùå9>Â3|˜§yŠk<ÉUáa.sÆ+“$’eV^*‰ç<[“F¹%gEÑ(‹He¹ê}\ÃS!5ðgZ¡FK’ó¯/WqÒ=³¾ŽÃÏDùœýQÍ‘XIPCdÏV=Xgª"ÓhÅÙRÕ‘¨T«Õĉr¥êyÕŒ!ñfnõ¡š(zæP•ÅÉ3"-Ý®••§GÎØè‚È/¼! +çNuõæå'¢nÿö›w»Øº
VVºz²'ëýŸyŽg$|rÑ|5]ód"%,”É‚¨§‚[t'³ðΩýÚÁoüÞÁü顳Z'âwf±T½‹PeYf1øƒ)”lé? +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d' bytes='2989'?> + +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' + xmlns:iX='http://ns.adobe.com/iX/1.0/'> + + <rdf:Description about='' + xmlns='http://ns.adobe.com/pdf/1.3/' + xmlns:pdf='http://ns.adobe.com/pdf/1.3/'> + <pdf:CreationDate>2006-04-02T13:01:22+01:00</pdf:CreationDate> + <pdf:ModDate>2006-04-02T13:01:23+01:00</pdf:ModDate> + <pdf:Creator>Adobe Illustrator 10.0.3</pdf:Creator> + <pdf:Producer>Adobe PDF library 5.00</pdf:Producer> + </rdf:Description> + + <rdf:Description about='' + xmlns='http://ns.adobe.com/xap/1.0/' + xmlns:xap='http://ns.adobe.com/xap/1.0/'> + <xap:CreateDate>2006-04-02T13:01:22+01:00</xap:CreateDate> + <xap:ModifyDate>2006-04-02T13:01:23+01:00</xap:ModifyDate> + <xap:CreatorTool>Adobe Illustrator 10.0.3</xap:CreatorTool> + <xap:MetadataDate>2006-04-02T13:01:23+01:00</xap:MetadataDate> + </rdf:Description> + +</rdf:RDF> +<?xpacket end='w'?>
endstream
endobj
xref
0 409
0000000004 65535 f
0000000016 00000 n
0000000088 00000 n
0000000152 00000 n
0000000006 00001 f
0000000321 00000 n
0000000049 00001 f
0000001342 00000 n
0000001421 00000 n
0000001856 00000 n
0000002265 00000 n
0000003493 00000 n
0000009907 00000 n
0000022661 00000 n
0000022684 00000 n
0000039791 00000 n
0000039814 00000 n
0000060651 00000 n
0000060674 00000 n
0000080690 00000 n
0000080713 00000 n
0000089845 00000 n
0000089867 00000 n
0000093572 00000 n
0000093594 00000 n
0000115452 00000 n
0000115475 00000 n
0000137402 00000 n
0000137425 00000 n
0000152310 00000 n
0000152333 00000 n
0000160521 00000 n
0000160543 00000 n
0000160572 00000 n
0000160601 00000 n
0000161657 00000 n
0000161759 00000 n
0000161996 00000 n
0000163215 00000 n
0000163323 00000 n
0000163559 00000 n
0000163646 00000 n
0000163744 00000 n
0000163901 00000 n
0000164010 00000 n
0000164036 00000 n
0000164065 00000 n
0000164087 00000 n
0000164173 00000 n
0000000057 00001 f
0000164199 00000 n
0000164297 00000 n
0000164453 00000 n
0000164562 00000 n
0000164588 00000 n
0000164617 00000 n
0000164639 00000 n
0000000065 00001 f
0000164725 00000 n
0000164823 00000 n
0000164980 00000 n
0000165089 00000 n
0000165115 00000 n
0000165144 00000 n
0000165166 00000 n
0000000073 00001 f
0000165283 00000 n
0000165381 00000 n
0000165538 00000 n
0000165647 00000 n
0000165673 00000 n
0000165702 00000 n
0000165724 00000 n
0000000081 00001 f
0000165841 00000 n
0000165939 00000 n
0000166096 00000 n
0000166205 00000 n
0000166231 00000 n
0000166260 00000 n
0000166282 00000 n
0000000089 00001 f
0000166399 00000 n
0000166497 00000 n
0000166655 00000 n
0000166764 00000 n
0000166790 00000 n
0000166819 00000 n
0000166841 00000 n
0000000097 00001 f
0000166958 00000 n
0000167056 00000 n
0000167213 00000 n
0000167322 00000 n
0000167348 00000 n
0000167377 00000 n
0000167399 00000 n
0000000105 00001 f
0000167520 00000 n
0000167618 00000 n
0000167774 00000 n
0000167887 00000 n
0000167914 00000 n
0000167945 00000 n
0000167968 00000 n
0000000113 00001 f
0000168055 00000 n
0000168155 00000 n
0000168312 00000 n
0000168425 00000 n
0000168452 00000 n
0000168483 00000 n
0000168506 00000 n
0000000121 00001 f
0000168593 00000 n
0000168693 00000 n
0000168850 00000 n
0000168963 00000 n
0000168990 00000 n
0000169021 00000 n
0000169044 00000 n
0000000129 00001 f
0000169131 00000 n
0000169231 00000 n
0000169389 00000 n
0000169502 00000 n
0000169529 00000 n
0000169560 00000 n
0000169583 00000 n
0000000137 00001 f
0000169670 00000 n
0000169770 00000 n
0000169928 00000 n
0000170041 00000 n
0000170068 00000 n
0000170099 00000 n
0000170122 00000 n
0000000145 00001 f
0000170209 00000 n
0000170309 00000 n
0000170468 00000 n
0000170581 00000 n
0000170608 00000 n
0000170639 00000 n
0000170662 00000 n
0000000153 00001 f
0000170749 00000 n
0000170849 00000 n
0000171005 00000 n
0000171118 00000 n
0000171145 00000 n
0000171176 00000 n
0000171199 00000 n
0000000161 00001 f
0000171309 00000 n
0000171409 00000 n
0000171566 00000 n
0000171679 00000 n
0000171706 00000 n
0000171737 00000 n
0000171760 00000 n
0000000169 00001 f
0000171883 00000 n
0000171983 00000 n
0000172140 00000 n
0000172253 00000 n
0000172280 00000 n
0000172311 00000 n
0000172334 00000 n
0000000177 00001 f
0000172457 00000 n
0000172557 00000 n
0000172713 00000 n
0000172826 00000 n
0000172853 00000 n
0000172884 00000 n
0000172907 00000 n
0000000185 00001 f
0000173030 00000 n
0000173130 00000 n
0000173289 00000 n
0000173402 00000 n
0000173429 00000 n
0000173460 00000 n
0000173483 00000 n
0000000193 00001 f
0000173606 00000 n
0000173706 00000 n
0000173865 00000 n
0000173978 00000 n
0000174005 00000 n
0000174036 00000 n
0000174059 00000 n
0000000201 00001 f
0000174182 00000 n
0000174282 00000 n
0000174441 00000 n
0000174554 00000 n
0000174581 00000 n
0000174612 00000 n
0000174635 00000 n
0000000209 00001 f
0000174758 00000 n
0000174858 00000 n
0000175015 00000 n
0000175128 00000 n
0000175155 00000 n
0000175186 00000 n
0000175209 00000 n
0000000217 00001 f
0000175332 00000 n
0000175432 00000 n
0000175590 00000 n
0000175703 00000 n
0000175730 00000 n
0000175761 00000 n
0000175784 00000 n
0000000225 00001 f
0000175895 00000 n
0000175995 00000 n
0000176153 00000 n
0000176266 00000 n
0000176293 00000 n
0000176324 00000 n
0000176347 00000 n
0000000233 00001 f
0000176458 00000 n
0000176558 00000 n
0000176716 00000 n
0000176829 00000 n
0000176856 00000 n
0000176887 00000 n
0000176910 00000 n
0000000242 00001 f
0000177021 00000 n
0000177121 00000 n
0000177281 00000 n
0000177394 00000 n
0000177421 00000 n
0000177452 00000 n
0000177475 00000 n
0000177586 00000 n
0000000250 00001 f
0000177672 00000 n
0000177772 00000 n
0000177930 00000 n
0000178043 00000 n
0000178070 00000 n
0000178101 00000 n
0000178124 00000 n
0000000261 00001 f
0000178211 00000 n
0000178311 00000 n
0000178470 00000 n
0000178583 00000 n
0000178610 00000 n
0000178641 00000 n
0000178664 00000 n
0000178775 00000 n
0000179999 00000 n
0000180108 00000 n
0000000269 00001 f
0000180343 00000 n
0000180443 00000 n
0000180599 00000 n
0000180712 00000 n
0000180739 00000 n
0000180770 00000 n
0000180793 00000 n
0000000277 00001 f
0000180880 00000 n
0000180980 00000 n
0000181134 00000 n
0000181247 00000 n
0000181274 00000 n
0000181305 00000 n
0000181328 00000 n
0000000285 00001 f
0000181438 00000 n
0000181538 00000 n
0000181695 00000 n
0000181808 00000 n
0000181835 00000 n
0000181866 00000 n
0000181889 00000 n
0000000293 00001 f
0000181999 00000 n
0000182099 00000 n
0000182258 00000 n
0000182371 00000 n
0000182398 00000 n
0000182429 00000 n
0000182452 00000 n
0000000301 00001 f
0000182539 00000 n
0000182639 00000 n
0000182797 00000 n
0000182910 00000 n
0000182937 00000 n
0000182968 00000 n
0000182991 00000 n
0000000309 00001 f
0000183102 00000 n
0000183202 00000 n
0000183361 00000 n
0000183474 00000 n
0000183501 00000 n
0000183532 00000 n
0000183555 00000 n
0000000317 00001 f
0000183642 00000 n
0000183742 00000 n
0000183901 00000 n
0000184014 00000 n
0000184041 00000 n
0000184072 00000 n
0000184095 00000 n
0000000325 00001 f
0000184182 00000 n
0000184282 00000 n
0000184440 00000 n
0000184553 00000 n
0000184580 00000 n
0000184611 00000 n
0000184634 00000 n
0000000333 00001 f
0000184721 00000 n
0000184821 00000 n
0000184980 00000 n
0000185093 00000 n
0000185120 00000 n
0000185151 00000 n
0000185174 00000 n
0000000341 00001 f
0000185285 00000 n
0000185385 00000 n
0000185544 00000 n
0000185657 00000 n
0000185684 00000 n
0000185715 00000 n
0000185738 00000 n
0000000349 00001 f
0000185849 00000 n
0000185949 00000 n
0000186108 00000 n
0000186221 00000 n
0000186248 00000 n
0000186279 00000 n
0000186302 00000 n
0000000357 00001 f
0000186413 00000 n
0000186513 00000 n
0000186671 00000 n
0000186784 00000 n
0000186811 00000 n
0000186842 00000 n
0000186865 00000 n
0000000365 00001 f
0000186976 00000 n
0000187076 00000 n
0000187235 00000 n
0000187348 00000 n
0000187375 00000 n
0000187406 00000 n
0000187429 00000 n
0000000373 00001 f
0000187516 00000 n
0000187616 00000 n
0000187775 00000 n
0000187888 00000 n
0000187915 00000 n
0000187946 00000 n
0000187969 00000 n
0000000381 00001 f
0000188056 00000 n
0000188156 00000 n
0000188313 00000 n
0000188426 00000 n
0000188453 00000 n
0000188484 00000 n
0000188507 00000 n
0000000389 00001 f
0000188625 00000 n
0000188725 00000 n
0000188883 00000 n
0000188996 00000 n
0000189023 00000 n
0000189054 00000 n
0000189077 00000 n
0000000000 00001 f
0000189195 00000 n
0000189295 00000 n
0000189453 00000 n
0000189566 00000 n
0000189593 00000 n
0000189624 00000 n
0000189647 00000 n
0000189765 00000 n
0000189859 00000 n
0000192465 00000 n
0000192488 00000 n
0000197049 00000 n
0000197072 00000 n
0000208254 00000 n
0000208278 00000 n
0000218773 00000 n
0000218797 00000 n
0000253413 00000 n
0000253437 00000 n
trailer
<<
/Size 409
/Info 3 0 R
/Root 1 0 R
/ID[<174909697a5296eb8594689fc782c55e><0e10b9eeb635850977ac283d5fcdaea2>]
>>
startxref
256512
%%EOF
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/util/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/util/package.html new file mode 100644 index 000000000..97f56c6e2 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/util/package.html @@ -0,0 +1,11 @@ +<html> + <head> + <title>Encoding Utilities</title> + </head> + <body> + <p> + This package contains a number of utility classes which can come handy + from time to time when writing Java code. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVException.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVException.java new file mode 100644 index 000000000..e263eeb59 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVException.java @@ -0,0 +1,132 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.IOException; +import java.io.PrintWriter; + + +/** + * <p>A {@link RuntimeException} representing a + * <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * response for a specified {@link DAVResource}.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVException extends RuntimeException { + + private DAVResource resource = null; + private int status = 0; + + /** + * <p>Create a new {@link DAVException} instance.</p> + */ + public DAVException(int status, String message) { + this(status, message, null, null); + } + + /** + * <p>Create a new {@link DAVException} instance.</p> + */ + public DAVException(int status, String message, Throwable throwable) { + this(status, message, throwable, null); + } + + /** + * <p>Create a new {@link DAVException} instance.</p> + */ + public DAVException(int status, String message, DAVResource resource) { + this(status, message, null, resource); + } + + /** + * <p>Create a new {@link DAVException} instance.</p> + */ + public DAVException(int s, String m, Throwable t, DAVResource r) { + super(m, t); + this.resource = r; + this.status = s; + } + + /** + * <p>Return the status code associated with this instance.</p> + */ + public int getStatus() { + return this.status; + } + + /** + * <p>Return the {@link DAVResource} associated with this instance.</p> + */ + public DAVResource getResource() { + return this.resource; + } + + /** + * <p>Write the body of this {@link DAVException} to the specified + * {@link DAVTransaction}'s output.</p> + */ + public void write(DAVTransaction transaction) + throws IOException { + transaction.setContentType("text/html; charset=\"UTF-8\""); + transaction.setStatus(this.getStatus()); + + /* Prepare and log the error message */ + String message = DAVUtilities.getStatusMessage(this.getStatus()); + if (message == null) { + transaction.setStatus(500); + message = Integer.toString(this.getStatus()) + " Unknown"; + } + + /* Write the error message to the client */ + PrintWriter out = transaction.write("UTF-8"); + out.println("<html>"); + out.print("<head><title>Error "); + out.print(message); + out.println("</title></head>"); + out.println("<body>"); + out.print("<p><b>Error "); + out.print(message); + out.println("</b></p>"); + + /* Check if we have a resource associated with the extension */ + if (this.getResource() != null) { + String r = transaction.lookup(this.getResource()).toASCIIString(); + out.print("<p>Resource in error: <a href=\""); + out.print(r); + out.println("\">"); + out.print(r); + out.println("</a></p>"); + } + + /* Process any exception and its cause */ + Throwable throwable = this; + out.println("<hr /><p>Exception details:</p>"); + while (throwable != null) { + out.print("<pre>"); + throwable.printStackTrace(out); + out.println("</pre>"); + throwable = throwable.getCause(); + if (throwable != null) out.println("<hr /><p>Caused by:</p>"); + } + + /* Close up the HTML */ + out.println("</body>"); + out.println("</html>"); + out.flush(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVInputStream.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVInputStream.java new file mode 100644 index 000000000..6a0976c71 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVInputStream.java @@ -0,0 +1,165 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + + +/** + * <p>A specialized {@link InputStream} to read from {@link DAVResource}s.</p> + * + * <p>This specialized {@link InputStream} never throws {@link IOException}s, + * but rather relies on the unchecked {@link DAVException} to notify the + * framework of the correct DAV errors.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVInputStream extends InputStream { + + /** <p>The {@link InputStream} of the source {@link File}. </p> */ + protected InputStream input = null; + /** <p>The {@link DAVResource} associated with this instance. </p> */ + private DAVResource resource = null; + + /** + * <p>Create a new {@link DAVInputStream} instance.</p> + */ + protected DAVInputStream(DAVResource resource) { + if (resource == null) throw new NullPointerException(); + init(resource); + } + + protected void init(DAVResource resource) + { + try { + this.input = new FileInputStream(resource.getFile()); + } catch (IOException e) { + String message = "Unable to read from resource"; + throw new DAVException (403, message, e, resource); + } + } + + /** + * <p>Read data from this {@link InputStream}.</p> + */ + public int read() { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + return input.read(); + } catch (IOException e) { + throw new DAVException(403, "Can't read data", e, this.resource); + } + } + + /** + * <p>Read data from this {@link InputStream}.</p> + */ + public int read(byte b[]) { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + return input.read(b); + } catch (IOException e) { + throw new DAVException(403, "Can't read data", e, this.resource); + } + } + + /** + * <p>Read data from this {@link InputStream}.</p> + */ + public int read(byte b[], int off, int len) { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + return input.read(b, off, len); + } catch (IOException e) { + throw new DAVException(403, "Can't read data", e, this.resource); + } + } + + /** + * <p>Skip a specified amount of data reading from this + * {@link InputStream}.</p> + */ + public long skip(long n) { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + return input.skip(n); + } catch (IOException e) { + throw new DAVException(403, "Can't skip over", e, this.resource); + } + } + + /** + * <p>Return the number of bytes that can be read or skipped from this + * {@link InputStream} without blocking.</p> + */ + public int available() { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + return input.available(); + } catch (IOException e) { + throw new DAVException(403, "Can't skip over", e, this.resource); + } + } + + /** + * <p>Return the number of bytes that can be read or skipped from this + * {@link InputStream} without blocking.</p> + */ + public void close() { + if (this.input == null) return; + try { + this.input.close(); + } catch (IOException e) { + throw new DAVException(403, "Can't close", e, this.resource); + } finally { + this.input = null; + } + } + + /** + * <p>Marks the current position in this {@link InputStream}.</p> + */ + public void mark(int readlimit) { + if (this.input == null) throw new IllegalStateException("Closed"); + this.input.mark(readlimit); + } + + /** + * <p>Repositions this stream to the position at the time the + * {@link #mark(int)} method was last called on this + * {@link InputStream}.</p> + */ + public void reset() { + if (this.input == null) throw new IllegalStateException("Closed"); + try { + input.reset(); + } catch (IOException e) { + throw new DAVException(403, "Can't reset", e, this.resource); + } + } + + /** + * <p>Tests if this {@link InputStream} supports the {@link #mark(int)} + * and {@link #reset()} methods.</p> + */ + public boolean markSupported() { + if (this.input == null) throw new IllegalStateException("Closed"); + return this.input.markSupported(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVListener.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVListener.java new file mode 100644 index 000000000..6357bcc16 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVListener.java @@ -0,0 +1,46 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +/** + * <p>A simple interface identifying a {@link DAVRepository} event listener.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public interface DAVListener { + + /** <p>An event representing the creation of a collection.</p> */ + public static final int COLLECTION_CREATED = 1; + /** <p>An event representing the deletion of a collection.</p> */ + public static final int COLLECTION_REMOVED = 2; + /** <p>An event representing the creation of a resource.</p> */ + public static final int RESOURCE_CREATED = 3; + /** <p>An event representing the deletion of a resource.</p> */ + public static final int RESOURCE_REMOVED = 4; + /** <p>An event representing the modification of a resource.</p> */ + public static final int RESOURCE_MODIFIED = 5; + + /** + * <p>Notify this {@link DAVListener} of an action occurred on a + * specified {@link DAVResource}.</p> + * + * @param resource the {@link DAVResource} associated with the notification. + * @param event a number identifying the type of the notification. + */ + public void notify(DAVResource resource, int event); + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVLogger.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVLogger.java new file mode 100644 index 000000000..9ba48466d --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVLogger.java @@ -0,0 +1,86 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; + +/** + * <p>A simplicisting class defining an esay way to log stuff to the + * {@link ServletContext}.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVLogger { + + private final ServletContext context; + private final String servletName; + private final boolean debug; + + /** + * <p>Create a new {@link DAVLogger} from a {@link ServletConfig}.</p> + */ + public DAVLogger(ServletConfig config, boolean debug) { + this.context = config.getServletContext(); + this.servletName = config.getServletName(); + this.debug = debug; + } + + /** + * <p>Log a debug message to the context logger.</p> + */ + public void debug(String message) { + if (this.debug) this.doLog(message, null); + } + + /** + * <p>Log a debug message and related exception to the context logger.</p> + */ + public void debug(String message, Throwable throwable) { + if (this.debug) this.doLog(message, throwable); + } + + /** + * <p>Log a message to the context logger.</p> + */ + public void log(String message) { + this.doLog(message, null); + } + + /** + * <p>Log a message and related exception to the context logger.</p> + */ + public void log(String message, Throwable throwable) { + this.doLog(message, throwable); + } + + /** + * <p>Internal method for formatting messages and logging.</p> + */ + private void doLog(String message, Throwable throwable) { + if ((message == null) && (throwable == null)) return; + if ((message == null) || ("".equals(message))) message = "No message"; + + StringBuffer buffer = new StringBuffer(); + buffer.append('['); + buffer.append(this.servletName); + buffer.append("] "); + buffer.append(message); + if (throwable == null) this.context.log(buffer.toString()); + else this.context.log(buffer.toString(), throwable); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMethod.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMethod.java new file mode 100644 index 000000000..e73eb52b5 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMethod.java @@ -0,0 +1,41 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.IOException; + + +/** + * <p>An interface describing the implementation of a + * <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * method.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public interface DAVMethod { + + /** + * <p>Process the specified {@link DAVTransaction}.</p> + * + * @param transaction An object encapsulaing a WebDAV request/response. + * @param resource The {@link DAVResource} to process. + * @throws IOException If an I/O error occurred. + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException; + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMultiStatus.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMultiStatus.java new file mode 100644 index 000000000..e7c2fc5da --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVMultiStatus.java @@ -0,0 +1,149 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + + +/** + * <p>A {@link DAVException} representing a + * <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>207</code> (Multi-Status) response.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVMultiStatus extends DAVException { + + private Set responses = new HashSet(); + + /** + * <p>Create a new {@link DAVMultiStatus} instance.</p> + */ + public DAVMultiStatus() { + super(207, "Multi-Status response"); + } + + /** + * <p>Write the body of the multi-status response to the specified + * {@link DAVTransaction}'s output.</p> + */ + public void write(DAVTransaction transaction) + throws IOException { + /* What to do on a collection resource */ + transaction.setStatus(207); + transaction.setContentType("text/xml; charset=\"UTF-8\""); + PrintWriter out = transaction.write("UTF-8"); + + /* Output the XML declaration and the root document tag */ + out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + out.println("<D:multistatus xmlns:D=\"DAV:\">"); + + Iterator responses = this.responses.iterator(); + while (responses.hasNext()) { + Response response = (Response) responses.next(); + out.println(" <D:response>"); + out.print(" <D:href>"); + out.print(transaction.lookup(response.resource)); + out.println("</D:href>"); + + if (response.status != 0) { + out.print(" <D:status>HTTP/1.1 "); + out.print(DAVUtilities.getStatusMessage(response.status)); + out.println("</D:status>"); + } + + if (response.message != null) { + out.print(" <D:responsedescription>"); + out.print(response.message); + out.println("</D:responsedescription>"); + } + + out.println(" </D:response>"); + } + + out.println("</D:multistatus>"); + out.flush(); + } + + /** + * <p>Return the number of responses held in this instance.</p> + */ + public int size() { + return this.responses.size(); + } + + /** + * <p>Merge the responses held into the specified {@link DAVMultiStatus} + * into this instance.</p> + */ + public void merge(DAVMultiStatus multistatus) { + if (multistatus == null) return; + Iterator iterator = multistatus.responses.iterator(); + while (iterator.hasNext()) this.responses.add(iterator.next()); + } + + /** + * <p>Merge the details held into the specified {@link DAVException} + * into this instance.</p> + */ + public void merge(DAVException exception) { + DAVResource resource = exception.getResource(); + if (resource == null) throw exception; + + int status = exception.getStatus(); + String message = exception.getMessage(); + this.responses.add(new Response(resource, status, message)); + } + + private static class Response implements Comparable { + private DAVResource resource = null; + private int status = 0; + private String message = null; + + public Response(Response response) { + this(response.resource, response.status, response.message); + } + + public Response(DAVResource resource, int status, String message) { + if (resource == null) throw new NullPointerException(); + this.resource = resource; + this.status = status; + this.message = message; + } + + public int hashCode() { + return this.resource.hashCode(); + } + + public int compareTo(Object object) { + Response response = (Response) object; + return (this.resource.compareTo(response.resource)); + } + + public boolean equals(Object object) { + if (object instanceof Response) { + Response response = (Response) object; + return (this.resource.equals(response.resource)); + } + return false; + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVNotModified.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVNotModified.java new file mode 100644 index 000000000..2d6726551 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVNotModified.java @@ -0,0 +1,56 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.IOException; + +/** + * <p>A simple {@link DAVException} encapsulating an + * <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> not modified + * response.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVNotModified extends DAVException { + + private DAVResource resource = null; + + /** + * <p>Create a new {@link DAVNotModified} instance.</p> + */ + public DAVNotModified(DAVResource resource) { + super(304, "Resource Not Modified"); + this.resource = resource; + } + + /** + * <p>Write the body of this {@link DAVNotModified} to the specified + * {@link DAVTransaction}'s output.</p> + */ + public void write(DAVTransaction transaction) + throws IOException { + transaction.setStatus(this.getStatus()); + + /* Figure out what we're dealing with here */ + String etag = resource.getEntityTag(); + String lmod = DAVUtilities.formatHttpDate(resource.getLastModified()); + + /* Set the normal headers that are required for a GET */ + if (etag != null) transaction.setHeader("ETag", etag); + if (lmod != null) transaction.setHeader("Last-Modified", lmod); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVOutputStream.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVOutputStream.java new file mode 100644 index 000000000..6a5c80601 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVOutputStream.java @@ -0,0 +1,187 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * <p>A specialized {@link OutputStream} to write to {@link DAVResource}s.</p> + * + * <p>When writing to this {@link OutputStream} the data will be written to + * a temporary file. This temporary file will be moved to its final destination + * (the original file identifying the resource) when the {@link #close()} + * method is called.</p> + * + * <p>This specialized {@link OutputStream} never throws {@link IOException}s, + * but rather relies on the unchecked {@link DAVException} to notify the + * framework of the correct DAV errors.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVOutputStream extends OutputStream { + + /** <p>The original resource {@link File}.</p> */ + private File temporary = null; + /** <p>The {@link OutputStream} of the temporary {@link File}. </p> */ + protected OutputStream output = null; + /** <p>The {@link DAVResource} associated with this instance. </p> */ + private DAVResource resource = null; + + /** + * <p>Create a new {@link DAVOutputStream} instance.</p> + */ + protected DAVOutputStream(DAVResource resource) { + if (resource == null) throw new NullPointerException(); + this.resource = resource; + init(resource); + } + + protected void init(DAVResource resource) { + try { + this.temporary = resource.getParent().getFile(); + this.temporary = File.createTempFile(DAVResource.PREFIX, + DAVResource.SUFFIX, + this.temporary); + this.output = new FileOutputStream(this.temporary); + } catch (IOException e) { + String message = "Unable to create temporary file"; + throw new DAVException(507, message, e, resource); + } + } + + /** + * <p>Rename the temporary {@link File} to the original one.</p> + */ + protected void rename(File temporary, File original) + throws IOException { + if ((original.exists()) && (!original.delete())) { + throw new IOException("Unable to delete original file"); + } + if (!temporary.renameTo(original)) { + throw new IOException("Unable to rename temporary file"); + } + } + + /** + * <p>Abort any data written to the temporary file and delete it.</p> + */ + public void abort() { + if (this.temporary.exists()) this.temporary.delete(); + if (this.output != null) try { + this.output.close(); + } catch (IOException exception) { + // Swallow the IOException on close + } finally { + this.output = null; + } + } + + /** + * <p>Close this {@link OutputStream} {@link #rename(File,File) renaming} + * the temporary file to the {@link DAVResource#getFile() original} one.</p> + */ + public void close() { + if (this.output == null) return; + try { + /* What kind of event should this invocation trigger? */ + int event = this.resource.getFile().exists() ? + DAVListener.RESOURCE_MODIFIED: + DAVListener.RESOURCE_CREATED; + + /* Make sure that everything is closed and named properly */ + this.output.close(); + this.output = null; + this.rename(this.temporary, this.resource.getFile()); + + /* Send notifications to all listeners of the repository */ + this.resource.getRepository().notify(this.resource, event); + + } catch (IOException e) { + String message = "Error processing temporary file"; + throw new DAVException(507, message, e, this.resource); + } finally { + this.abort(); + } + } + + /** + * <p>Flush any unwritten data to the disk.</p> + */ + public void flush() { + if (this.output == null) throw new IllegalStateException("Closed"); + try { + this.output.flush(); + } catch (IOException e) { + this.abort(); + String message = "Unable to flush buffers"; + throw new DAVException(507, message, e, this.resource); + } + } + + /** + * <p>Write data to this {@link OutputStream}.</p> + */ + public void write(int b) { + if (this.output == null) throw new IllegalStateException("Closed"); + try { + this.output.write(b); + } catch (IOException e) { + this.abort(); + String message = "Unable to write data"; + throw new DAVException(507, message, e, this.resource); + } + } + + /** + * <p>Write data to this {@link OutputStream}.</p> + */ + public void write(byte b[]) { + if (this.output == null) throw new IllegalStateException("Closed"); + try { + this.output.write(b); + } catch (IOException e) { + this.abort(); + String message = "Unable to write data"; + throw new DAVException(507, message, e, this.resource); + } + } + + /** + * <p>Write data to this {@link OutputStream}.</p> + */ + public void write(byte b[], int o, int l) { + if (this.output == null) throw new IllegalStateException("Closed"); + try { + this.output.write(b, o, l); + } catch (IOException e) { + this.abort(); + String message = "Unable to write data"; + throw new DAVException(507, message, e, this.resource); + } + } + + /** + * <p>Finalize this {@link DAVOutputStream} instance.</p> + */ + public void finalize() { + this.abort(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVProcessor.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVProcessor.java new file mode 100644 index 000000000..d50b65875 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVProcessor.java @@ -0,0 +1,92 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * <p>The <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * transactions processor.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVProcessor { + + /** <p>All the implemented methods, comma separated.</p> */ + public static final String METHODS = "COPY,DELETE,GET,HEAD,MKCOL,MOVE," + + "OPTIONS,PROPFIND,PROPPATCH,PUT"; + + /** <p>A static map of all known webdav methods.</p> */ + private static Map INSTANCES = new HashMap(); + static { + /* Load and verify all the known methods */ + final String thisName = DAVProcessor.class.getName(); + final int packageDelimiter = thisName.lastIndexOf('.'); + final String packageName = packageDelimiter < 1 ? "methods." : + thisName.substring(0, packageDelimiter) + ".methods."; + final StringTokenizer tokenizer = new StringTokenizer(METHODS, ","); + final ClassLoader classLoader = DAVProcessor.class.getClassLoader(); + while (tokenizer.hasMoreTokens()) try { + final String method = tokenizer.nextToken(); + final String className = packageName + method; + final Class clazz = classLoader.loadClass(className); + INSTANCES.put(method, (DAVMethod) clazz.newInstance()); + } catch (Throwable throwable) { + InternalError error = new InternalError("Error loading method"); + throw (InternalError) error.initCause(throwable); + } + } + + /** <p>The {@link DAVRepository} associated with this instance.</p> */ + private DAVRepository repository = null; + + /** + * <p>Create a new {@link DAVProcessor} instance.</p> + */ + public DAVProcessor(DAVRepository repository) { + if (repository == null) throw new NullPointerException(); + this.repository = repository; + } + + /** + * <p>Process the specified {@link DAVTransaction} fully.</p> + */ + public void process(DAVTransaction transaction) + throws IOException { + try { + String method = transaction.getMethod(); + if (INSTANCES.containsKey(method)) { + String path = transaction.getNormalizedPath(); + DAVResource resource = this.repository.getResource(path); + DAVMethod instance = ((DAVMethod) INSTANCES.get(method)); + instance.process(transaction, resource); + } else { + String message = "Method \"" + method + "\" not implemented"; + throw new DAVException(501, message); + } + } catch (DAVException exception) { + exception.write(transaction); + } + } + + public void setMethod( String methodKey, DAVMethod method ) { + INSTANCES.put( methodKey, method ); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVRepository.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVRepository.java new file mode 100644 index 000000000..aa3ed42d4 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVRepository.java @@ -0,0 +1,164 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * <p>A simple class representing a {@link File} based WebDAV repository.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVRepository { + + /** <p>A {@link String} of all acceptable characters in a URI.</p> */ + private static final String ACCEPTABLE = + "ABCDEFGHIJLKMNOPQRSTUVWXYZ" + // ALPHA (UPPER) + "abcdefghijklmnopqrstuvwxyz" + // ALPHA (LOWER) + "0123456789" + // DIGIT + "_-!.~'()*" + // UNRESERVED + ",;:$&+=" + // PUNCT + "?/[]@"; // RESERVED + + + /** <p>The {@link File} identifying the root of this repository.</p> */ + protected File root = null; + /** <p>The {@link URI} associated with the root of this repository.</p> */ + protected URI base = null; + /** <p>The {@link Set} of all configured {@link DAVListener}s.</p> */ + private Set listeners = new HashSet(); + + /** + * <p>Create a new {@link DAVRepository} instance.</p> + * + * @param root The {@link File} identifying the root of the repository. + * @throws IOException If the specified root is not a directory. + * @throws NullPointerExceptoin If the specified root was <b>null</b>. + */ + public DAVRepository(File root) + throws IOException { + init(root); + } + + protected void init(File root) + throws IOException { + if (root == null) throw new NullPointerException("Null root"); + if (root.isDirectory()) { + this.root = root.getCanonicalFile(); + this.base = this.root.toURI().normalize(); + } else { + throw new IOException("Root \"" + root + "\" is not a directory"); + } + } + + /** + * <p>Return the {@link URI} representing the root directory of this + * {@link DAVRepository}.</p> + * + * @return a <b>non-null</b> {@link URI} instance. + */ + protected URI getRepositoryURI() { + return (this.base); + } + + /** + * <p>Return the {@link DAVResource} associated with the given name.</p> + * + * @param name a {@link String} identifying the resource name. + * @return a <b>non-null</b> {@link DAVResource} instance. + * @throws IOException If the resource could not be resolved. + */ + public DAVResource getResource(String name) + throws IOException { + if (name == null) return this.getResource((URI) null); + + try { + /* Encode the string into a URI */ + StringBuffer buffer = new StringBuffer(); + byte encoded[] = name.getBytes("UTF-8"); + for (int x = 0; x < encoded.length; x ++) { + if (ACCEPTABLE.indexOf((int)encoded[x]) < 0) { + buffer.append('%'); + buffer.append(DAVUtilities.toHexString(encoded[x])); + continue; + } + buffer.append((char) encoded[x]); + } + + return this.getResource(new URI(buffer.toString())); + } catch (URISyntaxException exception) { + String message = "Invalid resource name \"" + name + "\""; + throw (IOException) new IOException(message).initCause(exception); + } + } + + /** + * <p>Return the {@link DAVResource} associated with a {@link URI}.</p> + * + * <p>If the specified {@link URI} is relative it will be resolved against + * the root of this {@link DAVRepository}.</p> + * + * @param uri an absolute or relative {@link URI} identifying the resource. + * @return a <b>non-null</b> {@link DAVResource} instance. + * @throws IOException If the resource could not be resolved. + */ + public DAVResource getResource(URI uri) + throws IOException { + if (uri == null) return new DAVResource(this, this.root); + + if (! uri.isAbsolute()) uri = this.base.resolve(uri).normalize(); + return new DAVResource(this, new File(uri).getAbsoluteFile()); + } + + /** + * <p>Add a new {@link DAVListener} to the list of instances notified by + * this {@link DAVRepository}.</p> + */ + public void addListener(DAVListener listener) { + if (listener != null) this.listeners.add(listener); + } + + /** + * <p>Remove a {@link DAVListener} from the list of instances notified by + * this {@link DAVRepository}.</p> + */ + public void removeListener(DAVListener listener) { + if (listener != null) this.listeners.remove(listener); + } + + /** + * <p>Notify all configured {@link DAVListener}s of an event.</p> + */ + protected void notify(DAVResource resource, int event) { + if (resource == null) throw new NullPointerException("Null resource"); + if (resource.getRepository() != this) + throw new IllegalArgumentException("Invalid resource"); + + Iterator iterator = this.listeners.iterator(); + while (iterator.hasNext()) try { + ((DAVListener)iterator.next()).notify(resource, event); + } catch (RuntimeException exception) { + // Swallow any RuntimeException thrown by listeners. + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVResource.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVResource.java new file mode 100644 index 000000000..f2d0911ee --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVResource.java @@ -0,0 +1,514 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; + + +/** + * <p>A simple representation of a WebDAV resource based on {@link File}s.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVResource implements Comparable { + + /** <p>The mime type when {@link #isCollection()} is <b>true</b>.</p> */ + public static final String COLLECTION_MIME_TYPE = "httpd/unix-directory"; + + /** <p>The prefix for all temporary resources.</p> */ + protected static final String PREFIX = ".dav_"; + /** <p>The suffix for all temporary resources.</p> */ + protected static final String SUFFIX = ".temp"; + /** <p>The {@link DAVRepository} instance containing this resource.</p> */ + private DAVRepository repository = null; + /** <p>The {@link File} associated with this resource.</p> */ + private File file = null; + + /* ====================================================================== */ + /* Constructors */ + /* ====================================================================== */ + + /** + * <p>Create a new {@link DAVResource} instance.</p> + */ + protected DAVResource(DAVRepository repo, File file) { + if (repo == null) throw new NullPointerException("Null repository"); + if (file == null) throw new NullPointerException("Null resource"); + init(repo, file); + } + + protected void init(DAVRepository repo, File file) + { + this.repository = repo; + this.file = file; + + if (this.getRelativeURI().isAbsolute()) + throw new DAVException(412, "Error relativizing resource"); + } + + /* ====================================================================== */ + /* Generic object methods */ + /* ====================================================================== */ + + /** + * <p>Return an integer number for the hash value of this instance.</p> + */ + public int hashCode() { + return this.file.hashCode(); + } + + /** + * <p>Compare this instance to another object for equality.</p> + */ + public boolean equals(Object object) { + if (object == null) return (false); + if (object instanceof DAVResource) { + DAVResource resource = (DAVResource) object; + boolean u = this.file.equals(resource.file); + boolean r = this.repository == resource.repository; + return (u && r); + } else { + return (false); + } + } + + /** + * <p>Compare this instance to another object for sorting.</p> + */ + public int compareTo(Object object) { + DAVResource resource = (DAVResource) object; + return (this.file.compareTo(resource.file)); + } + + /* ====================================================================== */ + /* Resource checkers */ + /* ====================================================================== */ + + /** + * <p>Checks if this {@link DAVResource} is a null (non existant) one.</p> + * + * @return <b>true</b> if this resource does not esist (is a null resource). + */ + public boolean isNull() { + return (! this.file.exists()); + } + + /** + * <p>Checks if this {@link DAVResource} is a collection.</p> + * + * @return <b>true</b> if this resource is a collection. + */ + public boolean isCollection() { + if (this.isNull()) return false; + return (this.file.isDirectory()); + } + + /** + * <p>Checks if this {@link DAVResource} is an existing resource.</p> + * + * @return <b>true</b> if this resource is a collection. + */ + public boolean isResource() { + if (this.isNull()) return false; + return (! this.isCollection()); + } + + /* ====================================================================== */ + /* Resource methods */ + /* ====================================================================== */ + + /** + * <p>Return the {@link File} associated with this resource.</p> + */ + protected File getFile() { + return this.file; + } + + /** + * <p>Return the {@link DAVRepository} associated with this resource.</p> + */ + public DAVRepository getRepository() { + return this.repository; + } + + /** + * <p>Return the bare name of this resource (without any "/" + * slashes at the end if it is a collection).</p> + * + * @return a <b>non null</b> {@link String}. + */ + public String getName() { + return this.file.getName(); + } + + /** + * <p>Return the display name of this resource (with an added "/" + * slash at the end if it is a collection).</p> + * + * @return a <b>non null</b> {@link String}. + */ + public String getDisplayName() { + String name = this.getName(); + if (this.isCollection()) return (name + "/"); + return name; + } + + /** + * <p>Return the path of this {@link DAVResource} relative to the root + * of the associated {@link DAVRepository}.</p> + * + * @return a <b>non null</b> {@link String}. + */ + public String getRelativePath() { + return this.getRelativeURI().toASCIIString(); + } + + /** + * <p>Return the {@link URI} of this {@link DAVResource} relative to the + * root of the associated {@link DAVRepository}.</p> + * + * @return a <b>non-null</b> {@link URI} instance. + */ + public URI getRelativeURI() { + URI uri = this.file.toURI(); + return this.repository.getRepositoryURI().relativize(uri).normalize(); + } + + /** + * <p>Return the parent {@link DAVResource} of this instance.</p> + * + * @return a <b>non-null</b> {@link DAVResource} instance or <b>null</b> + * if this {@link DAVResource} is the repository root. + */ + public DAVResource getParent() { + try { + return new DAVResource(this.repository, this.file.getParentFile()); + } catch (Throwable throwable) { + return null; + } + } + + /** + * <p>Return an {@link Iterator} over all children of this instance.</p> + * + * @return a <b>non-null</b> {@link Iterator} instance or <b>null</b> if + * this {@link DAVResource} is not a collection. + * @throws IOException If the resource could not be resolved. + */ + public Iterator getChildren() { + if (! this.isCollection()) return null; + + File children[] = this.file.listFiles(); + if (children == null) children = new File[0]; + List resources = new ArrayList(children.length); + + for (int x = 0; x < children.length; x++) { + String c = children[x].getName(); + if (c.startsWith(PREFIX) && c.endsWith(SUFFIX)) continue; + resources.add(new DAVResource(this.repository, children[x])); + } + + return resources.iterator(); + } + + /* ====================================================================== */ + /* DAV Properties */ + /* ====================================================================== */ + + /** + * <p>Return the MIME Content-Type of this {@link DAVResource}.</p> + * + * <p>If the {@link #isCollection()} method returns <b>true</b> this + * method always returns <code>text/html</code>.</p> + * + * @return a {@link String} instance or <b>null</b> if this resource does + * not exist. + */ + public String getContentType() { + if (this.isNull()) return null; + if (this.isCollection()) return COLLECTION_MIME_TYPE; + return DAVUtilities.getMimeType(this.getDisplayName()); + } + + /** + * <p>Return the MIME Content-Length of this {@link DAVResource}.</p> + * + * @return a {@link Long} instance or <b>null</b> if this resource does + * not exist or is a collection. + */ + public Long getContentLength() { + if (this.isNull() || this.isCollection()) return null; + return new Long(this.file.length()); + } + + /** + * <p>Return the creation date of this {@link DAVResource}.</p> + * + * <p>As this implementation relies on a {@link File} backend, this method + * will always return the same as {@link #getLastModified()}.</p> + * + * @return a {@link String} instance or <b>null</b> if this resource does + * not exist. + */ + public Date getCreationDate() { + if (this.isNull()) return null; + return new Date(this.file.lastModified()); + } + + /** + * <p>Return the last modification date of this {@link DAVResource}.</p> + * + * @return a {@link String} instance or <b>null</b> if this resource does + * not exist. + */ + public Date getLastModified() { + if (this.isNull()) return null; + return new Date(this.file.lastModified()); + } + + /** + * <p>Return a {@link String} representing the Entity Tag of this + * {@link DAVResource} as described by the + * <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP RFC</a>.</p> + * + * @return a {@link String} instance or <b>null</b> if this resource does + * not exist. + */ + public String getEntityTag() { + if (this.isNull()) return null; + + String path = this.getRelativePath(); + StringBuffer etag = new StringBuffer(); + etag.append('"'); + + /* Append the MD5 hash of this resource name */ + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + digester.reset(); + digester.update(path.getBytes("UTF8")); + etag.append(DAVUtilities.toHexString(digester.digest())); + etag.append('-'); + } catch (Exception e) { + // If we can't get the MD5 HASH, let's ignore and hope... + } + + /* Append the hashCode of this resource name */ + etag.append(DAVUtilities.toHexString(path.hashCode())); + + /* Append the last modification date if possible */ + Date date = this.getLastModified(); + if (date != null) { + etag.append('-'); + etag.append(DAVUtilities.toHexString(date.getTime())); + } + + /* Close the ETag */ + etag.append('"'); + return(etag.toString()); + } + + /* ====================================================================== */ + /* DAV Operations */ + /* ====================================================================== */ + + /** + * <p>Delete this resource.</p> + * + * @throws DAVException If for any reason this resource cannot be deleted. + */ + public void delete() + throws DAVMultiStatus { + if (this.isNull()) throw new DAVException(404, "Not found", this); + + if (this.isResource()) { + if (!windowsSafeDelete(this.file)) { + throw new DAVException(403, "Can't delete resource", this); + } else { + this.repository.notify(this, DAVListener.RESOURCE_REMOVED); + } + } else if (this.isCollection()) { + DAVMultiStatus multistatus = new DAVMultiStatus(); + + Iterator children = this.getChildren(); + while (children.hasNext()) try { + ((DAVResource)children.next()).delete(); + } catch (DAVException exception) { + multistatus.merge(exception); + } + + if (multistatus.size() > 0) throw multistatus; + if (!this.file.delete()) { + throw new DAVException(403, "Can't delete collection", this); + } else { + this.repository.notify(this, DAVListener.COLLECTION_REMOVED); + } + } + } + + /** + * <p>Copy this resource to the specified destination.</p> + * + * @throws DAVException If for any reason this resource cannot be deleted. + */ + public void copy(DAVResource dest, boolean overwrite, boolean recursive) + throws DAVMultiStatus { + + /* + * NOTE: Since the COPY operation relies on other operation defined in + * this class (and in DAVOutputStream for resources) rather than on + * files temselves, notifications are sent elsewhere, not here. + */ + + if (this.isNull()) throw new DAVException(404, "Not found", this); + + /* Check if the destination exists and delete if possible */ + if (!dest.isNull()) { + if (! overwrite) { + String msg = "Not overwriting existing destination"; + throw new DAVException(412, msg, dest); + } + dest.delete(); + } + + /* Copy a single resource (destination is null as we deleted it) */ + if (this.isResource()) { + DAVInputStream in = this.read(); + DAVOutputStream out = dest.write(); + byte buffer[] = new byte[4096]; + int k = -1; + while ((k = in.read(buffer)) != -1) out.write(buffer, 0, k); + in.close(); + out.close(); + } + + /* Copy the collection and all nested members */ + if (this.isCollection()) { + dest.makeCollection(); + if (! recursive) return; + + DAVMultiStatus multistatus = new DAVMultiStatus(); + Iterator children = this.getChildren(); + while (children.hasNext()) try { + DAVResource childResource = (DAVResource) children.next(); + File child = new File(dest.file, childResource.file.getName()); + DAVResource target = new DAVResource(this.repository, child); + childResource.copy(target, overwrite, recursive); + } catch (DAVException exception) { + multistatus.merge(exception); + } + if (multistatus.size() > 0) throw multistatus; + } + } + + /** + * <p>Moves this resource to the specified destination.</p> + * + * @throws DAVException If for any reason this resource cannot be deleted. + */ + public void move(DAVResource dest, boolean overwrite, boolean recursive) + throws DAVMultiStatus { + // the base class implementation is just copy-then-delete + copy(dest, overwrite, recursive); + this.delete(); + } + + /** + * <p>Create a collection identified by this {@link DAVResource}.</p> + * + * <p>This resource must be {@link #isNull() non-null} and its + * {@link #getParent() parent} must be accessible and be a + * {@link #isCollection() collection}.</p> + * + * @throws DAVException If for any reason a collection identified by this + * resource cannot be created. + */ + public void makeCollection() { + DAVResource parent = this.getParent(); + if (!this.isNull()) + throw new DAVException(405, "Resource exists", this); + if (parent.isNull()) + throw new DAVException(409, "Parent does not not exist", this); + if (!parent.isCollection()) + throw new DAVException(403, "Parent not a collection", this); + if (!this.file.mkdir()) + throw new DAVException(507, "Can't create collection", this); + this.repository.notify(this, DAVListener.COLLECTION_CREATED); + } + + /** + * <p>Return an {@link InputStream} reading the resource.</p> + * + * @return a <b>non-null</b> {@link InputStream} instance. + */ + public DAVInputStream read() { + if (this.isNull()) throw new DAVException(404, "Not found", this); + if (this.isCollection()) + throw new DAVException (403, "Resource is collection", this); + return new DAVInputStream(this); + } + + /** + * <p>Return a {@link DAVOutputStream} writing to this {@link DAVResource} + * instance.</p> + * + * @return a <b>non-null</b> {@link DAVOutputStream} instance. + */ + public DAVOutputStream write() { + DAVResource parent = this.getParent(); + if (this.isCollection()) + throw new DAVException(409, "Can't write a collection", this); + if (parent.isNull()) + throw new DAVException(409, "Parent doesn't exist", this); + if (! parent.isCollection()) + throw new DAVException(403, "Parent not a collection", this); + return new DAVOutputStream(this); + } + + /** File.delete(file) sometimes fails transiently on Windows. + * This occurs even in low-I/O conditions, with file Explorer closed. + * Delete can still fail (correctly) due to the separate Windows problem + * of file sharing violations. + * @return the status of the last attempt of File.delete() + */ + private static boolean windowsSafeDelete(File f) + { + // www.mail-archive.com/java-user@lucene.apache.org/msg08994.html + boolean success = f.delete(); + int attempts = 1; + while(!success && f.exists() && attempts < 3) { + if(attempts > 2) { + System.gc(); + } + try { + Thread.sleep(20); + } catch (InterruptedException ignore) { + } + success = f.delete(); + attempts++; + } + return success; + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVServlet.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVServlet.java new file mode 100644 index 000000000..de36a6f6c --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVServlet.java @@ -0,0 +1,280 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; + + +/** + * <p>A very simple servlet capable of processing very simple + * <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * requests.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVServlet implements Servlet, DAVListener { + + /** <p>The {@link DAVRepository} configured for this instance.</p> */ + protected DAVRepository repository = null; + /** <p>The {@link DAVLogger} configured for this instance.</p> */ + protected DAVLogger logger = null; + /** <p>The {@link DAVProcessor} configured for this instance.</p> */ + protected DAVProcessor processor = null; + /** <p>The {@link ServletContext} associated with this instance.</p> */ + private ServletContext context = null; + /** <p>The {@link ServletConfig} associated with this instance.</p> */ + private ServletConfig config= null; + + /** + * <p>Create a new {@link DAVServlet} instance.</p> + */ + public DAVServlet() { + super(); + } + + /** + * <p>Initialize this {@link Servlet} instance.</p> + * + * <p>The only initialization parameter required by this servlet is the + * "<code>rootPath</code>" parameter specifying the path + * of the repository root (either absolute or relative to the configured + * {@link ServletContext}.</p> + * + * <p>If the specified root is relative, it will be considered to + * be relative to the {@link ServletContext} deployment path.</p> + * + * <p>In any case, the specified root must ultimately point to an existing + * directory on a locally-accessible file system.</p> + * + * <p>When set to <code>true</code>, an optional parameter called + * <code>xmlOnly</code> will force this {@link DAVServlet} to use an + * {@link XMLRepository} instead of the default {@link DAVRepository}.</p> + * + * <p>Finally, when set to <code>true</code>, the optional parameter + * <code>debugEnabled</code> will enable logging of method invocation and + * events in the repository.</p> + */ + public void init(ServletConfig config) + throws ServletException { + /* Remember the configuration instance */ + this.config = config; + this.context = config.getServletContext(); + + /* Setup logging */ + boolean debug = "true".equals(config.getInitParameter("debugEnabled")); + this.logger = new DAVLogger(config, debug); + + /* Try to retrieve the WebDAV root path from the configuration */ + String rootPath = config.getInitParameter("rootPath"); + if (rootPath == null) + throw new ServletException("Parameter \"rootPath\" not specified"); + + /* Create repository and processor */ + try { + File root = new File(rootPath); + // The repository may not be the local filesystem. It may be rooted at "/". + // But then on Windows new File("/").isAbsolute() is false. + boolean unixAbsolute = rootPath.startsWith("/"); + boolean localAbsolute = root.isAbsolute(); + if (! unixAbsolute && !localAbsolute) { + URL url = this.context.getResource("/" + rootPath); + if (! "file".equals(url.getProtocol())) { + throw new ServletException("Invalid root \"" + url + "\""); + } else { + root = new File(url.getPath()); + } + } + + /* Discover the repository implementation at runtime */ + String repositoryClass = config.getInitParameter("repositoryClass"); + if(repositoryClass != null) { + this.repository = DAVServlet.newRepository(repositoryClass, root); + } else { + // legacy configuration format. keep for now + /* Make sure that we use the correct repository type */ + if ("true".equalsIgnoreCase(config.getInitParameter("xmlOnly"))) { + this.repository = new XMLRepository(root); + } else { + this.repository = new DAVRepository(root); + } + } + + /* Initialize the processor and register ourselves as listeners */ + this.processor = new DAVProcessor(this.repository); + this.repository.addListener(this); + this.logger.log("Initialized from " + root.getPath()); + + } catch (MalformedURLException e) { + throw new ServletException("Can't resolve \"" + rootPath + "\"", e); + } catch (IOException e) { + String msg = "Can't initialize repository at \"" + rootPath + "\""; + throw new ServletException(msg, e); + } + + /* Finally, register this repository in the servlet context */ + final String key = getRepositoryKey(config.getServletName()); + this.context.setAttribute(key, this.repository); + } + + /** + * <p>Retrieve a {@link DAVRepository} for a given {@link File}.</p> + */ + public DAVRepository getRepository(File root) + throws IOException { + return new XMLRepository(root); + } + + /** + * <p>Detroy this {@link Servlet} instance.</p> + */ + public void destroy() { + this.repository.removeListener(this); + } + + /** + * <p>Return the {@link ServletConfig} associated with this instance.</p> + */ + public ServletConfig getServletConfig() { + return (this.config); + } + + /** + * <p>Return the {@link ServletContext} associated with this instance.</p> + */ + public ServletContext getServletContext() { + return (this.context); + } + + /** + * <p>Return a informative {@link String} about this servlet.</p> + */ + public String getServletInfo() { + return DAVUtilities.SERVLET_INFORMATION; + } + + /** + * <p>Execute the current request.</p> + */ + public void service(ServletRequest request, ServletResponse response) + throws ServletException, IOException { + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + + /* Mark our presence */ + res.setHeader("Server", this.context.getServerInfo() + ' ' + + DAVUtilities.SERVLET_SIGNATURE); + + /* Normal methods are processed by their individual instances */ + DAVTransaction transaction = new DAVTransaction(req, res); + try { + this.processor.process(transaction); + } catch (RuntimeException exception) { + final String header = req.getMethod() + ' ' + req.getRequestURI() + + ' ' + req.getProtocol(); + this.context.log("Error processing: " + header); + this.context.log("Exception processing DAV transaction", exception); + throw exception; + } + } + + /* ====================================================================== */ + /* DAV LISTENER INTERFACE IMPLEMENTATION */ + /* ====================================================================== */ + + /** + * <p>Receive notification of an event occurred in a specific + * {@link DAVRepository}.</p> + */ + public void notify(DAVResource resource, int event) { + String message = "Unknown event"; + switch (event) { + case DAVListener.COLLECTION_CREATED: + message = "Collection created"; + break; + case DAVListener.COLLECTION_REMOVED: + message = "Collection removed"; + break; + case DAVListener.RESOURCE_CREATED: + message = "Resource created"; + break; + case DAVListener.RESOURCE_REMOVED: + message = "Resource removed"; + break; + case DAVListener.RESOURCE_MODIFIED: + message = "Resource modified"; + break; + } + this.logger.debug(message + ": \"" + resource.getRelativePath() + "\""); + } + + /* ====================================================================== */ + /* CONTEXT METHODS */ + /* ====================================================================== */ + + /** + * <p>Retrieve the key in the {@link ServletContext} where the instance of + * the {@link DAVRepository} associated with a named {@link DAVServlet} + * can be found.</p> + * + * @param servletName the name of the {@link DAVServlet} as specified in + * the <code>web.xml</code> deployment descriptor.</p> + */ + public static String getRepositoryKey(String servletName) { + if (servletName == null) throw new NullPointerException(); + return DAVRepository.class.getName() + "." + servletName; + } + + /** factory for subclasses configured in web.xml + * @param repositoryClass must extend DAVRepository and have a public constructor(File). + * */ + static DAVRepository newRepository(String repositoryClass, File root) + throws ServletException + { + try { + Class c = Class.forName(repositoryClass); + Constructor ctor = c.getConstructor(new Class[]{File.class}); + DAVRepository repo = (DAVRepository)ctor.newInstance(new Object[]{root}); + return repo; + } catch(ClassNotFoundException e) { + throw new ServletException(e); + } catch(LinkageError le) { + throw new ServletException(le); + } catch(NoSuchMethodException ns) { + throw new ServletException(ns); + } catch(InvocationTargetException it) { + throw new ServletException(it); + } catch(IllegalAccessException ia) { + throw new ServletException(ia); + } catch(InstantiationException ie) { + throw new ServletException(ie); + } + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVTransaction.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVTransaction.java new file mode 100644 index 000000000..20089f022 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVTransaction.java @@ -0,0 +1,280 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Date; + + +/** + * <p>A simple wrapper isolating the Java Servlet API from this + * <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVTransaction { + + /** + * <p>The identifyication of the <code>infinity</code> value + * in the <code>Depth</code> header.</p> + */ + public static final int INFINITY = Integer.MAX_VALUE; + + /** <p>The nested {@link HttpServletRequest}.</p> */ + private HttpServletRequest request = null; + /** <p>The nested {@link HttpServletResponse}.</p> */ + private HttpServletResponse response = null; + /** <p>The {@link URI} associated with the base of the repository.</p> */ + private URI base = null; + /** <p>The status for the HTTP response.</p> */ + private int status = -1; + + /* ====================================================================== */ + /* Constructors */ + /* ====================================================================== */ + + /** + * <p>Create a new {@link DAVTransaction} instance.</p> + */ + public DAVTransaction(ServletRequest request, ServletResponse response) + throws ServletException { + if (request == null) throw new NullPointerException("Null request"); + if (response == null) throw new NullPointerException("Null response"); + this.request = (HttpServletRequest) request; + this.response = (HttpServletResponse) response; + this.response.setHeader("DAV", "1"); + this.response.setHeader("MS-Author-Via", "DAV"); + + try { + String scheme = this.request.getScheme(); + String host = this.request.getServerName(); + String path = this.request.getContextPath() + + this.request.getServletPath(); + int port = this.request.getServerPort(); + if (! path.endsWith("/")) path += "/"; + this.base = new URI(scheme, null, host, port, path, null, null); + this.base = this.base.normalize(); + } catch (URISyntaxException exception) { + throw new ServletException("Unable to create base URI", exception); + } + } + + /* ====================================================================== */ + /* Request methods */ + /* ====================================================================== */ + + /** + * <p>Return the path originally requested by the client.</p> + */ + public String getMethod() { + return this.request.getMethod(); + } + + /** + * <p>Return the path originally requested by the client.</p> + */ + public String getOriginalPath() { + String path = this.request.getPathInfo(); + if (path == null) return ""; + if ((path.length() > 1) && (path.charAt(0) == '/')) { + return path.substring(1); + } else { + return path; + } + } + + /** + * <p>Return the path originally requested by the client.</p> + */ + public String getNormalizedPath() { + final String path = this.getOriginalPath(); + if (! path.endsWith("/")) return path; + return path.substring(0, path.length() - 1); + } + + /** + * <p>Return the depth requested by the client for this transaction.</p> + */ + public int getDepth() { + String depth = request.getHeader("Depth"); + if (depth == null) return INFINITY; + if ("infinity".equalsIgnoreCase(depth)) return INFINITY; + try { + return Integer.parseInt(depth); + } catch (NumberFormatException exception) { + throw new DAVException(412, "Unable to parse depth", exception); + } + } + + /** + * <p>Return a {@link URI} + */ + public URI getDestination() { + String destination = this.request.getHeader("Destination"); + if (destination != null) try { + return this.base.relativize(new URI(destination)); + } catch (URISyntaxException exception) { + throw new DAVException(412, "Can't parse destination", exception); + } + return null; + } + + /** + * <p>Return the overwrite flag requested by the client for this + * transaction.</p> + */ + public boolean getOverwrite() { + String overwrite = request.getHeader("Overwrite"); + if (overwrite == null) return true; + if ("T".equals(overwrite)) return true; + if ("F".equals(overwrite)) return false; + throw new DAVException(412, "Unable to parse overwrite flag"); + } + + /** + * <p>Check if the client requested a date-based conditional operation.</p> + */ + public Date getIfModifiedSince() { + String name = "If-Modified-Since"; + if (this.request.getHeader(name) == null) return null; + return new Date(this.request.getDateHeader(name)); + } + + /* ====================================================================== */ + /* Response methods */ + /* ====================================================================== */ + + /** + * <p>Set the HTTP status code of the response.</p> + */ + public void setStatus(int status) { + this.response.setStatus(status); + this.status = status; + } + + /** + * <p>Set the HTTP status code of the response.</p> + */ + public int getStatus() { + return this.status; + } + + /** + * <p>Set the HTTP <code>Content-Type</code> header.</p> + */ + public void setContentType(String type) { + this.response.setContentType(type); + } + + /** + * <p>Set an HTTP header in the response.</p> + */ + public void setHeader(String name, String value) { + this.response.setHeader(name, value); + } + + /* ====================================================================== */ + /* I/O methods */ + /* ====================================================================== */ + + /** + * <p>Check if there is a body in the request.</p> + * + * <p>This method differs from checking if the return value of the + * {@link #read()} method is not <b>null</b> as a request body of length + * zero will return <b>false</b> in this case, while in the {@link #read()} + * method will return an empty {@link InputStream}.</p> + */ + public boolean hasRequestBody() + throws IOException { + /* We don't support ranges */ + if (request.getHeader("Content-Range") != null) + throw new DAVException(501, "Content-Range not supported"); + + if (this.request.getContentLength() > 0) return true; + String len = this.request.getHeader("Content-Length"); + if (len != null) try { + return (Long.parseLong(len) > 0); + } catch (NumberFormatException exception) { + throw new DAVException(411, "Invalid Content-Length specified"); + } + return false; + } + + /** + * <p>Read from the body of the original request.</p> + */ + public InputStream read() + throws IOException { + /* We don't support ranges */ + if (request.getHeader("Content-Range") != null) + throw new DAVException(501, "Content-Range not supported"); + + if (this.request.getContentLength() >= 0) { + return this.request.getInputStream(); + } + + String len = this.request.getHeader("Content-Length"); + if (len != null) try { + if (Long.parseLong(len) >= 0) return this.request.getInputStream(); + } catch (NumberFormatException exception) { + throw new DAVException(411, "Invalid Content-Length specified"); + } + return null; + } + + /** + * <p>Write the body of the response.</p> + */ + public OutputStream write() + throws IOException { + return this.response.getOutputStream(); + } + + /** + * <p>Write the body of the response.</p> + */ + public PrintWriter write(String encoding) + throws IOException { + return new PrintWriter(new OutputStreamWriter(this.write(), encoding)); + } + + /* ====================================================================== */ + /* Lookup methods */ + /* ====================================================================== */ + + /** + * <p>Look up the final URI of a {@link DAVResource} as visible from the + * HTTP client requesting this transaction.</p> + */ + public URI lookup(DAVResource resource) { + URI uri = resource.getRelativeURI(); + return this.base.resolve(uri).normalize(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVUtilities.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVUtilities.java new file mode 100644 index 000000000..5e03f1e76 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/DAVUtilities.java @@ -0,0 +1,420 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.TimeZone; + + +/** + * <p>A collection of static utilities.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public final class DAVUtilities { + + /** <p>A {@link HashMap} of configured mime types.</p> */ + private static Map MIME_TYPES = new HashMap(); + /** <p>A {@link HashMap} of configured mime types.</p> */ + private static Properties PROPERTIES = new Properties(); + /** <p>The {@link SimpleDateFormat} RFC-822 date format.</p> */ + private static final String FORMAT_822 = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; + /** <p>The {@link SimpleDateFormat} RFC-822 date format.</p> */ + private static final String FORMAT_ISO = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + /** <p>The {@link TimeZone} to use for dates.</p> */ + private static final TimeZone TIMEZONE = TimeZone.getTimeZone("GMT"); + /** <p>The {@link Locale} to use for dates.</p> */ + private static final Locale LOCALE = Locale.US; + + /** + * <p>Load the mime types map from a resource.</p> + */ + static { + Class clazz = DAVUtilities.class; + ClassLoader loader = clazz.getClassLoader(); + + /* Load up the properties file */ + String webdavPropResource = "plexus-webdav/webdav.props"; + InputStream prop = loader.getResourceAsStream(webdavPropResource); + if (prop != null) try { + DAVUtilities.PROPERTIES.load(prop); + prop.close(); + } catch (Exception exception) { + exception.printStackTrace(); + } else { + System.err.println("Invalid resource: " + webdavPropResource); + } + + /* Load up the mime types table */ + String mimeTypeResource = "plexus-webdav/mime.types"; + InputStream mime = loader.getResourceAsStream(mimeTypeResource); + if (mime != null) try { + InputStreamReader read = new InputStreamReader(mime); + BufferedReader buff = new BufferedReader(read); + String line = null; + while ((line = buff.readLine()) != null) { + line = line.trim(); + if (line.length() == 0) continue; + if (line.charAt(0) == '#') continue; + StringTokenizer tokenizer = new StringTokenizer(line); + if (tokenizer.countTokens() > 1) { + String type = tokenizer.nextToken(); + while (tokenizer.hasMoreTokens()) { + String extension = '.' + tokenizer.nextToken(); + DAVUtilities.MIME_TYPES.put(extension, type); + } + } + } + buff.close(); + read.close(); + mime.close(); + } catch (Exception exception) { + exception.printStackTrace(); + } else { + System.err.println("Invalid resource: " + mimeTypeResource); + } + } + + /** <p>The signature of this package usable from a servlet.</p> */ + public static final String SERVLET_SIGNATURE = + DAVUtilities.getProperty("servlet.signature") + '/' + + DAVUtilities.getProperty("version"); + + /** <p>The information detail of this package usable from a servlet.</p> */ + public static final String SERVLET_INFORMATION = + DAVUtilities.getProperty("servlet.information") + " version " + + DAVUtilities.getProperty("version"); + + /** + * <p>Deny public construction of {@link DAVUtilities} instances.</p> + */ + private DAVUtilities() { + super(); + } + + /** + * <p>Return the value of a property configured for this package.</p> + * + * @param name the property name + * @return a {@link String} instance or <b>null</b> if unknown. + */ + public static String getProperty(String name) { + if (name == null) return null; + return DAVUtilities.PROPERTIES.getProperty(name); + } + + /** + * <p>Return the MIME Type configured for a given resource.</p> + * + * @param name the resource name whose MIME Type needs to be looked up. + * @return a {@link String} instance or <b>null</b> if the type is unknown. + */ + public static String getMimeType(String name) { + if (name == null) return null; + + Iterator iterator = DAVUtilities.MIME_TYPES.keySet().iterator(); + while (iterator.hasNext()) { + String extension = (String) iterator.next(); + if (name.endsWith(extension)) { + return (String) DAVUtilities.MIME_TYPES.get(extension); + } + } + + return null; + } + + /** + * <p>Return a {@link String} message given an HTTP status code.</p> + */ + public static String getStatusMessage(int status) { + switch (status) { + /* HTTP/1.1 RFC-2616 */ + case 100: return "100 Continue"; + case 101: return "101 Switching Protocols"; + case 200: return "200 OK"; + case 201: return "201 Created"; + case 202: return "202 Accepted"; + case 203: return "203 Non-Authoritative Information"; + case 204: return "204 No Content"; + case 205: return "205 Reset Content"; + case 206: return "206 Partial Content"; + case 300: return "300 Multiple Choices"; + case 301: return "301 Moved Permanently"; + case 302: return "302 Found"; + case 303: return "303 See Other"; + case 304: return "304 Not Modified"; + case 305: return "305 Use Proxy"; + case 306: return "306 (Unused)"; + case 307: return "307 Temporary Redirect"; + case 400: return "400 Bad Request"; + case 401: return "401 Unauthorized"; + case 402: return "402 Payment Required"; + case 403: return "403 Forbidden"; + case 404: return "404 Not Found"; + case 405: return "405 Method Not Allowed"; + case 406: return "406 Not Acceptable"; + case 407: return "407 Proxy Authentication Required"; + case 408: return "408 Request Timeout"; + case 409: return "409 Conflict"; + case 410: return "410 Gone"; + case 411: return "411 Length Required"; + case 412: return "412 Precondition Failed"; + case 413: return "413 Request Entity Too Large"; + case 414: return "414 Request-URI Too Long"; + case 415: return "415 Unsupported Media Type"; + case 416: return "416 Requested Range Not Satisfiable"; + case 417: return "417 Expectation Failed"; + case 500: return "500 Internal Server Error"; + case 501: return "501 Not Implemented"; + case 502: return "502 Bad Gateway"; + case 503: return "503 Service Unavailable"; + case 504: return "504 Gateway Timeout"; + case 505: return "505 HTTP Version Not Supported"; + + /* DAV/1.0 RFC-2518 */ + case 102: return "102 Processing"; + case 207: return "207 Multi-Status"; + case 422: return "422 Unprocessable Entity"; + case 423: return "423 Locked"; + case 424: return "424 Failed Dependency"; + case 507: return "507 Insufficient Storage"; + + /* Unknown */ + default: return null; + } + } + + /** + * <p>Format a {@link Number} into a {@link String} making sure that + * {@link NullPointerException}s are not thrown.</p> + * + * @param number the {@link Number} to format. + * @return a {@link String} instance or <b>null</b> if the object was null. + */ + public static String formatNumber(Number number) { + if (number == null) return null; + return (number.toString()); + } + + /** + * <p>Parse a {@link String} into a {@link Long}.</p> + * + * @param string the {@link String} to parse. + * @return a {@link Long} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Long parseNumber(String string) { + if (string == null) return null; + try { + return new Long(string); + } catch (NumberFormatException exception) { + return null; + } + } + + /** + * <p>Format a {@link Date} according to the HTTP/1.1 RFC.</p> + * + * @param date the {@link Date} to format. + * @return a {@link String} instance or <b>null</b> if the date was null. + */ + public static String formatHttpDate(Date date) { + if (date == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_822, LOCALE); + formatter.setTimeZone(TIMEZONE); + return formatter.format(date); + } + + /** + * <p>Format a {@link Date} according to the ISO 8601 specification.</p> + * + * @param date the {@link Date} to format. + * @return a {@link String} instance or <b>null</b> if the date was null. + */ + public static String formatIsoDate(Date date) { + if (date == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_ISO, LOCALE); + formatter.setTimeZone(TIMEZONE); + return formatter.format(date); + } + + /** + * <p>Parse a {@link String} into a {@link Date} according to the + * HTTP/1.1 RFC (<code>Mon, 31 Jan 2000 11:59:00 GMT</code>).</p> + * + * @param string the {@link String} to parse. + * @return a {@link Date} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Date parseHttpDate(String string) { + if (string == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_822, LOCALE); + formatter.setTimeZone(TIMEZONE); + try { + return formatter.parse(string); + } catch (ParseException exception) { + return null; + } + } + + /** + * <p>Parse a {@link String} into a {@link Date} according to the ISO 8601 + * specification (<code>2000-12-31T11:59:00Z</code>).</p> + * + * @param string the {@link String} to parse. + * @return a {@link Date} instance or <b>null</b> if the date was null or + * if there was an error parsing the specified {@link String}. + */ + public static Date parseIsoDate(String string) { + if (string == null) return null; + SimpleDateFormat formatter = new SimpleDateFormat(FORMAT_ISO, LOCALE); + formatter.setTimeZone(TIMEZONE); + try { + return formatter.parse(string); + } catch (ParseException exception) { + return null; + } + } + + /** + * <p>Return the HEX representation of an array of bytes.</p> + * + * @param buffer the array of bytes to convert in a HEX {@link String}. + * @return a <b>non-null</b> {@link String} instance. + */ + public static String toHexString(byte buffer[]) { + char output[] = new char[buffer.length * 2]; + int position = 0; + for (int x = 0; x < buffer.length; x++) { + output[position ++] = DAVUtilities.toHexDigit(buffer[x] >> 4); + output[position ++] = DAVUtilities.toHexDigit(buffer[x]); + } + return new String(output); + } + + /** + * <p>Return the HEX representation of a long integer.</p> + * + * @param number the long to convert in a HEX {@link String}. + * @return a <b>non-null</b> 16-characters {@link String} instance. + */ + public static String toHexString(long number) { + char output[] = new char[16]; + output[0] = DAVUtilities.toHexDigit((int)(number >> 60)); + output[1] = DAVUtilities.toHexDigit((int)(number >> 56)); + output[2] = DAVUtilities.toHexDigit((int)(number >> 52)); + output[3] = DAVUtilities.toHexDigit((int)(number >> 48)); + output[4] = DAVUtilities.toHexDigit((int)(number >> 44)); + output[5] = DAVUtilities.toHexDigit((int)(number >> 40)); + output[6] = DAVUtilities.toHexDigit((int)(number >> 36)); + output[7] = DAVUtilities.toHexDigit((int)(number >> 32)); + output[8] = DAVUtilities.toHexDigit((int)(number >> 28)); + output[9] = DAVUtilities.toHexDigit((int)(number >> 24)); + output[10] = DAVUtilities.toHexDigit((int)(number >> 20)); + output[11] = DAVUtilities.toHexDigit((int)(number >> 16)); + output[12] = DAVUtilities.toHexDigit((int)(number >> 12)); + output[13] = DAVUtilities.toHexDigit((int)(number >> 8)); + output[14] = DAVUtilities.toHexDigit((int)(number >> 4)); + output[15] = DAVUtilities.toHexDigit((int)(number)); + return new String(output); + } + + /** + * <p>Return the HEX representation of an integer.</p> + * + * @param number the int to convert in a HEX {@link String}. + * @return a <b>non-null</b> 8-characters {@link String} instance. + */ + public static String toHexString(int number) { + char output[] = new char[8]; + output[0] = DAVUtilities.toHexDigit((int)(number >> 28)); + output[1] = DAVUtilities.toHexDigit((int)(number >> 24)); + output[2] = DAVUtilities.toHexDigit((int)(number >> 20)); + output[3] = DAVUtilities.toHexDigit((int)(number >> 16)); + output[4] = DAVUtilities.toHexDigit((int)(number >> 12)); + output[5] = DAVUtilities.toHexDigit((int)(number >> 8)); + output[6] = DAVUtilities.toHexDigit((int)(number >> 4)); + output[7] = DAVUtilities.toHexDigit((int)(number)); + return new String(output); + } + + /** + * <p>Return the HEX representation of a char.</p> + * + * @param number the char to convert in a HEX {@link String}. + * @return a <b>non-null</b> 4-characters {@link String} instance. + */ + public static String toHexString(char number) { + char output[] = new char[4]; + output[0] = DAVUtilities.toHexDigit((int)(number >> 12)); + output[1] = DAVUtilities.toHexDigit((int)(number >> 8)); + output[2] = DAVUtilities.toHexDigit((int)(number >> 4)); + output[3] = DAVUtilities.toHexDigit((int)(number)); + return new String(output); + } + + /** + * <p>Return the HEX representation of a byte.</p> + * + * @param number the byte to convert in a HEX {@link String}. + * @return a <b>non-null</b> 2-characters {@link String} instance. + */ + public static String toHexString(byte number) { + char output[] = new char[2]; + output[0] = DAVUtilities.toHexDigit((int)(number >> 4)); + output[1] = DAVUtilities.toHexDigit((int)(number)); + return new String(output); + } + + /** + * <p>Return the single digit character representing the HEX encoding of + * the lower four bits of a given integer.</p> + */ + private static char toHexDigit(int number) { + switch (number & 0x0F) { + case 0x00: return '0'; + case 0x01: return '1'; + case 0x02: return '2'; + case 0x03: return '3'; + case 0x04: return '4'; + case 0x05: return '5'; + case 0x06: return '6'; + case 0x07: return '7'; + case 0x08: return '8'; + case 0x09: return '9'; + case 0x0A: return 'A'; + case 0x0B: return 'B'; + case 0x0C: return 'C'; + case 0x0D: return 'D'; + case 0x0E: return 'E'; + case 0x0F: return 'F'; + } + String message = "Invalid HEX digit " + Integer.toHexString(number); + throw new IllegalArgumentException(message); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/XMLRepository.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/XMLRepository.java new file mode 100644 index 000000000..57d34c6dc --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/XMLRepository.java @@ -0,0 +1,113 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.File; +import java.io.IOException; +import java.net.URI; + +/** + * <p>A {@link DAVRepository} instance enforcing all {@link DAVResource}s to + * be XML files.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class XMLRepository extends DAVRepository { + + /** + * <p>Create a new {@link XMLRepository} instance.</p> + */ + public XMLRepository(File root) + throws IOException { + super(root); + } + + /** + * <p>Return the {@link DAVResource} associated with a {@link URI}.</p> + */ + public DAVResource getResource(URI uri) + throws IOException { + return new XMLResource(this, super.getResource(uri)); + } + + /** + * <p>A simple {@link DAVResource} extension enforcing XML writes.</p> + */ + private static final class XMLResource extends DAVResource { + + /** + * <p>Create a new {@link XMLResource} instance.</p> + */ + public XMLResource(XMLRepository repository, DAVResource resource) { + super(repository, resource.getFile()); + } + + /** + * <p>Override the MIME Content-Type to <code>text/xml</code> for + * normal resources.</p> + */ + public String getContentType() { + if (this.isResource()) return "text/xml"; + return super.getContentType(); + } + + /** + * <p>Return a {@link DAVOutputStream} enforcing XML formatted data.</p> + */ + public DAVOutputStream write() { + return new XMLOutputStream(this); + } + } + + /** + * <p>A simple {@link DAVOutputStream} enforcing XML formatted data.</p> + */ + private static final class XMLOutputStream extends DAVOutputStream { + + /** + * <p>Create a new {@link XMLOutputStream} instance.</p> + */ + protected XMLOutputStream(XMLResource resource) { + super(resource); + } + + /** + * <p>Ensure that whatever is in the temporary file is XML.</p> + */ + protected void rename(File temporary, File original) + throws IOException { + try { + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + SAXParser parser = factory.newSAXParser(); + parser.parse(temporary, new DefaultHandler()); + super.rename(temporary, original); + } catch (ParserConfigurationException exception) { + throw new DAVException(500, "JAXP parser error", exception); + } catch (SAXException exception) { + throw new DAVException(415, "Error parsing data", exception); + } + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/COPY.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/COPY.java new file mode 100644 index 000000000..c598bdc23 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/COPY.java @@ -0,0 +1,71 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVMultiStatus; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; +import java.net.URI; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>COPY</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class COPY implements DAVMethod { + + /** + * <p>Create a new {@link COPY} instance.</p> + */ + public COPY() { + super(); + } + + /** + * <p>Process the <code>COPY</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + + URI target = transaction.getDestination(); + if (target == null) throw new DAVException(412, "No destination"); + DAVResource dest = resource.getRepository().getResource(target); + + int depth = transaction.getDepth(); + boolean recursive = false; + if (depth == 0) { + recursive = false; + } else if (depth == DAVTransaction.INFINITY) { + recursive = true; + } else { + throw new DAVException(412, "Invalid Depth specified"); + } + + try { + resource.copy(dest, transaction.getOverwrite(), recursive); + transaction.setStatus(transaction.getOverwrite() ? 204 : 201); + } catch (DAVMultiStatus multistatus) { + multistatus.write(transaction); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/DELETE.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/DELETE.java new file mode 100644 index 000000000..37192a8ee --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/DELETE.java @@ -0,0 +1,54 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVMultiStatus; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>DELETE</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DELETE implements DAVMethod { + + /** + * <p>Create a new {@link DELETE} instance.</p> + */ + public DELETE() { + super(); + } + + /** + * <p>Process the <code>DELETE</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + try { + resource.delete(); + transaction.setStatus(204); + } catch (DAVMultiStatus multistatus) { + multistatus.write(transaction); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/GET.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/GET.java new file mode 100644 index 000000000..e7dc3c66b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/GET.java @@ -0,0 +1,144 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVInputStream; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> + * <code>GET</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class GET extends HEAD { + + /** <p>The encoding charset to repsesent collections.</p> */ + public static final String ENCODING = "UTF-8"; + + /** <p>The mime type that {@link GET} will use serving collections.</p> */ + public static final String COLLECTION_MIME_TYPE = "text/html ;charset=\"" + + ENCODING + "\""; + + /** + * <p>Create a new {@link GET} instance.</p> + */ + public GET() { + super(); + } + + /** + * <p>Process the <code>GET</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + super.process(transaction, resource); + + final String originalPath = transaction.getOriginalPath(); + final String normalizedPath = transaction.getNormalizedPath(); + final String current; + final String parent; + if (originalPath.equals(normalizedPath)) { + final String relativePath = resource.getRelativePath(); + if (relativePath.equals("")) { + current = transaction.lookup(resource).toASCIIString(); + } else { + current = relativePath; + } + parent = "./"; + } else { + current = "./"; + parent = "../"; + } + + if (resource.isCollection()) { + transaction.setHeader( "Content-Disposition", "inline; filename=\"index.html\""); + PrintWriter out = transaction.write(ENCODING); + String path = resource.getRelativePath(); + out.println("<html>"); + out.println("<head>"); + out.println("<title>Collection: /" + path + "</title>"); + out.println("</head>"); + out.println("<body>"); + out.println("<h2>Collection: /" + path + "</h2>"); + out.println("<ul>"); + + /* Process the parent */ + final DAVResource parentResource = resource.getParent(); + if (parentResource != null) { + out.print("<li><a href=\""); + out.print(parent); + out.print("\">"); + out.print(parentResource.getDisplayName()); + out.println("</a> <i><small>(Parent)</small></i></li>"); + out.println("</ul>"); + out.println("<ul>"); + } + + /* Process the children (in two sorted sets, for nice ordering) */ + Set resources = new TreeSet(); + Set collections = new TreeSet(); + Iterator iterator = resource.getChildren(); + while (iterator.hasNext()) { + final DAVResource child = (DAVResource) iterator.next(); + final StringBuffer buffer = new StringBuffer(); + final String childPath = child.getDisplayName(); + buffer.append("<li><a href=\""); + buffer.append(current); + buffer.append(childPath); + buffer.append("\">"); + buffer.append(childPath); + buffer.append("</li>"); + if (child.isCollection()) { + collections.add(buffer.toString()); + } else { + resources.add(buffer.toString()); + } + } + + /* Spit out the collections first and the resources then */ + for (Iterator i = collections.iterator(); i.hasNext(); ) + out.println(i.next()); + for (Iterator i = resources.iterator(); i.hasNext(); ) + out.println(i.next()); + + out.println("</ul>"); + out.println("</body>"); + out.println("</html>"); + out.flush(); + return; + } + + /* Processing a normal resource request */ + OutputStream out = transaction.write(); + DAVInputStream in = resource.read(); + byte buffer[] = new byte[4096]; + int k = -1; + while ((k = in.read(buffer)) != -1) out.write(buffer, 0, k); + in.close(); + out.flush(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/HEAD.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/HEAD.java new file mode 100644 index 000000000..029437f12 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/HEAD.java @@ -0,0 +1,79 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVNotModified; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; +import it.could.webdav.DAVUtilities; + +import java.io.IOException; +import java.util.Date; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> + * <code>HEAD</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class HEAD implements DAVMethod { + + /** + * <p>Create a new {@link HEAD} instance.</p> + */ + public HEAD() { + super(); + } + + /** + * <p>Process the <code>HEAD</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + /* Check if we have to force a resource not found or a redirection */ + if (resource.isNull()) + throw new DAVException(404, "Not found", resource); + + /* Check if this is a conditional (processable only for resources) */ + Date ifmod = transaction.getIfModifiedSince(); + Date lsmod = resource.getLastModified(); + if (resource.isResource() && (ifmod != null) && (lsmod != null)) { + /* HTTP doesn't send milliseconds, but Java does, so, reset them */ + lsmod = new Date(((long)(lsmod.getTime() / 1000)) * 1000); + if (!ifmod.before(lsmod)) throw new DAVNotModified(resource); + } + + /* Get the headers of this method */ + String ctyp = resource.getContentType(); + String etag = resource.getEntityTag(); + String lmod = DAVUtilities.formatHttpDate(resource.getLastModified()); + String clen = DAVUtilities.formatNumber(resource.getContentLength()); + + /* Set the normal headers that are required for a GET */ + if (resource.isCollection()) { + transaction.setContentType(GET.COLLECTION_MIME_TYPE); + } else if (ctyp != null) { + transaction.setContentType(ctyp); + } + if (etag != null) transaction.setHeader("ETag", etag); + if (lmod != null) transaction.setHeader("Last-Modified", lmod); + if (clen != null) transaction.setHeader("Content-Length", clen); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MKCOL.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MKCOL.java new file mode 100644 index 000000000..167d4a167 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MKCOL.java @@ -0,0 +1,57 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>MKCOL</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class MKCOL implements DAVMethod { + + /** + * <p>Create a new {@link MKCOL} instance.</p> + */ + public MKCOL() { + super(); + } + + /** + * <p>Process the <code>MKCOL</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + + /* Unsupported media type, we don't want content */ + if (transaction.hasRequestBody()) { + throw new DAVException (415, "No request body allowed in request"); + } + + /* Create the collection */ + resource.makeCollection(); + transaction.setStatus(201); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MOVE.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MOVE.java new file mode 100644 index 000000000..672636beb --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/MOVE.java @@ -0,0 +1,80 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVMultiStatus; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; +import java.net.URI; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>MOVE</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class MOVE implements DAVMethod { + + /** + * <p>Create a new {@link MOVE} instance.</p> + */ + public MOVE() { + super(); + } + + /** + * <p>Process the <code>MOVE</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + URI target = transaction.getDestination(); + if (target == null) throw new DAVException(412, "No destination"); + DAVResource dest = resource.getRepository().getResource(target); + + int depth = transaction.getDepth(); + boolean recursive = false; + if (depth == 0) { + recursive = false; + } else if (depth == DAVTransaction.INFINITY) { + recursive = true; + } else { + throw new DAVException(412, "Invalid Depth specified"); + } + + try { + int status; + if(! dest.isNull() && ! transaction.getOverwrite()) { + status = 412; // MOVE-on-existing should fail with 412 + } else { + resource.move(dest, transaction.getOverwrite(), recursive); + if(transaction.getOverwrite()) { + status = 204; // No Content + } else { + status = 201; // Created + } + } + transaction.setStatus(status); + } catch (DAVMultiStatus multistatus) { + multistatus.write(transaction); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/OPTIONS.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/OPTIONS.java new file mode 100644 index 000000000..0228c09dc --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/OPTIONS.java @@ -0,0 +1,52 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVProcessor; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> + * <code>OPTIONS</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class OPTIONS implements DAVMethod { + + /** + * <p>Create a new {@link OPTIONS} instance.</p> + */ + public OPTIONS() { + super(); + } + + /** + * <p>Process the <code>OPTIONS</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + transaction.setHeader("Content-Type", resource.getContentType()); + transaction.setHeader("Allow", DAVProcessor.METHODS); + transaction.setStatus(200); + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPFIND.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPFIND.java new file mode 100644 index 000000000..3854f20f0 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPFIND.java @@ -0,0 +1,122 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; +import it.could.webdav.DAVUtilities; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Iterator; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>PROPFIND</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class PROPFIND implements DAVMethod { + + /** + * <p>Create a new {@link PROPFIND} instance.</p> + */ + public PROPFIND() { + super(); + } + + /** + * <p>Process the <code>PROPFIND</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + /* Check if we have to force a resource not found or a redirection */ + if (resource.isNull()) + throw new DAVException(404, "Not found", resource); + + /* Check depth */ + int depth = transaction.getDepth(); + if (depth > 1) new DAVException(403, "Invalid depth"); + + /* What to do on a collection resource */ + transaction.setStatus(207); + transaction.setContentType("text/xml; charset=\"UTF-8\""); + PrintWriter out = transaction.write("UTF-8"); + + /* Output the XML declaration and the root document tag */ + out.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + out.println("<D:multistatus xmlns:D=\"DAV:\">"); + + /* Process this resource's property (always) */ + this.process(transaction, out, resource); + + /* Process this resource's children (if required) */ + if (resource.isCollection() && (depth > 0)) { + Iterator children = resource.getChildren(); + while (children.hasNext()) { + DAVResource child = (DAVResource) children.next(); + this.process(transaction, out, child); + } + } + + /* Close up the XML Multi-Status response */ + out.println("</D:multistatus>"); + out.flush(); + } + + private void process(DAVTransaction txn, PrintWriter out, DAVResource res) { + /* The href of the resource is only the absolute path */ + out.println(" <D:response>"); + out.println(" <D:href>" + txn.lookup(res).getPath() + "</D:href>"); + out.println(" <D:propstat>"); + out.println(" <D:prop>"); + + /* Figure out what we're dealing with here */ + if (res.isCollection()) { + this.process(out, "resourcetype", "<D:collection/>"); + } + this.process(out, "getcontenttype", res.getContentType()); + + this.process(out, "getetag", res.getEntityTag()); + String date = DAVUtilities.formatIsoDate(res.getCreationDate()); + this.process(out, "creationdate", date); + String lmod = DAVUtilities.formatHttpDate(res.getLastModified()); + this.process(out, "getlastmodified", lmod); + String clen = DAVUtilities.formatNumber(res.getContentLength()); + this.process(out, "getcontentlength", clen); + + out.println(" </D:prop>"); + out.println(" <D:status>HTTP/1.1 200 OK</D:status>"); + out.println(" </D:propstat>"); + out.println(" </D:response>"); + } + + private void process(PrintWriter out, String name, String value) { + if (value == null) return; + out.print(" <D:"); + out.print(name); + out.print(">"); + out.print(value); + out.print("</D:"); + out.print(name); + out.println(">"); + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPPATCH.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPPATCH.java new file mode 100644 index 000000000..fbddd0e84 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PROPPATCH.java @@ -0,0 +1,55 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>PROPPATCH</code> metohd implementation.</p> + * + * <p>As this servlet does not handle the creation of custom properties, this + * method will always fail with a <code>403</code> (Forbidden).</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class PROPPATCH implements DAVMethod { + + /** + * <p>Create a new {@link PROPPATCH} instance.</p> + */ + public PROPPATCH() { + super(); + } + + /** + * <p>Process the <code>PROPPATCH</code> method.</p> + * + * <p>As this servlet does not handle the creation of custom properties, + * this method will always fail with a <code>403</code> (Forbidden).</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + throw new DAVException(403, "All properties are immutable"); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PUT.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PUT.java new file mode 100644 index 000000000..fb995e69d --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/PUT.java @@ -0,0 +1,72 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.methods; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVOutputStream; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * <p><a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + * <code>PUT</code> metohd implementation.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class PUT implements DAVMethod { + + /** + * <p>Create a new {@link PUT} instance.</p> + */ + public PUT() { + super(); + } + + /** + * <p>Process the <code>PUT</code> method.</p> + */ + public void process(DAVTransaction transaction, DAVResource resource) + throws IOException { + /* + * The HTTP status code will depend on the existance of the resource: + * if not found: HTTP/1.1 201 Created + * if existing: HTTP/1.1 204 No Content + */ + transaction.setStatus(resource.isNull()? 201: 204); + + /* Open the streams for reading and writing */ + InputStream in = transaction.read(); + if (in == null) throw new DAVException(411, "Content-Length required"); + DAVOutputStream out = resource.write(); + + /* Write the content from the PUT to the specified resource */ + try { + byte buffer[] = new byte[4096]; + int k = -1; + while ((k = in.read(buffer)) != -1) out.write(buffer, 0, k); + in.close(); + out.close(); + } finally { + out.abort(); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/package.html new file mode 100644 index 000000000..f74f01889 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/methods/package.html @@ -0,0 +1,12 @@ +<html> + <head> + <title>Could.IT WebDAV Servlet</title> + </head> + <body> + <p> + This package contains the implementation of all Level 1 + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + methods provided by this framework. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/package.html new file mode 100644 index 000000000..1354f9f33 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/package.html @@ -0,0 +1,134 @@ +<html> + <head> + <title>Could.IT WebDAV Servlet</title> + </head> + <body> + <p> + This package contains a minimal + <a href="http://java.sun.com/products/servlet/">Servlet</a> + based implementation of the + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + specification. + </p> + <p> + This implementation does not in any way try to replace or extend the + <a href="http://jakarta.apache.org/slide/">Apache Slide</a> + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + implementation, but tries to provide a <i>very light</i> and <i>extremely + minimal</i> alternative to be used in those scenarios where space is + a constraint (the <code>.jar</code> file is less than 100 kylobites), + and advanced features are not required. + </p> + <p> + The most visible limitations of this approach is that this + implementation does not offer any support for the <code>LOCK</code> + method (it is therefore not <i>DAV Level 2</i> compliant), and that + there limited support for properties: + </p> + <ul> + <li> + The <code>PROPFIND</code> will only return the <i>read-only</i> + <code>getcontenttype</code>, <code>getlastmodified</code>, + <code>getcontentlength</code>, <code>getetag</code> and + <code>resourcetype</code> properties. + </li> + <li> + The <code>PROPPATCH</code> will <i>always</i> fail with a + <code>403</code> <i>Not Found</i> error. + </li> + </ul> + <p> + Another important limitation is that this implementation will only and + exclusively provide access to a {@link java.io.File} based backend. + If you want to deploy your repository on another kind of backend (such + as SQL databases) please look at the WebDAV implementation provided by + <a href="http://jakarta.apache.org/slide/">Apache Slide</a>. + </p> + + <h2>Configuration</h2> + <p> + The main entry point of this implementation is defined in the + {@link it.could.webdav.DAVServlet} class, which will handle all + <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> and + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> requests + for the URI path it is configured to handle. + </p> + <p> + To operate properly the {@link it.could.webdav.DAVServlet} class + must be configured in the web-application's <code>web.xml</code> + deployment descriptor. The relevant parts of a snippet of an example + configuration deployment descriptor might look like the following: + </p> + <pre> +<servlet> + <servlet-name>dav</servlet-name> + <servlet-class>it.could.webdav.DAVServlet</servlet-class> + <init-param> + <param-name>rootPath</param-name> + <param-value>dav</param-value> + </init-param> + <init-param> + <param-name>xmlOnly</param-name> + <param-value>false</param-value> + </init-param> + <init-param> + <param-name>debugEnabled</param-name> + <param-value>false</param-value> + </init-param> + <load-on-startup>1</load-on-startup> +</servlet> + +... + +<servlet-mapping> + <servlet-name>dav</servlet-name> + <url-pattern>/dav/*</url-pattern> +</servlet-mapping> + </pre> + <p> + In this example the {@link it.could.webdav.DAVServlet} servlet + is configured with all parameters its parameters: + </p> + <dl> + <dt>rootPath</dt> + <dd> + <i>[required]</i> This parameter indicates the path of the root of the + repository.<br /> + If the specified parameter represents a relative path, it will be + treated as a {@link javax.servlet.ServletContext#getResource(String) + ServletContext resource}.<br /> + Note that if you choose to distribute your web application in a + <code>.war</code> archive, your container will have to expand it + before intializing the {@link javax.servlet.ServletContext} as this + this implementation <i>requires</i> a {@link java.io.File} based + repository. + </dd> + <dt>xmlOnly</dt> + <dd> + <i>[optional, default="<code>false</code>"]</i> This parameter + will instruct the {@link it.could.webdav.DAVServlet} to create + a very specialized version of the repository accepting only + <a href="http://www.w3.org/TR/REC-xml/#sec-well-formed">well-formed + XML</a> resources and collections.<br /> + Note that when set to <code>true</code> this implementation will rely + on the <a href="http://java.sun.com/xml/jaxp/">JAXP</a> specification + to access a XML parser used to verify the <code>PUT</code> content. + </dd> + <dt>debugEnabled</dt> + <dd> + <i>[optional, default="<code>false</code>"]</i> This parameter + will instruct the {@link it.could.webdav.DAVServlet} to log + unimportant debugging information (such as the methods called by the + client) in the {@link javax.servlet.ServletContext#log(String) context + log}. + </dd> + </dl> + <p> + The configured {@link it.could.webdav.DAVServlet} will then have + to be mapped to a path, and in the example above, every request for + any URL beginning in <code>/dav/</code> will be handled by this + implementation, with a repository rooted in the <code>/dav/</code> + directory of the web application. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplica.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplica.java new file mode 100644 index 000000000..56fc2e499 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplica.java @@ -0,0 +1,242 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.replication; + +import it.could.util.StreamTools; +import it.could.util.http.WebDavClient; +import it.could.util.location.Location; +import it.could.webdav.DAVListener; +import it.could.webdav.DAVLogger; +import it.could.webdav.DAVRepository; +import it.could.webdav.DAVResource; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * <p>TODO: Document this class.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVReplica extends Thread implements DAVListener { + + private static final int SYNCHRONIZE = -1; + + private final DAVRepository repository; + private final DAVLogger logger; + private final Location location; + private final List actions = new ArrayList(); + + public DAVReplica(DAVRepository repository, Location location, + DAVLogger logger) + throws IOException { + this.location = new WebDavClient(location).getLocation(); + this.repository = repository; + this.logger = logger; + this.start(); + } + + public void synchronize() + throws IOException { + this.logger.log("Scheduling full synchronization"); + this.notify(this.repository.getResource((String)null), SYNCHRONIZE); + } + + public void notify(DAVResource resource, int event) { + this.logger.debug("Event for \"" + resource.getRelativePath() + "\""); + if (resource.getRepository() != this.repository) return; + synchronized (this.actions) { + this.actions.add(new Action(resource, event)); + this.actions.notify(); + } + } + + public void run() { + this.logger.debug("Starting background replica thread on " + location); + while (true) try { + final DAVReplica.Action array[]; + synchronized(this.actions) { + try { + if (this.actions.isEmpty()) this.actions.wait(); + final int s = this.actions.size(); + array = (Action []) this.actions.toArray(new Action[s]); + this.actions.clear(); + } catch (InterruptedException exception) { + this.logger.debug("Exiting background replica thread"); + return; + } + } + + for (int x = 0; x < array.length; x ++) try { + this.replicate(array[x]); + } catch (Throwable throwable) { + final String path = array[x].resource.getRelativePath(); + final String message = "Error synchronizing resource " + path; + this.logger.log(message, throwable); + } + } catch (Throwable throwable) { + this.logger.log("Replica thread attempted suicide", throwable); + } + } + + private void replicate(DAVReplica.Action action) { + final DAVResource resource = action.resource; + + if (action.event == SYNCHRONIZE) { + this.synchronize(resource); + + } else try { + final String path = resource.getParent().getRelativePath(); + final Location location = this.location.resolve(path); + final WebDavClient client = new WebDavClient(location); + final String child = resource.getName(); + + switch(action.event) { + case RESOURCE_CREATED: + case RESOURCE_MODIFIED: + this.logger.debug("Putting resource " + path); + this.put(resource, client); + break; + case RESOURCE_REMOVED: + case COLLECTION_REMOVED: + this.logger.debug("Deleting resource " + path); + client.delete(child); + break; + case COLLECTION_CREATED: + this.logger.debug("Creating collection " + path); + client.mkcol(child); + break; + } + } catch (IOException exception) { + String message = "Error replicating " + resource.getRelativePath(); + this.logger.log(message, exception); + } + } + + private void put(DAVResource resource, WebDavClient client) + throws IOException { + final String name = resource.getName(); + final long length = resource.getContentLength().longValue(); + final OutputStream output = client.put(name, length); + final InputStream input = resource.read(); + StreamTools.copy(input, output); + } + + private void synchronize(DAVResource resource) { + /* Figure out the path of the resource */ + final String path = resource.getRelativePath(); + + /* If it's a file or null, just skip the whole thing */ + if (! resource.isCollection()) { + this.logger.log("Synchronization on non-collection " + path); + return; + } + + /* Open a webdav client to the collection to synchronize */ + this.logger.log("Synchronizing collection " + path); + final WebDavClient client; + try { + final Location location = this.location.resolve(path); + client = new WebDavClient(location); + } catch (IOException exception) { + this.logger.log("Error creating WebDAV client", exception); + return; + } + + /* Create a list of all children from the DAV client */ + final Set children = new HashSet(); + for (Iterator iter = client.iterator(); iter.hasNext(); ) + children.add(iter.next()); + + /* Process all resource children one by one and ensure they exist */ + for (Iterator iter = resource.getChildren(); iter.hasNext(); ) { + final DAVResource child = (DAVResource) iter.next(); + final String name = child.getName(); + + /* Remove this from the resources that will be removed later */ + children.remove(name); + + /* If the client doesn't have this child, add it to the replica */ + if (! client.hasChild(name)) try { + if (child.isCollection()) { + this.logger.debug("Client doesn't have collection " + name); + client.mkcol(name); + this.synchronize(child); + + } else { + this.logger.debug("Client doesn't have resource " + name); + this.put(child, client); + } + } catch (IOException exception) { + this.logger.log("Error creating new child " + name, exception); + + /* If this child is a collection, it must be a collection on dav */ + } else if (child.isCollection()) try { + if (!client.isCollection(name)) { + this.logger.debug("Recreating collection " + name); + client.delete(name).mkcol(name); + } + this.synchronize(child); + } catch (IOException exception) { + this.logger.log("Error creating collection " + name, exception); + + /* Ok, the resource is a normal one, verify size and timestamp */ + } else try { + final Date rlast = child.getLastModified(); + final Date dlast = client.getLastModified(name); + if ((rlast != null) && (rlast.equals(dlast))) { + final Long rlen = child.getContentLength(); + final long dlen = client.getContentLength(name); + if ((rlen == null) || (rlen.longValue() != dlen)) { + this.logger.debug("Resending resource " + name); + this.put(child, client.delete(name)); + } + } + } catch (IOException exception) { + this.logger.log("Error resending resource " + name, exception); + } + } + + /* Any other child that was not removed above, will go away now! */ + for (Iterator iter = children.iterator(); iter.hasNext(); ) { + final String name = (String) iter.next(); + try { + this.logger.debug("Removing leftovers " + name); + client.delete(name); + } catch (IOException exception) { + this.logger.log("Error removing left over " + name, exception); + } + } + } + + private static final class Action { + final DAVResource resource; + final int event; + + private Action(DAVResource resource, int event) { + this.resource = resource; + this.event = event; + } + } +}
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplicator.java b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplicator.java new file mode 100644 index 000000000..c7de67b14 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/DAVReplicator.java @@ -0,0 +1,131 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package it.could.webdav.replication; + +import it.could.util.location.Location; +import it.could.webdav.DAVListener; +import it.could.webdav.DAVLogger; +import it.could.webdav.DAVRepository; +import it.could.webdav.DAVServlet; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +/** + * <p>The {@link DAVReplicator} class is a {@link DAVListener} replicating + * all content to the WebDAV repository specified at construction.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + */ +public class DAVReplicator extends HttpServlet { + + /** <p>The {@link DAVReplica} instances managed by this.</p> */ + private final List replicas = new ArrayList(); + + /** + * <p>Create a new {@link DAVServlet} instance.</p> + */ + public DAVReplicator() { + super(); + } + + /** + * <p>Initialize this {@link Servlet} instance.</p> + * + * <p>This servlet requires a couple of initialization parameters: the + * first one is "<code>repository</code>" indicating the name of + * the {@link DAVServlet} in the "<code>web.xml</code>" deployment + * descriptor whose repository should be replicated.</p> + * + * <p>The second required parameter "<code>replicas</code>" + * must contain a (whitespace separated list of) URL(s) where the original + * repository should be replicated to.</p> + * + * <p>Finally, when set to <code>true</code>, the optional parameter + * <code>debugEnabled</code> will enable logging of method invocation and + * events in the repository.</p> + */ + public void init(ServletConfig config) + throws ServletException { + /* Initialize the super, just in case, and remember the context */ + super.init(config); + + /* Setup logging */ + boolean debug = "true".equals(config.getInitParameter("debugEnabled")); + DAVLogger logger = new DAVLogger(config, debug); + + /* Try to retrieve the WebDAV repository from the servlet context */ + final String repositoryName = config.getInitParameter("repository"); + final DAVRepository repository; + if (repositoryName == null) { + throw new ServletException("Parameter \"rootPath\" not specified"); + } else try { + final String key = DAVServlet.getRepositoryKey(repositoryName); + final ServletContext context = config.getServletContext(); + repository = (DAVRepository) context.getAttribute(key); + if (repository == null) + throw new ServletException("Unable to access repository from " + + "servlet \"" + repository + "\""); + } catch (ClassCastException exception) { + final String message = "Class cast exception accessing repository"; + throw new ServletException(message, exception); + } + + /* Access the different WebDAV replicas */ + final String replicas = config.getInitParameter("replicas"); + if (replicas == null) { + throw new ServletException("Parameter \"replicas\" not specified"); + } + + try { + final StringTokenizer tokenizer = new StringTokenizer(replicas); + while (tokenizer.hasMoreTokens()) { + final Location location = Location.parse(tokenizer.nextToken()); + final DAVReplica replica = new DAVReplica(repository, location, + logger); + logger.log("Added repository replica to \"" + location + "\""); + repository.addListener(replica); + this.replicas.add(replica); + replica.synchronize(); + } + } catch (IOException exception) { + throw new ServletException("Error creating replica", exception); + } + + /* Check that we have at least one replica in */ + if (this.replicas.size() != 0) return; + throw new ServletException("No replicas specified for repository"); + } + + /** + * <p>Destroy {@link DAVServlet} instance interrupting all running + * {@link DAVReplica} instances.</p> + */ + public void destroy() { + for (Iterator iter = this.replicas.iterator(); iter.hasNext() ; ) { + ((DAVReplica) iter.next()).interrupt(); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/package.html b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/package.html new file mode 100644 index 000000000..4ca6e5c1c --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/it/could/webdav/replication/package.html @@ -0,0 +1,12 @@ +<html> + <head> + <title>Could.IT WebDAV Servlet</title> + </head> + <body> + <p> + This package contains a framework for maintaining fully replicated + <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> + repositories. + </p> + </body> +</html>
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/AbstractDavServerComponent.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/AbstractDavServerComponent.java new file mode 100644 index 000000000..c674f6b9e --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/AbstractDavServerComponent.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +import java.io.File; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * AbstractDavServerComponent + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractDavServerComponent.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public abstract class AbstractDavServerComponent + implements DavServerComponent +{ + private List listeners; + protected boolean useIndexHtml = false; + + public AbstractDavServerComponent() + { + listeners = new ArrayList(); + } + + public void addListener( DavServerListener listener ) + { + listeners.add( listener ); + } + + public void removeListener( DavServerListener listener ) + { + listeners.remove( listener ); + } + + protected void triggerCollectionCreated( String resource ) + { + Iterator it = listeners.iterator(); + while ( it.hasNext() ) + { + DavServerListener listener = (DavServerListener) it.next(); + try + { + listener.serverCollectionCreated( this, resource ); + } + catch ( Exception e ) + { + /* ignore error */ + } + } + } + + protected void triggerCollectionRemoved( String resource ) + { + Iterator it = listeners.iterator(); + while ( it.hasNext() ) + { + DavServerListener listener = (DavServerListener) it.next(); + try + { + listener.serverCollectionRemoved( this, resource ); + } + catch ( Exception e ) + { + /* ignore error */ + } + } + } + + protected void triggerResourceCreated( String resource ) + { + Iterator it = listeners.iterator(); + while ( it.hasNext() ) + { + DavServerListener listener = (DavServerListener) it.next(); + try + { + listener.serverResourceCreated( this, resource ); + } + catch ( Exception e ) + { + /* ignore error */ + } + } + } + + protected void triggerResourceRemoved( String resource ) + { + Iterator it = listeners.iterator(); + while ( it.hasNext() ) + { + DavServerListener listener = (DavServerListener) it.next(); + try + { + listener.serverResourceRemoved( this, resource ); + } + catch ( Exception e ) + { + /* ignore error */ + } + } + } + + protected void triggerResourceModified( String resource ) + { + Iterator it = listeners.iterator(); + while ( it.hasNext() ) + { + DavServerListener listener = (DavServerListener) it.next(); + try + { + listener.serverResourceModified( this, resource ); + } + catch ( Exception e ) + { + /* ignore error */ + } + } + } + + public boolean hasResource( String resource ) + { + File rootDir = getRootDirectory(); + if ( rootDir == null ) + { + return false; + } + File resourceFile = new File( rootDir, resource ); + return resourceFile.exists(); + } + + public boolean isUseIndexHtml() + { + return this.useIndexHtml; + } + + public void setUseIndexHtml( boolean useIndexHtml ) + { + this.useIndexHtml = useIndexHtml; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerComponent.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerComponent.java new file mode 100644 index 000000000..db4389f46 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerComponent.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; + +/** + * DavServerComponent + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DavServerComponent.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public interface DavServerComponent +{ + /** The Plexus ROLE name */ + public static final String ROLE = DavServerComponent.class.getName(); + + /** + * Get the Prefix for this server component. + * @return the prefix associated with this component. + */ + public String getPrefix(); + + /** + * Set the prefix for this server component. + * @param prefix the prefix to use. + */ + public void setPrefix( String prefix ); + + /** + * <p> + * Flag to indicate how the dav server component should treat a GET request against + * a DAV Collection. + * </p> + * + * <p> + * If true, the collection being requested will be searched for an index.html (or index.htm) + * file to serve back, before it defaults to displaying the collection (directory) contents. + * </p> + * + * <p> + * If false, the collection will always be presented in as a list of contents. + * </p> + * + * @return true to use the index.html instead of directory contents. + */ + public boolean isUseIndexHtml(); + + /** + * <p> + * Flag to indicate how the dav server component should treat a GET request against + * a DAV Collection. + * </p> + * + * <p> + * If true, the collection being requested will be searched for an index.html (or index.htm) + * file to serve back, before it defaults to displaying the collection (directory) contents. + * </p> + * + * <p> + * If false, the collection will always be presented in as a list of contents. + * </p> + * + * @param useIndexHtml true to use the index.html instead of directory contents. + */ + public void setUseIndexHtml( boolean useIndexHtml ); + + /** + * Get the root directory for this server. + * + * @return the root directory for this server. + */ + public File getRootDirectory(); + + /** + * Set the root directory for this server's content. + * + * @param rootDirectory the root directory for this server's content. + */ + public void setRootDirectory( File rootDirectory ); + + /** + * Add a Server Listener for this server component. + * + * @param listener the listener to add for this component. + */ + public void addListener( DavServerListener listener ); + + /** + * Remove a server listener for this server component. + * + * @param listener the listener to remove. + */ + public void removeListener( DavServerListener listener ); + + /** + * Perform any initialization needed. + * + * @param servletConfig the servlet config that might be needed. + * @throws DavServerException if there was a problem initializing the server component. + */ + public void init( ServletConfig servletConfig ) throws DavServerException; + + /** + * Performs a simple filesystem check for the specified resource. + * + * @param resource the resource to check for. + * @return true if the resource exists. + */ + public boolean hasResource( String resource ); + + /** + * Process incoming request. + * + * @param request the incoming request to process. + * @param response the outgoing response to provide. + */ + public void process( DavServerRequest request, HttpServletResponse response ) + throws DavServerException, ServletException, IOException; +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerException.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerException.java new file mode 100644 index 000000000..893f059ef --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerException.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +/** + * DavServerException + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DavServerException.java 5379 2007-01-07 22:54:41Z joakime $ + */ +public class DavServerException + extends Exception +{ + + public DavServerException() + { + } + + public DavServerException( String message ) + { + super( message ); + } + + public DavServerException( Throwable cause ) + { + super( cause ); + } + + public DavServerException( String message, Throwable cause ) + { + super( message, cause ); + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerListener.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerListener.java new file mode 100644 index 000000000..251cee6ad --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerListener.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +/** + * DavServerListener + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DavServerListener.java 5379 2007-01-07 22:54:41Z joakime $ + */ +public interface DavServerListener +{ + public void serverCollectionCreated( DavServerComponent server, String resource ); + + public void serverCollectionRemoved( DavServerComponent server, String resource ); + + public void serverResourceCreated( DavServerComponent server, String resource ); + + public void serverResourceRemoved( DavServerComponent server, String resource ); + + public void serverResourceModified( DavServerComponent server, String resource ); +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerManager.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerManager.java new file mode 100644 index 000000000..7c86cd533 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DavServerManager.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +import java.io.File; +import java.util.Collection; + +/** + * DavServerManager + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DavServerManager.java 6017 2007-03-06 00:39:53Z joakime $ + */ +public interface DavServerManager +{ + /** The Plexus ROLE name. */ + public static final String ROLE = DavServerManager.class.getName(); + + /** + * Create a DavServerComponent and start tracking it. + * + * @param prefix the prefix for this component. + * @param rootDirectory the root directory for this component's content. null to not set a root directory. + * @return the created component, suitable for use. + * @throws DavServerException + */ + public DavServerComponent createServer( String prefix, File rootDirectory ) throws DavServerException; + + /** + * Get the collection of tracked servers. + * + * @return Collection of {@link DavServerComponent} objects. + */ + public Collection getServers(); + + /** + * Removes a specific server from the tracked list of servers. + * + * NOTE: This does not remove the associated files on disk, merely the reference being tracked. + * + * @param prefix the prefix to remove. + */ + public void removeServer( String prefix ); + + /** + * Get the {@link DavServerComponent} associated with the specified prefix. + * + * @param prefix the prefix for the dav server component to use. + * @return the DavServerComponent, or null if not found. + */ + public DavServerComponent getServer( String prefix ); + + /** + * Remove all servers being tracked by the manager. + */ + public void removeAllServers(); +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DefaultDavServerManager.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DefaultDavServerManager.java new file mode 100644 index 000000000..b66edee3f --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/DefaultDavServerManager.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * DefaultDavServerManager + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DefaultDavServerManager.java 7009 2007-10-25 23:34:43Z joakime $ + * + * @plexus.component role="org.apache.maven.archiva.webdav.DavServerManager" role-hint="default" + */ +public class DefaultDavServerManager + implements DavServerManager +{ + /** + * @plexus.requirement role-hint="simple" + */ + private DavServerComponent server; + + private Map servers; + + public DefaultDavServerManager() + { + servers = new HashMap(); + } + + public DavServerComponent createServer( String prefix, File rootDirectory ) + throws DavServerException + { + if ( servers.containsKey( prefix ) ) + { + throw new DavServerException( "Unable to create a new server on a pre-existing prefix [" + prefix + "]" ); + } + + server.setPrefix( prefix ); + if ( rootDirectory != null ) + { + server.setRootDirectory( rootDirectory ); + } + + servers.put( prefix, server ); + + return server; + } + + public DavServerComponent getServer( String prefix ) + { + return (DavServerComponent) servers.get( prefix ); + } + + public void removeServer( String prefix ) + { + servers.remove( prefix ); + } + + public Collection getServers() + { + return servers.values(); + } + + public void removeAllServers() + { + servers.clear(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/AbstractWebDavServlet.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/AbstractWebDavServlet.java new file mode 100644 index 000000000..b654f359a --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/AbstractWebDavServlet.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet; + +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.maven.archiva.webdav.DavServerManager; +import org.codehaus.plexus.spring.PlexusToSpringUtils; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Enumeration; + +/** + * AbstractWebDavServlet + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractWebDavServlet.java 7009 2007-10-25 23:34:43Z joakime $ + */ +public abstract class AbstractWebDavServlet + extends HttpServlet +{ + public static final String INIT_USE_INDEX_HTML = "dav.use.index.html"; + + private boolean debug = false; + + protected DavServerManager davManager; + + public String getServletInfo() + { + return "Plexus WebDAV Servlet"; + } + + public void init( ServletConfig config ) + throws ServletException + { + super.init( config ); + + WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext( config.getServletContext() ); + davManager = (DavServerManager) wac.getBean( PlexusToSpringUtils.buildSpringId( DavServerManager.ROLE ) ); + if ( davManager == null ) + { + throw new ServletException( "Unable to lookup davManager" ); + } + } + + /** + * Perform any authentication steps here. + * + * If authentication fails, it is the responsibility of the implementor to issue + * the appropriate status codes and/or challenge back on the response object, then + * return false on the overridden version of this method. + * + * To effectively not have authentication, just implement this method and always + * return true. + * + * @param davRequest the incoming dav request. + * @param httpResponse the outgoing http response. + * @return true if user is authenticated, false if not. + * @throws ServletException if there was a problem performing authencation. + * @throws IOException if there was a problem obtaining credentials or issuing challenge. + */ + public boolean isAuthenticated( DavServerRequest davRequest, HttpServletResponse httpResponse ) + throws ServletException, IOException + { + // Always return true. Effectively no Authentication done. + return true; + } + + /** + * Perform any authorization steps here. + * + * If authorization fails, it is the responsibility of the implementor to issue + * the appropriate status codes and/or challenge back on the response object, then + * return false on the overridden version of this method. + * + * to effectively not have authorization, just implement this method and always + * return true. + * + * @param davRequest + * @param httpResponse + * @return + * @throws ServletException + * @throws IOException + */ + public boolean isAuthorized( DavServerRequest davRequest, HttpServletResponse httpResponse ) + throws ServletException, IOException + { + // Always return true. Effectively no Authorization done. + return true; + } + + public boolean isDebug() + { + return debug; + } + + public void setDebug( boolean debug ) + { + this.debug = debug; + } + + protected void requestDebug( HttpServletRequest request ) + { + if ( debug ) + { + System.out.println( "-->>> request ----------------------------------------------------------" ); + System.out.println( "--> " + request.getScheme() + "://" + request.getServerName() + ":" + + request.getServerPort() + request.getServletPath() ); + System.out.println( request.getMethod() + " " + request.getRequestURI() + + ( request.getQueryString() != null ? "?" + request.getQueryString() : "" ) + " " + "HTTP/1.1" ); + + Enumeration enHeaders = request.getHeaderNames(); + while ( enHeaders.hasMoreElements() ) + { + String headerName = (String) enHeaders.nextElement(); + String headerValue = request.getHeader( headerName ); + System.out.println( headerName + ": " + headerValue ); + } + + System.out.println(); + + System.out.println( "------------------------------------------------------------------------" ); + } + } + + public abstract void setUseIndexHtml( boolean useIndexHtml ); + + public boolean getUseIndexHtml( ServletConfig config ) + throws ServletException + { + String useIndexHtml = config.getInitParameter( INIT_USE_INDEX_HTML ); + + if ( StringUtils.isEmpty( useIndexHtml ) ) + { + return false; + } + + return BooleanUtils.toBoolean( useIndexHtml ); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/DavServerRequest.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/DavServerRequest.java new file mode 100644 index 000000000..913e5a7f8 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/DavServerRequest.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet; + +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; + +/** + * DavServerRequest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: DavServerRequest.java 7073 2007-11-22 04:04:50Z brett $ + */ +public interface DavServerRequest +{ + public String getPrefix(); + + public String getLogicalResource(); + + public void setLogicalResource( String logicalResource ); + + public WrappedRepositoryRequest getRequest(); +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicDavServerRequest.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicDavServerRequest.java new file mode 100644 index 000000000..3bbdc1703 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicDavServerRequest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet.basic; + +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; + +/** + * BasicDavServerRequest - for requests that have a prefix based off of the servlet path id. + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: BasicDavServerRequest.java 7073 2007-11-22 04:04:50Z brett $ + */ +public class BasicDavServerRequest + implements DavServerRequest +{ + private WrappedRepositoryRequest request; + + private String prefix; + + private String logicalResource; + + public BasicDavServerRequest( WrappedRepositoryRequest request ) + { + this.request = request; + this.prefix = request.getServletPath(); + this.logicalResource = request.getPathInfo(); + } + + public void setLogicalResource( String logicalResource ) + { + this.logicalResource = logicalResource; + this.request.setPathInfo( logicalResource ); + } + + public String getLogicalResource() + { + return this.logicalResource; + } + + public String getPrefix() + { + return this.prefix; + } + + public WrappedRepositoryRequest getRequest() + { + return request; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicWebDavServlet.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicWebDavServlet.java new file mode 100644 index 000000000..49193e31f --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/basic/BasicWebDavServlet.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet.basic; + +import org.apache.maven.archiva.webdav.DavServerComponent; +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.servlet.AbstractWebDavServlet; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; +import org.codehaus.plexus.util.StringUtils; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; + +/** + * BasicWebDavServlet - Basic implementation of a single WebDAV server as servlet. + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: BasicWebDavServlet.java 6017 2007-03-06 00:39:53Z joakime $ + */ +public class BasicWebDavServlet + extends AbstractWebDavServlet +{ + public static final String INIT_ROOT_DIRECTORY = "dav.root"; + + private DavServerComponent davServer; + + // ----------------------------------------------------------------------- + // Servlet Implementation + // ----------------------------------------------------------------------- + + public void init( ServletConfig config ) + throws ServletException + { + super.init( config ); + + String prefix = config.getServletName(); + + boolean useIndexHtml = getUseIndexHtml( config ); + File rootDir = getRootDirectory( config ); + + if ( rootDir != null && !rootDir.isDirectory() ) + { + log( "Invalid configuration, the dav root " + rootDir.getPath() + + " is not a directory: [" + rootDir.getAbsolutePath() + "]" ); + } + + try + { + davServer = davManager.createServer( prefix, rootDir ); + davServer.setUseIndexHtml( useIndexHtml ); + davServer.init( config ); + } + catch ( DavServerException e ) + { + throw new ServletException( "Unable to create DAV Server component for prefix [" + prefix + + "] mapped to root directory [" + rootDir.getPath() + "]", e ); + } + } + + public File getRootDirectory( ServletConfig config ) + throws ServletException + { + String rootDirName = config.getInitParameter( INIT_ROOT_DIRECTORY ); + + if ( StringUtils.isEmpty( rootDirName ) ) + { + log( "Init Parameter '" + INIT_ROOT_DIRECTORY + "' is empty." ); + return null; + } + + return new File( rootDirName ); + } + + protected void service( HttpServletRequest httpRequest, HttpServletResponse httpResponse ) + throws ServletException, IOException + { + DavServerRequest davRequest = new BasicDavServerRequest( new WrappedRepositoryRequest( httpRequest ) ); + + if ( davServer == null ) + { + throw new ServletException( "Unable to service DAV request due to unconfigured DavServerComponent." ); + } + + requestDebug( httpRequest ); + + if ( !isAuthenticated( davRequest, httpResponse ) ) + { + return; + } + + if ( !isAuthorized( davRequest, httpResponse ) ) + { + return; + } + + try + { + davServer.process( davRequest, httpResponse ); + } + catch ( DavServerException e ) + { + throw new ServletException( "Unable to process request.", e ); + } + } + + public void setUseIndexHtml( boolean useIndexHtml ) + { + davServer.setUseIndexHtml( useIndexHtml ); + } + + public DavServerComponent getDavServer() + { + return davServer; + } + + public void setDavServer( DavServerComponent davServer ) + { + this.davServer = davServer; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequest.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequest.java new file mode 100644 index 000000000..4d9ec43cb --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequest.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet.multiplexed; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; + +/** + * <p/> + * MultiplexedDavServerRequest - For requests that contain the server prefix information within the requested + * servlet's pathInfo parameter (as the first path entry). + * </p> + * <p/> + * <p/> + * You would use this dav server request object when you are working with a single servlet that is handling + * multiple dav server components. + * </p> + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: MultiplexedDavServerRequest.java 7073 2007-11-22 04:04:50Z brett $ + */ +public class MultiplexedDavServerRequest + implements DavServerRequest +{ + private WrappedRepositoryRequest request; + + private String prefix; + + private String logicalResource; + + public MultiplexedDavServerRequest( WrappedRepositoryRequest request ) + { + String requestPathInfo = StringUtils.defaultString( request.getPathInfo() ); + + // Remove prefixing slash as the repository id doesn't contain it; + if ( requestPathInfo.startsWith( "/" ) ) + { + requestPathInfo = requestPathInfo.substring( 1 ); + } + + // Find first element, if slash exists. + int slash = requestPathInfo.indexOf( '/' ); + if ( slash > 0 ) + { + // Filtered: "central/org/apache/maven/" -> "central" + this.prefix = requestPathInfo.substring( 0, slash ); + + this.logicalResource = requestPathInfo.substring( slash ); + + if ( this.logicalResource.endsWith( "/.." ) ) + { + this.logicalResource += "/"; + } + + /* Perform a simple security normalization of the requested pathinfo. + * This is to prevent requests for information outside of the root directory. + */ + this.logicalResource = FilenameUtils.normalize( logicalResource ); + + if ( logicalResource != null && logicalResource.startsWith( "//" ) ) + { + logicalResource = logicalResource.substring( 1 ); + } + + if ( this.logicalResource == null ) + { + this.logicalResource = "/"; + } + } + else + { + this.prefix = requestPathInfo; + this.logicalResource = "/"; + } + + this.request = request; + this.request.setPathInfo( logicalResource ); + } + + public void setLogicalResource( String logicalResource ) + { + this.logicalResource = logicalResource; + this.request.setPathInfo( logicalResource ); + } + + public String getLogicalResource() + { + return this.logicalResource; + } + + public String getPrefix() + { + return this.prefix; + } + + public WrappedRepositoryRequest getRequest() + { + return request; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedWebDavServlet.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedWebDavServlet.java new file mode 100644 index 000000000..19d03093b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedWebDavServlet.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet.multiplexed; + +import org.apache.maven.archiva.webdav.DavServerComponent; +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.DavServerManager; +import org.apache.maven.archiva.webdav.servlet.AbstractWebDavServlet; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Iterator; + +/** + * <p> + * MultiplexedWebDavServlet - and abstracted multiplexed webdav servlet. + * </p> + * + * <p> + * Implementations of this servlet should override the {@link #initServers} method and create all of the + * appropriate DavServerComponents needed using the {@link DavServerManager} obtained via the {@link #getDavManager()} + * method. + * </p> + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: MultiplexedWebDavServlet.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public abstract class MultiplexedWebDavServlet + extends AbstractWebDavServlet +{ + private boolean useIndexHtml = false; + + public void init( ServletConfig config ) + throws ServletException + { + super.init( config ); + + this.useIndexHtml = getUseIndexHtml( config ); + + try + { + initServers( config ); + } + catch ( DavServerException e ) + { + throw new ServletException( e ); + } + } + + /** + * Create any DavServerComponents here. + * Use the {@link #createServer(String, File, ServletConfig)} method to create your servers. + * + * @param config the config to use. + * @throws DavServerException if there was a problem initializing the server components. + */ + public abstract void initServers( ServletConfig config ) + throws DavServerException; + + public DavServerComponent createServer( String prefix, File rootDirectory, ServletConfig config ) + throws DavServerException + { + DavServerComponent serverComponent = davManager.createServer( prefix, rootDirectory ); + serverComponent.setUseIndexHtml( useIndexHtml ); + serverComponent.init( config ); + return serverComponent; + } + + protected void service( HttpServletRequest httpRequest, HttpServletResponse httpResponse ) + throws ServletException, IOException + { + DavServerRequest davRequest = new MultiplexedDavServerRequest( new WrappedRepositoryRequest( httpRequest ) ); + + DavServerComponent davServer = davManager.getServer( davRequest.getPrefix() ); + + if ( davServer == null ) + { + String errorMessage = "[" + davRequest.getPrefix() + "] Not Found (Likely Unconfigured)."; + httpResponse.sendError( HttpURLConnection.HTTP_NOT_FOUND, errorMessage ); + return; + } + + requestDebug( httpRequest ); + + if ( !isAuthenticated( davRequest, httpResponse ) ) + { + return; + } + + if ( !isAuthorized( davRequest, httpResponse ) ) + { + return; + } + + try + { + davServer.process( davRequest, httpResponse ); + } + catch ( DavServerException e ) + { + throw new ServletException( "Unable to process request.", e ); + } + } + + public void setUseIndexHtml( boolean useIndexHtml ) + { + for ( Iterator it = davManager.getServers().iterator(); it.hasNext(); ) + { + DavServerComponent davServer = (DavServerComponent) it.next(); + davServer.setUseIndexHtml( useIndexHtml ); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/HackedMoveMethod.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/HackedMoveMethod.java new file mode 100644 index 000000000..736a3c07e --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/HackedMoveMethod.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import it.could.webdav.DAVException; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVMultiStatus; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; + +import java.io.IOException; +import java.net.URI; + +/** + * HackedMoveMethod - Created to address the needs for inter-repository moves. + * + * @author Pier Fumagalli (Original it.could.webdav 0.4 version) + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> (Hacked Version) + * @version $Id: HackedMoveMethod.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public class HackedMoveMethod + implements DAVMethod +{ + + public HackedMoveMethod() + { + super(); + } + + /** + * <p>Process the <code>MOVE</code> method.</p> + */ + public void process( DAVTransaction transaction, DAVResource resource ) + throws IOException + { + URI target = transaction.getDestination(); + if ( target == null ) + throw new DAVException( 412, "No destination" ); + + if ( target.getScheme() == null ) + { + // This is a relative file system destination target. + DAVResource dest = resource.getRepository().getResource( target ); + moveWithinRepository( transaction, resource, dest ); + } + else + { + // This is a inter-repository move request. + URI dest = target; + moveInterRepository( transaction, resource, dest ); + } + } + + private void moveInterRepository( DAVTransaction transaction, DAVResource resource, URI dest ) + throws DAVException + { + /* TODO: Figure out how to handle a Repository to Repository MOVE of content, and still maintain + * the security credentials from the original request. (Need to support NTLM, Digest, BASIC) + * + * IDEA: Could support non-secured Webdav Destination using slide client libraries. + */ + transaction.setStatus( 501 ); + throw new DAVException( 501, "Server side MOVE to external WebDAV instance not supported." ); + } + + private void moveWithinRepository( DAVTransaction transaction, DAVResource resource, DAVResource dest ) + throws IOException + { + int depth = transaction.getDepth(); + boolean recursive = false; + if ( depth == 0 ) + { + recursive = false; + } + else if ( depth == DAVTransaction.INFINITY ) + { + recursive = true; + } + else + { + throw new DAVException( 412, "Invalid Depth specified" ); + } + + try + { + int status; + if ( !dest.isNull() && !transaction.getOverwrite() ) + { + status = 412; // MOVE-on-existing should fail with 412 + } + else + { + resource.copy( dest, transaction.getOverwrite(), recursive ); + resource.delete(); + + if ( transaction.getOverwrite() ) + { + status = 204; // No Content + } + else + { + status = 201; // Created + } + } + transaction.setStatus( status ); + } + catch ( DAVMultiStatus multistatus ) + { + multistatus.write( transaction ); + } + } + +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/ReplacementGetMethod.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/ReplacementGetMethod.java new file mode 100644 index 000000000..3eee8786b --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/ReplacementGetMethod.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import it.could.util.StreamTools; +import it.could.webdav.DAVException; +import it.could.webdav.DAVInputStream; +import it.could.webdav.DAVMethod; +import it.could.webdav.DAVNotModified; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; +import it.could.webdav.DAVUtilities; +import org.apache.commons.lang.StringUtils; +import org.apache.maven.archiva.webdav.util.MimeTypes; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +/** + * ReplacementGetMethod + * + * @author Pier Fumagalli (Original it.could.webdav 0.4 version) + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> (Replacement Version) + * @version $Id: ReplacementGetMethod.java 7002 2007-10-23 22:40:37Z joakime $ + * + * @plexus.component + * role="it.could.webdav.DAVMethod" + * role-hint="get-with-indexing" + */ +public class ReplacementGetMethod implements DAVMethod +{ + /** <p>The encoding charset to repsesent collections.</p> */ + public static final String ENCODING = "UTF-8"; + + /** <p>The mime type that {@link ReplacementGetMethod} will use serving index.html files.</p> */ + public static final String HTML_MIME_TYPE = "text/html"; + + /** <p>The mime type that {@link ReplacementGetMethod} will use serving collections.</p> */ + public static final String COLLECTION_MIME_TYPE = HTML_MIME_TYPE + "; charset=\"" + ENCODING + "\""; + + /** <p>The header for content disposition.</p> */ + public static final String CONTENT_DISPOSITION = "Content-Disposition"; + + /** <p>The content-disposition for fancy-indexing.</p> */ + public static final String INLINE_INDEX_HTML = "inline; filename=\"index.html\""; + + /** + * @plexus.requirement + */ + private MimeTypes mimeTypes; + + private boolean useIndexHtml = false; + + /** + * <p>Create a new {@link ReplacementGetMethod} instance.</p> + */ + public ReplacementGetMethod() + { + super(); + } + + /** + * <p>Process the <code>GET</code> method.</p> + */ + public void process( DAVTransaction transaction, DAVResource resource ) throws IOException + { + // Handle boilerplate + if ( resource.isNull() ) + throw new DAVException( 404, "Not found", resource ); + + notModified( transaction, resource ); + + copyHeaders( transaction, resource ); + + // Process the request. + final String originalPath = transaction.getOriginalPath(); + final String normalizedPath = transaction.getNormalizedPath(); + final String current; + final String parent; + + if ( originalPath.equals( normalizedPath ) ) + { + final String relativePath = resource.getRelativePath(); + if ( relativePath.equals( "" ) ) + { + current = transaction.lookup( resource ).toASCIIString(); + } + else + { + current = relativePath; + } + parent = "./"; + } + else + { + current = "./"; + parent = "../"; + } + + if ( resource.isCollection() ) + { + DAVResource indexHtml = null; + + if ( useIndexHtml ) + { + for ( Iterator it = resource.getChildren(); it.hasNext(); ) + { + DAVResource child = (DAVResource) it.next(); + String name = child.getDisplayName().toLowerCase(); + if ( StringUtils.equals( "index.html", name ) || StringUtils.equals( "index.htm", name ) ) + { + indexHtml = child; + break; + } + } + } + + if ( useIndexHtml && indexHtml != null ) + { + transaction.setContentType( COLLECTION_MIME_TYPE ); + transaction.setHeader( CONTENT_DISPOSITION, INLINE_INDEX_HTML ); + sendResource( transaction, indexHtml ); + } + else + { + transaction.setContentType( COLLECTION_MIME_TYPE ); + transaction.setHeader( CONTENT_DISPOSITION, INLINE_INDEX_HTML ); + sendFancyIndex( transaction, resource, current, parent ); + } + } + else + { + /* Processing a normal resource request */ + transaction.setContentType( mimeTypes.getMimeType( resource.getDisplayName() ) ); + transaction.setHeader( CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getDisplayName() + "\"" ); + sendResource( transaction, resource ); + } + } + + private void copyHeaders( DAVTransaction transaction, DAVResource resource ) + { + /* Get the headers of this method */ + String ctyp = resource.getContentType(); + String etag = resource.getEntityTag(); + String lmod = DAVUtilities.formatHttpDate( resource.getLastModified() ); + String clen = DAVUtilities.formatNumber( resource.getContentLength() ); + + /* Set the normal headers that are required for a GET */ + if ( ctyp != null ) + { + transaction.setContentType( ctyp ); + } + + if ( etag != null ) + { + transaction.setHeader( "ETag", etag ); + } + + if ( lmod != null ) + { + transaction.setHeader( "Last-Modified", lmod ); + } + + if ( clen != null ) + { + transaction.setHeader( "Content-Length", clen ); + } + } + + private void sendResource( DAVTransaction transaction, DAVResource resource ) throws IOException + { + OutputStream out = null; + DAVInputStream in = null; + + try + { + out = transaction.write(); + in = resource.read(); + + byte buffer[] = new byte[4096 * 16]; + int k = -1; + while ( ( k = in.read( buffer ) ) != -1 ) + { + out.write( buffer, 0, k ); + } + + out.flush(); + } + finally + { + StreamTools.close( in ); + StreamTools.close( out ); + } + } + + private void sendFancyIndex( DAVTransaction transaction, DAVResource resource, final String current, + final String parent ) throws IOException + { + PrintWriter out = transaction.write( ENCODING ); + String path = resource.getRelativePath(); + out.println( "<html>" ); + out.println( "<head>" ); + out.println( "<title>Collection: /" + path + "</title>" ); + out.println( "</head>" ); + out.println( "<body>" ); + out.println( "<h2>Collection: /" + path + "</h2>" ); + out.println( "<ul>" ); + + /* Process the parent */ + final DAVResource parentResource = resource.getParent(); + if ( parentResource != null ) + { + out.print( "<li><a href=\"" ); + out.print( parent ); + out.print( "\">" ); + out.print( parentResource.getDisplayName() ); + out.println( "</a> <i><small>(Parent)</small></i></li>" ); + out.println( "</ul>" ); + out.println( "<ul>" ); + } + + /* Process the children (in two sorted sets, for nice ordering) */ + Set resources = new TreeSet(); + Set collections = new TreeSet(); + Iterator iterator = resource.getChildren(); + while ( iterator.hasNext() ) + { + final DAVResource child = (DAVResource) iterator.next(); + final StringBuffer buffer = new StringBuffer(); + final String childPath = child.getDisplayName(); + buffer.append( "<li><a href=\"" ); + buffer.append( current ); + buffer.append( childPath ); + buffer.append( "\">" ); + buffer.append( childPath ); + buffer.append( "</li>" ); + if ( child.isCollection() ) + { + collections.add( buffer.toString() ); + } + else + { + resources.add( buffer.toString() ); + } + } + + /* Spit out the collections first and the resources then */ + for ( Iterator i = collections.iterator(); i.hasNext(); ) + out.println( i.next() ); + for ( Iterator i = resources.iterator(); i.hasNext(); ) + out.println( i.next() ); + + out.println( "</ul>" ); + out.println( "</body>" ); + out.println( "</html>" ); + out.flush(); + } + + private void notModified( DAVTransaction transaction, DAVResource resource ) + { + Date ifmod = transaction.getIfModifiedSince(); + Date lsmod = resource.getLastModified(); + if ( resource.isResource() && ( ifmod != null ) && ( lsmod != null ) ) + { + /* HTTP doesn't send milliseconds, but Java does, so, reset them */ + lsmod = new Date( ( (long) ( lsmod.getTime() / 1000 ) ) * 1000 ); + if ( !ifmod.before( lsmod ) ) + throw new DAVNotModified( resource ); + } + } + + public boolean isUseIndexHtml() + { + return useIndexHtml; + } + + public void setUseIndexHtml( boolean useIndexHtml ) + { + this.useIndexHtml = useIndexHtml; + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponent.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponent.java new file mode 100644 index 000000000..af6794faf --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponent.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import it.could.webdav.DAVListener; +import it.could.webdav.DAVProcessor; +import it.could.webdav.DAVRepository; +import it.could.webdav.DAVResource; +import it.could.webdav.DAVTransaction; +import org.apache.commons.lang.StringUtils; +import org.apache.maven.archiva.webdav.AbstractDavServerComponent; +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.servlet.DavServerRequest; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; + +/** + * SimpleDavServerComponent + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: SimpleDavServerComponent.java 7097 2007-11-30 12:57:29Z handyande $ + * + * @plexus.component role="org.apache.maven.archiva.webdav.DavServerComponent" + * role-hint="simple" + * instantiation-strategy="per-lookup" + */ +public class SimpleDavServerComponent + extends AbstractDavServerComponent + implements DAVListener +{ + /** + * @plexus.requirement + * role="it.could.webdav.DAVMethod" + * role-hint="get-with-indexing" + */ + public ReplacementGetMethod methodGet; + + private String prefix; + + private File rootDirectory; + + private DAVRepository davRepository; + + private DAVProcessor davProcessor; + + public String getPrefix() + { + return prefix; + } + + public File getRootDirectory() + { + return rootDirectory; + } + + public void setPrefix( String prefix ) + { + this.prefix = prefix; + } + + public void setRootDirectory( File rootDirectory ) + { + this.rootDirectory = rootDirectory; + } + + public void init( ServletConfig servletConfig ) + throws DavServerException + { + servletConfig.getServletContext().log( "Initializing " + this.getClass().getName() ); + try + { + davRepository = new DAVRepository( rootDirectory ); + davProcessor = new DAVProcessor( davRepository ); + davRepository.addListener( this ); + + hackDavProcessor( davProcessor ); + } + catch ( IOException e ) + { + throw new DavServerException( "Unable to initialize DAVRepository.", e ); + } + } + + /** + * Replace the problematic dav methods with local hacked versions. + * + * @param davProcessor + * @throws DavServerException + */ + private void hackDavProcessor( DAVProcessor davProcessor ) + throws DavServerException + { + davProcessor.setMethod( "MOVE", new HackedMoveMethod() ); + davProcessor.setMethod( "GET", methodGet ); + + /* Reflection based technique. + try + { + Field fldInstance = davProcessor.getClass().getDeclaredField( "INSTANCES" ); + fldInstance.setAccessible( true ); + + Map mapInstances = (Map) fldInstance.get( davProcessor ); + + // Replace MOVE method. + // TODO: Remove MOVE method when upgrading it.could.webdav to v0.5 + mapInstances.put( "MOVE", (DAVMethod) new HackedMoveMethod() ); + + // Replace GET method. + mapInstances.put( "GET", (DAVMethod) methodGet ); + } + catch ( Throwable e ) + { + throw new DavServerException( "Unable to twiddle DAVProcessor.INSTANCES field.", e ); + } + */ + } + + public void process( DavServerRequest request, HttpServletResponse response ) + throws ServletException, IOException + { + DAVTransaction transaction = new DAVTransaction( request.getRequest(), response ); + + /* BEGIN - it.could.webdav hacks + * TODO: Remove hacks with release of it.could.webdav 0.5 (or newer) + */ + String depthValue = request.getRequest().getHeader( "Depth" ); + if ( StringUtils.equalsIgnoreCase( "infinity", depthValue ) ) + { + // See - http://could.it/bugs/browse/DAV-3 + request.getRequest().setHeader( "Depth", "infinity" ); + } + /* END - it.could.webdav hacks */ + + davProcessor.process( transaction ); + } + + public void notify( DAVResource resource, int event ) + { + switch ( event ) + { + case DAVListener.COLLECTION_CREATED: + triggerCollectionCreated( resource.getRelativePath() ); + break; + case DAVListener.COLLECTION_REMOVED: + triggerCollectionRemoved( resource.getRelativePath() ); + break; + case DAVListener.RESOURCE_CREATED: + triggerResourceCreated( resource.getRelativePath() ); + break; + case DAVListener.RESOURCE_REMOVED: + triggerResourceRemoved( resource.getRelativePath() ); + break; + case DAVListener.RESOURCE_MODIFIED: + triggerResourceModified( resource.getRelativePath() ); + break; + } + } + + public void setUseIndexHtml( boolean useIndexHtml ) + { + super.setUseIndexHtml( useIndexHtml ); + this.methodGet.setUseIndexHtml( useIndexHtml ); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/MimeTypes.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/MimeTypes.java new file mode 100644 index 000000000..1219e712e --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/MimeTypes.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.util; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.codehaus.plexus.logging.AbstractLogEnabled; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * MimeTypes + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: MimeTypes.java 7010 2007-10-25 23:35:02Z joakime $ + * + * @plexus.component role="org.apache.maven.archiva.webdav.util.MimeTypes" + */ +public class MimeTypes + extends AbstractLogEnabled + implements Initializable +{ + /** + * @plexus.configuration default-value="org/apache/maven/archiva/webdav/util/mime-types.txt" + */ + private String resource; + + private Map mimeMap = new HashMap(); + + /** + * Get the Mime Type for the provided filename. + * + * @param filename the filename to obtain the mime type for. + * @return a mime type String, or null if filename is null, has no extension, or no mime type is associated with it. + */ + public String getMimeType( String filename ) + { + String value = null; + if ( !StringUtils.isEmpty( filename ) ) + { + int index = filename.lastIndexOf( '.' ); + + if ( index >= 0 ) + { + value = (String) mimeMap.get( filename.substring( index + 1 ).toLowerCase() ); + } + } + return value; + + } + + public void initialize() + throws InitializationException + { + load( resource ); + } + + public void load( File file ) + { + if ( !file.exists() || !file.isFile() || !file.canRead() ) + { + getLogger().error( "Unable to load mime types from file " + file.getAbsolutePath() + " : not a readable file." ); + return; + } + + FileInputStream fis = null; + + try + { + fis = new FileInputStream( file ); + } + catch ( FileNotFoundException e ) + { + getLogger().error( "Unable to load mime types from file " + file.getAbsolutePath() + " : " + e.getMessage(), e ); + } + finally + { + IOUtils.closeQuietly( fis ); + } + } + + public void load( String resourceName ) + { + ClassLoader cloader = this.getClass().getClassLoader(); + + /* Load up the mime types table */ + URL mimeURL = cloader.getResource( resourceName ); + + if ( mimeURL == null ) + { + throw new IllegalStateException( "Unable to find resource " + resourceName ); + } + + InputStream mimeStream = null; + + try + { + mimeStream = mimeURL.openStream(); + load( mimeStream ); + } + catch ( IOException e ) + { + getLogger().error( "Unable to load mime map " + resourceName + " : " + e.getMessage(), e ); + } + finally + { + IOUtils.closeQuietly( mimeStream ); + } + } + + public void load( InputStream mimeStream ) + { + mimeMap.clear(); + + InputStreamReader reader = null; + BufferedReader buf = null; + + try + { + reader = new InputStreamReader( mimeStream ); + buf = new BufferedReader( reader ); + String line = null; + + while ( ( line = buf.readLine() ) != null ) + { + line = line.trim(); + + if ( line.length() == 0 ) + { + // empty line. skip it + continue; + } + + if ( line.startsWith( "#" ) ) + { + // Comment. skip it + continue; + } + + StringTokenizer tokenizer = new StringTokenizer( line ); + if ( tokenizer.countTokens() > 1 ) + { + String type = tokenizer.nextToken(); + while ( tokenizer.hasMoreTokens() ) + { + String extension = tokenizer.nextToken().toLowerCase(); + this.mimeMap.put( extension, type ); + } + } + } + } + catch ( IOException e ) + { + getLogger().error( "Unable to read mime types from input stream : " + e.getMessage(), e ); + } + finally + { + IOUtils.closeQuietly( buf ); + IOUtils.closeQuietly( reader ); + } + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WebdavMethodUtil.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WebdavMethodUtil.java new file mode 100644 index 000000000..a551e8c17 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WebdavMethodUtil.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.util; + +import org.apache.commons.lang.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * WebdavMethodUtil + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: WebdavMethodUtil.java 5412 2007-01-13 01:18:47Z joakime $ + */ +public class WebdavMethodUtil +{ + private static final List READ_METHODS; + + static + { + READ_METHODS = new ArrayList(); + READ_METHODS.add( "HEAD" ); + READ_METHODS.add( "GET" ); + READ_METHODS.add( "PROPFIND" ); + READ_METHODS.add( "OPTIONS" ); + READ_METHODS.add( "REPORT" ); + } + + public static boolean isReadMethod( String method ) + { + if ( StringUtils.isBlank( method ) ) + { + return false; + } + + return READ_METHODS.contains( method.toUpperCase() ); + } + + public static boolean isWriteMethod( String method ) + { + if ( StringUtils.isBlank( method ) ) + { + return false; + } + + return !READ_METHODS.contains( method.toUpperCase() ); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequest.java b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequest.java new file mode 100644 index 000000000..29ef2f556 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequest.java @@ -0,0 +1,184 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.util; + +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * RepositoryRequest - wrapped servlet request to adjust the incoming request before the components get it. + * It eliminates the prefix from the pathInfo portion of the URL requested. + * And also allows for Header adjustment. + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: WrappedRepositoryRequest.java 7001 2007-10-23 22:40:14Z joakime $ + */ +public class WrappedRepositoryRequest + extends HttpServletRequestWrapper +{ + private String pathInfo; + + private Map headers; + + /** + * The Date Formats most commonly seen in Request Headers. + */ + private SimpleDateFormat dateFormats[]; + + public WrappedRepositoryRequest( HttpServletRequest request ) + { + super( request ); + + dateFormats = new SimpleDateFormat[] { + new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss zzz" ), + new SimpleDateFormat( "EEE, dd-MMM-yy HH:mm:ss" ), + new SimpleDateFormat( "EEE MMM dd HH:mm:ss yyyy" ) }; + + headers = new HashMap(); + + Enumeration enHeaders = request.getHeaderNames(); + while ( enHeaders.hasMoreElements() ) + { + String name = (String) enHeaders.nextElement(); + String value = request.getHeader( name ); + headers.put( name, value ); + } + } + + public void setHeader( String name, String value ) + { + headers.put( name, value ); + } + + public long getDateHeader( String name ) + { + String value = (String) headers.get( name ); + if ( StringUtils.isEmpty( value ) ) + { + // no value? return -1 + return -1; + } + + // Try most common formats first. + for ( int i = 0; i < dateFormats.length; i++ ) + { + try + { + Date date = (Date) dateFormats[i].parseObject( value ); + return date.getTime(); + } + catch ( java.lang.Exception e ) + { + /* ignore exception */ + } + } + + // Now check for the odd "GMT" formats (hey, it happens) + if ( value.endsWith( " GMT" ) ) + { + value = value.substring( 0, value.length() - 4 ); + + for ( int i = 0; i < dateFormats.length; i++ ) + { + try + { + Date date = (Date) dateFormats[i].parseObject( value ); + return date.getTime(); + } + catch ( java.lang.Exception e ) + { + /* ignore exception */ + } + } + } + + // unrecognized format? return -1 + return -1; + } + + public String getHeader( String name ) + { + return (String) headers.get( name ); + } + + public Enumeration getHeaderNames() + { + return new Enumeration() + { + private Iterator iter = headers.keySet().iterator(); + + public boolean hasMoreElements() + { + return iter.hasNext(); + } + + public Object nextElement() + { + return iter.next(); + } + }; + } + + public int getIntHeader( String name ) + { + String value = getHeader( name ); + try + { + return Integer.parseInt( value ); + } + catch ( NumberFormatException e ) + { + return -1; + } + } + + public void setPathInfo( String alternatePathInfo ) + { + this.pathInfo = alternatePathInfo; + } + + public String getPathInfo() + { + if ( this.pathInfo != null ) + { + return this.pathInfo; + } + + return super.getPathInfo(); + } + + public String getServletPath() + { + if ( this.pathInfo != null ) + { + return super.getServletPath() + "/" + this.pathInfo; + } + + return super.getServletPath(); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/DAVServlet.java b/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/DAVServlet.java new file mode 100644 index 000000000..4b25e84c8 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/DAVServlet.java @@ -0,0 +1,57 @@ +/* ========================================================================== * + * Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/> * + * All rights reserved. * + * ========================================================================== * + * * + * Licensed under the Apache License, Version 2.0 (the "License"). You may * + * not use this file except in compliance with the License. You may obtain a * + * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>. * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * + * License for the specific language governing permissions and limitations * + * under the License. * + * * + * ========================================================================== */ +package org.betaversion.webdav; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +/** + * <p>The {@link DAVServlet} class has been moved to a new package and should + * now be referred as {@link it.could.webdav.DAVServlet}.</p> + * + * <p>This class will be preserved for some time (not so long) to give people + * time to update their servlet deployment descriptors.</p> + * + * @author <a href="http://could.it/">Pier Fumagalli</a> + * @deprecated This class has been moved into the <code>it.could.webdav</code> + * package. Reconfigure your <code>web.xml</code> deployment + * descriptor to use {@link it.could.webdav.DAVServlet}. + */ +public class DAVServlet extends it.could.webdav.DAVServlet { + + /** + * <p>Create a new {@link DAVServlet} instance.</p> + */ + public DAVServlet() { + super(); + } + + /** + * <p>Initialize this {@link DAVServlet} instance reporting to the + * {@link ServletContext} log that this class is deprecated.</p> + */ + public void init(ServletConfig config) + throws ServletException { + final ServletContext context = config.getServletContext(); + context.log("The class \"" + this.getClass().getName() + + "\" is deprecated"); + context.log("Modify the \"web.xml\" deployment descriptor to use \"" + + it.could.webdav.DAVServlet.class.getName() + "\""); + super.init(config); + } +} diff --git a/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/package.html b/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/package.html new file mode 100644 index 000000000..cdc5bf1ee --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/java/org/betaversion/webdav/package.html @@ -0,0 +1,15 @@ +<html> + <head> + <title>Could.IT WebDAV Servlet</title> + </head> + <body> + <p> + This package is deprecated, but preserved to maintain compatibility + with previous versions. + </p> + <p> + Please refer to the documentation in the {@link it.could.webdav} package + for the new version description. + </p> + </body> +</html> diff --git a/archiva-web/archiva-webdav/src/main/resources/org/apache/maven/archiva/webdav/util/mime-types.txt b/archiva-web/archiva-webdav/src/main/resources/org/apache/maven/archiva/webdav/util/mime-types.txt new file mode 100644 index 000000000..56ab6f59e --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/resources/org/apache/maven/archiva/webdav/util/mime-types.txt @@ -0,0 +1,128 @@ +# This is a comment. I love comments. + +# This file controls what Internet media types are sent to the client for +# given file extension(s). Sending the correct media type to the client +# is important so they know how to handle the content of the file. +# Extra types can either be added here or by using an AddType directive +# in your config files. For more information about Internet media types, +# please read RFC 2045, 2046, 2047, 2048, and 2077. The Internet media type +# registry is at <http://www.iana.org/assignments/media-types/>. + +# MIME type Extensions + +application/andrew-inset ez +application/atom+xml atom +application/java-archive jar +application/mac-binhex40 hqx +application/mac-compactpro cpt +application/mathml+xml mathml +application/msword doc +application/octet-stream bin dms lha lzh exe class so dll dmg +application/oda oda +application/ogg ogg +application/pdf pdf +application/postscript ai eps ps +application/rdf+xml rdf +application/smil smi smil +application/srgs gram +application/srgs+xml grxml +application/vnd.mif mif +application/vnd.mozilla.xul+xml xul +application/vnd.ms-excel xls +application/vnd.ms-powerpoint ppt +application/vnd.rn-realmedia rm +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/voicexml+xml vxml +application/x-bcpio bcpio +application/x-cdlink vcd +application/x-chess-pgn pgn +application/x-cpio cpio +application/x-csh csh +application/x-director dcr dir dxr +application/x-dvi dvi +application/x-futuresplash spl +application/x-gtar gtar +application/x-hdf hdf +application/x-java-jnlp-file jnlp +application/x-javascript js +application/x-koan skp skd skt skm +application/x-latex latex +application/x-netcdf nc cdf +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-stuffit sit +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-texinfo texinfo texi +application/x-troff t tr roff +application/x-troff-man man +application/x-troff-me me +application/x-troff-ms ms +application/x-ustar ustar +application/x-wais-source src +application/xhtml+xml xhtml xht +application/xml xml xsl +application/xml-dtd dtd +application/xslt+xml xslt +application/zip zip +audio/basic au snd +audio/midi mid midi kar +audio/mpeg mpga mp2 mp3 +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-pn-realaudio ram ra +audio/x-wav wav +chemical/x-pdb pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +image/gif gif +image/ief ief +image/jp2 jp2 +image/jpeg jpeg jpg jpe +image/pict pict pic pct +image/png png +image/svg+xml svg +image/tiff tiff tif +image/vnd.djvu djvu djv +image/vnd.wap.wbmp wbmp +image/x-cmu-raster ras +image/x-icon ico +image/x-macpaint pntg pnt mac +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-quicktime qtif qti +image/x-rgb rgb +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +model/iges igs iges +model/mesh msh mesh silo +model/vrml wrl vrml +text/calendar ics ifb +text/css css +text/html html htm +text/plain asc txt +text/richtext rtx +text/rtf rtf +text/sgml sgml sgm +text/tab-separated-values tsv +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-setext etx +video/mp4 mp4 +video/mpeg mpeg mpg mpe +video/quicktime qt mov +video/vnd.mpegurl mxu m4u +video/x-dv dv dif +video/x-msvideo avi +video/x-sgi-movie movie +x-conference/x-cooltalk ice diff --git a/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/mime.types b/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/mime.types new file mode 100644 index 000000000..5baed56f7 --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/mime.types @@ -0,0 +1,127 @@ +# This is a comment. I love comments. + +# This file controls what Internet media types are sent to the client for +# given file extension(s). Sending the correct media type to the client +# is important so they know how to handle the content of the file. +# Extra types can either be added here or by using an AddType directive +# in your config files. For more information about Internet media types, +# please read RFC 2045, 2046, 2047, 2048, and 2077. The Internet media type +# registry is at <http://www.iana.org/assignments/media-types/>. + +# MIME type Extensions + +application/andrew-inset ez +application/atom+xml atom +application/mac-binhex40 hqx +application/mac-compactpro cpt +application/mathml+xml mathml +application/msword doc +application/octet-stream bin dms lha lzh exe class so dll dmg +application/oda oda +application/ogg ogg +application/pdf pdf +application/postscript ai eps ps +application/rdf+xml rdf +application/smil smi smil +application/srgs gram +application/srgs+xml grxml +application/vnd.mif mif +application/vnd.mozilla.xul+xml xul +application/vnd.ms-excel xls +application/vnd.ms-powerpoint ppt +application/vnd.rn-realmedia rm +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/voicexml+xml vxml +application/x-bcpio bcpio +application/x-cdlink vcd +application/x-chess-pgn pgn +application/x-cpio cpio +application/x-csh csh +application/x-director dcr dir dxr +application/x-dvi dvi +application/x-futuresplash spl +application/x-gtar gtar +application/x-hdf hdf +application/x-java-jnlp-file jnlp +application/x-javascript js +application/x-koan skp skd skt skm +application/x-latex latex +application/x-netcdf nc cdf +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-stuffit sit +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-texinfo texinfo texi +application/x-troff t tr roff +application/x-troff-man man +application/x-troff-me me +application/x-troff-ms ms +application/x-ustar ustar +application/x-wais-source src +application/xhtml+xml xhtml xht +application/xml xml xsl +application/xml-dtd dtd +application/xslt+xml xslt +application/zip zip +audio/basic au snd +audio/midi mid midi kar +audio/mpeg mpga mp2 mp3 +audio/x-aiff aif aiff aifc +audio/x-mpegurl m3u +audio/x-pn-realaudio ram ra +audio/x-wav wav +chemical/x-pdb pdb +chemical/x-xyz xyz +image/bmp bmp +image/cgm cgm +image/gif gif +image/ief ief +image/jp2 jp2 +image/jpeg jpeg jpg jpe +image/pict pict pic pct +image/png png +image/svg+xml svg +image/tiff tiff tif +image/vnd.djvu djvu djv +image/vnd.wap.wbmp wbmp +image/x-cmu-raster ras +image/x-icon ico +image/x-macpaint pntg pnt mac +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-quicktime qtif qti +image/x-rgb rgb +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +model/iges igs iges +model/mesh msh mesh silo +model/vrml wrl vrml +text/calendar ics ifb +text/css css +text/html html htm +text/plain asc txt +text/richtext rtx +text/rtf rtf +text/sgml sgml sgm +text/tab-separated-values tsv +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-setext etx +video/mp4 mp4 +video/mpeg mpeg mpg mpe +video/quicktime qt mov +video/vnd.mpegurl mxu m4u +video/x-dv dv dif +video/x-msvideo avi +video/x-sgi-movie movie +x-conference/x-cooltalk ice diff --git a/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/webdav.props b/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/webdav.props new file mode 100644 index 000000000..815c14baf --- /dev/null +++ b/archiva-web/archiva-webdav/src/main/resources/plexus-webdav/webdav.props @@ -0,0 +1,13 @@ +# +# A simple property file defining some strings that will be returned and/or +# used by the Could.IT DAVServlet at different stages of processing +# + +# Returned by DAVServlet in the "getServletInfo()" method +servlet.information = Could.IT WebDAV Servlet + +# Added to the "Server" header every time a request is processed +servlet.signature = CouldIT-WebDAV + +# Version used in build files and combined to information and signature +version = 0.5-dev diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/TestableHttpServletRequest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/TestableHttpServletRequest.java new file mode 100644 index 000000000..f214139db --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/TestableHttpServletRequest.java @@ -0,0 +1,495 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav; + +import org.apache.commons.lang.NotImplementedException; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletInputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Principal; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + +/** + * TestableHttpServletRequest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: TestableHttpServletRequest.java 6940 2007-10-16 01:02:02Z joakime $ + */ +public class TestableHttpServletRequest + implements HttpServletRequest +{ + + public TestableHttpServletRequest() + { + setDefaults(); + } + + public void setDefaults() + { + authType = null; + scheme = "http"; + protocol = "HTTP/1.1"; + serverName = "localhost"; + serverPort = 80; + remoteHost = "localhost"; + } + + private String authType; + + private String characterEncoding; + + private int contentLength; + + private String contentType; + + private String contextPath; + + private Locale locale; + + private String method; + + private String pathInfo; + + private String pathTranslated; + + private String protocol; + + private String queryString; + + private String remoteAddr; + + private String remoteHost; + + private String remoteUser; + + private String requestedSessionId; + + private boolean requestedSessionIdFromCookie; + + private boolean requestedSessionIdFromUrl; + + private boolean requestedSessionIdValid; + + private StringBuffer requestURL; + + private String scheme; + + private boolean secure; + + private String serverName; + + private int serverPort; + + private String servletPath; + + public Object getAttribute( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getAttribute(String)" ) ); + } + + public Enumeration getAttributeNames() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getAttributeNames()" ) ); + } + + public String getAuthType() + { + return authType; + } + + public String getCharacterEncoding() + { + return characterEncoding; + } + + public int getContentLength() + { + return contentLength; + } + + public String getContentType() + { + return contentType; + } + + public String getContextPath() + { + return contextPath; + } + + public Cookie[] getCookies() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getCookies()" ) ); + } + + public long getDateHeader( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getDateHeader(String)" ) ); + } + + public String getHeader( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getHeader(String)" ) ); + } + + private Map headers = new HashMap(); + + public Enumeration getHeaderNames() + { + return new IterEnumeration( headers.keySet().iterator() ); + } + + public Enumeration getHeaders( String name ) + { + throw new NotImplementedException( notImplemented( ".getHeaders(String)" ) ); + } + + public ServletInputStream getInputStream() + throws IOException + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getInputStream()" ) ); + } + + public int getIntHeader( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getIntHeader(String)" ) ); + } + + public Locale getLocale() + { + return locale; + } + + public Enumeration getLocales() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getLocales()" ) ); + } + + public String getMethod() + { + return method; + } + + public String getParameter( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getParameter(String)" ) ); + } + + public Map getParameterMap() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getParameterMap()" ) ); + } + + public Enumeration getParameterNames() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getParameterNames()" ) ); + } + + public String[] getParameterValues( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getParameterValues(String)" ) ); + } + + public String getPathInfo() + { + return pathInfo; + } + + public String getPathTranslated() + { + return pathTranslated; + } + + public String getProtocol() + { + return protocol; + } + + public String getQueryString() + { + return queryString; + } + + public BufferedReader getReader() + throws IOException + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getReader()" ) ); + } + + public String getRealPath( String path ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getRealPath(String)" ) ); + } + + public String getRemoteAddr() + { + return remoteAddr; + } + + public String getRemoteHost() + { + return remoteHost; + } + + public String getRemoteUser() + { + return remoteUser; + } + + public RequestDispatcher getRequestDispatcher( String path ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getRequestDispatcher(String)" ) ); + } + + public String getRequestedSessionId() + { + return requestedSessionId; + } + + public String getRequestURI() + { + return requestURL.toString(); + } + + public StringBuffer getRequestURL() + { + return requestURL; + } + + public String getScheme() + { + return scheme; + } + + public String getServerName() + { + return serverName; + } + + public int getServerPort() + { + return serverPort; + } + + public String getServletPath() + { + return servletPath; + } + + public HttpSession getSession() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getSession()" ) ); + } + + public HttpSession getSession( boolean create ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getSession(boolean)" ) ); + } + + public Principal getUserPrincipal() + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".getUserPrincipal()" ) ); + } + + public boolean isRequestedSessionIdFromCookie() + { + return requestedSessionIdFromCookie; + } + + public boolean isRequestedSessionIdFromUrl() + { + return requestedSessionIdFromUrl; + } + + public boolean isRequestedSessionIdFromURL() + { + return requestedSessionIdFromUrl; + } + + public boolean isRequestedSessionIdValid() + { + return requestedSessionIdValid; + } + + public boolean isSecure() + { + return secure; + } + + public boolean isUserInRole( String role ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".isUserInRole(String)" ) ); + } + + public void removeAttribute( String name ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".removeAttribute(String)" ) ); + } + + public void setAttribute( String name, Object o ) + { + // TODO: Implement if needed. + throw new NotImplementedException( notImplemented( ".setAttribute(String, Object)" ) ); + } + + public void setCharacterEncoding( String encoding ) + throws UnsupportedEncodingException + { + this.characterEncoding = encoding; + } + + public void setContentLength( int contentLength ) + { + this.contentLength = contentLength; + } + + public void setContentType( String contentType ) + { + this.contentType = contentType; + } + + public void setContextPath( String contextPath ) + { + this.contextPath = contextPath; + } + + public void setMethod( String method ) + { + this.method = method; + } + + public void setPathInfo( String pathInfo ) + { + this.pathInfo = pathInfo; + } + + public void setProtocol( String protocol ) + { + this.protocol = protocol; + } + + public void setQueryString( String queryString ) + { + this.queryString = queryString; + } + + public void setScheme( String scheme ) + { + this.scheme = scheme; + } + + public void setSecure( boolean secure ) + { + this.secure = secure; + } + + public void setServerName( String serverName ) + { + this.serverName = serverName; + } + + public void setServerPort( int serverPort ) + { + this.serverPort = serverPort; + } + + public void setServletPath( String servletPath ) + { + this.servletPath = servletPath; + } + + public void setUrl( String urlString ) + throws MalformedURLException + { + URL url = new URL( urlString ); + this.queryString = url.getQuery(); + this.scheme = url.getProtocol(); + this.serverName = url.getHost(); + this.serverPort = url.getPort(); + + String path = url.getPath(); + if ( !path.startsWith( this.servletPath ) ) + { + throw new MalformedURLException( "Unable to operate on request path [" + path + + "] outside of servletPath [" + this.servletPath + "]." ); + } + + this.pathInfo = path.substring( this.servletPath.length() ); + this.requestURL = new StringBuffer( this.pathInfo ); + } + + private String notImplemented( String msg ) + { + return msg + " is not implemented in " + this.getClass().getName(); + } + + class IterEnumeration + implements Enumeration + { + private Iterator iter; + + public IterEnumeration( Iterator it ) + { + this.iter = it; + } + + public boolean hasMoreElements() + { + return this.iter.hasNext(); + } + + public Object nextElement() + { + return this.iter.next(); + } + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequestTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequestTest.java new file mode 100644 index 000000000..62719cfdb --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/servlet/multiplexed/MultiplexedDavServerRequestTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.servlet.multiplexed; + +import junit.framework.TestCase; +import org.apache.maven.archiva.webdav.TestableHttpServletRequest; +import org.apache.maven.archiva.webdav.util.WrappedRepositoryRequest; + +import java.net.MalformedURLException; + +/** + * MultiplexedDavServerRequestTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: MultiplexedDavServerRequestTest.java 6940 2007-10-16 01:02:02Z joakime $ + */ +public class MultiplexedDavServerRequestTest + extends TestCase +{ + private void assertMultiURL( String expectedPrefix, String expectedLogicalResource, String url ) + throws MalformedURLException + { + TestableHttpServletRequest testrequest = new TestableHttpServletRequest(); + testrequest.setMethod( "GET" ); + testrequest.setServletPath( "/repository" ); + testrequest.setUrl( url ); + + WrappedRepositoryRequest wraprequest = new WrappedRepositoryRequest( testrequest ); + MultiplexedDavServerRequest multirequest = new MultiplexedDavServerRequest( wraprequest ); + + assertEquals( expectedPrefix, multirequest.getPrefix() ); + assertEquals( expectedLogicalResource, multirequest.getLogicalResource() ); + } + + public void testNormalUsage() + throws MalformedURLException + { + assertMultiURL( "corporate", "/", "http://localhost:9091/repository/corporate" ); + assertMultiURL( "corporate", "/dom4j/dom4j/1.4", "http://localhost:9091/repository/corporate/dom4j/dom4j/1.4" ); + } + + public void testHacker() + throws MalformedURLException + { + assertMultiURL( "corporate", "/etc/passwd", "http://localhost:9091/repository/corporate//etc/passwd" ); + // Since the double ".." puts the path outside of the /corporate/, it will return "/" as a hack fallback. + assertMultiURL( "corporate", "/", "http://localhost:9091/repository/corporate/dom4j/../../etc/passwd" ); + assertMultiURL( "corporate", "/", "http://localhost:9091/repository/corporate/../.." ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.java new file mode 100644 index 000000000..801c59bdf --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import org.apache.maven.archiva.webdav.test.AbstractBasicWebdavProviderTestCase; + +/** + * SimpleDavServerComponentBasicTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: SimpleDavServerComponentBasicTest.java 5408 2007-01-12 19:42:37Z joakime $ + */ +public class SimpleDavServerComponentBasicTest + extends AbstractBasicWebdavProviderTestCase +{ + public SimpleDavServerComponentBasicTest() + { + super(); + setProviderHint( "simple" ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.java new file mode 100644 index 000000000..c51b05a5b --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import org.apache.maven.archiva.webdav.test.AbstractWebdavIndexHtmlTestCase; + +/** + * SimpleDavServerComponentIndexHtmlTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: SimpleDavServerComponentIndexHtmlTest.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public class SimpleDavServerComponentIndexHtmlTest + extends AbstractWebdavIndexHtmlTestCase +{ + public SimpleDavServerComponentIndexHtmlTest() + { + super(); + setProviderHint( "simple" ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.java new file mode 100644 index 000000000..bb17ca8ef --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import org.apache.maven.archiva.webdav.test.AbstractMultiWebdavProviderTestCase; + +/** + * SimpleDavServerComponentCrossTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: SimpleDavServerComponentMultiTest.java 5408 2007-01-12 19:42:37Z joakime $ + */ +public class SimpleDavServerComponentMultiTest + extends AbstractMultiWebdavProviderTestCase +{ + public SimpleDavServerComponentMultiTest() + { + super(); + setProviderHint( "simple" ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.java new file mode 100644 index 000000000..7cec7d7af --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.simple; + +import org.apache.maven.archiva.webdav.test.AbstractWebdavServer; + +/** + * SimpleWebdavServer + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: SimpleWebdavServer.java 5379 2007-01-07 22:54:41Z joakime $ + */ +public class SimpleWebdavServer + extends AbstractWebdavServer +{ + public static void main( String[] args ) + { + try + { + SimpleWebdavServer server = new SimpleWebdavServer(); + server.init(); + server.startServer(); + } + catch ( Exception e ) + { + e.printStackTrace(); + } + } + + protected String getProviderHint() + { + return "simple"; + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractBasicWebdavProviderTestCase.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractBasicWebdavProviderTestCase.java new file mode 100644 index 000000000..82eab6cf8 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractBasicWebdavProviderTestCase.java @@ -0,0 +1,255 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.commons.httpclient.HttpURL; +import org.apache.maven.archiva.webdav.servlet.basic.BasicWebDavServlet; +import org.apache.webdav.lib.WebdavResource; +import org.codehaus.plexus.util.IOUtil; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.servlet.ServletHolder; +import org.mortbay.jetty.webapp.WebAppContext; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * AbstractBasicWebdavProviderTestCase + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractBasicWebdavProviderTestCase.java 6000 2007-03-04 22:01:49Z joakime $ + */ +public abstract class AbstractBasicWebdavProviderTestCase + extends AbstractWebdavProviderTestCase +{ + private File serverRepoDir; + + private WebdavResource davRepo; + + /** The Jetty Server. */ + private Server server; + + protected void setUp() + throws Exception + { + super.setUp(); + + // Initialize server contents directory. + + serverRepoDir = getTestDir( "sandbox" ); + + // Setup the Jetty Server. + + System.setProperty( "DEBUG", "" ); + System.setProperty( "org.mortbay.log.class", "org.slf4j.impl.SimpleLogger" ); + + server = new Server( PORT ); + WebAppContext webAppConfig = new WebAppContext( server, getTestFile( "src/test/webapp" ).getCanonicalPath(), "/" ); + + ServletHandler servletHandler = webAppConfig.getServletHandler(); + + ServletHolder holder = servletHandler.addServletWithMapping( BasicWebDavServlet.class, CONTEXT + "/*" ); + + holder.setInitParameter( "dav.root", serverRepoDir.getAbsolutePath() ); + + server.start(); + + // Setup Client Side + + HttpURL httpSandboxUrl = new HttpURL( "http://localhost:" + PORT + CONTEXT + "/" ); + + try + { + davRepo = new WebdavResource( httpSandboxUrl ); + + davRepo.setDebug( 8 ); + + davRepo.setPath( CONTEXT ); + } + catch ( IOException e ) + { + tearDown(); + throw e; + } + } + + protected void tearDown() + throws Exception + { + serverRepoDir = null; + + if ( server != null ) + { + try + { + server.stop(); + } + catch ( Exception e ) + { + /* ignore */ + } + server = null; + } + + if ( davRepo != null ) + { + try + { + davRepo.close(); + } + catch ( Exception e ) + { + /* ignore */ + } + + davRepo = null; + } + + super.tearDown(); + } + + // -------------------------------------------------------------------- + // Actual Test Cases. + // -------------------------------------------------------------------- + + public void testPutGet() + throws Exception + { + // Quote: Rocky + String contents = "yo!\n"; + + assertDavTouchFile( davRepo, CONTEXT, "data.txt", contents ); + + InputStream inputStream = davRepo.getMethodData( CONTEXT + "/data.txt" ); + + assertEquals( contents, IOUtil.toString( inputStream ) ); + } + + public void testCollectionTasks() + throws Exception + { + // Create a few collections. + assertDavMkDir( davRepo, CONTEXT + "/bar" ); + assertDavMkDir( davRepo, CONTEXT + "/bar/foo" ); + + // Remove a collection + davRepo.setPath( CONTEXT ); + if ( !davRepo.deleteMethod( CONTEXT + "/bar/foo" ) ) + { + fail( "Unable to remove <" + CONTEXT + "/bar/foo> on <" + davRepo.getHttpURL().toString() + "> due to <" + + davRepo.getStatusMessage() + ">" ); + } + + assertDavDirNotExists( davRepo, CONTEXT + "/bar/foo" ); + } + + public void testResourceCopy() + throws Exception + { + // Lyrics: Cool and the Gang - Celebrate Good Times + String contents = "we're gonna have a good time tonite. lets celebrate. it's a celebration. " + + "cel-e-brate good times, come on!"; + + // Create a few collections. + assertDavMkDir( davRepo, CONTEXT + "/bar" ); + assertDavMkDir( davRepo, CONTEXT + "/foo" ); + + // Create a resource + assertDavTouchFile( davRepo, CONTEXT + "/bar", "data.txt", contents ); + + // Test for existance of resource + assertDavFileExists( davRepo, CONTEXT + "/bar", "data.txt" ); + assertDavFileNotExists( davRepo, CONTEXT + "/foo", "data.txt" ); + + // Copy resource + String source = CONTEXT + "/bar/data.txt"; + String dest = CONTEXT + "/foo/data.txt"; + if ( !davRepo.copyMethod( source, dest ) ) + { + fail( "Unable to copy <" + source + "> to <" + dest + "> on <" + davRepo.getHttpURL().toString() + + "> due to <" + davRepo.getStatusMessage() + ">" ); + } + + // Test for existance of resource + assertDavFileExists( davRepo, CONTEXT + "/bar", "data.txt" ); + assertDavFileExists( davRepo, CONTEXT + "/foo", "data.txt" ); + } + + public void testResourceMove() + throws Exception + { + // Lyrics: Men At Work - Who Can It Be Now + String contents = "Who can it be knocking at my door?\n" + "Make no sound, tip-toe across the floor.\n" + + "If he hears, he'll knock all day,\n" + "I'll be trapped, and here I'll have to stay.\n" + + "I've done no harm, I keep to myself;\n" + "There's nothing wrong with my state of mental health.\n" + + "I like it here with my childhood friend;\n" + "Here they come, those feelings again!\n"; + + // Create a few collections. + assertDavMkDir( davRepo, CONTEXT + "/bar" ); + assertDavMkDir( davRepo, CONTEXT + "/foo" ); + + // Create a resource + assertDavTouchFile( davRepo, CONTEXT + "/bar", "data.txt", contents ); + + // Test for existance of resource + assertDavFileExists( davRepo, CONTEXT + "/bar", "data.txt" ); + assertDavFileNotExists( davRepo, CONTEXT + "/foo", "data.txt" ); + + // Copy resource + String source = CONTEXT + "/bar/data.txt"; + String dest = CONTEXT + "/foo/data.txt"; + if ( !davRepo.moveMethod( source, dest ) ) + { + fail( "Unable to move <" + source + "> to <" + dest + "> on <" + davRepo.getHttpURL().toString() + + "> due to <" + davRepo.getStatusMessage() + ">" ); + } + + // Test for existance of resource + assertDavFileNotExists( davRepo, CONTEXT + "/bar", "data.txt" ); + assertDavFileExists( davRepo, CONTEXT + "/foo", "data.txt" ); + } + + public void testResourceDelete() + throws Exception + { + // Lyrics: Men At Work - Down Under + String contents = "Lying in a den in Bombay\n" + "With a slack jaw, and not much to say\n" + + "I said to the man, \"Are you trying to tempt me\"\n" + "Because I come from the land of plenty?\n"; + + // Create a few collections. + assertDavMkDir( davRepo, CONTEXT + "/bar" ); + + // Create a resource + assertDavTouchFile( davRepo, CONTEXT + "/bar", "data.txt", contents ); + + // Move resource + davRepo.setPath( CONTEXT ); + if ( !davRepo.deleteMethod( CONTEXT + "/bar/data.txt" ) ) + { + fail( "Unable to remove <" + CONTEXT + "/bar/data.txt> on <" + davRepo.getHttpURL().toString() + + "> due to <" + davRepo.getStatusMessage() + ">" ); + } + + // Test for existance via webdav interface. + assertDavFileNotExists( davRepo, CONTEXT + "/bar", "data.txt" ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractMultiWebdavProviderTestCase.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractMultiWebdavProviderTestCase.java new file mode 100644 index 000000000..71d2d46c0 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractMultiWebdavProviderTestCase.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.HttpURL; +import org.apache.webdav.lib.WebdavResource; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.servlet.ServletHolder; +import org.mortbay.jetty.webapp.WebAppContext; + +import java.io.File; +import java.io.IOException; + +/** + * AbstractMultiWebdavProviderTestCase + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractMultiWebdavProviderTestCase.java 5997 2007-03-04 19:41:15Z joakime $ + */ +public abstract class AbstractMultiWebdavProviderTestCase + extends AbstractWebdavProviderTestCase +{ + File serverSandboxDir; + + File serverSnapshotsDir; + + /** The Jetty Server. */ + private Server server; + + private WebdavResource davSnapshots; + + private WebdavResource davSandbox; + + protected void setUp() + throws Exception + { + super.setUp(); + + // Initialize server contents directory. + + serverSandboxDir = getTestDir( "sandbox" ); + serverSnapshotsDir = getTestDir( "snapshots" ); + + // Setup the Jetty Server. + + System.setProperty( "DEBUG", "" ); + System.setProperty( "org.mortbay.log.class", "org.slf4j.impl.SimpleLogger" ); + + server = new Server( PORT ); + + WebAppContext webAppConfig = new WebAppContext( server, getTestFile( "src/test/webapp" ).getCanonicalPath(), "/" ); + ServletHandler servletHandler = webAppConfig.getServletHandler(); + + ServletHolder holder = servletHandler.addServletWithMapping( TestMultiWebDavServlet.class, CONTEXT + "/*" ); + holder.setInitParameter( "root.sandbox", serverSandboxDir.getAbsolutePath() ); + holder.setInitParameter( "root.snapshots", serverSnapshotsDir.getAbsolutePath() ); + + System.out.println( "root.sandbox = " + serverSandboxDir.getAbsolutePath() ); + System.out.println( "root.snapshots = " + serverSnapshotsDir.getAbsolutePath() ); + + server.start(); + + // Setup Client Side + + HttpURL httpSandboxUrl = new HttpURL( "http://localhost:" + PORT + CONTEXT + "/sandbox/" ); + HttpURL httpSnapshotsUrl = new HttpURL( "http://localhost:" + PORT + CONTEXT + "/snapshots/" ); + + try + { + davSandbox = new WebdavResource( httpSandboxUrl ); + davSnapshots = new WebdavResource( httpSnapshotsUrl ); + + davSandbox.setDebug( 8 ); + davSnapshots.setDebug( 8 ); + + davSandbox.setPath( CONTEXT + "/sandbox/" ); + davSnapshots.setPath( CONTEXT + "/snapshots/" ); + } + catch ( IOException e ) + { + tearDown(); + throw e; + } + } + + protected void tearDown() + throws Exception + { + serverRootDir = null; + + if ( server != null ) + { + try + { + server.stop(); + } + catch ( Exception e ) + { + /* ignore */ + } + server = null; + } + + if ( davSandbox != null ) + { + try + { + davSandbox.close(); + } + catch ( Exception e ) + { + /* ignore */ + } + + davSandbox = null; + } + + if ( davSnapshots != null ) + { + try + { + davSnapshots.close(); + } + catch ( Exception e ) + { + /* ignore */ + } + + davSnapshots = null; + } + + super.tearDown(); + } + + public void testResourceMoveCrossWebdav() + throws Exception + { + // Create a few collections. + assertDavMkDir( davSandbox, CONTEXT + "/sandbox/bar" ); + assertDavMkDir( davSnapshots, CONTEXT + "/snapshots/foo" ); + + // Create a resource + assertDavTouchFile( davSandbox, CONTEXT + "/sandbox/bar", "data.txt", "yo!" ); + + // Move resource URL to URL (Across the WebDav Servlets) + davSandbox.setPath( CONTEXT + "/sandbox/bar" ); + String source = CONTEXT + "/sandbox/bar/data.txt"; + String dest = "http://localhost:" + PORT + CONTEXT + "/snapshots/foo/data.txt"; + if ( !davSandbox.moveMethod( source, dest ) ) + { + // TODO: remove when fully implemented. + if ( davSandbox.getStatusCode() == HttpStatus.SC_NOT_IMPLEMENTED ) + { + // return quietly, as the server reported no support for this method. + return; + } + + fail( "Unable to move <" + source + "> to <" + dest + "> on <" + davSandbox.getHttpURL().toString() + + "> due to <" + davSandbox.getStatusMessage() + ">" ); + } + + assertDavFileNotExists( davSandbox, CONTEXT + "/sandbox/bar", "data.txt" ); + assertDavFileExists( davSnapshots, CONTEXT + "/snapshots/foo", "data.txt" ); + } + + public void testResourceDoesNotExist() + throws Exception + { + // Create a few collections. + assertDavMkDir( davSandbox, CONTEXT + "/sandbox/bar" ); + assertDavMkDir( davSnapshots, CONTEXT + "/snapshots/foo" ); + + // Create a resource + assertDavTouchFile( davSandbox, CONTEXT + "/sandbox/bar", "data.txt", "yo!" ); + + // Get bad resources URLs + String urlPrefix = "http://localhost:" + PORT + CONTEXT; + assertGet404( urlPrefix + "/sandbox/a/resource/that/does/not/exist.html" ); + assertGet404( urlPrefix + "/" ); + assertGet404( urlPrefix + "/snapshots/foo/index.html" ); + assertGet404( urlPrefix + "/sandbox/bar.html" ); + assertGet404( urlPrefix + "/nonexistant/index.html" ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavIndexHtmlTestCase.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavIndexHtmlTestCase.java new file mode 100644 index 000000000..f01790430 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavIndexHtmlTestCase.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.commons.httpclient.HttpURL; +import org.apache.maven.archiva.webdav.servlet.basic.BasicWebDavServlet; +import org.apache.webdav.lib.WebdavResource; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.servlet.ServletHolder; +import org.mortbay.jetty.webapp.WebAppContext; + +import java.io.File; +import java.io.IOException; + +public abstract class AbstractWebdavIndexHtmlTestCase + extends AbstractWebdavProviderTestCase +{ + private File serverRepoDir; + + private WebdavResource davRepo; + + /** The Jetty Server. */ + private Server server; + + protected void setUp() + throws Exception + { + super.setUp(); + + // Initialize server contents directory. + + serverRepoDir = getTestDir( "sandbox" ); + + // Setup the Jetty Server. + + System.setProperty( "DEBUG", "" ); + System.setProperty( "org.mortbay.log.class", "org.slf4j.impl.SimpleLogger" ); + + server = new Server( PORT ); + WebAppContext webAppConfig = new WebAppContext( server, getTestFile( "src/test/webapp" ).getCanonicalPath(), "/" ); + + ServletHandler servletHandler = webAppConfig.getServletHandler(); + + ServletHolder holder = servletHandler.addServletWithMapping( BasicWebDavServlet.class, CONTEXT + "/*" ); + + holder.setInitParameter( BasicWebDavServlet.INIT_ROOT_DIRECTORY, serverRepoDir.getAbsolutePath() ); + holder.setInitParameter( BasicWebDavServlet.INIT_USE_INDEX_HTML, "true" ); + + server.start(); + + // Setup Client Side + + HttpURL httpSandboxUrl = new HttpURL( "http://localhost:" + PORT + CONTEXT + "/" ); + + try + { + davRepo = new WebdavResource( httpSandboxUrl ); + + davRepo.setDebug( 8 ); + + davRepo.setPath( CONTEXT ); + } + catch ( IOException e ) + { + tearDown(); + throw e; + } + } + + protected void tearDown() + throws Exception + { + serverRepoDir = null; + + if ( server != null ) + { + try + { + server.stop(); + } + catch ( Exception e ) + { + /* ignore */ + } + server = null; + } + + if ( davRepo != null ) + { + try + { + davRepo.close(); + } + catch ( Exception e ) + { + /* ignore */ + } + + davRepo = null; + } + + super.tearDown(); + } + + public void testCollectionIndexHtml() + throws Exception + { + // Lyrics: Colin Hay - Overkill + String contents = "I cant get to sleep\n" + "I think about the implications\n" + "Of diving in too deep\n" + + "And possibly the complications\n" + "Especially at night\n" + "I worry over situations\n" + + "I know will be alright\n" + "Perahaps its just my imagination\n" + "Day after day it reappears\n" + + "Night after night my heartbeat, shows the fear\n" + "Ghosts appear and fade away"; + + // Create a few collections. + assertDavMkDir( davRepo, CONTEXT + "/bar" ); + assertDavMkDir( davRepo, CONTEXT + "/foo" ); + + // Create a resource + assertDavTouchFile( davRepo, CONTEXT + "/bar", "index.html", contents ); + + // Test for existance of resource + assertDavFileExists( davRepo, CONTEXT + "/bar", "index.html" ); + assertDavFileNotExists( davRepo, CONTEXT + "/foo", "index.html" ); + + // Copy resource + String actual = davRepo.getMethodDataAsString( CONTEXT + "/bar/" ); + + assertEquals( contents, actual ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavProviderTestCase.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavProviderTestCase.java new file mode 100644 index 000000000..33a210a8c --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavProviderTestCase.java @@ -0,0 +1,401 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.io.FileUtils; +import org.apache.maven.archiva.webdav.DavServerManager; +import org.apache.webdav.lib.WebdavResource; +import org.apache.webdav.lib.WebdavResources; +import org.codehaus.plexus.spring.PlexusInSpringTestCase; + +import java.io.File; +import java.io.IOException; + +/** + * AbstractWebdavProviderTestCase + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractWebdavProviderTestCase.java 5997 2007-03-04 19:41:15Z joakime $ + */ +public abstract class AbstractWebdavProviderTestCase + extends PlexusInSpringTestCase +{ + public static final int PORT = 4321; + + public static final String CONTEXT = "/repos"; + + protected File serverRootDir = null; + + private DavServerManager manager; + + private String providerHint = "simple"; + + public DavServerManager getManager() + { + return manager; + } + + public String getProviderHint() + { + return providerHint; + } + + public void setManager( DavServerManager manager ) + { + this.manager = manager; + } + + public void setProviderHint( String providerHint ) + { + this.providerHint = providerHint; + } + + protected void setUp() + throws Exception + { + super.setUp(); + try + { + manager = (DavServerManager) lookup( DavServerManager.ROLE, getProviderHint() ); + serverRootDir = getRootDir(); + } + catch ( Exception e ) + { + tearDown(); + throw e; + } + } + + protected void tearDown() + throws Exception + { + serverRootDir = null; + + super.tearDown(); + } + + protected void dumpCollection( WebdavResource webdavResource, String path ) + throws Exception + { + webdavResource.setPath( path ); + WebdavResource resources[] = webdavResource.listWebdavResources(); + + System.out.println( "Dump Collection [" + path + "]: " + resources.length + " hits." ); + + dumpCollectionRecursive( "", webdavResource, path ); + } + + protected void dumpCollectionRecursive( String indent, WebdavResource webdavResource, String path ) + throws Exception + { + if ( indent.length() > 12 ) + { + return; + } + + WebdavResource resources[] = webdavResource.listWebdavResources(); + + for ( int i = 0; i < resources.length; i++ ) + { + System.out.println( indent + "WebDavResource[" + path + "|" + i + "]: " + + ( resources[i].isCollection() ? "(collection) " : "" ) + resources[i].getName() ); + + if ( resources[i].isCollection() ) + { + dumpCollectionRecursive( indent + " ", resources[i], path + "/" + resources[i].getName() ); + } + } + } + + // -------------------------------------------------------------------- + // Actual Test Cases. + // -------------------------------------------------------------------- + + public void assertNotExists( File basedir, String relativePath ) + { + assertNotExists( new File( basedir, relativePath ) ); + } + + public void assertNotExists( File file ) + { + if ( file.exists() ) + { + fail( "Unexpected path <" + file.getAbsolutePath() + "> should not exist." ); + } + } + + public void assertExists( File basedir, String relativePath ) + { + assertExists( new File( basedir, relativePath ) ); + } + + public void assertExists( File file ) + { + if ( !file.exists() ) + { + fail( "Expected path <" + file.getAbsolutePath() + "> does not exist." ); + } + } + + private void resetDirectory( File dir ) + { + try + { + FileUtils.deleteDirectory( dir ); + } + catch ( IOException e ) + { + fail( "Unable to delete test directory [" + dir.getAbsolutePath() + "]." ); + } + + if ( dir.exists() ) + { + fail( "Unable to execute test, test directory [" + dir.getAbsolutePath() + + "] exists, and cannot be deleted by the test case." ); + } + + if ( !dir.mkdirs() ) + { + fail( "Unable to execute test, test directory [" + dir.getAbsolutePath() + "] cannot be created." ); + } + } + + private File getRootDir() + { + if ( this.serverRootDir == null ) + { + String clazz = this.getClass().getName(); + clazz = clazz.substring( clazz.lastIndexOf( "." ) + 1 ); + serverRootDir = new File( "target/test-contents-" + clazz + "/" + getName() ); + + resetDirectory( serverRootDir ); + } + + return serverRootDir; + } + + protected File getTestDir( String subdir ) + { + File testDir = new File( getRootDir(), subdir ); + resetDirectory( testDir ); + return testDir; + } + + public boolean isHttpStatusOk( WebdavResource webdavResource ) + { + int statusCode = webdavResource.getStatusCode(); + + if ( statusCode == HttpStatus.SC_MULTI_STATUS ) + { + // TODO: find out multi-status values. + } + + return ( statusCode >= 200 ) && ( statusCode < 300 ); + } + + public void assertDavMkDir( WebdavResource webdavResource, String collectionName ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + if ( !webdavResource.mkcolMethod( collectionName ) ) + { + fail( "Unable to create collection/dir <" + collectionName + "> against <" + httpurl + "> due to <" + + webdavResource.getStatusMessage() + ">" ); + } + + assertDavDirExists( webdavResource, collectionName ); + } + + public void assertDavFileExists( WebdavResource webdavResource, String path, String filename ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + if ( !webdavResource.headMethod( path + "/" + filename ) ) + { + fail( "Unable to verify that file/contents <" + path + "/" + filename + "> exists against <" + httpurl + + "> due to <" + webdavResource.getStatusMessage() + ">" ); + } + + String oldPath = webdavResource.getPath(); + try + { + webdavResource.setPath( path ); + + WebdavResources resources = webdavResource.getChildResources(); + + WebdavResource testResource = resources.getResource( filename ); + + if ( testResource == null ) + { + fail( "The file/contents <" + path + "/" + filename + "> does not exist in <" + httpurl + ">" ); + } + + if ( testResource.isCollection() ) + { + fail( "The file/contents <" + path + "/" + filename + + "> is incorrectly being reported as a collection." ); + } + } + finally + { + webdavResource.setPath( oldPath ); + } + } + + public void assertDavFileNotExists( WebdavResource webdavResource, String path, String filename ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + if ( webdavResource.headMethod( path + "/" + filename ) ) + { + fail( "Encountered unexpected file/contents <" + path + "/" + filename + "> at <" + httpurl + ">" ); + } + + String oldPath = webdavResource.getPath(); + try + { + webdavResource.setPath( path ); + + WebdavResources resources = webdavResource.getChildResources(); + + WebdavResource testResource = resources.getResource( filename ); + + if ( testResource == null ) + { + // Nothing found. we're done. + return; + } + + if ( !testResource.isCollection() ) + { + fail( "Encountered unexpected file/contents <" + path + "/" + filename + "> at <" + httpurl + ">" ); + } + } + finally + { + webdavResource.setPath( oldPath ); + } + } + + public void assertDavDirExists( WebdavResource webdavResource, String path ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + String oldPath = webdavResource.getPath(); + try + { + webdavResource.setPath( path ); + + if ( !webdavResource.isCollection() ) + { + if ( !isHttpStatusOk( webdavResource ) ) + { + fail( "Unable to verify that path <" + path + "> is really a collection against <" + httpurl + + "> due to <" + webdavResource.getStatusMessage() + ">" ); + } + } + } + finally + { + webdavResource.setPath( oldPath ); + } + } + + public void assertDavDirNotExists( WebdavResource webdavResource, String path ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + String oldPath = webdavResource.getPath(); + try + { + webdavResource.setPath( path ); + + if ( webdavResource.isCollection() ) + { + fail( "Encountered unexpected collection <" + path + "> at <" + httpurl + ">" ); + } + } + catch ( HttpException e ) + { + if ( e.getReasonCode() == HttpStatus.SC_NOT_FOUND ) + { + // Expected path. + return; + } + + fail( "Unable to set path due to HttpException: " + e.getReasonCode() + ":" + e.getReason() ); + } + finally + { + webdavResource.setPath( oldPath ); + } + } + + public void assertDavTouchFile( WebdavResource webdavResource, String path, String filename, String contents ) + throws Exception + { + String httpurl = webdavResource.getHttpURL().toString(); + + webdavResource.setPath( path ); + + if ( !webdavResource.putMethod( path + "/" + filename, contents ) ) + { + fail( "Unable to create file/contents <" + path + "/" + filename + "> against <" + httpurl + "> due to <" + + webdavResource.getStatusMessage() + ">" ); + } + + assertDavFileExists( webdavResource, path, filename ); + } + + protected void assertGet404( String url ) + throws IOException + { + HttpClient client = new HttpClient(); + GetMethod method = new GetMethod( url ); + + try + { + client.executeMethod( method ); + + if ( method.getStatusCode() == 404 ) + { + // Expected path. + return; + } + + fail( "Request for resource " + url + " should have resulted in an HTTP 404 (Not Found) response, " + + "instead got code " + method.getStatusCode() + " <" + method.getStatusText() + ">." ); + } + catch ( HttpException e ) + { + System.err.println( "HTTP Response: " + e.getReasonCode() + " " + e.getReason() ); + throw e; + } + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavServer.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavServer.java new file mode 100644 index 000000000..066659098 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/AbstractWebdavServer.java @@ -0,0 +1,267 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.maven.archiva.webdav.DavServerManager; +import org.apache.maven.archiva.webdav.servlet.basic.BasicWebDavServlet; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusConstants; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.PlexusContainerException; +import org.codehaus.plexus.context.DefaultContext; +import org.codehaus.plexus.util.FileUtils; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.Context; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.jetty.servlet.ServletHolder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * AbstractWebdavServer - Baseline server for starting up a BasicWebDavServlet to allow experimentation with. + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: AbstractWebdavServer.java 5407 2007-01-12 19:41:09Z joakime $ + */ +public abstract class AbstractWebdavServer +{ + public static final int PORT = 14541; + + protected PlexusContainer container; + + protected String basedir; + + protected Map context; + + /** the jetty server */ + protected Server server; + + private DavServerManager manager; + + public void init() + { + context = new HashMap(); + } + + public String getBasedir() + { + if ( basedir != null ) + { + return basedir; + } + + basedir = System.getProperty( "basedir" ); + if ( basedir == null ) + { + basedir = new File( "" ).getAbsolutePath(); + } + + return basedir; + } + + public File getTestFile( String path ) + { + return new File( getBasedir(), path ); + } + + protected abstract String getProviderHint(); + + public void startServer() + throws Exception + { + basedir = getBasedir(); + + // ---------------------------------------------------------------------------- + // Context Setup + // ---------------------------------------------------------------------------- + + context = new HashMap(); + + context.put( "basedir", getBasedir() ); + + customizeContext( new DefaultContext( context ) ); + + boolean hasPlexusHome = context.containsKey( "plexus.home" ); + + if ( !hasPlexusHome ) + { + File f = getTestFile( "target/plexus-home" ); + + if ( !f.isDirectory() ) + { + f.mkdir(); + } + + context.put( "plexus.home", f.getAbsolutePath() ); + } + + // ---------------------------------------------------------------------------- + // Configuration + // ---------------------------------------------------------------------------- + + String config = getCustomConfigurationName(); + InputStream is; + + if ( config != null ) + { + is = getClass().getClassLoader().getResourceAsStream( config ); + + if ( is == null ) + { + try + { + File configFile = new File( config ); + + if ( configFile.exists() ) + { + is = new FileInputStream( configFile ); + } + } + catch ( IOException e ) + { + throw new Exception( "The custom configuration specified is null: " + config ); + } + } + + } + else + { + config = getConfigurationName( null ); + + is = getClass().getClassLoader().getResourceAsStream( config ); + } + + // Look for a configuration associated with this test but return null if we + // can't find one so the container doesn't look for a configuration that we + // know doesn't exist. Not all tests have an associated Foo.xml for testing. + + if ( is == null ) + { + config = null; + } + else + { + is.close(); + } + + // ---------------------------------------------------------------------------- + // Create the container + // ---------------------------------------------------------------------------- + + container = createContainerInstance( context, config ); + + // ---------------------------------------------------------------------------- + // Create the DavServerManager + // ---------------------------------------------------------------------------- + + manager = (DavServerManager) container.lookup( DavServerManager.ROLE, getProviderHint() ); + + // ---------------------------------------------------------------------------- + // Create the jetty server + // ---------------------------------------------------------------------------- + + System.setProperty( "DEBUG", "" ); + System.setProperty( "org.mortbay.log.class", "org.slf4j.impl.SimpleLogger" ); + + server = new Server( PORT ); + Context root = new Context( server, "/", Context.SESSIONS ); + ServletHandler servletHandler = root.getServletHandler(); + root.setContextPath( "/" ); + root.setAttribute( PlexusConstants.PLEXUS_KEY, container ); + + // ---------------------------------------------------------------------------- + // Configure the webdav servlet + // ---------------------------------------------------------------------------- + + ServletHolder holder = servletHandler.addServletWithMapping( BasicWebDavServlet.class, "/projects/*" ); + + // Initialize server contents directory. + File serverContentsDir = new File( "target/test-server/" ); + + FileUtils.deleteDirectory( serverContentsDir ); + if ( serverContentsDir.exists() ) + { + throw new IllegalStateException( "Unable to execute test, server contents test directory [" + + serverContentsDir.getAbsolutePath() + "] exists, and cannot be deleted by the test case." ); + } + + if ( !serverContentsDir.mkdirs() ) + { + throw new IllegalStateException( "Unable to execute test, server contents test directory [" + + serverContentsDir.getAbsolutePath() + "] cannot be created." ); + } + + holder.setInitParameter( "dav.root", serverContentsDir.getAbsolutePath() ); + + // ---------------------------------------------------------------------------- + // Start the jetty server + // ---------------------------------------------------------------------------- + + server.start(); + } + + protected PlexusContainer createContainerInstance( Map context, String configuration ) + throws PlexusContainerException + { + return new DefaultPlexusContainer( "test", context, configuration ); + } + + protected void customizeContext( DefaultContext ctx ) + { + /* override to specify more */ + } + + protected String getCustomConfigurationName() + { + /* override to specify */ + return null; + } + + protected String getConfigurationName( String subname ) + throws Exception + { + return getClass().getName().replace( '.', '/' ) + ".xml"; + } + + public void stopServer() + { + if ( server != null ) + { + try + { + server.stop(); + } + catch ( Exception e ) + { + e.printStackTrace(); + } + } + + if ( container != null ) + { + container.dispose(); + } + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/TestMultiWebDavServlet.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/TestMultiWebDavServlet.java new file mode 100644 index 000000000..4a8506eb0 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/test/TestMultiWebDavServlet.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.test; + +import org.apache.maven.archiva.webdav.DavServerException; +import org.apache.maven.archiva.webdav.servlet.multiplexed.MultiplexedWebDavServlet; + +import javax.servlet.ServletConfig; +import java.io.File; + +/** + * TestServlet + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: TestMultiWebDavServlet.java 5407 2007-01-12 19:41:09Z joakime $ + */ +public class TestMultiWebDavServlet + extends MultiplexedWebDavServlet +{ + public void initServers( ServletConfig config ) + throws DavServerException + { + String rootSandbox = config.getInitParameter( "root.sandbox" ); + String rootSnapshots = config.getInitParameter( "root.snapshots" ); + + createServer( "sandbox", new File( rootSandbox ), config ); + createServer( "snapshots", new File( rootSnapshots ), config ); + } +}
\ No newline at end of file diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/MimeTypesTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/MimeTypesTest.java new file mode 100644 index 000000000..9bdbf94f6 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/MimeTypesTest.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.util; + +import org.codehaus.plexus.PlexusTestCase; + +/** + * MimeTypesTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: MimeTypesTest.java 6556 2007-06-20 20:44:46Z joakime $ + */ +public class MimeTypesTest extends PlexusTestCase +{ + public void testGetMimeType() throws Exception + { + MimeTypes mime = (MimeTypes) lookup( MimeTypes.class ); + assertNotNull( "MimeTypes should not be null.", mime ); + + assertEquals( "application/pdf", mime.getMimeType( "big-book.pdf" ) ); + assertEquals( "application/octet-stream", mime.getMimeType( "BookMaker.class" ) ); + assertEquals( "application/vnd.ms-powerpoint", mime.getMimeType( "TypeSetting.ppt" ) ); + assertEquals( "application/java-archive", mime.getMimeType( "BookViewer.jar" ) ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequestTest.java b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequestTest.java new file mode 100644 index 000000000..a0b665735 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/java/org/apache/maven/archiva/webdav/util/WrappedRepositoryRequestTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.maven.archiva.webdav.util; + +import org.apache.maven.archiva.webdav.TestableHttpServletRequest; +import org.codehaus.plexus.PlexusTestCase; + +import javax.servlet.http.HttpServletRequest; +import java.net.MalformedURLException; + +/** + * WrappedRepositoryRequestTest + * + * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a> + * @version $Id: WrappedRepositoryRequestTest.java 6940 2007-10-16 01:02:02Z joakime $ + */ +public class WrappedRepositoryRequestTest + extends PlexusTestCase +{ + private HttpServletRequest createHttpServletGetRequest( String url ) + throws MalformedURLException + { + TestableHttpServletRequest testrequest = new TestableHttpServletRequest(); + testrequest.setMethod( "GET" ); + testrequest.setServletPath( "/repository" ); + testrequest.setUrl( url ); + + return testrequest; + } + + public void testShort() + throws Exception + { + HttpServletRequest request = createHttpServletGetRequest( "http://machine.com/repository/org" ); + WrappedRepositoryRequest wrapreq = new WrappedRepositoryRequest( request ); + assertNotNull( wrapreq ); + + assertEquals( "/repository", wrapreq.getServletPath() ); + assertEquals( "/org", wrapreq.getPathInfo() ); + assertEquals( "/org", wrapreq.getRequestURI() ); + } + + public void testLonger() + throws Exception + { + HttpServletRequest request = createHttpServletGetRequest( "http://machine.com/repository/" + + "org/codehaus/plexus/webdav/plexus-webdav-simple/1.0-alpha-3/plexus-webdav-simple-1.0-alpha-3.jar" ); + + WrappedRepositoryRequest wrapreq = new WrappedRepositoryRequest( request ); + assertNotNull( wrapreq ); + + assertEquals( "/repository", wrapreq.getServletPath() ); + + String expected = "/org/codehaus/plexus/webdav/plexus-webdav-simple/1.0-alpha-3/plexus-webdav-simple-1.0-alpha-3.jar"; + assertEquals( expected, wrapreq.getPathInfo() ); + assertEquals( expected, wrapreq.getRequestURI() ); + } +} diff --git a/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.xml b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.xml new file mode 100644 index 000000000..323862265 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentBasicTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<plexus> + <components> + + <component> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> + <role-hint>simple</role-hint> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> + <description>DefaultDavServerManager</description> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>simple</role-hint> + </requirement> + </requirements> + </component> + + </components> +</plexus> diff --git a/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.xml b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.xml new file mode 100644 index 000000000..323862265 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentIndexHtmlTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<plexus> + <components> + + <component> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> + <role-hint>simple</role-hint> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> + <description>DefaultDavServerManager</description> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>simple</role-hint> + </requirement> + </requirements> + </component> + + </components> +</plexus> diff --git a/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.xml b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.xml new file mode 100644 index 000000000..323862265 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleDavServerComponentMultiTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<plexus> + <components> + + <component> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> + <role-hint>simple</role-hint> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> + <description>DefaultDavServerManager</description> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>simple</role-hint> + </requirement> + </requirements> + </component> + + </components> +</plexus> diff --git a/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.xml b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.xml new file mode 100644 index 000000000..323862265 --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/resources/org/apache/maven/archiva/webdav/simple/SimpleWebdavServer.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<plexus> + <components> + + <component> + <role>org.apache.maven.archiva.webdav.DavServerManager</role> + <role-hint>simple</role-hint> + <implementation>org.apache.maven.archiva.webdav.DefaultDavServerManager</implementation> + <description>DefaultDavServerManager</description> + <requirements> + <requirement> + <role>org.apache.maven.archiva.webdav.DavServerComponent</role> + <role-hint>simple</role-hint> + </requirement> + </requirements> + </component> + + </components> +</plexus> diff --git a/archiva-web/archiva-webdav/src/test/webapp/WEB-INF/web.xml b/archiva-web/archiva-webdav/src/test/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..93634b40e --- /dev/null +++ b/archiva-web/archiva-webdav/src/test/webapp/WEB-INF/web.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you under the Apache License, Version 2.0 (the + ~ "License"); you may not use this file except in compliance + ~ with the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, + ~ software distributed under the License is distributed on an + ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + ~ KIND, either express or implied. See the License for the + ~ specific language governing permissions and limitations + ~ under the License. + --> + +<web-app xmlns="http://java.sun.com/xml/ns/j2ee" version="2.4" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> + + <display-name>Apache Archiva</display-name> + + <listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> + </listener> + + <context-param> + <param-name>contextClass</param-name> + <param-value>org.codehaus.plexus.spring.PlexusWebApplicationContext</param-value> + </context-param> + + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value> + classpath*:/META-INF/plexus/components.xml + </param-value> + </context-param> + +</web-app> diff --git a/archiva-web/pom.xml b/archiva-web/pom.xml index 082817ba1..21aa1d08d 100644 --- a/archiva-web/pom.xml +++ b/archiva-web/pom.xml @@ -33,6 +33,7 @@ <module>archiva-applet</module> <module>archiva-security</module> <module>archiva-webapp</module> + <module>archiva-webdav</module> <module>archiva-standalone</module> </modules> @@ -159,8 +159,7 @@ </dependency> <dependency> <groupId>org.slf4j</groupId> - <artifactId>slf4j-simple</artifactId> - <version>1.4.3</version> + <artifactId>slf4j-log4j12</artifactId> <scope>test</scope> </dependency> </dependencies> @@ -311,9 +310,9 @@ <version>1.1-SNAPSHOT</version> </dependency> <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>1.4.3</version> + <groupId>org.apache.maven.archiva</groupId> + <artifactId>archiva-webdav</artifactId> + <version>1.1-SNAPSHOT</version> </dependency> <dependency> <groupId>commons-collections</groupId> @@ -406,7 +405,7 @@ <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> - <version>1.2.8</version> + <version>1.2.14</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> @@ -651,16 +650,6 @@ </exclusions> </dependency> <dependency> - <groupId>org.codehaus.plexus.webdav</groupId> - <artifactId>plexus-webdav-simple</artifactId> - <version>1.0-beta-2</version> - </dependency> - <dependency> - <groupId>org.codehaus.plexus.webdav</groupId> - <artifactId>plexus-webdav-api</artifactId> - <version>1.0-beta-2</version> - </dependency> - <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.4</version> @@ -671,6 +660,16 @@ <version>2.2.1</version> </dependency> <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.5.0</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.5.0</version> + </dependency> + <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> @@ -748,6 +747,7 @@ </plugin> <plugin> <artifactId>maven-javadoc-plugin</artifactId> + <version>2.2</version> <configuration> <source>1.5</source> <aggregate>true</aggregate> |