From 3a99fc508aa19716452a0337fc701bc5d2a51e49 Mon Sep 17 00:00:00 2001 From: Elliot West Date: Thu, 1 Sep 2011 12:24:13 +0100 Subject: [PATCH] Initial check-in --- LICENSE.txt | 202 +++++++++++++ README.md | 147 ++++++++++ changelog.txt | 2 + eclipse-codeformatter-profile.xml | 267 ++++++++++++++++++ pom.xml | 255 +++++++++++++++++ src/main/java/fm/last/moji/Moji.java | 84 ++++++ src/main/java/fm/last/moji/MojiFactory.java | 41 +++ src/main/java/fm/last/moji/MojiFile.java | 123 ++++++++ .../fm/last/moji/impl/DefaultMojiFactory.java | 49 ++++ .../java/fm/last/moji/impl/DeleteCommand.java | 48 ++++ src/main/java/fm/last/moji/impl/Executor.java | 69 +++++ .../java/fm/last/moji/impl/ExistsCommand.java | 63 +++++ .../moji/impl/FileDownloadInputStream.java | 95 +++++++ .../fm/last/moji/impl/FileLengthCommand.java | 100 +++++++ .../moji/impl/FileUploadOutputStream.java | 140 +++++++++ .../last/moji/impl/GetInputStreamCommand.java | 87 ++++++ .../moji/impl/GetOutputStreamCommand.java | 94 ++++++ .../fm/last/moji/impl/GetPathsCommand.java | 57 ++++ .../last/moji/impl/HttpConnectionFactory.java | 35 +++ .../fm/last/moji/impl/ListFilesCommand.java | 67 +++++ .../java/fm/last/moji/impl/MojiCommand.java | 26 ++ .../java/fm/last/moji/impl/MojiFileImpl.java | 241 ++++++++++++++++ src/main/java/fm/last/moji/impl/MojiImpl.java | 108 +++++++ .../last/moji/impl/PropertyMojiFactory.java | 131 +++++++++ .../java/fm/last/moji/impl/RenameCommand.java | 52 ++++ .../moji/impl/UpdateStorageClassCommand.java | 39 +++ .../moji/local/DefaultFileNamingStrategy.java | 93 ++++++ .../moji/local/LocalFileNamingStrategy.java | 37 +++ .../last/moji/local/LocalFileSystemMoji.java | 122 ++++++++ .../fm/last/moji/local/LocalMojiFile.java | 136 +++++++++ .../fm/last/moji/spring/SpringMojiBean.java | 180 ++++++++++++ .../fm/last/moji/tracker/Destination.java | 60 ++++ .../tracker/KeyExistsAlreadyException.java | 41 +++ .../java/fm/last/moji/tracker/Tracker.java | 82 ++++++ .../last/moji/tracker/TrackerException.java | 43 +++ .../fm/last/moji/tracker/TrackerFactory.java | 49 ++++ .../moji/tracker/UnknownKeyException.java | 41 +++ .../tracker/UnknownStorageClassException.java | 36 +++ .../tracker/impl/AbstractTrackerFactory.java | 84 ++++++ .../fm/last/moji/tracker/impl/Charsets.java | 25 ++ .../tracker/impl/CommunicationException.java | 43 +++ .../tracker/impl/CreateOpenOperation.java | 113 ++++++++ .../fm/last/moji/tracker/impl/ErrorCode.java | 44 +++ .../moji/tracker/impl/GetPathsOperation.java | 92 ++++++ .../impl/InetSocketAddressFactory.java | 56 ++++ .../moji/tracker/impl/ListKeysOperation.java | 91 ++++++ .../fm/last/moji/tracker/impl/Request.java | 132 +++++++++ .../moji/tracker/impl/RequestHandler.java | 79 ++++++ .../fm/last/moji/tracker/impl/Response.java | 95 +++++++ .../moji/tracker/impl/ResponseStatus.java | 46 +++ .../impl/SingleHostTrackerFactory.java | 73 +++++ .../last/moji/tracker/impl/TrackerImpl.java | 169 +++++++++++ .../moji/tracker/pool/BorrowedTracker.java | 187 ++++++++++++ .../tracker/pool/HostPriorityComparator.java | 51 ++++ .../moji/tracker/pool/ManagedTrackerHost.java | 155 ++++++++++ .../tracker/pool/MultiHostTrackerPool.java | 246 ++++++++++++++++ src/main/resources/spring/moji-context.xml | 18 ++ src/test/data/fileOfKnownSize.dat | Bin 0 -> 3832 bytes src/test/data/mogileFileCopyToFile.dat | Bin 0 -> 1933 bytes .../java/fm/last/moji/FakeMogileFsServer.java | 154 ++++++++++ .../fm/last/moji/MojiInstantiationTest.java | 45 +++ .../fm/last/moji/impl/DeleteCommandTest.java | 46 +++ .../java/fm/last/moji/impl/ExecutorTest.java | 84 ++++++ .../fm/last/moji/impl/ExistsCommandTest.java | 71 +++++ .../impl/FileDownloadInputStreamTest.java | 112 ++++++++ .../moji/impl/FileUploadOutputStreamTest.java | 155 ++++++++++ .../moji/impl/HttpConnectionFactoryTest.java | 63 +++++ .../last/moji/impl/ListFilesCommandTest.java | 89 ++++++ .../fm/last/moji/impl/MojiFileImplTest.java | 254 +++++++++++++++++ .../fm/last/moji/impl/RenameCommandTest.java | 46 +++ .../impl/UpdateStorageClassCommandTest.java | 46 +++ .../last/moji/integration/AbstractMojiIT.java | 133 +++++++++ .../fm/last/moji/integration/MojiFileIT.java | 203 +++++++++++++ .../java/fm/last/moji/integration/MojiIT.java | 79 ++++++ .../local/DefaultFileNamingStrategyTest.java | 70 +++++ .../SpringMojiBeanInstantiationTest.java | 44 +++ .../last/moji/spring/SpringMojiBeanTest.java | 67 +++++ .../tracker/impl/CreateOpenOperationTest.java | 182 ++++++++++++ .../tracker/impl/GetPathsOperationTest.java | 147 ++++++++++ .../impl/InetSocketAddressFactoryTest.java | 79 ++++++ .../moji/tracker/impl/RequestHandlerTest.java | 156 ++++++++++ .../last/moji/tracker/impl/RequestTest.java | 42 +++ .../last/moji/tracker/impl/ResponseTest.java | 57 ++++ .../impl/SingleHostTrackerFactoryTest.java | 72 +++++ .../moji/tracker/impl/TrackerImplTest.java | 218 ++++++++++++++ .../tracker/pool/BorrowedTrackerTest.java | 126 +++++++++ src/test/resources/checkstyle.xml | 142 ++++++++++ src/test/resources/findbugsExclude.xml | 10 + src/test/resources/log4j.properties | 8 + src/test/resources/moji.properties | 7 + src/test/script/init-mogile-test-data.sh | 53 ++++ src/test/script/permission-change.sh | 3 + target/classes/fm/last/moji/Moji.class | Bin 0 -> 631 bytes target/classes/fm/last/moji/MojiFactory.class | Bin 0 -> 270 bytes target/classes/fm/last/moji/MojiFile.class | Bin 0 -> 679 bytes .../last/moji/impl/DefaultMojiFactory.class | Bin 0 -> 1277 bytes .../fm/last/moji/impl/DeleteCommand.class | Bin 0 -> 1151 bytes .../classes/fm/last/moji/impl/Executor.class | Bin 0 -> 2146 bytes .../fm/last/moji/impl/ExistsCommand.class | Bin 0 -> 1714 bytes .../moji/impl/FileDownloadInputStream.class | Bin 0 -> 2506 bytes .../fm/last/moji/impl/FileLengthCommand.class | Bin 0 -> 3684 bytes .../moji/impl/FileUploadOutputStream.class | Bin 0 -> 5864 bytes .../moji/impl/GetInputStreamCommand.class | Bin 0 -> 3323 bytes .../moji/impl/GetOutputStreamCommand.class | Bin 0 -> 3734 bytes .../fm/last/moji/impl/GetPathsCommand.class | Bin 0 -> 1509 bytes .../moji/impl/HttpConnectionFactory.class | Bin 0 -> 748 bytes .../fm/last/moji/impl/ListFilesCommand.class | Bin 0 -> 2498 bytes .../fm/last/moji/impl/MojiCommand.class | Bin 0 -> 234 bytes .../fm/last/moji/impl/MojiFileImpl.class | Bin 0 -> 7617 bytes .../classes/fm/last/moji/impl/MojiImpl.class | Bin 0 -> 4439 bytes .../last/moji/impl/PropertyMojiFactory.class | Bin 0 -> 3985 bytes .../fm/last/moji/impl/RenameCommand.class | Bin 0 -> 1258 bytes .../moji/impl/UpdateStorageClassCommand.class | Bin 0 -> 904 bytes ...mingStrategy$KeyPrefixFileNameFilter.class | Bin 0 -> 1867 bytes .../local/DefaultFileNamingStrategy.class | Bin 0 -> 2440 bytes .../moji/local/LocalFileNamingStrategy.class | Bin 0 -> 475 bytes .../last/moji/local/LocalFileSystemMoji.class | Bin 0 -> 4504 bytes .../fm/last/moji/local/LocalMojiFile.class | Bin 0 -> 3483 bytes .../fm/last/moji/spring/SpringMojiBean.class | Bin 0 -> 4169 bytes .../fm/last/moji/tracker/Destination.class | Bin 0 -> 1222 bytes .../tracker/KeyExistsAlreadyException.class | Bin 0 -> 938 bytes .../fm/last/moji/tracker/Tracker.class | Bin 0 -> 1132 bytes .../last/moji/tracker/TrackerException.class | Bin 0 -> 814 bytes .../fm/last/moji/tracker/TrackerFactory.class | Bin 0 -> 405 bytes .../moji/tracker/UnknownKeyException.class | Bin 0 -> 920 bytes .../UnknownStorageClassException.class | Bin 0 -> 790 bytes .../tracker/impl/AbstractTrackerFactory.class | Bin 0 -> 3209 bytes .../fm/last/moji/tracker/impl/Charsets.class | Bin 0 -> 1097 bytes .../tracker/impl/CommunicationException.class | Bin 0 -> 860 bytes .../tracker/impl/CreateOpenOperation.class | Bin 0 -> 4626 bytes .../fm/last/moji/tracker/impl/ErrorCode.class | Bin 0 -> 2173 bytes .../moji/tracker/impl/GetPathsOperation.class | Bin 0 -> 4058 bytes .../impl/InetSocketAddressFactory.class | Bin 0 -> 2538 bytes .../moji/tracker/impl/ListKeysOperation.class | Bin 0 -> 3938 bytes .../moji/tracker/impl/Request$Builder.class | Bin 0 -> 2529 bytes .../fm/last/moji/tracker/impl/Request.class | Bin 0 -> 3392 bytes .../moji/tracker/impl/RequestHandler.class | Bin 0 -> 2931 bytes .../fm/last/moji/tracker/impl/Response.class | Bin 0 -> 3454 bytes .../moji/tracker/impl/ResponseStatus.class | Bin 0 -> 1856 bytes .../impl/SingleHostTrackerFactory.class | Bin 0 -> 1830 bytes .../last/moji/tracker/impl/TrackerImpl.class | Bin 0 -> 7317 bytes .../moji/tracker/pool/BorrowedTracker.class | Bin 0 -> 5822 bytes .../tracker/pool/HostPriorityComparator.class | Bin 0 -> 1300 bytes .../pool/ManagedTrackerHost$ResetTask.class | Bin 0 -> 1288 bytes .../tracker/pool/ManagedTrackerHost.class | Bin 0 -> 4067 bytes ...ool$BorrowedTrackerObjectPoolFactory.class | Bin 0 -> 3128 bytes .../tracker/pool/MultiHostTrackerPool.class | Bin 0 -> 5911 bytes target/classes/init-mogile-test-data.sh | 53 ++++ target/classes/permission-change.sh | 3 + target/classes/spring/moji-context.xml | 18 ++ target/test-classes/checkstyle.xml | 142 ++++++++++ target/test-classes/findbugsExclude.xml | 10 + .../moji/FakeMogileFsServer$Builder.class | Bin 0 -> 1918 bytes .../last/moji/FakeMogileFsServer$Stanza.class | Bin 0 -> 1155 bytes .../FakeMogileFsServer$TrackerServer.class | Bin 0 -> 3079 bytes .../fm/last/moji/FakeMogileFsServer.class | Bin 0 -> 2942 bytes .../fm/last/moji/MojiInstantiationTest.class | Bin 0 -> 2103 bytes .../fm/last/moji/impl/DeleteCommandTest.class | Bin 0 -> 1225 bytes .../fm/last/moji/impl/ExecutorTest.class | Bin 0 -> 2796 bytes .../fm/last/moji/impl/ExistsCommandTest.class | Bin 0 -> 2406 bytes .../impl/FileDownloadInputStreamTest.class | Bin 0 -> 3011 bytes .../impl/FileUploadOutputStreamTest.class | Bin 0 -> 5254 bytes .../moji/impl/HttpConnectionFactoryTest.class | Bin 0 -> 2293 bytes .../moji/impl/ListFilesCommandTest$1.class | Bin 0 -> 1622 bytes .../last/moji/impl/ListFilesCommandTest.class | Bin 0 -> 3306 bytes .../fm/last/moji/impl/MojiFileImplTest.class | Bin 0 -> 9015 bytes .../fm/last/moji/impl/RenameCommandTest.class | Bin 0 -> 1259 bytes .../impl/UpdateStorageClassCommandTest.class | Bin 0 -> 1340 bytes .../moji/integration/AbstractMojiIT.class | Bin 0 -> 4946 bytes .../fm/last/moji/integration/MojiFileIT.class | Bin 0 -> 5791 bytes .../fm/last/moji/integration/MojiIT.class | Bin 0 -> 3419 bytes .../local/DefaultFileNamingStrategyTest.class | Bin 0 -> 2274 bytes .../SpringMojiBeanInstantiationTest.class | Bin 0 -> 2021 bytes .../last/moji/spring/SpringMojiBeanTest.class | Bin 0 -> 2197 bytes .../impl/CreateOpenOperationTest.class | Bin 0 -> 6241 bytes .../tracker/impl/GetPathsOperationTest.class | Bin 0 -> 5197 bytes .../impl/InetSocketAddressFactoryTest.class | Bin 0 -> 2151 bytes .../impl/RequestHandlerTest$TrueMatcher.class | Bin 0 -> 1255 bytes .../tracker/impl/RequestHandlerTest.class | Bin 0 -> 5513 bytes .../last/moji/tracker/impl/RequestTest.class | Bin 0 -> 2125 bytes .../last/moji/tracker/impl/ResponseTest.class | Bin 0 -> 2101 bytes .../impl/SingleHostTrackerFactoryTest.class | Bin 0 -> 2450 bytes .../moji/tracker/impl/TrackerImplTest.class | Bin 0 -> 7735 bytes .../tracker/pool/BorrowedTrackerTest.class | Bin 0 -> 4093 bytes target/test-classes/log4j.properties | 8 + target/test-classes/moji.properties | 7 + 186 files changed, 8645 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 changelog.txt create mode 100644 eclipse-codeformatter-profile.xml create mode 100644 pom.xml create mode 100644 src/main/java/fm/last/moji/Moji.java create mode 100644 src/main/java/fm/last/moji/MojiFactory.java create mode 100644 src/main/java/fm/last/moji/MojiFile.java create mode 100644 src/main/java/fm/last/moji/impl/DefaultMojiFactory.java create mode 100644 src/main/java/fm/last/moji/impl/DeleteCommand.java create mode 100644 src/main/java/fm/last/moji/impl/Executor.java create mode 100644 src/main/java/fm/last/moji/impl/ExistsCommand.java create mode 100644 src/main/java/fm/last/moji/impl/FileDownloadInputStream.java create mode 100644 src/main/java/fm/last/moji/impl/FileLengthCommand.java create mode 100644 src/main/java/fm/last/moji/impl/FileUploadOutputStream.java create mode 100644 src/main/java/fm/last/moji/impl/GetInputStreamCommand.java create mode 100644 src/main/java/fm/last/moji/impl/GetOutputStreamCommand.java create mode 100644 src/main/java/fm/last/moji/impl/GetPathsCommand.java create mode 100644 src/main/java/fm/last/moji/impl/HttpConnectionFactory.java create mode 100644 src/main/java/fm/last/moji/impl/ListFilesCommand.java create mode 100644 src/main/java/fm/last/moji/impl/MojiCommand.java create mode 100644 src/main/java/fm/last/moji/impl/MojiFileImpl.java create mode 100644 src/main/java/fm/last/moji/impl/MojiImpl.java create mode 100644 src/main/java/fm/last/moji/impl/PropertyMojiFactory.java create mode 100644 src/main/java/fm/last/moji/impl/RenameCommand.java create mode 100644 src/main/java/fm/last/moji/impl/UpdateStorageClassCommand.java create mode 100644 src/main/java/fm/last/moji/local/DefaultFileNamingStrategy.java create mode 100644 src/main/java/fm/last/moji/local/LocalFileNamingStrategy.java create mode 100644 src/main/java/fm/last/moji/local/LocalFileSystemMoji.java create mode 100644 src/main/java/fm/last/moji/local/LocalMojiFile.java create mode 100644 src/main/java/fm/last/moji/spring/SpringMojiBean.java create mode 100644 src/main/java/fm/last/moji/tracker/Destination.java create mode 100644 src/main/java/fm/last/moji/tracker/KeyExistsAlreadyException.java create mode 100644 src/main/java/fm/last/moji/tracker/Tracker.java create mode 100644 src/main/java/fm/last/moji/tracker/TrackerException.java create mode 100644 src/main/java/fm/last/moji/tracker/TrackerFactory.java create mode 100644 src/main/java/fm/last/moji/tracker/UnknownKeyException.java create mode 100644 src/main/java/fm/last/moji/tracker/UnknownStorageClassException.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/AbstractTrackerFactory.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/Charsets.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/CommunicationException.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/CreateOpenOperation.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/ErrorCode.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/GetPathsOperation.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/InetSocketAddressFactory.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/ListKeysOperation.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/Request.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/RequestHandler.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/Response.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/ResponseStatus.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/SingleHostTrackerFactory.java create mode 100644 src/main/java/fm/last/moji/tracker/impl/TrackerImpl.java create mode 100644 src/main/java/fm/last/moji/tracker/pool/BorrowedTracker.java create mode 100644 src/main/java/fm/last/moji/tracker/pool/HostPriorityComparator.java create mode 100644 src/main/java/fm/last/moji/tracker/pool/ManagedTrackerHost.java create mode 100644 src/main/java/fm/last/moji/tracker/pool/MultiHostTrackerPool.java create mode 100644 src/main/resources/spring/moji-context.xml create mode 100644 src/test/data/fileOfKnownSize.dat create mode 100644 src/test/data/mogileFileCopyToFile.dat create mode 100644 src/test/java/fm/last/moji/FakeMogileFsServer.java create mode 100644 src/test/java/fm/last/moji/MojiInstantiationTest.java create mode 100644 src/test/java/fm/last/moji/impl/DeleteCommandTest.java create mode 100644 src/test/java/fm/last/moji/impl/ExecutorTest.java create mode 100644 src/test/java/fm/last/moji/impl/ExistsCommandTest.java create mode 100644 src/test/java/fm/last/moji/impl/FileDownloadInputStreamTest.java create mode 100644 src/test/java/fm/last/moji/impl/FileUploadOutputStreamTest.java create mode 100644 src/test/java/fm/last/moji/impl/HttpConnectionFactoryTest.java create mode 100644 src/test/java/fm/last/moji/impl/ListFilesCommandTest.java create mode 100644 src/test/java/fm/last/moji/impl/MojiFileImplTest.java create mode 100644 src/test/java/fm/last/moji/impl/RenameCommandTest.java create mode 100644 src/test/java/fm/last/moji/impl/UpdateStorageClassCommandTest.java create mode 100644 src/test/java/fm/last/moji/integration/AbstractMojiIT.java create mode 100644 src/test/java/fm/last/moji/integration/MojiFileIT.java create mode 100644 src/test/java/fm/last/moji/integration/MojiIT.java create mode 100644 src/test/java/fm/last/moji/local/DefaultFileNamingStrategyTest.java create mode 100644 src/test/java/fm/last/moji/spring/SpringMojiBeanInstantiationTest.java create mode 100644 src/test/java/fm/last/moji/spring/SpringMojiBeanTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/CreateOpenOperationTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/GetPathsOperationTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/RequestHandlerTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/RequestTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/ResponseTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/SingleHostTrackerFactoryTest.java create mode 100644 src/test/java/fm/last/moji/tracker/impl/TrackerImplTest.java create mode 100644 src/test/java/fm/last/moji/tracker/pool/BorrowedTrackerTest.java create mode 100644 src/test/resources/checkstyle.xml create mode 100644 src/test/resources/findbugsExclude.xml create mode 100644 src/test/resources/log4j.properties create mode 100644 src/test/resources/moji.properties create mode 100755 src/test/script/init-mogile-test-data.sh create mode 100755 src/test/script/permission-change.sh create mode 100644 target/classes/fm/last/moji/Moji.class create mode 100644 target/classes/fm/last/moji/MojiFactory.class create mode 100644 target/classes/fm/last/moji/MojiFile.class create mode 100644 target/classes/fm/last/moji/impl/DefaultMojiFactory.class create mode 100644 target/classes/fm/last/moji/impl/DeleteCommand.class create mode 100644 target/classes/fm/last/moji/impl/Executor.class create mode 100644 target/classes/fm/last/moji/impl/ExistsCommand.class create mode 100644 target/classes/fm/last/moji/impl/FileDownloadInputStream.class create mode 100644 target/classes/fm/last/moji/impl/FileLengthCommand.class create mode 100644 target/classes/fm/last/moji/impl/FileUploadOutputStream.class create mode 100644 target/classes/fm/last/moji/impl/GetInputStreamCommand.class create mode 100644 target/classes/fm/last/moji/impl/GetOutputStreamCommand.class create mode 100644 target/classes/fm/last/moji/impl/GetPathsCommand.class create mode 100644 target/classes/fm/last/moji/impl/HttpConnectionFactory.class create mode 100644 target/classes/fm/last/moji/impl/ListFilesCommand.class create mode 100644 target/classes/fm/last/moji/impl/MojiCommand.class create mode 100644 target/classes/fm/last/moji/impl/MojiFileImpl.class create mode 100644 target/classes/fm/last/moji/impl/MojiImpl.class create mode 100644 target/classes/fm/last/moji/impl/PropertyMojiFactory.class create mode 100644 target/classes/fm/last/moji/impl/RenameCommand.class create mode 100644 target/classes/fm/last/moji/impl/UpdateStorageClassCommand.class create mode 100644 target/classes/fm/last/moji/local/DefaultFileNamingStrategy$KeyPrefixFileNameFilter.class create mode 100644 target/classes/fm/last/moji/local/DefaultFileNamingStrategy.class create mode 100644 target/classes/fm/last/moji/local/LocalFileNamingStrategy.class create mode 100644 target/classes/fm/last/moji/local/LocalFileSystemMoji.class create mode 100644 target/classes/fm/last/moji/local/LocalMojiFile.class create mode 100644 target/classes/fm/last/moji/spring/SpringMojiBean.class create mode 100644 target/classes/fm/last/moji/tracker/Destination.class create mode 100644 target/classes/fm/last/moji/tracker/KeyExistsAlreadyException.class create mode 100644 target/classes/fm/last/moji/tracker/Tracker.class create mode 100644 target/classes/fm/last/moji/tracker/TrackerException.class create mode 100644 target/classes/fm/last/moji/tracker/TrackerFactory.class create mode 100644 target/classes/fm/last/moji/tracker/UnknownKeyException.class create mode 100644 target/classes/fm/last/moji/tracker/UnknownStorageClassException.class create mode 100644 target/classes/fm/last/moji/tracker/impl/AbstractTrackerFactory.class create mode 100644 target/classes/fm/last/moji/tracker/impl/Charsets.class create mode 100644 target/classes/fm/last/moji/tracker/impl/CommunicationException.class create mode 100644 target/classes/fm/last/moji/tracker/impl/CreateOpenOperation.class create mode 100644 target/classes/fm/last/moji/tracker/impl/ErrorCode.class create mode 100644 target/classes/fm/last/moji/tracker/impl/GetPathsOperation.class create mode 100644 target/classes/fm/last/moji/tracker/impl/InetSocketAddressFactory.class create mode 100644 target/classes/fm/last/moji/tracker/impl/ListKeysOperation.class create mode 100644 target/classes/fm/last/moji/tracker/impl/Request$Builder.class create mode 100644 target/classes/fm/last/moji/tracker/impl/Request.class create mode 100644 target/classes/fm/last/moji/tracker/impl/RequestHandler.class create mode 100644 target/classes/fm/last/moji/tracker/impl/Response.class create mode 100644 target/classes/fm/last/moji/tracker/impl/ResponseStatus.class create mode 100644 target/classes/fm/last/moji/tracker/impl/SingleHostTrackerFactory.class create mode 100644 target/classes/fm/last/moji/tracker/impl/TrackerImpl.class create mode 100644 target/classes/fm/last/moji/tracker/pool/BorrowedTracker.class create mode 100644 target/classes/fm/last/moji/tracker/pool/HostPriorityComparator.class create mode 100644 target/classes/fm/last/moji/tracker/pool/ManagedTrackerHost$ResetTask.class create mode 100644 target/classes/fm/last/moji/tracker/pool/ManagedTrackerHost.class create mode 100644 target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool$BorrowedTrackerObjectPoolFactory.class create mode 100644 target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool.class create mode 100644 target/classes/init-mogile-test-data.sh create mode 100644 target/classes/permission-change.sh create mode 100644 target/classes/spring/moji-context.xml create mode 100644 target/test-classes/checkstyle.xml create mode 100644 target/test-classes/findbugsExclude.xml create mode 100644 target/test-classes/fm/last/moji/FakeMogileFsServer$Builder.class create mode 100644 target/test-classes/fm/last/moji/FakeMogileFsServer$Stanza.class create mode 100644 target/test-classes/fm/last/moji/FakeMogileFsServer$TrackerServer.class create mode 100644 target/test-classes/fm/last/moji/FakeMogileFsServer.class create mode 100644 target/test-classes/fm/last/moji/MojiInstantiationTest.class create mode 100644 target/test-classes/fm/last/moji/impl/DeleteCommandTest.class create mode 100644 target/test-classes/fm/last/moji/impl/ExecutorTest.class create mode 100644 target/test-classes/fm/last/moji/impl/ExistsCommandTest.class create mode 100644 target/test-classes/fm/last/moji/impl/FileDownloadInputStreamTest.class create mode 100644 target/test-classes/fm/last/moji/impl/FileUploadOutputStreamTest.class create mode 100644 target/test-classes/fm/last/moji/impl/HttpConnectionFactoryTest.class create mode 100644 target/test-classes/fm/last/moji/impl/ListFilesCommandTest$1.class create mode 100644 target/test-classes/fm/last/moji/impl/ListFilesCommandTest.class create mode 100644 target/test-classes/fm/last/moji/impl/MojiFileImplTest.class create mode 100644 target/test-classes/fm/last/moji/impl/RenameCommandTest.class create mode 100644 target/test-classes/fm/last/moji/impl/UpdateStorageClassCommandTest.class create mode 100644 target/test-classes/fm/last/moji/integration/AbstractMojiIT.class create mode 100644 target/test-classes/fm/last/moji/integration/MojiFileIT.class create mode 100644 target/test-classes/fm/last/moji/integration/MojiIT.class create mode 100644 target/test-classes/fm/last/moji/local/DefaultFileNamingStrategyTest.class create mode 100644 target/test-classes/fm/last/moji/spring/SpringMojiBeanInstantiationTest.class create mode 100644 target/test-classes/fm/last/moji/spring/SpringMojiBeanTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/CreateOpenOperationTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/GetPathsOperationTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/RequestHandlerTest$TrueMatcher.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/RequestHandlerTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/RequestTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/ResponseTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/SingleHostTrackerFactoryTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/impl/TrackerImplTest.class create mode 100644 target/test-classes/fm/last/moji/tracker/pool/BorrowedTrackerTest.class create mode 100644 target/test-classes/log4j.properties create mode 100644 target/test-classes/moji.properties diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf4ddfd --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +#About +A file-like [MogileFS](http://danga.com/mogilefs/ "Danga Interactive - MogileFS") client for Java. + +#Features +* `java.io.File` like API +* Supports streams of unknown length +* Unit/Integration tests +* Spring friendly +* Tracker connection pooling with balancing between hosts and strategies for dealing with failed nodes +* Local file system implementation for faking in tests (`fm.last.moji.local.LocalFileSystemMoji`) + +#Configuration +### Using plain-old-Java + String hosts = "192.168.0.1,192.168.0.2"; + String domain = "testdomain"; + + Moji moji = new SpringMojiBean(hosts, domain); + moji.setTestOnBorrow(true); +### Using the Spring framework +Set some properties for your context: + + moji.tracker.address=192.168.0.1,192.168.0.2 + moji.domain=testdomain + +Import the Moji Spring context: + + + +*Or* create a Moji spring bean: + + + + + + + + + +#Usage +####Create/update a remote file + MojiFile rickRoll = moji.getFile("rick-astley"); + moji.copyToMogile(new File("never-gonna-give-you-up.mp3"), rickRoll); + +Or in a given storage class: + + MojiFile rickRoll = moji.getFile("rick-astley", "music-meme"); + +####Get the remote file size + long length = rickRoll.length(); +####Rename the remote file + rickRoll.rename("stairway-to-heaven"); +####Check the existence of a remote file + MojiFile abba = moji.getFile("voulez-vous"); + if (abba.exists()) { + ... +####Delete the remote file + abba.delete(); +####Download a remote file + MojiFile fooFighters = moji.getFile("stacked-actors"); + fooFighters.copyToFile(new File("foo-fighters.mp3")); +####Modify the storage class of a remote file + fooFighters.modifyStorageClass("awesome"); + +####Stream from a remote file + InputStream stream = null; + try { + stream = fooFighters.getInputStream(); + // Do something streamy + // stream.read(); + } finally { + stream.close(); + } + +####Stream to a remote file +This will either create a new file or overwrite an existing file's contents + + OutputStream stream = null; + try { + stream = fooFighters.getOutputStream(); + // Do something streamy + // stream.write(...); + stream.flush(); + } finally { + stream.close(); + } +####List remote files by prefix + List files = moji.list("abba-"); + for(MojiFile file : files) { + // abba-waterloo, abba-voulez-vous, abba-fernado, etc. + } + +Impose a limit on the number of items returned: + + List files = moji.list("abba-", 10); + for(MojiFile file : files) { + // abba-waterloo, abba-voulez-vous, abba-fernado, etc. - maximum of 10 + } + +####Get the locations of a remote file + File fooFighters = moji.getFile("in-your-honour"); + List paths = fooFighters.getPaths(); + // http://192.168.0.2:7500/dev2/0/000/000/0000000819.fid, http://192.168.0.4:7500/dev3/0/000/000/0000000819.fid, etc + +#Running the integration tests +To run the integration tests you need: + +* A Linux system - FIXME +* A test MogileFS tracker and a storage node ([installation instructions](http://code.google.com/p/mogilefs/wiki/InstallHowTo "Google Code - MogileFS installation instructions")) +* `mogtool, mogupload` +* `uuencode` (apt-get install sharutil) + +MogileFS integration test properties config: + +* These properties should be set in `/moji.properties` +* Set your Tracker address with the property: + + moji.tracker.hosts +* Declare your Mogile domain with the property: + + moji.domain +* Declare two storage classes in your Mogile instance and assign them with these properties: + + test.moji.class.a + test.moji.class.b +* Choose a key prefix to avoid any key clashes with real data (you're using a test instance right?) or other tests. Otherwise we might get unexpected behaviour and file deletions: + + test.moji.key.prefix + +#Building +This project uses the [Maven](http://maven.apache.org/) build system. +#Further work +We plan to support the [Java 7 FileSystem abstraction](http://openjdk.java.net/projects/nio/ "OpenJDK: NIO") now it's been officially released. + +#Legal +Copyright 2011 [Last.fm](http://www.last.fm/) + +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](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. \ No newline at end of file diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..3b24f74 --- /dev/null +++ b/changelog.txt @@ -0,0 +1,2 @@ +TBA +- Initial release \ No newline at end of file diff --git a/eclipse-codeformatter-profile.xml b/eclipse-codeformatter-profile.xml new file mode 100644 index 0000000..78b960c --- /dev/null +++ b/eclipse-codeformatter-profile.xml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b6334e9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,255 @@ + + + 4.0.0 + + fm.last.commons + moji + 1.1.0-SNAPSHOT + Moji + A File-like MogileFS client + + https://github.com/lastfm/moji + + + scm:git:ssh:git@github.com:lastfm/moji.git + + + + + Elliot West + teabot@gmail.com + http://www.last.fm/user/teabot + + Documentation + Java Developer + + Last.fm + http://www.last.fm/ + + + James Grant + Last.fm + http://www.last.fm/ + + Documentation + Java Developer + + + + + + Last.fm + http://www.last.fm + + + + true + true + + + + + + ${project.basedir}/src/test/resources/moji.properties${moji.env} + + + + + src/main/resources + + + src/main/conf + + + src/test/script + true + + **/*.sh + + + + src/test/script + true + + **/*.sh + + + + + + + src/test/resources + + + src/test/conf + + + + + + maven-compiler-plugin + + 1.6 + 1.6 + UTF-8 + true + + + + org.apache.maven.plugins + maven-resources-plugin + + UTF-8 + + + + org.codehaus.mojo + exec-maven-plugin + 1.1 + + + annoying-chmod + pre-integration-test + + ${project.basedir}/src/test/script/permission-change.sh + + + exec + + + + init-mogile-fs-test-data + pre-integration-test + + ${project.basedir}/target/classes/init-mogile-test-data.sh + ${project.basedir}/src/test + + + exec + + + + + + maven-failsafe-plugin + 2.6 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.0 + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.7 + + + http://commons.apache.org/pool/api-1.5.6/ + + true + true + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.5.1 + + + xml + html + + + + + org.codehaus.mojo + findbugs-maven-plugin + 2.3.2 + + ${project.basedir}/src/test/resources/findbugsExclude.xml + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.6 + + ${project.basedir}/src/test/resources/checkstyle.xml + + + + + + + + hudson + + + env + hudson + + + + .hudson + + + + + + + org.slf4j + slf4j-api + 1.6.1 + + + commons-io + commons-io + 2.0.1 + + + org.apache.commons + commons-pool + 1.5.6 + + + + commons-lang + commons-lang + 2.1 + test + + + org.slf4j + slf4j-log4j12 + 1.6.1 + test + + + junit + junit + 4.8.2 + test + + + org.mockito + mockito-all + 1.8.5 + test + + + diff --git a/src/main/java/fm/last/moji/Moji.java b/src/main/java/fm/last/moji/Moji.java new file mode 100644 index 0000000..d5b03d5 --- /dev/null +++ b/src/main/java/fm/last/moji/Moji.java @@ -0,0 +1,84 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * The Moji entry point. A representation of a MogileFS domain that allows interactions with remote files. + *

+ * Example usage: + *

+ * + *

+ * MojiFactory factory = new PropertyMojiFactory();
+ * Moji moji = factory.getInstance();
+ * MojiFile file = moji.getFile("some-key");
+ * file.copyToFile(new File("localFile.dat"));
+ * 
+ */ +public interface Moji { + + /** + * Creates an abstract representation of a remote MogileFS file for the given key. + * + * @param key MogileFS file key. + * @return Representation of the remote file. + */ + MojiFile getFile(String key); + + /** + * Creates an abstract representation of a remote MogileFS file for the given key. When the file content is modified + * the file will also be assigned the specified storage class. Note that storage class parameter has no effect when + * reading files. + * + * @param key MogileFS file key. + * @param storageClass The storage class to which a new file will be assigned. + * @return Representation of the remote file. + */ + MojiFile getFile(String key, String storageClass); + + /** + * Copies a local source file to the given remote MogileFS destination file. + * + * @param source The local source file. + * @param destination The remote destination of the file. + * @throws IOException If there was a problem writing the file. + */ + void copyToMogile(File source, MojiFile destination) throws IOException; + + /** + * Get remote MogileFS file representations that match the given key prefix. + * + * @param keyPrefix The Key prefix to match remote files on. + * @return A list of matching MofileFS file representations or an empty list if there were no matches. + * @throws IOException If there was a problem communicating with MogileFS. + */ + List list(String keyPrefix) throws IOException; + + /** + * Get a bounded list of remote MogileFS file representations that match the given key prefix. + * + * @param keyPrefix The Key prefix to match remote files on. + * @param limit The maximum number of files to return. + * @return A list of matching MofileFS file representations or an empty list if there were no matches. + * @throws IOException If there was a problem communicating with MogileFS. + */ + List list(String keyPrefix, int limit) throws IOException; + +} diff --git a/src/main/java/fm/last/moji/MojiFactory.java b/src/main/java/fm/last/moji/MojiFactory.java new file mode 100644 index 0000000..f14f60a --- /dev/null +++ b/src/main/java/fm/last/moji/MojiFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji; + +import java.io.IOException; + +/** + * Factory interface for creating {@link fm.last.moji.Moji Mojis}. + */ +public interface MojiFactory { + + /** + * Gets a Moji instance using whatever strategy the implemention provides. + * + * @return The Moji instance. + * @throws IOException If there was a problem creating the instance. + */ + Moji getInstance() throws IOException; + + /** + * Gets a Moji instance using whatever strategy the implemention provides. + * + * @return The Moji instance. + * @throws IOException If there was a problem creating the instance. + */ + Moji getInstance(String domain) throws IOException; + +} diff --git a/src/main/java/fm/last/moji/MojiFile.java b/src/main/java/fm/last/moji/MojiFile.java new file mode 100644 index 0000000..d2dd4e1 --- /dev/null +++ b/src/main/java/fm/last/moji/MojiFile.java @@ -0,0 +1,123 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.List; + +/** + * An abstract representation of a remote MogileFS file. + */ +public interface MojiFile { + + /** + * Determines whether or not the file represented by this object exists in MogileFS. + * + * @return true if the file exists in MogileFS, false otherwise. + * @throws IOException If there was a problem communicating with MogileFS. + */ + boolean exists() throws IOException; + + /** + * Deletes this file in MogileFS if it exists. If the file doesn't exists then this method will return without error. + * + * @throws IOException If there was a problem communicating with MogileFS. + */ + void delete() throws IOException; + + /** + * Assigns a new key to the MogileFS file represented by this object. + * + * @param key The new key for the file. + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws fm.last.moji.tracker.KeyExistsAlreadyException If the new key already points to a file in MogileFS. + * @throws IOException If there was a problem communicating with MogileFS. + */ + void rename(String key) throws IOException; + + /** + * Gets an InputStream to the content of the MogileFS file represented by this object. + * + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws IOException If there was a problem communicating with MogileFS. + */ + InputStream getInputStream() throws IOException; + + /** + * Gets an OutputStream for writing content to the MogileFS file represented by this object. If the file does not + * exist then it will be created. + * + * @throws IOException If there was a problem communicating with MogileFS. + */ + OutputStream getOutputStream() throws IOException; + + /** + * Copies the content of the file represented by this object to a local file destination. + * + * @param file + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws IOException + */ + void copyToFile(File file) throws IOException; + + /** + * Returns the length in bytes of this remote file. + * + * @return File length in bytes. + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws IOException If there was a problem communicating with MogileFS. + */ + long length() throws IOException; + + /** + * Assigns this file to the specified storage class. + * + * @param storageClass The new storage class + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws fm.last.moji.tracker.UnknownStorageClassException If the specified storage class is not defined in + * MogileFS. + * @throws IOException If there was a problem communicating with MogileFS. + */ + void modifyStorageClass(String storageClass) throws IOException; + + /** + * Returns a list of storage node paths from which this file can be accessed. + * + * @return A list of storage node paths or an empty list. + * @throws fm.last.moji.tracker.UnknownKeyException If this file doesn't exist in MogileFS. + * @throws IOException If there was a problem communicating with MogileFS. + */ + List getPaths() throws IOException; + + /** + * The key of this file in MogileFS. + * + * @return This files key. + */ + String getKey(); + + /** + * The MogileFS domain in which this file is located. + * + * @return This files key. + */ + String getDomain(); + +} diff --git a/src/main/java/fm/last/moji/impl/DefaultMojiFactory.java b/src/main/java/fm/last/moji/impl/DefaultMojiFactory.java new file mode 100644 index 0000000..13808d6 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/DefaultMojiFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFactory; +import fm.last.moji.tracker.TrackerFactory; + +/** + * Creates a {@link fm.last.moji.Moji Moji} instance. + */ +public class DefaultMojiFactory implements MojiFactory { + + private final String defaultDomain; + private final TrackerFactory trackerFactory; + private final HttpConnectionFactory httpFactory; + + public DefaultMojiFactory(TrackerFactory trackerFactory, String defaultDomain) { + this.trackerFactory = trackerFactory; + this.defaultDomain = defaultDomain; + httpFactory = new HttpConnectionFactory(trackerFactory.getProxy()); + } + + @Override + public Moji getInstance() { + return new MojiImpl(trackerFactory, httpFactory, defaultDomain); + } + + @Override + public Moji getInstance(String domain) throws IOException { + return new MojiImpl(trackerFactory, httpFactory, domain); + } + +} diff --git a/src/main/java/fm/last/moji/impl/DeleteCommand.java b/src/main/java/fm/last/moji/impl/DeleteCommand.java new file mode 100644 index 0000000..1ebb48e --- /dev/null +++ b/src/main/java/fm/last/moji/impl/DeleteCommand.java @@ -0,0 +1,48 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import fm.last.moji.tracker.Tracker; + +class DeleteCommand implements MojiCommand { + + final String key; + final String domain; + + DeleteCommand(String key, String domain) { + this.key = key; + this.domain = domain; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + tracker.delete(key, domain); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("DeleteCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/Executor.java b/src/main/java/fm/last/moji/impl/Executor.java new file mode 100644 index 0000000..ad65705 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/Executor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerFactory; +import fm.last.moji.tracker.impl.CommunicationException; + +class Executor { + + private static final Logger log = LoggerFactory.getLogger(Executor.class); + + private final TrackerFactory trackerFactory; + private int maxAttempts; + + Executor(TrackerFactory trackerFactory) { + this.trackerFactory = trackerFactory; + maxAttempts = trackerFactory.getAddresses().size(); + } + + public void executeCommand(MojiCommand command) throws IOException { + Tracker tracker = null; + CommunicationException lastException = null; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + tracker = trackerFactory.getTracker(); + log.debug("executing {}", command); + if (maxAttempts > 1) { + log.debug("Attempt #{}", attempt); + } + command.executeWithTracker(tracker); + return; + } catch (CommunicationException e) { + lastException = e; + } finally { + if (tracker != null) { + tracker.close(); + } + } + } + if (maxAttempts > 1) { + log.debug("All {} attempts failed", maxAttempts); + } + throw lastException; + } + + void setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + } + +} diff --git a/src/main/java/fm/last/moji/impl/ExistsCommand.java b/src/main/java/fm/last/moji/impl/ExistsCommand.java new file mode 100644 index 0000000..be8752c --- /dev/null +++ b/src/main/java/fm/last/moji/impl/ExistsCommand.java @@ -0,0 +1,63 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.UnknownKeyException; + +class ExistsCommand implements MojiCommand { + + final String key; + final String domain; + private boolean exists; + + ExistsCommand(String key, String domain) { + this.key = key; + this.domain = domain; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + List paths = null; + try { + paths = tracker.getPaths(key, domain); + exists = !paths.isEmpty(); + } catch (UnknownKeyException e) { + } + } + + boolean getExists() { + return exists; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ExistsCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", exists="); + builder.append(exists); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/FileDownloadInputStream.java b/src/main/java/fm/last/moji/impl/FileDownloadInputStream.java new file mode 100644 index 0000000..b07a922 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/FileDownloadInputStream.java @@ -0,0 +1,95 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.locks.Lock; + +import org.apache.commons.io.input.CountingInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class FileDownloadInputStream extends InputStream { + + private static final Logger log = LoggerFactory.getLogger(FileDownloadInputStream.class); + + private final CountingInputStream delegate; + private final Lock readLock; + + FileDownloadInputStream(InputStream delegate, Lock readLock) { + this.readLock = readLock; + this.delegate = new CountingInputStream(delegate); + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return delegate.read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return delegate.skip(n); + } + + @Override + public int available() throws IOException { + return delegate.available(); + } + + @Override + public void close() throws IOException { + log.debug("Read {} bytes", delegate.getCount()); + try { + delegate.close(); + } finally { + unlockQuietly(readLock); + } + } + + @Override + public void mark(int readlimit) { + delegate.mark(readlimit); + } + + @Override + public void reset() throws IOException { + delegate.reset(); + } + + @Override + public boolean markSupported() { + return delegate.markSupported(); + } + + private void unlockQuietly(Lock lock) { + try { + lock.unlock(); + } catch (IllegalMonitorStateException e) { + } + } + +} diff --git a/src/main/java/fm/last/moji/impl/FileLengthCommand.java b/src/main/java/fm/last/moji/impl/FileLengthCommand.java new file mode 100644 index 0000000..a9c3fe6 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/FileLengthCommand.java @@ -0,0 +1,100 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Tracker; + +class FileLengthCommand implements MojiCommand { + + private static final Logger log = LoggerFactory.getLogger(FileLengthCommand.class); + + private final HttpConnectionFactory httpFactory; + final String key; + final String domain; + private long length = -1L; + + FileLengthCommand(HttpConnectionFactory httpFactory, String key, String domain) { + this.httpFactory = httpFactory; + this.key = key; + this.domain = domain; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + List paths = tracker.getPaths(key, domain); + if (!paths.isEmpty()) { + HttpURLConnection httpConnection = null; + IOException lastException = null; + for (URL path : paths) { + try { + log.debug("HTTP HEAD -> {}", path); + httpConnection = httpFactory.newConnection(path); + httpConnection.setRequestMethod("HEAD"); + length = httpConnection.getContentLength(); + log.debug("Content-Length: {}", length); + return; + } catch (IOException e) { + log.debug("Failed to open input -> {}", path); + log.debug("Exception was: ", e); + lastException = e; + } finally { + if (httpConnection != null) { + httpConnection.disconnect(); + } + } + } + throw lastException; + } else { + log.debug("No paths found for domain={},key={} - throwing", domain, key); + throw new FileNotFoundException("domain=" + domain + ",key=" + key); + } + } + + long getLength() { + return length; + } + + String getKey() { + return key; + } + + String getDomain() { + return domain; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("FileLengthCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", length="); + builder.append(length); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/FileUploadOutputStream.java b/src/main/java/fm/last/moji/impl/FileUploadOutputStream.java new file mode 100644 index 0000000..25d7e5f --- /dev/null +++ b/src/main/java/fm/last/moji/impl/FileUploadOutputStream.java @@ -0,0 +1,140 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.util.concurrent.locks.Lock; + +import org.apache.commons.io.output.CountingOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerFactory; + +class FileUploadOutputStream extends OutputStream { + + private static final Logger log = LoggerFactory.getLogger(FileUploadOutputStream.class); + + private static final int CHUNK_LENGTH = 4096; + private final Destination destination; + private final TrackerFactory trackerFactory; + private final String key; + private final String domain; + private final Lock writeLock; + private final HttpURLConnection httpConnection; + private final CountingOutputStream delegate; + + FileUploadOutputStream(TrackerFactory trackerFactory, HttpConnectionFactory httpFactory, String key, String domain, + Destination destination, Lock writeLock) throws IOException { + this.destination = destination; + this.trackerFactory = trackerFactory; + this.domain = domain; + this.key = key; + this.writeLock = writeLock; + + log.debug("HTTP PUT -> opening chunked stream -> {}", destination.getPath()); + httpConnection = httpFactory.newConnection(destination.getPath()); + httpConnection.setRequestMethod("PUT"); + httpConnection.setChunkedStreamingMode(CHUNK_LENGTH); + httpConnection.setDoOutput(true); + delegate = new CountingOutputStream(httpConnection.getOutputStream()); + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + delegate.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + delegate.write(b, off, len); + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + log.debug("Close called on {}", this); + long size = -1L; + try { + try { + delegate.flush(); + size = delegate.getByteCount(); + } finally { + try { + delegate.close(); + } finally { + try { + String message = httpConnection.getResponseMessage(); + int code = httpConnection.getResponseCode(); + if (HttpURLConnection.HTTP_OK != code) { + throw new IOException(code + " " + message); + } else { + log.debug("Status: HTTP {} - {}", code, message); + } + } finally { + httpConnection.disconnect(); + } + } + } + } finally { + log.debug("Bytes written: {}", size); + Tracker tracker = trackerFactory.getTracker(); + try { + tracker.createClose(key, domain, destination, size); + } finally { + try { + tracker.close(); + } finally { + unlockQuietly(writeLock); + } + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("FileUploadOutputStream [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", destination="); + builder.append(destination); + builder.append("]"); + return builder.toString(); + } + + private void unlockQuietly(Lock lock) { + try { + lock.unlock(); + } catch (IllegalMonitorStateException e) { + } + } + +} diff --git a/src/main/java/fm/last/moji/impl/GetInputStreamCommand.java b/src/main/java/fm/last/moji/impl/GetInputStreamCommand.java new file mode 100644 index 0000000..5167fff --- /dev/null +++ b/src/main/java/fm/last/moji/impl/GetInputStreamCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.concurrent.locks.Lock; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Tracker; + +class GetInputStreamCommand implements MojiCommand { + + private static final Logger log = LoggerFactory.getLogger(GetInputStreamCommand.class); + + final String key; + final String domain; + private final HttpConnectionFactory httpFactory; + private final Lock readLock; + private InputStream stream; + + GetInputStreamCommand(String key, String domain, HttpConnectionFactory httpFactory, Lock readLock) { + this.key = key; + this.domain = domain; + this.httpFactory = httpFactory; + this.readLock = readLock; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + List paths = tracker.getPaths(key, domain); + if (paths.isEmpty()) { + throw new FileNotFoundException("key=" + key + ", domain=" + domain); + } + IOException lastException = null; + for (URL path : paths) { + try { + log.debug("Opened: {}", path); + HttpURLConnection urlConnection = httpFactory.newConnection(path); + stream = new FileDownloadInputStream(urlConnection.getInputStream(), readLock); + return; + } catch (IOException e) { + log.debug("Failed to open input -> {}", path); + log.debug("Exception was: ", e); + lastException = e; + IOUtils.closeQuietly(stream); + } + } + throw lastException; + } + + InputStream getInputStream() { + return stream; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("GetInputStreamCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/GetOutputStreamCommand.java b/src/main/java/fm/last/moji/impl/GetOutputStreamCommand.java new file mode 100644 index 0000000..aef7b91 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/GetOutputStreamCommand.java @@ -0,0 +1,94 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.concurrent.locks.Lock; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.TrackerFactory; + +class GetOutputStreamCommand implements MojiCommand { + + private static final Logger log = LoggerFactory.getLogger(GetOutputStreamCommand.class); + + final String key; + final String domain; + final String storageClass; + private final TrackerFactory trackerFactory; + private final HttpConnectionFactory httpFactory; + private OutputStream stream; + private final Lock writeLock; + + GetOutputStreamCommand(TrackerFactory trackerFactory, HttpConnectionFactory httpFactory, String key, String domain, + String storageClass, Lock writeLock) { + this.trackerFactory = trackerFactory; + this.httpFactory = httpFactory; + this.key = key; + this.domain = domain; + this.storageClass = storageClass; + this.writeLock = writeLock; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + List destinations = tracker.createOpen(key, domain, storageClass); + if (destinations.isEmpty()) { + throw new TrackerException("Failed to obtain destinations for domain=" + domain + ",key=" + key + + ",storageClass=" + storageClass); + } + IOException lastException = null; + for (Destination destination : destinations) { + log.debug("Creating output stream to: {}", destination); + try { + stream = new FileUploadOutputStream(trackerFactory, httpFactory, key, domain, destinations.get(0), writeLock); + return; + } catch (IOException e) { + log.debug("Failed to open output -> {}", destination); + log.debug("Exception was: ", e); + lastException = e; + IOUtils.closeQuietly(stream); + } + } + throw lastException; + } + + OutputStream getOutputStream() { + return stream; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("GetOutputStreamCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", storageClass="); + builder.append(storageClass); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/GetPathsCommand.java b/src/main/java/fm/last/moji/impl/GetPathsCommand.java new file mode 100644 index 0000000..f0d752e --- /dev/null +++ b/src/main/java/fm/last/moji/impl/GetPathsCommand.java @@ -0,0 +1,57 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import fm.last.moji.tracker.Tracker; + +class GetPathsCommand implements MojiCommand { + + private final String key; + private final String domain; + private List paths; + + GetPathsCommand(String key, String domain) { + this.key = key; + this.domain = domain; + paths = Collections.emptyList(); + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + paths = tracker.getPaths(key, domain); + } + + List getPaths() { + return paths; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("GetPathsCommand [key="); + builder.append(key); + builder.append(", domain="); + builder.append(domain); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/HttpConnectionFactory.java b/src/main/java/fm/last/moji/impl/HttpConnectionFactory.java new file mode 100644 index 0000000..408c7db --- /dev/null +++ b/src/main/java/fm/last/moji/impl/HttpConnectionFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URL; + +class HttpConnectionFactory { + + private final Proxy proxy; + + HttpConnectionFactory(Proxy proxy) { + this.proxy = proxy; + } + + HttpURLConnection newConnection(URL url) throws IOException { + return (HttpURLConnection) url.openConnection(proxy); + } + +} diff --git a/src/main/java/fm/last/moji/impl/ListFilesCommand.java b/src/main/java/fm/last/moji/impl/ListFilesCommand.java new file mode 100644 index 0000000..ba51cf7 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/ListFilesCommand.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFile; +import fm.last.moji.tracker.Tracker; + +class ListFilesCommand implements MojiCommand { + + final String keyPrefix; + final String domain; + final Integer limit; + private List files; + private final Moji moji; + + ListFilesCommand(Moji moji, String keyPrefix, String domain, int limit) { + this(moji, keyPrefix, domain, Integer.valueOf(limit)); + } + + ListFilesCommand(Moji moji, String keyPrefix, String domain) { + this(moji, keyPrefix, domain, null); + } + + private ListFilesCommand(Moji moji, String keyPrefix, String domain, Integer limit) { + this.moji = moji; + this.keyPrefix = keyPrefix; + this.domain = domain; + this.limit = limit; + files = Collections.emptyList(); + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + List keys = tracker.list(domain, keyPrefix, limit); + if (!keys.isEmpty()) { + files = new ArrayList(keys.size()); + for (String key : keys) { + MojiFile file = moji.getFile(key); + files.add(file); + } + } + } + + List getFileList() { + return files; + } + +} diff --git a/src/main/java/fm/last/moji/impl/MojiCommand.java b/src/main/java/fm/last/moji/impl/MojiCommand.java new file mode 100644 index 0000000..8efeee4 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/MojiCommand.java @@ -0,0 +1,26 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import fm.last.moji.tracker.Tracker; + +interface MojiCommand { + + void executeWithTracker(Tracker tracker) throws IOException; + +} diff --git a/src/main/java/fm/last/moji/impl/MojiFileImpl.java b/src/main/java/fm/last/moji/impl/MojiFileImpl.java new file mode 100644 index 0000000..1887023 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/MojiFileImpl.java @@ -0,0 +1,241 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.MojiFile; +import fm.last.moji.tracker.TrackerFactory; + +class MojiFileImpl implements MojiFile { + + private static final Logger log = LoggerFactory.getLogger(MojiFileImpl.class); + + private final String domain; + private final TrackerFactory trackerFactory; + private final HttpConnectionFactory httpFactory; + private final ReadWriteLock lock; + private Executor executor; + private String storageClass; + private String key; + + MojiFileImpl(String key, String domain, String storageClass, TrackerFactory trackerFactory, + HttpConnectionFactory httpFactory) { + this.key = key; + this.domain = domain; + this.storageClass = storageClass; + this.trackerFactory = trackerFactory; + this.httpFactory = httpFactory; + executor = new Executor(trackerFactory); + lock = new ReentrantReadWriteLock(); + } + + @Override + public boolean exists() throws IOException { + log.debug("exists() : {}", this); + boolean exists = false; + try { + lock.readLock().lock(); + ExistsCommand command = new ExistsCommand(key, domain); + executor.executeCommand(command); + exists = command.getExists(); + log.debug("exists() -> {}", exists); + } finally { + lock.readLock().unlock(); + } + return exists; + } + + @Override + public void delete() throws IOException { + log.debug("delete() : {}", this); + try { + lock.writeLock().lock(); + DeleteCommand command = new DeleteCommand(key, domain); + executor.executeCommand(command); + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public long length() throws IOException { + log.debug("length() : {}", this); + long length = -1L; + try { + lock.readLock().lock(); + FileLengthCommand command = new FileLengthCommand(httpFactory, key, domain); + executor.executeCommand(command); + length = command.getLength(); + log.debug("length() -> {}", length); + } finally { + lock.readLock().unlock(); + } + return length; + } + + @Override + public InputStream getInputStream() throws IOException { + log.debug("getInputStream() : {}", this); + InputStream inputStream = null; + try { + Lock readLock = lock.readLock(); + readLock.lock(); + GetInputStreamCommand command = new GetInputStreamCommand(key, domain, httpFactory, readLock); + executor.executeCommand(command); + inputStream = command.getInputStream(); + log.debug("getInputStream() -> {}", inputStream); + } catch (Throwable e) { + unlockQuietly(lock.readLock()); + IOUtils.closeQuietly(inputStream); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e); + } + } + // calling close will release the lock + return inputStream; + } + + @Override + public OutputStream getOutputStream() throws IOException { + log.debug("getOutputStream() : {}", this); + OutputStream outputStream = null; + try { + Lock writeLock = lock.writeLock(); + writeLock.lock(); + GetOutputStreamCommand command = new GetOutputStreamCommand(trackerFactory, httpFactory, key, domain, + storageClass, writeLock); + executor.executeCommand(command); + outputStream = command.getOutputStream(); + log.debug("getOutputStream() -> {}", outputStream); + } catch (Throwable e) { + unlockQuietly(lock.writeLock()); + IOUtils.closeQuietly(outputStream); + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new RuntimeException(e); + } + } + // calling close will release the lock + return outputStream; + } + + @Override + public void rename(String newKey) throws IOException { + log.debug("rename() : {}", this); + try { + lock.writeLock().lock(); + RenameCommand command = new RenameCommand(key, domain, newKey); + executor.executeCommand(command); + key = newKey; + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public void modifyStorageClass(String newStorageClass) throws IOException { + log.debug("setStorageClass() : {}", this); + try { + lock.writeLock().lock(); + UpdateStorageClassCommand command = new UpdateStorageClassCommand(key, domain, newStorageClass); + executor.executeCommand(command); + storageClass = newStorageClass; + } finally { + lock.writeLock().unlock(); + } + } + + @Override + public List getPaths() throws IOException { + log.debug("getPaths() : {}", this); + List paths = Collections.emptyList(); + try { + lock.readLock().lock(); + GetPathsCommand command = new GetPathsCommand(key, domain); + executor.executeCommand(command); + paths = command.getPaths(); + log.debug("getPaths() -> {}", paths); + } finally { + lock.readLock().unlock(); + } + return paths; + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getDomain() { + return domain; + } + + // We cannot know the storage class currently + // @Override + // public String getStorageClass() { + // return storageClass; + // } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("MogileFileImpl [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append("]"); + return builder.toString(); + } + + @Override + public void copyToFile(File destination) throws IOException { + InputStream inputStream = null; + inputStream = getInputStream(); + // buffers internally and closes + FileUtils.copyInputStreamToFile(inputStream, destination); + } + + void setExecutor(Executor executor) { + this.executor = executor; + } + + private void unlockQuietly(Lock lock) { + try { + lock.unlock(); + } catch (IllegalMonitorStateException e) { + } + } + +} diff --git a/src/main/java/fm/last/moji/impl/MojiImpl.java b/src/main/java/fm/last/moji/impl/MojiImpl.java new file mode 100644 index 0000000..b52e3d4 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/MojiImpl.java @@ -0,0 +1,108 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFile; +import fm.last.moji.tracker.TrackerFactory; + +class MojiImpl implements Moji { + + private static final Logger log = LoggerFactory.getLogger(MojiImpl.class); + + private final TrackerFactory trackerFactory; + private final HttpConnectionFactory httpFactory; + private final String domain; + private final Executor executor; + + MojiImpl(TrackerFactory trackerFactory, HttpConnectionFactory httpFactory, String domain) { + this.domain = domain; + this.httpFactory = httpFactory; + this.trackerFactory = trackerFactory; + executor = new Executor(trackerFactory); + } + + @Override + public MojiFile getFile(String key) { + log.debug("new {}()", MojiFileImpl.class.getSimpleName()); + return new MojiFileImpl(key, domain, null, trackerFactory, httpFactory); + } + + @Override + public MojiFile getFile(String key, String storageClass) { + log.debug("new {}() with storage class", MojiFileImpl.class.getSimpleName()); + return new MojiFileImpl(key, domain, storageClass, trackerFactory, httpFactory); + } + + @Override + public void copyToMogile(File source, MojiFile destination) throws IOException { + OutputStream outputStream = null; + InputStream inputStream = new FileInputStream(source); + try { + outputStream = destination.getOutputStream(); + IOUtils.copy(inputStream, outputStream); // buffers internally + outputStream.flush(); + } finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + + @Override + public List list(String keyPrefix) throws IOException { + log.debug("list() : {}", keyPrefix); + List list = null; + ListFilesCommand command = new ListFilesCommand(this, keyPrefix, domain); + executor.executeCommand(command); + list = command.getFileList(); + log.debug("list() -> {}", list); + return list; + } + + @Override + public List list(String keyPrefix, int limit) throws IOException { + log.debug("list() : {}, {}", keyPrefix, limit); + List list = null; + ListFilesCommand command = new ListFilesCommand(this, keyPrefix, domain, limit); + executor.executeCommand(command); + list = command.getFileList(); + log.debug("list() -> {}", list); + return list; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("MojiImpl [domain="); + builder.append(domain); + builder.append(", trackerFactory="); + builder.append(trackerFactory); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/PropertyMojiFactory.java b/src/main/java/fm/last/moji/impl/PropertyMojiFactory.java new file mode 100644 index 0000000..0875b6f --- /dev/null +++ b/src/main/java/fm/last/moji/impl/PropertyMojiFactory.java @@ -0,0 +1,131 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Properties; +import java.util.Set; + +import org.apache.commons.io.IOUtils; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFactory; +import fm.last.moji.tracker.TrackerFactory; +import fm.last.moji.tracker.impl.InetSocketAddressFactory; +import fm.last.moji.tracker.pool.MultiHostTrackerPool; + +/** + * Creates a {@link fm.last.moji.Moji Moji} instance using configuration information obtained from the following + * properties: + *

+ *

    + *
  • moji.domain
  • + *
  • moji.tracker.hosts
  • + *
+ *

+ * The properties are loaded from a /moji.properties classpath resource by default. The resource path can + * be specified by setting the moji.properties.resource.path system property. + */ +public class PropertyMojiFactory implements MojiFactory { + + public static final String RESOURCE_PATH_PROPERTY = "moji.properties.resource.path"; + private static final String DEFAULT_RESOURCE_PATH = "/moji.properties"; + + private static final String HOSTS_PROPERTY = "moji.tracker.hosts"; + private static final String DOMAIN_PROPERTY = "moji.domain"; + + private final Proxy proxy; + private volatile boolean initialised; + private String defaultDomain; + private TrackerFactory trackerFactory; + private HttpConnectionFactory httpFactory; + private final String propertiesPath; + + public PropertyMojiFactory(String propertiesPath, Proxy proxy) throws IOException { + this.propertiesPath = System.getProperty(RESOURCE_PATH_PROPERTY, propertiesPath); + this.proxy = proxy; + } + + public PropertyMojiFactory(String propertiesPath) throws IOException { + this(propertiesPath, Proxy.NO_PROXY); + } + + public PropertyMojiFactory(Proxy proxy) throws IOException { + this(DEFAULT_RESOURCE_PATH, proxy); + } + + public PropertyMojiFactory() throws IOException { + this(DEFAULT_RESOURCE_PATH, Proxy.NO_PROXY); + } + + @Override + public Moji getInstance() throws IOException { + initialise(); + return new MojiImpl(trackerFactory, httpFactory, defaultDomain); + } + + @Override + public Moji getInstance(String domain) throws IOException { + initialise(); + return new MojiImpl(trackerFactory, httpFactory, domain); + } + + private void initialise() throws IOException { + synchronized (this) { + if (!initialised) { + Properties properties = loadProperties(); + String addressesCsv = getHosts(properties); + defaultDomain = getDomain(properties); + Set addresses = InetSocketAddressFactory.newAddresses(addressesCsv); + + trackerFactory = new MultiHostTrackerPool(addresses, proxy); + httpFactory = new HttpConnectionFactory(trackerFactory.getProxy()); + initialised = true; + } + } + } + + private Properties loadProperties() throws IOException { + Properties properties = new Properties(); + InputStream stream = getClass().getResourceAsStream(propertiesPath); + try { + properties.load(stream); + } finally { + IOUtils.closeQuietly(stream); + } + return properties; + } + + private String getDomain(Properties properties) { + String domain = properties.getProperty(DOMAIN_PROPERTY); + if (domain == null || domain.isEmpty()) { + throw new IllegalStateException(DOMAIN_PROPERTY + " cannot be empty or null"); + } + return domain; + } + + private String getHosts(Properties properties) { + String host = properties.getProperty(HOSTS_PROPERTY); + if (host == null || host.isEmpty()) { + throw new IllegalStateException(HOSTS_PROPERTY + " cannot be empty or null"); + } + return host; + } + +} diff --git a/src/main/java/fm/last/moji/impl/RenameCommand.java b/src/main/java/fm/last/moji/impl/RenameCommand.java new file mode 100644 index 0000000..eec9ca6 --- /dev/null +++ b/src/main/java/fm/last/moji/impl/RenameCommand.java @@ -0,0 +1,52 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import fm.last.moji.tracker.Tracker; + +class RenameCommand implements MojiCommand { + + final String key; + final String domain; + final String newKey; + + RenameCommand(String key, String domain, String newKey) { + this.key = key; + this.domain = domain; + this.newKey = newKey; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + tracker.rename(key, domain, newKey); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("RenameCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", newKey="); + builder.append(newKey); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/impl/UpdateStorageClassCommand.java b/src/main/java/fm/last/moji/impl/UpdateStorageClassCommand.java new file mode 100644 index 0000000..884057e --- /dev/null +++ b/src/main/java/fm/last/moji/impl/UpdateStorageClassCommand.java @@ -0,0 +1,39 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import java.io.IOException; + +import fm.last.moji.tracker.Tracker; + +class UpdateStorageClassCommand implements MojiCommand { + + final String key; + final String domain; + final String newStorageClass; + + UpdateStorageClassCommand(String key, String domain, String newStorageClass) { + this.key = key; + this.domain = domain; + this.newStorageClass = newStorageClass; + } + + @Override + public void executeWithTracker(Tracker tracker) throws IOException { + tracker.updateStorageClass(key, domain, newStorageClass); + } + +} diff --git a/src/main/java/fm/last/moji/local/DefaultFileNamingStrategy.java b/src/main/java/fm/last/moji/local/DefaultFileNamingStrategy.java new file mode 100644 index 0000000..324aa08 --- /dev/null +++ b/src/main/java/fm/last/moji/local/DefaultFileNamingStrategy.java @@ -0,0 +1,93 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.local; + +import java.io.File; +import java.io.FilenameFilter; + +class DefaultFileNamingStrategy implements LocalFileNamingStrategy { + + private final File baseFolder; + + DefaultFileNamingStrategy(File baseFolder) { + this.baseFolder = baseFolder; + } + + @Override + public String newfileName(String domain, String key) { + return domain + "-" + key + ".dat"; + } + + @Override + public String domainForFileName(String fileName) { + int dashPosition = fileName.indexOf('-'); + String domain = fileName.substring(0, dashPosition); + return domain; + } + + @Override + public String keyForFileName(String fileName) { + int dashPosition = fileName.indexOf('-'); + int periodPosition = fileName.lastIndexOf('.'); + String key = fileName.substring(dashPosition + 1, periodPosition); + return key; + } + + @Override + public File folderForDomain(String domain) { + return baseFolder; + } + + @Override + public FilenameFilter filterForPrefix(String domain, final String keyPrefix) { + return new KeyPrefixFileNameFilter(domain, keyPrefix); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("DefaultFileNamingStrategy [baseFolder="); + builder.append(baseFolder); + builder.append("]"); + return builder.toString(); + } + + private final class KeyPrefixFileNameFilter implements FilenameFilter { + private final String keyPrefix; + private final String domain; + + private KeyPrefixFileNameFilter(String domain, String keyPrefix) { + this.domain = domain; + this.keyPrefix = keyPrefix; + } + + @Override + public boolean accept(File dir, String name) { + if (!baseFolder.equals(dir)) { + return false; + } + if (name.startsWith(".")) { + return false; + } + if (!name.startsWith(domain + "-")) { + return false; + } + String key = keyForFileName(name); + return key.startsWith(keyPrefix); + } + } + +} diff --git a/src/main/java/fm/last/moji/local/LocalFileNamingStrategy.java b/src/main/java/fm/last/moji/local/LocalFileNamingStrategy.java new file mode 100644 index 0000000..6253973 --- /dev/null +++ b/src/main/java/fm/last/moji/local/LocalFileNamingStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.local; + +import java.io.File; +import java.io.FilenameFilter; + +/** + * Used by {@link fm.last.moji.local.LocalFileSystemMoji LocalFileSystemMoji} to generate and resolve local filenames + * for given keys and domains. + */ +public interface LocalFileNamingStrategy { + + String newfileName(String domain, String key); + + String domainForFileName(String fileName); + + String keyForFileName(String fileName); + + File folderForDomain(String domain); + + FilenameFilter filterForPrefix(String domain, String keyPrefix); + +} diff --git a/src/main/java/fm/last/moji/local/LocalFileSystemMoji.java b/src/main/java/fm/last/moji/local/LocalFileSystemMoji.java new file mode 100644 index 0000000..dfbe019 --- /dev/null +++ b/src/main/java/fm/last/moji/local/LocalFileSystemMoji.java @@ -0,0 +1,122 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.local; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFile; + +/** + * A simple {@link fm.last.moji.Moji Moji} implementation that uses the local filesystem for storage. This is intended + * for testing only. + */ +public class LocalFileSystemMoji implements Moji { + + private final File baseFolder; + private final LocalFileNamingStrategy namingStrategy; + private final String domain; + + public LocalFileSystemMoji(File baseFolder, String domain) { + this(baseFolder, domain, new DefaultFileNamingStrategy(baseFolder)); + } + + public LocalFileSystemMoji(File baseFolder, String domain, LocalFileNamingStrategy namingStrategy) { + createBaseFolderIfNeeded(baseFolder); + this.baseFolder = baseFolder; + this.domain = domain; + this.namingStrategy = namingStrategy; + } + + public File getBaseFolder() { + return baseFolder; + } + + public LocalFileNamingStrategy getNamingStrategy() { + return namingStrategy; + } + + public String getDomain() { + return domain; + } + + @Override + public MojiFile getFile(String key) { + return new LocalMojiFile(namingStrategy, baseFolder, domain, key); + } + + @Override + public MojiFile getFile(String key, String storageClass) { + return new LocalMojiFile(namingStrategy, baseFolder, domain, key); + } + + @Override + public void copyToMogile(File source, MojiFile destination) throws IOException { + LocalMojiFile localDestination = (LocalMojiFile) destination; + FileUtils.copyFile(source, localDestination.file); + } + + @Override + public List list(final String keyPrefix) { + File[] files = baseFolder.listFiles(namingStrategy.filterForPrefix(domain, keyPrefix)); + List mojiFiles = new ArrayList(files.length); + for (File file : files) { + String key = namingStrategy.keyForFileName(file.getName()); + mojiFiles.add(new LocalMojiFile(namingStrategy, baseFolder, domain, key)); + } + return null; + } + + @Override + public List list(String keyPrefix, int limit) { + List list = list(keyPrefix); + int count = limit > list.size() ? list.size() : limit; + List mojiFiles = new ArrayList(); + for (int i = 0; i < count; i++) { + mojiFiles.add(list.get(i)); + } + return mojiFiles; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("LocalFileSystemMoji [baseFolder="); + builder.append(baseFolder); + builder.append(", domain="); + builder.append(domain); + builder.append(", namingStrategy="); + builder.append(namingStrategy); + builder.append("]"); + return builder.toString(); + } + + private void createBaseFolderIfNeeded(File baseFolder) { + boolean exists = baseFolder.exists(); + if (!exists) { + boolean mkdirs = baseFolder.mkdirs(); + if (!mkdirs) { + throw new IllegalStateException("Could not create base directory: " + baseFolder.getAbsolutePath()); + } + } + } + +} diff --git a/src/main/java/fm/last/moji/local/LocalMojiFile.java b/src/main/java/fm/last/moji/local/LocalMojiFile.java new file mode 100644 index 0000000..838e18c --- /dev/null +++ b/src/main/java/fm/last/moji/local/LocalMojiFile.java @@ -0,0 +1,136 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.local; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FileUtils; + +import fm.last.moji.MojiFile; + +class LocalMojiFile implements MojiFile { + + private final String domain; + private final File baseDir; + final File file; + private String key; + private final LocalFileNamingStrategy namingStrategy; + + LocalMojiFile(LocalFileNamingStrategy namingStrategy, File baseDir, String domain, String key) { + this.namingStrategy = namingStrategy; + this.baseDir = baseDir; + this.key = key; + this.domain = domain; + file = new File(baseDir, namingStrategy.newfileName(domain, key)); + } + + @Override + public boolean exists() throws IOException { + return file.exists(); + } + + @Override + public void delete() throws IOException { + if (!file.exists()) { + throw new FileNotFoundException(file.getCanonicalPath()); + } + file.delete(); + } + + @Override + public InputStream getInputStream() throws IOException { + if (!file.exists()) { + throw new FileNotFoundException(file.getCanonicalPath()); + } + return new FileInputStream(file); + } + + @Override + public OutputStream getOutputStream() throws IOException { + if (!file.exists()) { + file.createNewFile(); + } + return new FileOutputStream(file); + } + + @Override + public void copyToFile(File destination) throws IOException { + if (!file.exists()) { + throw new FileNotFoundException(file.getCanonicalPath()); + } + FileUtils.copyFile(file, destination); + } + + @Override + public long length() throws IOException { + if (!file.exists()) { + throw new FileNotFoundException(file.getCanonicalPath()); + } + return file.length(); + } + + @Override + public void rename(String newKey) throws IOException { + if (!file.exists()) { + throw new FileNotFoundException(file.getCanonicalPath()); + } + file.renameTo(new File(baseDir, namingStrategy.newfileName(domain, newKey))); + key = newKey; + } + + @Override + public List getPaths() throws IOException { + return Collections.singletonList(file.toURI().toURL()); + } + + @Override + public String getKey() { + return key; + } + + @Override + public String getDomain() { + return domain; + } + + @Override + public void modifyStorageClass(String storageClass) throws IOException { + // ignored + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("LocalMogileFile [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", file="); + builder.append(file); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/spring/SpringMojiBean.java b/src/main/java/fm/last/moji/spring/SpringMojiBean.java new file mode 100644 index 0000000..4c7f638 --- /dev/null +++ b/src/main/java/fm/last/moji/spring/SpringMojiBean.java @@ -0,0 +1,180 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.spring; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.List; +import java.util.Set; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFile; +import fm.last.moji.impl.DefaultMojiFactory; +import fm.last.moji.tracker.impl.InetSocketAddressFactory; +import fm.last.moji.tracker.pool.MultiHostTrackerPool; + +/** + * A {@link fm.last.moji.Moji Moji} delegate that exposes pool properties and is easily configured in Spring. + */ +public class SpringMojiBean implements Moji { + + private final Moji moji; + private final MultiHostTrackerPool poolingTrackerFactory; + + public SpringMojiBean(String addressesCsv, String domain) { + this(addressesCsv, Proxy.NO_PROXY, domain); + } + + public SpringMojiBean(String addressesCsv, Proxy proxy, String domain) { + Set addresses = InetSocketAddressFactory.newAddresses(addressesCsv); + poolingTrackerFactory = new MultiHostTrackerPool(addresses, proxy); + DefaultMojiFactory factory = new DefaultMojiFactory(poolingTrackerFactory, domain); + moji = factory.getInstance(); + } + + @Override + public MojiFile getFile(String key) { + return moji.getFile(key); + } + + @Override + public MojiFile getFile(String key, String storageClass) { + return moji.getFile(key, storageClass); + } + + @Override + public void copyToMogile(File source, MojiFile destination) throws IOException { + moji.copyToMogile(source, destination); + } + + @Override + public List list(String keyPrefix) throws IOException { + return moji.list(keyPrefix); + } + + @Override + public List list(String keyPrefix, int limit) throws IOException { + return moji.list(keyPrefix, limit); + } + + /** + * See: {@link fm.last.moji.tracker.TrackerFactory#getProxy()} + */ + public Proxy getProxy() { + return poolingTrackerFactory.getProxy(); + } + + /** + * See: {@link fm.last.moji.tracker.TrackerFactory#getAddresses()} + */ + public Set getAddresses() { + return poolingTrackerFactory.getAddresses(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#close()} + */ + public void close() throws Exception { + poolingTrackerFactory.close(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxActive()} + */ + public int getMaxActive() { + return poolingTrackerFactory.getMaxActive(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setMaxActive(int)} + */ + public void setMaxActive(int maxActive) { + poolingTrackerFactory.setMaxActive(maxActive); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxWait()} + */ + public long getMaxWait() { + return poolingTrackerFactory.getMaxWait(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setMaxWait(long)} + */ + public void setMaxWait(long maxWait) { + poolingTrackerFactory.setMaxWait(maxWait); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxIdle()} + */ + public int getMaxIdle() { + return poolingTrackerFactory.getMaxIdle(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setMaxIdle(int)} + */ + public void setMaxIdle(int maxIdle) { + poolingTrackerFactory.setMaxIdle(maxIdle); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getTestOnBorrow()} + */ + public boolean getTestOnBorrow() { + return poolingTrackerFactory.getTestOnBorrow(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setTestOnBorrow(boolean)} + */ + public void setTestOnBorrow(boolean testOnBorrow) { + poolingTrackerFactory.setTestOnBorrow(testOnBorrow); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getTestOnReturn()} + */ + public boolean getTestOnReturn() { + return poolingTrackerFactory.getTestOnReturn(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setTestOnReturn(boolean)} + */ + public void setTestOnReturn(boolean testOnReturn) { + poolingTrackerFactory.setTestOnReturn(testOnReturn); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getNumActive()} + */ + public int getNumActive() { + return poolingTrackerFactory.getNumActive(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getNumIdle()} + */ + public int getNumIdle() { + return poolingTrackerFactory.getNumIdle(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/Destination.java b/src/main/java/fm/last/moji/tracker/Destination.java new file mode 100644 index 0000000..b9193d8 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/Destination.java @@ -0,0 +1,60 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +import java.net.URL; + +/** + * Represents a MogileFS remote file location. + */ +public class Destination { + + private final URL path; + private final int devId; + private final int fid; + + public Destination(URL path, int devId, int fid) { + this.path = path; + this.devId = devId; + this.fid = fid; + } + + public URL getPath() { + return path; + } + + public int getDevId() { + return devId; + } + + public int getFid() { + return fid; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Destination [path="); + builder.append(path); + builder.append(", devId="); + builder.append(devId); + builder.append(", fid="); + builder.append(fid); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/KeyExistsAlreadyException.java b/src/main/java/fm/last/moji/tracker/KeyExistsAlreadyException.java new file mode 100644 index 0000000..ade78ae --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/KeyExistsAlreadyException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +/** + * An attempt was made to assign an existing key to a new {@link fm.last.moji.MojiFile}. + */ +public class KeyExistsAlreadyException extends TrackerException { + + private static final long serialVersionUID = 1L; + private final String domain; + private final String key; + + public KeyExistsAlreadyException(String domain, String key) { + super("domain=" + domain + ",key=" + key); + this.domain = domain; + this.key = key; + } + + public String getDomain() { + return domain; + } + + public String getKey() { + return key; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/Tracker.java b/src/main/java/fm/last/moji/tracker/Tracker.java new file mode 100644 index 0000000..f96edc8 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/Tracker.java @@ -0,0 +1,82 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +import java.net.URL; +import java.util.List; + +/** + * Service interface of the MogileFS 'backend'. + * + * @see The Perl MogileFS + * client. + */ +public interface Tracker { + + List getPaths(String key, String domain) throws TrackerException; + + List createOpen(String key, String domain, String storageClass) throws TrackerException; + + void createClose(String key, String domain, Destination destination, long size) throws TrackerException; + + /** + * Delete a key from MogileFS in the given domain. + * + * @param key The key to delete. + * @param domain The domain in which the key resides. + * @throws TrackerException If there was a problem deleting the key. + */ + void delete(String key, String domain) throws TrackerException; + + /** + * Rename file (key) in MogileFS from oldKey to newKey. + * + * @param oldKey The key to rename. + * @param domain The domain in which the old key resides. + * @param domain The new key. + * @throws TrackerException If there was a problem deleting the key. + */ + void rename(String oldKey, String domain, String newKey) throws TrackerException; + + /** + * Update the storage class of a pre-existing file, causing the file to become more or less replicated + * + * @param key The key of the file to modify. + * @param domain The domain in which the key resides. + * @param newStorageClass The new storage class. + * @throws TrackerException If there was a problem updaing the storage class. + */ + void updateStorageClass(String key, String domain, String newStorageClass) throws TrackerException; + + void noop() throws TrackerException; + + /** + * Closes the resources used by this tracker. Pooled implementations may just return the tracker to the pool. + */ + void close(); + + /** + * Get a list of keys matching a given prefix in the target domain. + * + * @param domain The domain in which to perform the key search. + * @param keyPrefix The key prefix to match against. + * @param limit The maximim number of matches to return. + * @return A list of matched keys, or an empty list if there were no matches. + * @throws TrackerException If there was a problem matching. + */ + List list(String domain, String keyPrefix, Integer limit) throws TrackerException; + +} \ No newline at end of file diff --git a/src/main/java/fm/last/moji/tracker/TrackerException.java b/src/main/java/fm/last/moji/tracker/TrackerException.java new file mode 100644 index 0000000..e8befad --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/TrackerException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +import java.io.IOException; + +/** + * General problem when using a {@link fm.last.moji.tracker.Tracker Tracker}. + */ +public class TrackerException extends IOException { + + private static final long serialVersionUID = 1L; + + public TrackerException() { + super(); + } + + public TrackerException(String message, Throwable cause) { + super(message, cause); + } + + public TrackerException(String message) { + super(message); + } + + public TrackerException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/TrackerFactory.java b/src/main/java/fm/last/moji/tracker/TrackerFactory.java new file mode 100644 index 0000000..286cd42 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/TrackerFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Set; + +/** + * Provides usable {@link Tracker} instances for communicating with the MogileFS 'backend'. + */ +public interface TrackerFactory { + + /** + * Gets a new usable {@link Tracker}. + * + * @return A valid tracker. + * @throws TrackerException If there was a problem obtaining a tracker. + */ + Tracker getTracker() throws TrackerException; + + /** + * The host addresses of {@link Tracker Trackers} that this factory may return. + * + * @return A set of host addresses. + */ + Set getAddresses(); + + /** + * The network proxy used by the {@link Tracker Trackers} returned from this factory. + * + * @return The proxy, or {@link Proxy#NO_PROXY NO_PROXY} if no proxy has been set. + */ + Proxy getProxy(); + +} diff --git a/src/main/java/fm/last/moji/tracker/UnknownKeyException.java b/src/main/java/fm/last/moji/tracker/UnknownKeyException.java new file mode 100644 index 0000000..4e33897 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/UnknownKeyException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +/** + * An attempt was made to read from or modify a non-existent {@link fm.last.moji.MojiFile}. + */ +public class UnknownKeyException extends TrackerException { + + private static final long serialVersionUID = 1L; + private final String domain; + private final String key; + + public UnknownKeyException(String domain, String key) { + super("domain=" + domain + ",key=" + key); + this.domain = domain; + this.key = key; + } + + public String getDomain() { + return domain; + } + + public String getKey() { + return key; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/UnknownStorageClassException.java b/src/main/java/fm/last/moji/tracker/UnknownStorageClassException.java new file mode 100644 index 0000000..dcd7bc4 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/UnknownStorageClassException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker; + +/** + * An attempt was made to assign an non-existent storage class to a {@link fm.last.moji.MojiFile}. + */ +public class UnknownStorageClassException extends TrackerException { + + private static final long serialVersionUID = 1L; + + private final String storageClass; + + public UnknownStorageClassException(String storageClass) { + super("storageClass=" + storageClass); + this.storageClass = storageClass; + } + + public String getStorageClass() { + return storageClass; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/AbstractTrackerFactory.java b/src/main/java/fm/last/moji/tracker/impl/AbstractTrackerFactory.java new file mode 100644 index 0000000..6da1c38 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/AbstractTrackerFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; + +/** + * Provides some common {@link fm.last.moji.tracker.TrackerFactory TrackerFactory} functionality. + */ +public class AbstractTrackerFactory { + + private static final Logger log = LoggerFactory.getLogger(AbstractTrackerFactory.class); + + private final Proxy proxy; + + public AbstractTrackerFactory(Proxy proxy) { + this.proxy = proxy; + } + + public Tracker newTracker(InetSocketAddress newAddress) throws TrackerException { + log.debug("new {}()", TrackerImpl.class.getSimpleName()); + Tracker tracker = null; + BufferedReader reader = null; + Writer writer = null; + Socket socket = null; + try { + socket = new Socket(proxy); + log.debug("Connecting to: {}:", newAddress, socket.getPort()); + socket.connect(newAddress); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + RequestHandler requestHandler = new RequestHandler(writer, reader); + tracker = new TrackerImpl(socket, requestHandler); + } catch (IOException e) { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(socket); + throw new TrackerException(e); + } + return tracker; + } + + public Proxy getProxy() { + return proxy; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AbstractTrackerFactory [proxy="); + builder.append(proxy); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/Charsets.java b/src/main/java/fm/last/moji/tracker/impl/Charsets.java new file mode 100644 index 0000000..5c92820 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/Charsets.java @@ -0,0 +1,25 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +enum Charsets { + UTF_8; + + public String value() { + return "UTF-8"; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/CommunicationException.java b/src/main/java/fm/last/moji/tracker/impl/CommunicationException.java new file mode 100644 index 0000000..015f9bc --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/CommunicationException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import fm.last.moji.tracker.TrackerException; + +/** + * Communication problem when using a {@link fm.last.moji.tracker.Tracker Tracker}. + */ +public class CommunicationException extends TrackerException { + + private static final long serialVersionUID = 1L; + + public CommunicationException() { + super(); + } + + CommunicationException(String message, Throwable cause) { + super(message, cause); + } + + CommunicationException(String message) { + super(message); + } + + CommunicationException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/CreateOpenOperation.java b/src/main/java/fm/last/moji/tracker/impl/CreateOpenOperation.java new file mode 100644 index 0000000..b02a330 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/CreateOpenOperation.java @@ -0,0 +1,113 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.ErrorCode.UNKNOWN_CLASS; +import static fm.last.moji.tracker.impl.ErrorCode.UNKNOWN_KEY; +import static fm.last.moji.tracker.impl.ResponseStatus.OK; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.UnknownStorageClassException; + +class CreateOpenOperation { + private final String domain; + private final String key; + private final String storageClass; + private final boolean multipleDestinations; + private final RequestHandler requestHandler; + + private List destinations; + + CreateOpenOperation(RequestHandler requestHandler, String domain, String key, String storageClass, + boolean multipleDestinations) { + this.requestHandler = requestHandler; + this.domain = domain; + this.key = key; + this.storageClass = storageClass; + this.multipleDestinations = multipleDestinations; + destinations = Collections.emptyList(); + } + + public void execute() throws TrackerException { + Request request = buildRequest(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + if (UNKNOWN_CLASS.isContainedInLine(response.getMessage())) { + throw new UnknownStorageClassException(storageClass); + } + if (!UNKNOWN_KEY.isContainedInLine(response.getMessage())) { + throw new TrackerException(response.getMessage()); + } + } else { + extractReturnValues(response); + } + } + + List getDestinations() { + return destinations; + } + + private Request buildRequest() { + Request.Builder builder = new Request.Builder(4).command("create_open").arg("domain", domain).arg("key", key) + .arg("multi_dest", multipleDestinations); + if (storageClass != null && !storageClass.isEmpty()) { + builder.arg("class", storageClass); + } + Request request = builder.build(); + return request; + } + + private void extractReturnValues(Response response) throws TrackerException { + int fid = Integer.parseInt(response.getValue("fid")); + int pathCount = Integer.parseInt(response.getValue("dev_count")); + destinations = new ArrayList(pathCount); + + for (int i = 1; i <= pathCount; i++) { + URL url; + Integer devId; + try { + url = new URL(response.getValue("path_" + i)); + devId = Integer.valueOf(response.getValue("devid_" + i)); + destinations.add(new Destination(url, devId, fid)); + } catch (MalformedURLException e) { + throw new TrackerException(e); + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CreateOpenCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", storageClass="); + builder.append(storageClass); + builder.append(", multipleDestinations="); + builder.append(multipleDestinations); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/ErrorCode.java b/src/main/java/fm/last/moji/tracker/impl/ErrorCode.java new file mode 100644 index 0000000..a2bae48 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/ErrorCode.java @@ -0,0 +1,44 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +enum ErrorCode { + UNKNOWN_KEY("unknown_key"), KEY_EXISTS("key_exists"), UNKNOWN_CLASS("unreg_class", "class_not_found"), NONE_MATCH( + "none_match"); + + private Set messages; + + private ErrorCode(String... messages) { + this.messages = new HashSet(Arrays.asList(messages)); + } + + boolean isContainedInLine(String line) { + if (line == null || line.isEmpty()) { + return false; + } + for (String message : messages) { + if (line.contains(message)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/GetPathsOperation.java b/src/main/java/fm/last/moji/tracker/impl/GetPathsOperation.java new file mode 100644 index 0000000..38ee4eb --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/GetPathsOperation.java @@ -0,0 +1,92 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.ErrorCode.UNKNOWN_KEY; +import static fm.last.moji.tracker.impl.ResponseStatus.OK; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.UnknownKeyException; + +class GetPathsOperation { + + private final String domain; + private final String key; + private final boolean verify; + private final RequestHandler requestHandler; + private List paths; + + GetPathsOperation(RequestHandler requestHandler, String domain, String key, boolean verify) { + this.domain = domain; + this.key = key; + this.verify = verify; + this.requestHandler = requestHandler; + paths = Collections.emptyList(); + } + + public void execute() throws TrackerException { + Request request = new Request.Builder(3).command("get_paths").arg("domain", domain).arg("key", key) + .arg("noverify", !verify).build(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + if (UNKNOWN_KEY.isContainedInLine(response.getMessage())) { + throw new UnknownKeyException(domain, key); + } + throw new TrackerException(response.getMessage()); + } else { + paths = extractReturnValue(response); + } + } + + List getPaths() { + return paths; + } + + private List extractReturnValue(Response response) throws TrackerException { + int pathCount = Integer.parseInt(response.getValue("paths")); + List urls = new ArrayList(pathCount); + for (int i = 1; i <= pathCount; i++) { + URL url; + try { + url = new URL(response.getValue("path" + i)); + urls.add(url); + } catch (MalformedURLException e) { + throw new TrackerException(e); + } + } + return urls; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("GetPathsCommand [domain="); + builder.append(domain); + builder.append(", key="); + builder.append(key); + builder.append(", verify="); + builder.append(verify); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/InetSocketAddressFactory.java b/src/main/java/fm/last/moji/tracker/impl/InetSocketAddressFactory.java new file mode 100644 index 0000000..3e881bd --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/InetSocketAddressFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.HashSet; +import java.util.Set; + +public class InetSocketAddressFactory { + + private InetSocketAddressFactory() { + } + + public static Set newAddresses(String addressesCsv) { + Set addresses = new HashSet(); + for (String addressElement : addressesCsv.split(",")) { + addresses.add(addressElement.trim()); + } + Set socketAddresses = new HashSet(); + for (String address : addresses) { + InetSocketAddress socketAddress = newAddress(address); + socketAddresses.add(socketAddress); + } + return socketAddresses; + } + + public static InetSocketAddress newAddress(String addressString) { + String[] parts = addressString.split(":"); + String host = parts[0]; + int port = Integer.valueOf(parts[1]); + InetAddress address; + try { + address = InetAddress.getByName(host); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid ':': '" + addressString + "'", e); + } + InetSocketAddress socketAddress = new InetSocketAddress(address, port); + return socketAddress; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/ListKeysOperation.java b/src/main/java/fm/last/moji/tracker/impl/ListKeysOperation.java new file mode 100644 index 0000000..3313e84 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/ListKeysOperation.java @@ -0,0 +1,91 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.ErrorCode.NONE_MATCH; +import static fm.last.moji.tracker.impl.ResponseStatus.OK; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import fm.last.moji.tracker.TrackerException; + +class ListKeysOperation { + + private final RequestHandler requestHandler; + private final String domain; + private final String keyPrefix; + private final Integer limit; + private List keys; + + ListKeysOperation(RequestHandler requestHandler, String domain, String keyPrefix, Integer limit) { + this.requestHandler = requestHandler; + this.domain = domain; + this.keyPrefix = keyPrefix; + this.limit = limit; + keys = Collections.emptyList(); + } + + void execute() throws TrackerException { + Request request = buildRequest(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + if (!NONE_MATCH.isContainedInLine(response.getMessage())) { + throw new TrackerException(response.getMessage()); + } + } else { + keys = extractReturnValue(response); + } + } + + List getKeys() { + return keys; + } + + private Request buildRequest() { + Request.Builder builder = new Request.Builder(3).command("list_keys").arg("domain", domain) + .arg("prefix", keyPrefix); + if (limit != null) { + builder.arg("limit", limit); + } + Request request = builder.build(); + return request; + } + + private List extractReturnValue(Response response) { + int keyCount = Integer.parseInt(response.getValue("key_count")); + List keys = new ArrayList(); + for (int i = 1; i <= keyCount; i++) { + keys.add(response.getValue("key_" + i)); + } + return keys; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ListKeysOperation [domain="); + builder.append(domain); + builder.append(", keyPrefix="); + builder.append(keyPrefix); + builder.append(", limit="); + builder.append(limit); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/Request.java b/src/main/java/fm/last/moji/tracker/impl/Request.java new file mode 100644 index 0000000..169e35c --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/Request.java @@ -0,0 +1,132 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.Charsets.UTF_8; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Request { + + private static final Logger log = LoggerFactory.getLogger(Request.class); + + private final String command; + private final Map arguments; + + private Request(Builder builder) { + command = builder.command; + arguments = builder.arguments; + } + + String getCommand() { + return command; + } + + Map getArguments() { + return arguments; + } + + void writeTo(Writer writer) throws IOException { + StringBuilder wire = new StringBuilder(); + wire.append(command); + wire.append(' '); + boolean first = true; + for (Entry entry : arguments.entrySet()) { + if (first) { + first = false; + } else { + wire.append('&'); + } + try { + wire.append(URLEncoder.encode(entry.getKey(), UTF_8.value())); + wire.append('='); + wire.append(URLEncoder.encode(entry.getValue(), UTF_8.value())); + } catch (UnsupportedEncodingException ignored) { + } + } + wire.append('\r'); + wire.append('\n'); + log.debug("Sent: {}", wire); + writer.write(wire.toString()); + writer.flush(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Request [command="); + builder.append(command); + builder.append(", arguments="); + builder.append(arguments); + builder.append("]"); + return builder.toString(); + } + + static class Builder { + + private final Map arguments; + private String command; + + Builder(int expectedSize) { + arguments = new HashMap(expectedSize); + } + + Builder command(String command) { + this.command = command; + return this; + } + + Builder arg(String key, String value) { + arguments.put(key, value); + return this; + } + + Builder arg(String key, int value) { + arguments.put(key, Integer.toString(value)); + return this; + } + + Builder arg(String key, long value) { + arguments.put(key, Long.toString(value)); + return this; + } + + Builder arg(String key, boolean value) { + arguments.put(key, value ? "1" : "0"); + return this; + } + + Builder arg(String key, URL value) { + arguments.put(key, value.toString()); + return this; + } + + Request build() { + return new Request(this); + } + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/RequestHandler.java b/src/main/java/fm/last/moji/tracker/impl/RequestHandler.java new file mode 100644 index 0000000..37c8a7a --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/RequestHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Writer; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.TrackerException; + +class RequestHandler { + + private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); + + private final Writer writer; + private final BufferedReader reader; + + RequestHandler(Writer writer, BufferedReader reader) { + this.writer = writer; + this.reader = reader; + } + + Response performRequest(Request request) throws CommunicationException { + Response response = null; + String line = null; + + try { + log.debug("{}", request); + request.writeTo(writer); + line = reader.readLine(); + log.debug("Read: {}", line); + response = createResponseFromLine(line); + log.debug("{}", response); + } catch (IOException e) { + throw new CommunicationException(e); + } + return response; + } + + void close() { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(writer); + } + + private Response createResponseFromLine(String line) throws TrackerException { + if (line == null) { + throw new TrackerException("Empty response from tracker"); + } + int firstSpace = line.indexOf(' '); + if (firstSpace < 0) { + throw new TrackerException("Invalid response from tracker: '" + line + "'"); + } + ResponseStatus status = ResponseStatus.valueOfCode(line.substring(0, firstSpace)); + if (status == null) { + throw new TrackerException("Invalid response from tracker: '" + line + "'"); + } + String payload = line.substring(firstSpace + 1); + Response response = new Response(status, payload); + return response; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/Response.java b/src/main/java/fm/last/moji/tracker/impl/Response.java new file mode 100644 index 0000000..482003d --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/Response.java @@ -0,0 +1,95 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.Charsets.UTF_8; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Response { + + private static final Logger log = LoggerFactory.getLogger(Response.class); + + private final Map values; + private final ResponseStatus status; + private final String message; + + Response(ResponseStatus status, String payload) { + this.status = status; + if (status == ResponseStatus.OK) { + values = decodePayload(payload); + message = null; + } else { + message = payload; + values = Collections.emptyMap(); + } + } + + ResponseStatus getStatus() { + return status; + } + + String getValue(String key) { + return values.get(key); + } + + String getMessage() { + return message; + } + + private Map decodePayload(String encoded) { + HashMap map = new HashMap(); + try { + if (encoded == null || encoded.length() == 0) { + return map; + } + String[] parts = encoded.split("&"); + for (String part : parts) { + String[] pair = part.split("="); + if (pair.length != 2) { + log.error("Poorly encoded string: {} ", encoded); + continue; + } + map.put(pair[0], URLDecoder.decode(pair[1], UTF_8.value())); + } + return map; + } catch (UnsupportedEncodingException e) { + log.error("Problem decoding response", e); + return null; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Response [status="); + builder.append(status); + builder.append(", values="); + builder.append(values); + builder.append(", message="); + builder.append(message); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/ResponseStatus.java b/src/main/java/fm/last/moji/tracker/impl/ResponseStatus.java new file mode 100644 index 0000000..08b16bc --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/ResponseStatus.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.util.HashMap; +import java.util.Map; + +enum ResponseStatus { + OK("OK"), ERROR("ERR"); + + private static Map wireCodeToVal = new HashMap(); + + static { + for (ResponseStatus status : ResponseStatus.values()) { + wireCodeToVal.put(status.value(), status); + } + } + + private final String code; + + private ResponseStatus(String code) { + this.code = code; + } + + String value() { + return code; + } + + static ResponseStatus valueOfCode(String code) { + return wireCodeToVal.get(code); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/SingleHostTrackerFactory.java b/src/main/java/fm/last/moji/tracker/impl/SingleHostTrackerFactory.java new file mode 100644 index 0000000..c4682ca --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/SingleHostTrackerFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Collections; +import java.util.Set; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.TrackerFactory; + +/** + * Single host tracker factory that creates a new connection on each {@link #getTracker()} request. Do not use this + * directly - only as a building block for pooling {@link TrackerFactory TrackerFactorys}. + */ +public class SingleHostTrackerFactory implements TrackerFactory { + + private final AbstractTrackerFactory delegateFactory; + private final InetSocketAddress address; + + /** + * Creates a tracker factory for the given host address and use the supplied network proxy. + * + * @param addresses Tracker host address + * @param proxy Network proxy - use Proxy.NO_PROXY if a proxy isn't needed. + */ + public SingleHostTrackerFactory(InetSocketAddress address, Proxy proxy) { + delegateFactory = new AbstractTrackerFactory(proxy); + this.address = address; + } + + @Override + public Tracker getTracker() throws TrackerException { + return delegateFactory.newTracker(address); + } + + @Override + public Set getAddresses() { + return Collections.singleton(address); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("SingletonTrackerFactory [address="); + builder.append(address); + builder.append(", proxy="); + builder.append(getProxy()); + builder.append("]"); + return builder.toString(); + } + + @Override + public Proxy getProxy() { + return delegateFactory.getProxy(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/impl/TrackerImpl.java b/src/main/java/fm/last/moji/tracker/impl/TrackerImpl.java new file mode 100644 index 0000000..eddc7f1 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/impl/TrackerImpl.java @@ -0,0 +1,169 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static fm.last.moji.tracker.impl.ErrorCode.KEY_EXISTS; +import static fm.last.moji.tracker.impl.ErrorCode.UNKNOWN_CLASS; +import static fm.last.moji.tracker.impl.ErrorCode.UNKNOWN_KEY; +import static fm.last.moji.tracker.impl.ResponseStatus.OK; + +import java.net.Socket; +import java.net.URL; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.KeyExistsAlreadyException; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.UnknownKeyException; +import fm.last.moji.tracker.UnknownStorageClassException; +import fm.last.moji.tracker.impl.Request.Builder; + +class TrackerImpl implements Tracker { + + private static final Logger log = LoggerFactory.getLogger(TrackerImpl.class); + + private final Socket socket; + private final RequestHandler requestHandler; + + public TrackerImpl(Socket socket, RequestHandler requestHandler) { + this.socket = socket; + this.requestHandler = requestHandler; + } + + @Override + public List getPaths(String key, String domain) throws TrackerException { + GetPathsOperation operation = new GetPathsOperation(requestHandler, domain, key, false); + operation.execute(); + return operation.getPaths(); + } + + @Override + public List createOpen(String key, String domain, String storageClass) throws TrackerException { + CreateOpenOperation operation = new CreateOpenOperation(requestHandler, domain, key, storageClass, true); + operation.execute(); + return operation.getDestinations(); + } + + @Override + public List list(String domain, String keyPrefix, Integer limit) throws TrackerException { + ListKeysOperation operation = new ListKeysOperation(requestHandler, domain, keyPrefix, limit); + operation.execute(); + return operation.getKeys(); + } + + @Override + public void createClose(String key, String domain, Destination destination, long size) throws TrackerException { + Request request = new Builder(6).command("create_close").arg("domain", domain).arg("key", key) + .arg("devid", destination.getDevId()).arg("path", destination.getPath()).arg("fid", destination.getFid()) + .arg("size", size).build(); + Response response = requestHandler.performRequest(request); + handleGeneralResponseError(response); + } + + @Override + public void delete(String key, String domain) throws TrackerException { + Request request = new Request.Builder(2).command("delete").arg("domain", domain).arg("key", key).build(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + String message = response.getMessage(); + handleUnknownKeyException(key, domain, message); + throw new TrackerException(message); + } + } + + @Override + public void rename(String fromKey, String domain, String toKey) throws TrackerException { + Request request = new Request.Builder(3).command("rename").arg("domain", domain).arg("from_key", fromKey) + .arg("to_key", toKey).build(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + String message = response.getMessage(); + handleUnknownKeyException(fromKey, domain, message); + handleKeyAlreadyExists(domain, toKey, message); + throw new TrackerException(message); + } + } + + @Override + public void updateStorageClass(String key, String domain, String newStorageClass) throws TrackerException { + Request request = new Request.Builder(3).command("updateclass").arg("domain", domain).arg("key", key) + .arg("class", newStorageClass).build(); + Response response = requestHandler.performRequest(request); + if (response.getStatus() != OK) { + String message = response.getMessage(); + handleUnknownKeyException(key, domain, message); + handleUnknownStorageClass(newStorageClass, message); + throw new TrackerException(message); + } + } + + @Override + public void noop() throws TrackerException { + Request request = new Request.Builder(0).command("noop").build(); + Response response = requestHandler.performRequest(request); + handleGeneralResponseError(response); + } + + @Override + public void close() { + if (requestHandler != null) { + requestHandler.close(); + } + IOUtils.closeQuietly(socket); + log.debug("Closed"); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TrackerImpl [socket="); + builder.append(socket); + builder.append(", requestHandler="); + builder.append(requestHandler); + builder.append("]"); + return builder.toString(); + } + + private void handleUnknownStorageClass(String storageClass, String message) throws UnknownStorageClassException { + if (UNKNOWN_CLASS.isContainedInLine(message)) { + throw new UnknownStorageClassException(storageClass); + } + } + + private void handleKeyAlreadyExists(String domain, String key, String message) throws KeyExistsAlreadyException { + if (KEY_EXISTS.isContainedInLine(message)) { + throw new KeyExistsAlreadyException(domain, key); + } + } + + private void handleUnknownKeyException(String key, String domain, String message) throws UnknownKeyException { + if (UNKNOWN_KEY.isContainedInLine(message)) { + throw new UnknownKeyException(domain, key); + } + } + + private void handleGeneralResponseError(Response response) throws TrackerException { + if (response.getStatus() != OK) { + throw new TrackerException(response.getMessage()); + } + } + +} diff --git a/src/main/java/fm/last/moji/tracker/pool/BorrowedTracker.java b/src/main/java/fm/last/moji/tracker/pool/BorrowedTracker.java new file mode 100644 index 0000000..5a54840 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/pool/BorrowedTracker.java @@ -0,0 +1,187 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.pool; + +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.pool.KeyedObjectPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.impl.CommunicationException; + +class BorrowedTracker implements Tracker { + + private static final Logger log = LoggerFactory.getLogger(BorrowedTracker.class); + + private final Tracker delegate; + private final KeyedObjectPool pool; + private final ManagedTrackerHost host; + private CommunicationException lastException; + + BorrowedTracker(ManagedTrackerHost host, Tracker delegate, KeyedObjectPool pool) { + this.delegate = delegate; + this.host = host; + this.pool = pool; + } + + @Override + public List getPaths(String key, String domain) throws TrackerException { + List paths = Collections.emptyList(); + try { + paths = delegate.getPaths(key, domain); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + return paths; + } + + @Override + public List createOpen(String key, String domain, String storageClass) throws TrackerException { + List destinations = Collections.emptyList(); + try { + destinations = delegate.createOpen(key, domain, storageClass); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + return destinations; + } + + @Override + public void createClose(String key, String domain, Destination destination, long size) throws TrackerException { + try { + delegate.createClose(key, domain, destination, size); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + } + + @Override + public void delete(String key, String domain) throws TrackerException { + try { + delegate.delete(key, domain); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + } + + @Override + public void rename(String key, String domain, String newKey) throws TrackerException { + try { + delegate.rename(key, domain, newKey); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + } + + @Override + public void updateStorageClass(String key, String domain, String newStorageClass) throws TrackerException { + try { + delegate.updateStorageClass(key, domain, newStorageClass); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + } + + @Override + public void noop() throws TrackerException { + try { + delegate.noop(); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + } + + @Override + public List list(String domain, String keyPrefix, Integer limit) throws TrackerException { + List keys = Collections.emptyList(); + try { + keys = delegate.list(domain, keyPrefix, limit); + host.markSuccess(); + } catch (CommunicationException e) { + lastException = e; + throw e; + } + return keys; + } + + @Override + public void close() { + try { + if (lastException != null) { + log.debug("Invalidating: {}", lastException); + try { + pool.invalidateObject(host, this); + } finally { + delegate.close(); + } + } else { + log.debug("Returning to pool"); + pool.returnObject(host, this); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + void reallyClose() { + log.debug("Really closing"); + delegate.close(); + } + + CommunicationException getLastException() { + return lastException; + } + + ManagedTrackerHost getHost() { + return host; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("BorrowedTracker [host="); + builder.append(host); + builder.append(", lastException="); + builder.append(lastException); + builder.append(", delegate="); + builder.append(delegate); + builder.append(", pool="); + builder.append(pool); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/src/main/java/fm/last/moji/tracker/pool/HostPriorityComparator.java b/src/main/java/fm/last/moji/tracker/pool/HostPriorityComparator.java new file mode 100644 index 0000000..af408a3 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/pool/HostPriorityComparator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.pool; + +import java.io.Serializable; +import java.util.Comparator; + +class HostPriorityComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 1L; + + @Override + public int compare(ManagedTrackerHost a, ManagedTrackerHost b) { + long failTime1 = a.getLastFailed(); + long failTime2 = b.getLastFailed(); + if (failTime1 == failTime2) { + // they both failed at the same time (or not at all) so we just want to + // priotitise the least recently used. + long useTime1 = a.getLastUsed(); + long useTime2 = b.getLastUsed(); + if (useTime1 == useTime2) { + return 0; + } else if (useTime1 > useTime2) { + // 'a' was used more recently than 'b' - reduce the priority and + // chose 'b' before it + return -1; + } + return 1; + } else if (failTime1 < failTime2) { + // 'b' failed more recently - so we prioritise 'a' in the hopes that it + // has had longer to recover + return 1; + } + // 'a' failed more recently - reduce it's priority + return -1; + } + +} diff --git a/src/main/java/fm/last/moji/tracker/pool/ManagedTrackerHost.java b/src/main/java/fm/last/moji/tracker/pool/ManagedTrackerHost.java new file mode 100644 index 0000000..5bc2a7c --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/pool/ManagedTrackerHost.java @@ -0,0 +1,155 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.pool; + +import java.net.InetSocketAddress; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the status of a given tracker host. Knows when it last failed, when the last successful request occurred. + */ +public class ManagedTrackerHost { + + private static final Logger log = LoggerFactory.getLogger(ManagedTrackerHost.class); + + private static final int ONE_MINUTE_IN_MS = 60000; + + private final AtomicLong lastUsed = new AtomicLong(); + private final AtomicLong lastFailed = new AtomicLong(); + private final InetSocketAddress address; + private Timer resetTimer; + private ResetTask resetTask; + + ManagedTrackerHost(InetSocketAddress address) { + this.address = address; + } + + /** + * The address of the host that this object manages. + * + * @return Host address + */ + public InetSocketAddress getAddress() { + lastUsed.set(System.currentTimeMillis()); + return address; + } + + /** + * The time when this host was last used. + * + * @return Date/Time formatted string + */ + public String getLastUsedTime() { + return formatTime(getLastUsed()); + } + + /** + * The time when an operation on this host last failed. + * + * @return Date/Time formatted string + */ + public String getLastFailedTime() { + return formatTime(getLastFailed()); + } + + long getLastUsed() { + return lastUsed.get(); + } + + long getLastFailed() { + return lastFailed.get(); + } + + void markAsFailed() { + lastFailed.set(System.currentTimeMillis()); + synchronized (resetTimer) { + if (resetTask != null) { + resetTask.cancel(); + } + log.debug("Scheduling reset of {} in {}ms", address, ONE_MINUTE_IN_MS); + resetTimer.schedule(new ResetTask(), ONE_MINUTE_IN_MS); + } + } + + void markSuccess() { + lastFailed.set(0); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (address == null ? 0 : address.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ManagedTrackerHost)) { + return false; + } + ManagedTrackerHost other = (ManagedTrackerHost) obj; + if (address == null) { + if (other.address != null) { + return false; + } + } else if (!address.equals(other.address)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ManagedTrackerAddress [address="); + builder.append(address); + builder.append(", lastUsed="); + builder.append(formatTime(lastUsed.get())); + builder.append(", lastFailed="); + builder.append(formatTime(lastFailed.get())); + builder.append("]"); + return builder.toString(); + } + + private String formatTime(long time) { + if (time == 0L) { + return ""; + } + return new Date(time).toString(); + } + + private final class ResetTask extends TimerTask { + @Override + public void run() { + lastFailed.set(0); + log.debug("Reset failure monitor for {}", address); + } + } + +} diff --git a/src/main/java/fm/last/moji/tracker/pool/MultiHostTrackerPool.java b/src/main/java/fm/last/moji/tracker/pool/MultiHostTrackerPool.java new file mode 100644 index 0000000..def01c1 --- /dev/null +++ b/src/main/java/fm/last/moji/tracker/pool/MultiHostTrackerPool.java @@ -0,0 +1,246 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.pool; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.pool.BaseKeyedPoolableObjectFactory; +import org.apache.commons.pool.KeyedPoolableObjectFactory; +import org.apache.commons.pool.impl.GenericKeyedObjectPool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.TrackerFactory; +import fm.last.moji.tracker.impl.AbstractTrackerFactory; + +/** + * {@link fm.last.moji.tracker.TrackerFactory TrackerFactory} implementation that provides a + * {@link fm.last.moji.tracker.Tracker Tracker} connection pool that can distribute requests across many hosts. + */ +public class MultiHostTrackerPool implements TrackerFactory { + + private static final Logger log = LoggerFactory.getLogger(MultiHostTrackerPool.class); + private static final HostPriorityComparator PRIORITY_COMPARATOR = new HostPriorityComparator(); + + private final Proxy proxy; + private final GenericKeyedObjectPool pool; + private final List managedHosts; + + /** + * Creates a tracker pool for the given host addresses and use the supplied network proxy. + * + * @param addresses Tracker host addresses + * @param proxy Network proxy - use Proxy.NO_PROXY if a proxy isn't needed. + */ + public MultiHostTrackerPool(Set addresses, Proxy proxy) { + this.proxy = proxy; + managedHosts = new ArrayList(); + for (InetSocketAddress address : addresses) { + managedHosts.add(new ManagedTrackerHost(address)); + } + KeyedPoolableObjectFactory objectFactory = new BorrowedTrackerObjectPoolFactory(); + pool = new GenericKeyedObjectPool(objectFactory); + log.debug("Pool created"); + } + + @Override + public Tracker getTracker() throws TrackerException { + ManagedTrackerHost managedHost = nextHost(); + Tracker tracker = null; + try { + tracker = (Tracker) pool.borrowObject(managedHost); + } catch (Exception e) { + throw new TrackerException(e); + } + return tracker; + } + + @Override + public Set getAddresses() { + Set addresses = new HashSet(); + for (ManagedTrackerHost host : managedHosts) { + addresses.add(host.getAddress()); + } + return Collections.unmodifiableSet(new HashSet(addresses)); + } + + @Override + public Proxy getProxy() { + return proxy; + } + + /** + * Returns the status of the hosts managed by this pool. + * + * @return A list of hosts statuses. + */ + public List getManagedHosts() { + return managedHosts; + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setMaxActive(int)} + */ + public void setMaxActive(int maxActive) { + pool.setMaxActive(maxActive); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxWait()} + */ + public long getMaxWait() { + return pool.getMaxWait(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setMaxWait(long)} + */ + public void setMaxWait(long maxWait) { + pool.setMaxWait(maxWait); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxIdle()} + */ + public void setMaxIdle(int maxIdle) { + pool.setMaxIdle(maxIdle); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getTestOnBorrow()} + */ + public boolean getTestOnBorrow() { + return pool.getTestOnBorrow(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setTestOnBorrow(boolean)} + */ + public void setTestOnBorrow(boolean testOnBorrow) { + pool.setTestOnBorrow(testOnBorrow); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getTestOnReturn()} + */ + public boolean getTestOnReturn() { + return pool.getTestOnReturn(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#setTestOnReturn(boolean)} + */ + public void setTestOnReturn(boolean testOnReturn) { + pool.setTestOnReturn(testOnReturn); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getNumActive()} + */ + public int getNumActive() { + return pool.getNumActive(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getNumIdle()} + */ + public int getNumIdle() { + return pool.getNumIdle(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxActive()} + */ + public int getMaxActive() { + return pool.getMaxActive(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#getMaxIdle()} + */ + public int getMaxIdle() { + return pool.getMaxIdle(); + } + + /** + * See: {@link org.apache.commons.pool.impl.GenericKeyedObjectPool#close()} + */ + public void close() throws Exception { + pool.close(); + } + + private ManagedTrackerHost nextHost() throws TrackerException { + ManagedTrackerHost managedHost = null; + synchronized (managedHosts) { + Collections.sort(managedHosts, PRIORITY_COMPARATOR); + managedHost = managedHosts.get(managedHosts.size() - 1); + } + return managedHost; + } + + private class BorrowedTrackerObjectPoolFactory extends BaseKeyedPoolableObjectFactory { + + private final AbstractTrackerFactory delegateFactory; + + BorrowedTrackerObjectPoolFactory() { + delegateFactory = new AbstractTrackerFactory(proxy); + } + + @Override + public Object makeObject(Object key) throws Exception { + ManagedTrackerHost host = (ManagedTrackerHost) key; + Tracker delegate = delegateFactory.newTracker(host.getAddress()); + BorrowedTracker borrowedTracker = new BorrowedTracker(host, delegate, pool); + log.debug("Requested new tracker instance: {}", key); + return borrowedTracker; + } + + @Override + public void destroyObject(Object key, Object obj) throws Exception { + BorrowedTracker borrowed = (BorrowedTracker) obj; + if (borrowed.getLastException() != null) { + log.debug("Error occurred on tracker: {}", borrowed.getLastException().getMessage()); + borrowed.getHost().markAsFailed(); + } + log.debug("Destroying {}", borrowed); + borrowed.reallyClose(); + } + + @Override + public boolean validateObject(Object key, Object obj) { + BorrowedTracker borrowed = (BorrowedTracker) obj; + log.debug("Validating {}", borrowed); + try { + borrowed.noop(); + } catch (TrackerException e) { + // returning false will result in a destroyObject invocation + // The address will then be marked out of service + return false; + } + return true; + } + + } + +} diff --git a/src/main/resources/spring/moji-context.xml b/src/main/resources/spring/moji-context.xml new file mode 100644 index 0000000..2f25d06 --- /dev/null +++ b/src/main/resources/spring/moji-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/data/fileOfKnownSize.dat b/src/test/data/fileOfKnownSize.dat new file mode 100644 index 0000000000000000000000000000000000000000..7d1f19ef2ac09cd107c62f53fcd6d1de279d3a69 GIT binary patch literal 3832 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 TxnBL-BAV$6^BcRH literal 0 HcmV?d00001 diff --git a/src/test/java/fm/last/moji/FakeMogileFsServer.java b/src/test/java/fm/last/moji/FakeMogileFsServer.java new file mode 100644 index 0000000..3e4fae0 --- /dev/null +++ b/src/test/java/fm/last/moji/FakeMogileFsServer.java @@ -0,0 +1,154 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.junit.Ignore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Ignore +public class FakeMogileFsServer { + + private static final Logger log = LoggerFactory.getLogger(FakeMogileFsServer.class); + + private ServerSocket trackerSocket; + private Thread trackerThread; + + FakeMogileFsServer(final Builder builder) throws Exception { + startTracker(builder); + } + + public String getAddressAsString() { + return trackerSocket.getInetAddress().getHostName() + ":" + trackerSocket.getLocalPort(); + } + + public InetAddress getInetAddress() { + return trackerSocket.getInetAddress(); + } + + public InetSocketAddress getInetSocketAddress() { + return new InetSocketAddress(trackerSocket.getInetAddress().getHostName(), trackerSocket.getLocalPort()); + } + + public int getPort() { + return trackerSocket.getLocalPort(); + } + + public void close() throws Exception { + log.info("Closing"); + try { + trackerThread.interrupt(); + } finally { + trackerSocket.close(); + } + } + + private void startTracker(final Builder builder) throws IOException { + trackerSocket = new ServerSocket(0); + trackerThread = new TrackerServer(builder.conversation); + trackerThread.start(); + log.info("Tracker server running on: {}", getAddressAsString()); + } + + private final class TrackerServer extends Thread { + private final List conversation; + + private TrackerServer(List conversation) { + this.conversation = conversation; + } + + @Override + public void run() { + Socket accept = null; + BufferedReader reader = null; + OutputStreamWriter writer = null; + try { + accept = trackerSocket.accept(); + writer = new OutputStreamWriter(accept.getOutputStream()); + reader = new BufferedReader(new InputStreamReader(accept.getInputStream())); + + for (Stanza stanza : conversation) { + String line = reader.readLine(); + for (String element : stanza.responses) { + if (!line.contains(element)) { + return; + } + } + writer.write(stanza.request); + writer.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(accept); + } + } + } + + public static class Builder { + + private final List conversation; + private Stanza current; + + public Builder() { + conversation = new ArrayList(); + } + + public Builder whenRequestContains(String... strings) { + current = new Stanza(); + current.responses = new HashSet(Arrays.asList(strings)); + return this; + } + + public Builder thenRespond(String string) { + current.request = string; + conversation.add(current); + current = null; + return this; + } + + public FakeMogileFsServer build() throws Exception { + return new FakeMogileFsServer(this); + } + + } + + private static class Stanza { + private String request; + private Set responses; + + private Stanza() { + } + } + +} diff --git a/src/test/java/fm/last/moji/MojiInstantiationTest.java b/src/test/java/fm/last/moji/MojiInstantiationTest.java new file mode 100644 index 0000000..d3cc2dd --- /dev/null +++ b/src/test/java/fm/last/moji/MojiInstantiationTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji; + +import java.net.Proxy; + +import org.junit.Test; + +import fm.last.moji.impl.DefaultMojiFactory; +import fm.last.moji.tracker.TrackerFactory; +import fm.last.moji.tracker.impl.SingleHostTrackerFactory; + +public class MojiInstantiationTest { + + @Test(timeout = 2000) + public void delete() throws Exception { + FakeMogileFsServer server = null; + try { + FakeMogileFsServer.Builder builder = new FakeMogileFsServer.Builder(); + builder.whenRequestContains("delete ", "key=myKey", "domain=myDomain").thenRespond("OK "); + server = builder.build(); + TrackerFactory trackerFactory = new SingleHostTrackerFactory(server.getInetSocketAddress(), Proxy.NO_PROXY); + DefaultMojiFactory mojiFactory = new DefaultMojiFactory(trackerFactory, "myDomain"); + Moji moji = mojiFactory.getInstance(); + MojiFile file = moji.getFile("myKey"); + file.delete(); + } finally { + server.close(); + } + } + +} diff --git a/src/test/java/fm/last/moji/impl/DeleteCommandTest.java b/src/test/java/fm/last/moji/impl/DeleteCommandTest.java new file mode 100644 index 0000000..4c4e27b --- /dev/null +++ b/src/test/java/fm/last/moji/impl/DeleteCommandTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Tracker; + +@RunWith(MockitoJUnitRunner.class) +public class DeleteCommandTest { + + @Mock + private Tracker mockTracker; + private DeleteCommand command; + + @Before + public void init() { + command = new DeleteCommand("key", "domain"); + } + + @Test + public void delegatesToTracker() throws Exception { + command.executeWithTracker(mockTracker); + verify(mockTracker).delete("key", "domain"); + } + +} diff --git a/src/test/java/fm/last/moji/impl/ExecutorTest.java b/src/test/java/fm/last/moji/impl/ExecutorTest.java new file mode 100644 index 0000000..b836be0 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/ExecutorTest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.TrackerFactory; +import fm.last.moji.tracker.UnknownKeyException; + +@RunWith(MockitoJUnitRunner.class) +public class ExecutorTest { + + @Mock + private Tracker mockTracker; + @Mock + private TrackerFactory mockFactory; + @Mock + private MojiCommand mockCommand; + @Mock + private InetSocketAddress mockAddress; + + private Executor executor; + + @Before + public void init() throws TrackerException { + when(mockFactory.getTracker()).thenReturn(mockTracker); + when(mockFactory.getAddresses()).thenReturn(Collections.singleton(mockAddress)); + executor = new Executor(mockFactory); + } + + @Test + public void executeCommandOk() throws Exception { + executor.executeCommand(mockCommand); + verify(mockFactory).getTracker(); + verify(mockCommand).executeWithTracker(mockTracker); + verify(mockTracker).close(); + } + + @Test(expected = UnknownKeyException.class) + public void trackerClosedOnUnknownKeyException() throws Exception { + UnknownKeyException e = new UnknownKeyException("domain", "key"); + doThrow(e).when(mockCommand).executeWithTracker(mockTracker); + + executor.executeCommand(mockCommand); + verify(mockTracker).close(); + } + + @Test(expected = IOException.class) + public void trackerClosedOnIOException() throws Exception { + IOException e = new IOException(); + doThrow(e).when(mockCommand).executeWithTracker(mockTracker); + + executor.executeCommand(mockCommand); + verify(mockTracker).close(); + } + +} diff --git a/src/test/java/fm/last/moji/impl/ExistsCommandTest.java b/src/test/java/fm/last/moji/impl/ExistsCommandTest.java new file mode 100644 index 0000000..3476e75 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/ExistsCommandTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.UnknownKeyException; + +@RunWith(MockitoJUnitRunner.class) +public class ExistsCommandTest { + + @Mock + private Tracker mockTracker; + private ExistsCommand command; + + @Before + public void init() { + command = new ExistsCommand("key", "domain"); + } + + @Test + public void onePath() throws Exception { + List paths = Collections.singletonList(new URL("http://www.last.fm")); + when(mockTracker.getPaths("key", "domain")).thenReturn(paths); + command.executeWithTracker(mockTracker); + assertTrue(command.getExists()); + } + + @Test + public void zeroPaths() throws Exception { + List paths = Collections.emptyList(); + when(mockTracker.getPaths("key", "domain")).thenReturn(paths); + command.executeWithTracker(mockTracker); + assertFalse(command.getExists()); + } + + @Test + public void unknownKeyException() throws Exception { + UnknownKeyException e = new UnknownKeyException("key", "domain"); + when(mockTracker.getPaths("key", "domain")).thenThrow(e); + command.executeWithTracker(mockTracker); + assertFalse(command.getExists()); + } + +} diff --git a/src/test/java/fm/last/moji/impl/FileDownloadInputStreamTest.java b/src/test/java/fm/last/moji/impl/FileDownloadInputStreamTest.java new file mode 100644 index 0000000..9027f25 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/FileDownloadInputStreamTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.locks.Lock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FileDownloadInputStreamTest { + + @Mock + private InputStream mockInputStream; + @Mock + private Lock mockReadLock; + private FileDownloadInputStream stream; + + @Before + public void setUp() { + stream = new FileDownloadInputStream(mockInputStream, mockReadLock); + } + + @Test + public void readDelegates() throws IOException { + stream.read(); + verify(mockInputStream).read(); + } + + @Test + public void readByteArrayDelegates() throws IOException { + byte[] b = new byte[4]; + stream.read(b); + verify(mockInputStream).read(b); + } + + @Test + public void readByteArrayWithOffsetDelegates() throws IOException { + byte[] b = new byte[4]; + stream.read(b, 2, 4); + verify(mockInputStream).read(b, 2, 4); + } + + @Test + public void skipDelegates() throws IOException { + stream.skip(1); + verify(mockInputStream).skip(1); + } + + @Test + public void availableDelegates() throws IOException { + stream.available(); + verify(mockInputStream).available(); + } + + @Test + public void closeDelegates() throws IOException { + stream.close(); + verify(mockInputStream).close(); + verify(mockReadLock).unlock(); + } + + @Test + public void closeReleasesLockEvenOnError() throws IOException { + doThrow(new IOException()).when(mockInputStream).close(); + try { + stream.close(); + } catch (Exception e) { + } + verify(mockReadLock).unlock(); + } + + @Test + public void markDelegates() { + stream.mark(21); + verify(mockInputStream).mark(21); + } + + @Test + public void resetDelegates() throws IOException { + stream.reset(); + verify(mockInputStream).reset(); + } + + @Test + public void markSupportedDelegates() { + stream.markSupported(); + verify(mockInputStream).markSupported(); + } + +} diff --git a/src/test/java/fm/last/moji/impl/FileUploadOutputStreamTest.java b/src/test/java/fm/last/moji/impl/FileUploadOutputStreamTest.java new file mode 100644 index 0000000..4f0aa8c --- /dev/null +++ b/src/test/java/fm/last/moji/impl/FileUploadOutputStreamTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.locks.Lock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerFactory; + +@RunWith(MockitoJUnitRunner.class) +public class FileUploadOutputStreamTest { + + private static final String KEY = "key"; + private static final String DOMAIN = "domain"; + @Mock + private TrackerFactory mockTrackerFactory; + @Mock + private HttpConnectionFactory mockHttpFactory; + @Mock + private Destination mockDestination; + @Mock + private HttpURLConnection mockHttpConnection; + @Mock + private OutputStream mockOutputStream; + @Mock + private InputStream mockInputStream; + @Mock + private Tracker mockTracker; + @Mock + private Lock mockWriteLock; + private FileUploadOutputStream stream; + + @Before + public void setUp() throws IOException { + URL url = new URL("http://www.last.fm/"); + when(mockDestination.getPath()).thenReturn(url); + when(mockHttpFactory.newConnection(url)).thenReturn(mockHttpConnection); + when(mockHttpConnection.getInputStream()).thenReturn(mockInputStream); + when(mockHttpConnection.getOutputStream()).thenReturn(mockOutputStream); + when(mockHttpConnection.getResponseMessage()).thenReturn("message"); + when(mockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockTrackerFactory.getTracker()).thenReturn(mockTracker); + + stream = new FileUploadOutputStream(mockTrackerFactory, mockHttpFactory, KEY, DOMAIN, mockDestination, + mockWriteLock); + } + + @Test + public void httpConnectionSetUp() throws IOException { + verify(mockHttpConnection).setRequestMethod("PUT"); + verify(mockHttpConnection).setChunkedStreamingMode(4096); + verify(mockHttpConnection).setDoOutput(true); + } + + @Test + public void everyThingCloses() throws IOException { + stream.write(1); + stream.close(); + + verify(mockOutputStream).flush(); + verify(mockOutputStream).close(); + verify(mockHttpConnection).disconnect(); + verify(mockTracker).createClose(KEY, DOMAIN, mockDestination, 1); + verify(mockTracker).close(); + verify(mockWriteLock).unlock(); + } + + @Test + public void everyThingClosesEvenOnFail() throws IOException { + doThrow(new RuntimeException()).when(mockOutputStream).flush(); + doThrow(new RuntimeException()).when(mockOutputStream).close(); + when(mockHttpConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_INTERNAL_ERROR); + doThrow(new RuntimeException()).when(mockInputStream).close(); + doThrow(new RuntimeException()).when(mockHttpConnection).disconnect(); + doThrow(new RuntimeException()).when(mockTracker).createClose(KEY, DOMAIN, mockDestination, 1); + + try { + stream.write(1); + stream.close(); + } catch (Exception e) { + } + + verify(mockOutputStream).flush(); + verify(mockOutputStream).close(); + verify(mockHttpConnection).disconnect(); + verify(mockTracker).createClose(KEY, DOMAIN, mockDestination, -1); + verify(mockTracker).close(); + verify(mockWriteLock).unlock(); + } + + @Test + public void writeIntDelegates() throws IOException { + stream.write(1); + verify(mockOutputStream).write(1); + } + + @Test + public void writeByteArrayDelegates() throws IOException { + byte[] b = new byte[4]; + stream.write(b); + verify(mockOutputStream).write(b); + } + + @Test + public void writeByteArrayWithOffsetDelegates() throws IOException { + byte[] b = new byte[4]; + stream.write(b, 2, 4); + verify(mockOutputStream).write(b, 2, 4); + } + + @Test + public void flushDelegates() throws IOException { + stream.flush(); + verify(mockOutputStream).flush(); + } + + @Test + public void countOnWrite() throws IOException { + byte[] b = new byte[] { 1, 2, 3, 4, 5 }; + stream.write(b); + stream.flush(); + stream.close(); + verify(mockTracker).createClose(KEY, DOMAIN, mockDestination, 5); + } + +} diff --git a/src/test/java/fm/last/moji/impl/HttpConnectionFactoryTest.java b/src/test/java/fm/last/moji/impl/HttpConnectionFactoryTest.java new file mode 100644 index 0000000..bb613bc --- /dev/null +++ b/src/test/java/fm/last/moji/impl/HttpConnectionFactoryTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ServerSocket; +import java.net.URL; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class HttpConnectionFactoryTest { + + private HttpConnectionFactory factory; + private ServerSocket serverSocket; + private InetSocketAddress address; + private HttpURLConnection connection; + + @Before + public void init() throws IOException { + serverSocket = new ServerSocket(0); + address = new InetSocketAddress(serverSocket.getInetAddress(), serverSocket.getLocalPort()); + factory = new HttpConnectionFactory(Proxy.NO_PROXY); + } + + @After + public void tearDown() throws IOException { + try { + connection.disconnect(); + } finally { + serverSocket.close(); + } + } + + @Test + public void newConnection() throws Exception { + connection = factory.newConnection(new URL("http://" + address.getHostName() + ":" + address.getPort() + "/")); + assertThat(connection, is(not(nullValue()))); + } + +} diff --git a/src/test/java/fm/last/moji/impl/ListFilesCommandTest.java b/src/test/java/fm/last/moji/impl/ListFilesCommandTest.java new file mode 100644 index 0000000..64b7971 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/ListFilesCommandTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import fm.last.moji.Moji; +import fm.last.moji.MojiFile; +import fm.last.moji.tracker.Tracker; + +@RunWith(MockitoJUnitRunner.class) +public class ListFilesCommandTest { + + private static final String DOMAIN = "domain"; + private static final String KEY_PREFIX = "key"; + + @Mock + private Moji mockMoji; + @Mock + private Tracker mockTracker; + + @Before + public void setUp() { + when(mockMoji.getFile(anyString())).thenAnswer(new Answer() { + @Override + public MojiFile answer(InvocationOnMock invocation) throws Throwable { + Object[] arguments = invocation.getArguments(); + MojiFile mock = mock(MojiFile.class); + when(mock.getKey()).thenReturn((String) arguments[0]); + return mock; + } + }); + } + + @Test + public void list() throws IOException { + List keys = Arrays.asList(new String[] { "key1", "key2", "key3" }); + when(mockTracker.list(DOMAIN, KEY_PREFIX, null)).thenReturn(keys); + ListFilesCommand command = new ListFilesCommand(mockMoji, KEY_PREFIX, DOMAIN); + command.executeWithTracker(mockTracker); + List fileList = command.getFileList(); + assertThat(fileList.size(), is(3)); + assertThat(fileList.get(0).getKey(), is("key1")); + assertThat(fileList.get(1).getKey(), is("key2")); + assertThat(fileList.get(2).getKey(), is("key3")); + } + + @Test + public void listWithLimit() throws IOException { + List keys = Collections.singletonList("key1"); + when(mockTracker.list(DOMAIN, KEY_PREFIX, 1)).thenReturn(keys); + ListFilesCommand command = new ListFilesCommand(mockMoji, KEY_PREFIX, DOMAIN, 1); + command.executeWithTracker(mockTracker); + List fileList = command.getFileList(); + assertThat(fileList.size(), is(1)); + assertThat(fileList.get(0).getKey(), is("key1")); + } + +} diff --git a/src/test/java/fm/last/moji/impl/MojiFileImplTest.java b/src/test/java/fm/last/moji/impl/MojiFileImplTest.java new file mode 100644 index 0000000..aa07738 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/MojiFileImplTest.java @@ -0,0 +1,254 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.TrackerFactory; + +@RunWith(MockitoJUnitRunner.class) +public class MojiFileImplTest { + + private static final String DOMAIN = "domain"; + private static final String STORAGE_CLASS = "storageClass"; + private static final String STORAGE_CLASS_2 = "newStorageClass"; + private static final String KEY = "key"; + private static final String KEY_2 = "newKey"; + + @Mock + private Tracker mockTracker; + @Mock + private TrackerFactory mockTrackerFactory; + @Mock + private HttpConnectionFactory mockHttpFactory; + @Mock + private Executor mockExecutor; + @Mock + private HttpURLConnection mockUrlConnection; + @Mock + private InputStream mockInputStream; + @Mock + private OutputStream mockOutputStream; + @Mock + private InetSocketAddress mockAddress; + + private MojiFileImpl file; + + @Before + public void init() throws TrackerException { + when(mockTrackerFactory.getTracker()).thenReturn(mockTracker); + when(mockTrackerFactory.getAddresses()).thenReturn(Collections.singleton(mockAddress)); + file = new MojiFileImpl(KEY, DOMAIN, STORAGE_CLASS, mockTrackerFactory, mockHttpFactory); + } + + @Test + public void existsCommand() throws IOException { + file.setExecutor(mockExecutor); + file.exists(); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExistsCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + } + + @Test + public void existsReturn() throws IOException { + List paths = Collections.singletonList(new URL("http://localhost:80/")); + when(mockTracker.getPaths(KEY, DOMAIN)).thenReturn(paths); + boolean exists = file.exists(); + assertThat(exists, is(true)); + } + + @Test + public void deleteCommand() throws IOException { + file.setExecutor(mockExecutor); + file.delete(); + ArgumentCaptor captor = ArgumentCaptor.forClass(DeleteCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + } + + @Test + public void renameCommand() throws IOException { + file.setExecutor(mockExecutor); + file.rename(KEY_2); + ArgumentCaptor captor = ArgumentCaptor.forClass(RenameCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + assertThat(captor.getValue().newKey, is(KEY_2)); + } + + @Test + public void renameChangesKeyInFile() throws IOException { + assertThat(file.getKey(), is(KEY)); + file.rename(KEY_2); + assertThat(file.getKey(), is(KEY_2)); + } + + @Test + public void renameKeyNotModifiedOnError() throws IOException { + file.setExecutor(mockExecutor); + doThrow(new IOException()).when(mockExecutor).executeCommand(any(RenameCommand.class)); + assertThat(file.getKey(), is(KEY)); + try { + file.rename(KEY_2); + } catch (IOException ignored) { + } + assertThat(file.getKey(), is(KEY)); + } + + @Test + public void storageClassCommand() throws IOException { + file.setExecutor(mockExecutor); + file.modifyStorageClass(STORAGE_CLASS_2); + ArgumentCaptor captor = ArgumentCaptor.forClass(UpdateStorageClassCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + assertThat(captor.getValue().newStorageClass, is(STORAGE_CLASS_2)); + } + + // We cannot know the storage class currently + // @Test + // public void storageClassChangesInFile() throws IOException { + // assertThat(file.getStorageClass(), is(STORAGE_CLASS)); + // file.modifyStorageClass(STORAGE_CLASS_2); + // assertThat(file.getStorageClass(), is(STORAGE_CLASS_2)); + // } + + // We cannot know the storage class currently + // @Test + // public void storageClassNotModifiedOnError() throws IOException { + // file.setExecutor(mockExecutor); + // doThrow(new IOException()).when(mockExecutor).executeCommand(any(UpdateStorageClassCommand.class)); + // assertThat(file.getStorageClass(), is(STORAGE_CLASS)); + // try { + // file.modifyStorageClass(STORAGE_CLASS_2); + // } catch (IOException ignored) { + // } + // assertThat(file.getStorageClass(), is(STORAGE_CLASS)); + // } + + @Test + public void lengthCommand() throws IOException { + file.setExecutor(mockExecutor); + file.length(); + ArgumentCaptor captor = ArgumentCaptor.forClass(FileLengthCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + } + + @Test + public void lengthReturn() throws IOException { + URL path = new URL("http://localhost:80/"); + List paths = Collections.singletonList(path); + when(mockTracker.getPaths(KEY, DOMAIN)).thenReturn(paths); + when(mockHttpFactory.newConnection(path)).thenReturn(mockUrlConnection); + when(mockUrlConnection.getContentLength()).thenReturn(74634654); + + // check that whatever we have delegates to the expected stream + long length = file.length(); + assertThat(length, is(74634654L)); + } + + @Test + public void getInputStreamCommand() throws IOException { + file.setExecutor(mockExecutor); + file.getInputStream(); + ArgumentCaptor captor = ArgumentCaptor.forClass(GetInputStreamCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + } + + @Test + public void getInputStreamCommandReturn() throws IOException { + URL path = new URL("http://localhost:80/"); + List paths = Collections.singletonList(path); + when(mockTracker.getPaths(KEY, DOMAIN)).thenReturn(paths); + when(mockHttpFactory.newConnection(path)).thenReturn(mockUrlConnection); + when(mockUrlConnection.getInputStream()).thenReturn(mockInputStream); + + // check that whatever we have delegates to the expected stream + InputStream inputStream = file.getInputStream(); + byte[] myBuffer = new byte[2]; + inputStream.read(myBuffer, 2, 43); + verify(mockInputStream).read(myBuffer, 2, 43); + } + + @Test + public void getOutputStreamCommand() throws IOException { + file.setExecutor(mockExecutor); + file.getOutputStream(); + ArgumentCaptor captor = ArgumentCaptor.forClass(GetOutputStreamCommand.class); + verify(mockExecutor).executeCommand(captor.capture()); + assertThat(captor.getValue().key, is(KEY)); + assertThat(captor.getValue().domain, is(DOMAIN)); + assertThat(captor.getValue().storageClass, is(STORAGE_CLASS)); + } + + @Test + public void getOutputStreamCommandReturn() throws IOException { + URL path = new URL("http://localhost:80/"); + Destination destination = new Destination(path, 2, 4); + List destinations = Collections.singletonList(destination); + when(mockTracker.createOpen(KEY, DOMAIN, STORAGE_CLASS)).thenReturn(destinations); + when(mockHttpFactory.newConnection(path)).thenReturn(mockUrlConnection); + when(mockUrlConnection.getOutputStream()).thenReturn(mockOutputStream); + + // check that whatever we have delegates to the expected stream + OutputStream outputStream = file.getOutputStream(); + byte[] myBuffer = new byte[2]; + outputStream.write(myBuffer, 3, 5); + verify(mockOutputStream).write(myBuffer, 3, 5); + } + + @Test + public void getPaths() throws IOException { + List trackerPaths = Collections.singletonList(new URL("http://www.last.fm/1/2")); + when(mockTracker.getPaths(KEY, DOMAIN)).thenReturn(trackerPaths); + List paths = file.getPaths(); + assertThat(paths, is(trackerPaths)); + } + +} diff --git a/src/test/java/fm/last/moji/impl/RenameCommandTest.java b/src/test/java/fm/last/moji/impl/RenameCommandTest.java new file mode 100644 index 0000000..051ff90 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/RenameCommandTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Tracker; + +@RunWith(MockitoJUnitRunner.class) +public class RenameCommandTest { + + @Mock + private Tracker mockTracker; + private RenameCommand command; + + @Before + public void init() { + command = new RenameCommand("key", "domain", "newKey"); + } + + @Test + public void delegatesToTracker() throws Exception { + command.executeWithTracker(mockTracker); + verify(mockTracker).rename("key", "domain", "newKey"); + } + +} diff --git a/src/test/java/fm/last/moji/impl/UpdateStorageClassCommandTest.java b/src/test/java/fm/last/moji/impl/UpdateStorageClassCommandTest.java new file mode 100644 index 0000000..367c189 --- /dev/null +++ b/src/test/java/fm/last/moji/impl/UpdateStorageClassCommandTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.impl; + +import static org.mockito.Mockito.verify; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Tracker; + +@RunWith(MockitoJUnitRunner.class) +public class UpdateStorageClassCommandTest { + + @Mock + private Tracker mockTracker; + private UpdateStorageClassCommand command; + + @Before + public void init() { + command = new UpdateStorageClassCommand("key", "domain", "newStorageClass"); + } + + @Test + public void delegatesToTracker() throws Exception { + command.executeWithTracker(mockTracker); + verify(mockTracker).updateStorageClass("key", "domain", "newStorageClass"); + } + +} diff --git a/src/test/java/fm/last/moji/integration/AbstractMojiIT.java b/src/test/java/fm/last/moji/integration/AbstractMojiIT.java new file mode 100644 index 0000000..080ef47 --- /dev/null +++ b/src/test/java/fm/last/moji/integration/AbstractMojiIT.java @@ -0,0 +1,133 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.integration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.math.RandomUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; + +import fm.last.moji.MojiFile; +import fm.last.moji.spring.SpringMojiBean; +import fm.last.moji.tracker.UnknownKeyException; + +@Ignore("abstract") +abstract public class AbstractMojiIT { + + private final List mojiFiles = new ArrayList(); + + SpringMojiBean moji; + String keyPrefix; + String storageClassA; + String storageClassB; + + @Before + public void setUp() throws Exception { + String env = System.getProperty("env", ""); + if (!"".equals(env)) { + env = "." + env; + } + Properties properties = new Properties(); + properties.load(getClass().getResourceAsStream("/moji.properties" + env)); + + String hosts = properties.getProperty("moji.tracker.hosts"); + String domain = properties.getProperty("moji.domain"); + + keyPrefix = properties.getProperty("test.moji.key.prefix"); + storageClassA = properties.getProperty("test.moji.class.a"); + storageClassB = properties.getProperty("test.moji.class.b"); + + moji = new SpringMojiBean(hosts, domain); + moji.setTestOnBorrow(true); + } + + @After + public void tearDown() throws Exception { + for (MojiFile file : mojiFiles) { + if (file != null) { + try { + file.delete(); + } catch (UnknownKeyException e) { + } + } + } + moji.close(); + } + + void writeDataToMogileFile(MojiFile file, String data) throws IOException { + OutputStream streamToMogile = null; + StringReader toUpload = null; + try { + toUpload = new StringReader(data); + streamToMogile = file.getOutputStream(); + IOUtils.copy(toUpload, streamToMogile); + streamToMogile.flush(); + } finally { + IOUtils.closeQuietly(streamToMogile); + IOUtils.closeQuietly(toUpload); + } + } + + String downloadDataFromMogileFile(MojiFile file) throws IOException { + StringWriter downloaded = null; + InputStream streamFromMogile = null; + try { + downloaded = new StringWriter(); + streamFromMogile = file.getInputStream(); + IOUtils.copy(streamFromMogile, downloaded); + } finally { + IOUtils.closeQuietly(downloaded); + IOUtils.closeQuietly(streamFromMogile); + } + return downloaded.toString(); + } + + MojiFile getFile(String key) { + MojiFile file = moji.getFile(key); + mojiFiles.add(file); + return file; + } + + MojiFile getFile(String key, String storageClass) { + MojiFile file = moji.getFile(key, storageClass); + mojiFiles.add(file); + return file; + } + + String newData() { + return RandomStringUtils.randomAscii(RandomUtils.nextInt(4096) + 512); + } + + String newKey(String suffix) { + return keyPrefix + suffix; + } + + String newKey() { + return newKey(RandomStringUtils.randomAlphanumeric(16)); + } + +} diff --git a/src/test/java/fm/last/moji/integration/MojiFileIT.java b/src/test/java/fm/last/moji/integration/MojiFileIT.java new file mode 100644 index 0000000..2789deb --- /dev/null +++ b/src/test/java/fm/last/moji/integration/MojiFileIT.java @@ -0,0 +1,203 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.integration; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import fm.last.moji.MojiFile; +import fm.last.moji.tracker.KeyExistsAlreadyException; +import fm.last.moji.tracker.UnknownKeyException; +import fm.last.moji.tracker.UnknownStorageClassException; + +public class MojiFileIT extends AbstractMojiIT { + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Test + public void writeNewThenReadBack() throws Exception { + MojiFile newFile = getFile(newKey()); + assertFalse(newFile.exists()); + + String data = newData(); + + writeDataToMogileFile(newFile, data); + assertTrue(newFile.exists()); + + assertEquals(data, downloadDataFromMogileFile(newFile)); + } + + @Test + public void overwriteThenReadBack() throws Exception { + MojiFile existingFile = getFile(newKey("overwriteThenReadBack")); + String overwrite = newData(); + + writeDataToMogileFile(existingFile, overwrite); + assertEquals(overwrite, downloadDataFromMogileFile(existingFile)); + } + + @Test + public void writeNewWithStorageClassThenReadBack() throws Exception { + MojiFile newFile = getFile(newKey(), storageClassA); + assertFalse(newFile.exists()); + + String data = newData(); + + writeDataToMogileFile(newFile, data); + assertTrue(newFile.exists()); + // We cannot know the storage class currently + // assertEquals(newFile.getStorageClass(), storageClassA); + + assertEquals(data, downloadDataFromMogileFile(newFile)); + } + + @Test(expected = UnknownStorageClassException.class) + public void writeWithstorageClassUnknown() throws IOException { + MojiFile fileInUnknownClass = getFile(newKey(), "madeup" + RandomStringUtils.randomAlphanumeric(8)); + assertFalse(fileInUnknownClass.exists()); + + String data = newData(); + + writeDataToMogileFile(fileInUnknownClass, data); + } + + @Test + public void fileSize() throws IOException { + MojiFile fileWithSize = getFile(newKey("fileOfKnownSize")); + assertEquals(3832, fileWithSize.length()); + } + + @Test + public void exists() throws IOException { + MojiFile existentFile = getFile(newKey("exists")); + assertTrue(existentFile.exists()); + } + + @Test + public void notExists() throws IOException { + MojiFile existentFile = getFile(newKey()); + assertFalse(existentFile.exists()); + } + + @Test + public void notExistsAfterDelete() throws IOException { + MojiFile existentFile = getFile(newKey("notExistsAfterDelete")); + assertTrue(existentFile.exists()); + existentFile.delete(); + assertFalse(existentFile.exists()); + } + + @Test + public void rename() throws IOException { + String originalKey = newKey("rename"); + MojiFile fileToRename = getFile(originalKey); + + String newKey = newKey(); + fileToRename.rename(newKey); + assertEquals(newKey, fileToRename.getKey()); + + MojiFile renamed = getFile(newKey); + assertTrue(renamed.exists()); + + MojiFile oldName = getFile(originalKey); + assertFalse(oldName.exists()); + } + + @Test(expected = UnknownKeyException.class) + public void renameUnknownKey() throws IOException { + MojiFile nonExistentFile = getFile(newKey()); + nonExistentFile.rename(newKey()); + } + + @Test(expected = KeyExistsAlreadyException.class) + public void renameExistingKey() throws IOException { + String alreadyHereKey = newKey("renameExistingKey1"); + String toRenameKey = newKey("renameExistingKey2"); + + MojiFile newFile = getFile(toRenameKey); + newFile.rename(alreadyHereKey); + } + + @Test + public void updateStorageClass() throws IOException { + String key = newKey("updateStorageClass"); + MojiFile fileToUpdate = getFile(key, storageClassA); + // We cannot know the storage class currently + // assertEquals(fileToUpdate.getStorageClass(), storageClassA); + + fileToUpdate.modifyStorageClass(storageClassB); + // We cannot know the storage class currently + // assertEquals(fileToUpdate.getStorageClass(), storageClassB); + + MojiFile exists = getFile(key, storageClassB); + assertTrue(exists.exists()); + } + + @Test(expected = UnknownStorageClassException.class) + public void updateStorageClassToUnknown() throws IOException { + MojiFile fileToUpdate = getFile(newKey("updateStorageClassToUnknown")); + fileToUpdate.modifyStorageClass("madeup" + RandomStringUtils.randomAlphanumeric(8)); + } + + @Test(expected = UnknownKeyException.class) + public void deleteNonExistent() throws IOException { + MojiFile fileToDelete = getFile(newKey()); + fileToDelete.delete(); + } + + @Test + public void copyToFile() throws IOException { + MojiFile copyFile = getFile(newKey("mogileFileCopyToFile")); + + File file = testFolder.newFile(newKey() + ".dat"); + copyFile.copyToFile(file); + + byte[] actualData = FileUtils.readFileToByteArray(file); + + File original = new File("src/test/data/mogileFileCopyToFile.dat"); + byte[] expectedData = FileUtils.readFileToByteArray(original); + assertArrayEquals(expectedData, actualData); + } + + @Test(expected = UnknownKeyException.class) + public void copyToFileUnknownKey() throws IOException { + MojiFile copyFile = getFile(newKey()); + File file = testFolder.newFile(newKey() + ".dat"); + copyFile.copyToFile(file); + } + + @Test + public void getPaths() throws Exception { + MojiFile file = getFile(newKey("getPaths")); + List paths = file.getPaths(); + assertFalse(paths.isEmpty()); + } + +} diff --git a/src/test/java/fm/last/moji/integration/MojiIT.java b/src/test/java/fm/last/moji/integration/MojiIT.java new file mode 100644 index 0000000..20f1a1e --- /dev/null +++ b/src/test/java/fm/last/moji/integration/MojiIT.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.integration; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import fm.last.moji.MojiFile; + +public class MojiIT extends AbstractMojiIT { + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + @Test + public void copyToMogile() throws IOException { + File tempFile = testFolder.newFile(newKey() + ".txt"); + String data = newData(); + + FileUtils.write(tempFile, data); + MojiFile destination = getFile(newKey()); + + moji.copyToMogile(tempFile, destination); + + assertEquals(data, downloadDataFromMogileFile(destination)); + } + + @Test + public void list() throws IOException { + List list = moji.list(keyPrefix + "list"); + assertThat(list.size(), is(3)); + Set keys = new HashSet(); + for (MojiFile mojiFile : list) { + keys.add(mojiFile.getKey()); + } + assertTrue(keys.contains(keyPrefix + "list1")); + assertTrue(keys.contains(keyPrefix + "list2")); + assertTrue(keys.contains(keyPrefix + "list3")); + } + + @Test + public void listWithLimit() throws IOException { + List list = moji.list(keyPrefix + "list", 1); + assertThat(list.size(), is(1)); + } + + @Test + public void listNoMatches() throws IOException { + List list = moji.list("XXX" + keyPrefix); + assertTrue(list.isEmpty()); + } + +} diff --git a/src/test/java/fm/last/moji/local/DefaultFileNamingStrategyTest.java b/src/test/java/fm/last/moji/local/DefaultFileNamingStrategyTest.java new file mode 100644 index 0000000..24cbd5d --- /dev/null +++ b/src/test/java/fm/last/moji/local/DefaultFileNamingStrategyTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.local; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FilenameFilter; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class DefaultFileNamingStrategyTest { + + @Rule + public TemporaryFolder testFolder = new TemporaryFolder(); + + private File folder; + private DefaultFileNamingStrategy namingStrategy; + + @Before + public void setUp() { + folder = testFolder.newFolder("local-moji"); + namingStrategy = new DefaultFileNamingStrategy(folder); + } + + @Test + public void keyForFile() { + String key = namingStrategy.keyForFileName("lastfm-8473848737.dat"); + assertThat(key, is("8473848737")); + } + + @Test + public void domainForFile() { + String domain = namingStrategy.domainForFileName("lastfm-8473848737.dat"); + assertThat(domain, is("lastfm")); + } + + @Test + public void fileNameFilter() { + File anotherFolder = testFolder.newFolder("some-other-folder"); + FilenameFilter filter = namingStrategy.filterForPrefix("lastfm", "100"); + assertTrue(filter.accept(folder, "lastfm-1003848737.dat")); + assertFalse(filter.accept(folder, "lastfm-1013848737.dat")); + assertFalse(filter.accept(folder, "another-1003848737.dat")); + assertFalse(filter.accept(folder, ".ssh")); + assertFalse(filter.accept(folder, "..")); + assertFalse(filter.accept(folder, ".")); + assertFalse(filter.accept(anotherFolder, "lastfm-1003848737.dat")); + } + +} diff --git a/src/test/java/fm/last/moji/spring/SpringMojiBeanInstantiationTest.java b/src/test/java/fm/last/moji/spring/SpringMojiBeanInstantiationTest.java new file mode 100644 index 0000000..18b5f9d --- /dev/null +++ b/src/test/java/fm/last/moji/spring/SpringMojiBeanInstantiationTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.spring; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +import fm.last.moji.FakeMogileFsServer; +import fm.last.moji.MojiFile; + +public class SpringMojiBeanInstantiationTest { + + @Test(timeout = 2000) + public void delete() throws Exception { + FakeMogileFsServer server = null; + try { + FakeMogileFsServer.Builder builder = new FakeMogileFsServer.Builder(); + builder.whenRequestContains("delete ", "key=myKey", "domain=myDomain").thenRespond("OK "); + server = builder.build(); + SpringMojiBean bean = new SpringMojiBean(server.getAddressAsString(), "myDomain"); + MojiFile file = bean.getFile("myKey"); + file.delete(); + assertThat(bean.getNumIdle(), is(1)); + } finally { + server.close(); + } + } + +} diff --git a/src/test/java/fm/last/moji/spring/SpringMojiBeanTest.java b/src/test/java/fm/last/moji/spring/SpringMojiBeanTest.java new file mode 100644 index 0000000..250e3b6 --- /dev/null +++ b/src/test/java/fm/last/moji/spring/SpringMojiBeanTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.spring; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class SpringMojiBeanTest { + + private SpringMojiBean bean; + + @Before + public void init() { + bean = new SpringMojiBean("localhost:80", "domain"); + } + + @After + public void close() throws Exception { + bean.close(); + } + + @Test + public void defaultProxy() { + assertEquals(Proxy.NO_PROXY, bean.getProxy()); + } + + @Test + public void address() { + Set addresses = bean.getAddresses(); + assertEquals(1, addresses.size()); + InetSocketAddress address = addresses.iterator().next(); + assertEquals("localhost", address.getHostName()); + assertEquals(address.getPort(), 80); + } + + @Test + public void getFile() { + assertNotNull(bean.getFile("123")); + } + + @Test + public void getFileWithStorageClass() { + assertNotNull(bean.getFile("123", "class")); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/CreateOpenOperationTest.java b/src/test/java/fm/last/moji/tracker/impl/CreateOpenOperationTest.java new file mode 100644 index 0000000..57f311c --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/CreateOpenOperationTest.java @@ -0,0 +1,182 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.TrackerException; + +@RunWith(MockitoJUnitRunner.class) +public class CreateOpenOperationTest { + + private static final String DOMAIN = "domain"; + private static final String STORAGE_CLASS = "storageClass"; + private static final String KEY = "key"; + private static final String FID = "5"; + private static final String DEV_COUNT = "2"; + private static final String PATH_1 = "http://www.last.fm/1/"; + private static final String DEV_ID_1 = "1"; + private static final String PATH_2 = "http://www.last.fm/2/"; + private static final String DEV_ID_2 = "2"; + + @Mock + private RequestHandler mockRequestHandler; + @Mock + private Response mockResponse; + @Mock + private Response mockEmptyResponse; + @Mock + private Response mockUnknownKeyResponse; + @Mock + private Response mockFailResponse; + + private ArgumentCaptor captorRequest; + private CreateOpenOperation operation; + + @Before + public void setUp() throws TrackerException { + captorRequest = ArgumentCaptor.forClass(Request.class); + when(mockResponse.getStatus()).thenReturn(ResponseStatus.OK); + when(mockResponse.getValue("fid")).thenReturn(FID); + when(mockResponse.getValue("dev_count")).thenReturn(DEV_COUNT); + when(mockResponse.getValue("path_1")).thenReturn(PATH_1); + when(mockResponse.getValue("devid_1")).thenReturn(DEV_ID_1); + when(mockResponse.getValue("path_2")).thenReturn(PATH_2); + when(mockResponse.getValue("devid_2")).thenReturn(DEV_ID_2); + + when(mockEmptyResponse.getStatus()).thenReturn(ResponseStatus.OK); + when(mockEmptyResponse.getValue("fid")).thenReturn(FID); + when(mockEmptyResponse.getValue("dev_count")).thenReturn("0"); + + when(mockUnknownKeyResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockUnknownKeyResponse.getMessage()).thenReturn("unknown_key unknown key"); + + when(mockFailResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockFailResponse.getMessage()).thenReturn("unexpected error"); + } + + @Test + public void requestNormal() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, STORAGE_CLASS, true); + operation.execute(); + + Request request = captorRequest.getValue(); + assertThat(request.getCommand(), is("create_open")); + assertThat(request.getArguments().size(), is(4)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("class"), is(STORAGE_CLASS)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("multi_dest"), is("1")); + } + + @Test + public void requestNormalNoClass() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, null, true); + operation.execute(); + + Request request = captorRequest.getValue(); + assertThat(request.getCommand(), is("create_open")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("multi_dest"), is("1")); + } + + @Test + public void requestNormalNoMulti() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, "", false); + operation.execute(); + + Request request = captorRequest.getValue(); + assertThat(request.getCommand(), is("create_open")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("multi_dest"), is("0")); + } + + @Test + public void responseNormal() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, STORAGE_CLASS, true); + operation.execute(); + + List destinations = operation.getDestinations(); + assertThat(destinations.size(), is(2)); + + Destination destination1 = destinations.get(0); + assertThat(destination1.getDevId(), is(1)); + assertThat(destination1.getFid(), is(5)); + assertThat(destination1.getPath(), is(new URL(PATH_1))); + + Destination destination2 = destinations.get(1); + assertThat(destination2.getDevId(), is(2)); + assertThat(destination2.getFid(), is(5)); + assertThat(destination2.getPath(), is(new URL(PATH_2))); + } + + @Test + public void zeroDestinations() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockEmptyResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, STORAGE_CLASS, true); + operation.execute(); + + List destinations = operation.getDestinations(); + assertThat(destinations.size(), is(0)); + } + + @Test + public void unknownKey() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockUnknownKeyResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, STORAGE_CLASS, true); + operation.execute(); + + List destinations = operation.getDestinations(); + assertThat(destinations.size(), is(0)); + } + + @Test(expected = TrackerException.class) + public void unexpectedError() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockFailResponse); + + operation = new CreateOpenOperation(mockRequestHandler, DOMAIN, KEY, STORAGE_CLASS, true); + operation.execute(); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/GetPathsOperationTest.java b/src/test/java/fm/last/moji/tracker/impl/GetPathsOperationTest.java new file mode 100644 index 0000000..8db247f --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/GetPathsOperationTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.UnknownKeyException; + +@RunWith(MockitoJUnitRunner.class) +public class GetPathsOperationTest { + + private static final String DOMAIN = "domain"; + private static final String KEY = "key"; + private static final String PATH_COUNT = "2"; + private static final String PATH_1 = "http://www.last.fm/1/"; + private static final String PATH_2 = "http://www.last.fm/2/"; + + @Mock + private RequestHandler mockRequestHandler; + @Mock + private Response mockResponse; + @Mock + private Response mockEmptyResponse; + @Mock + private Response mockUnknownKeyResponse; + @Mock + private Response mockFailResponse; + + private ArgumentCaptor captorRequest; + private GetPathsOperation operation; + + @Before + public void setUp() throws TrackerException { + captorRequest = ArgumentCaptor.forClass(Request.class); + when(mockResponse.getStatus()).thenReturn(ResponseStatus.OK); + when(mockResponse.getValue("paths")).thenReturn(PATH_COUNT); + when(mockResponse.getValue("path1")).thenReturn(PATH_1); + when(mockResponse.getValue("path2")).thenReturn(PATH_2); + + when(mockEmptyResponse.getStatus()).thenReturn(ResponseStatus.OK); + when(mockEmptyResponse.getValue("paths")).thenReturn("0"); + + when(mockUnknownKeyResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockUnknownKeyResponse.getMessage()).thenReturn("unknown_key unknown key"); + + when(mockFailResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockFailResponse.getMessage()).thenReturn("unexpected error"); + } + + @Test + public void requestNormal() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, false); + operation.execute(); + + Request request = captorRequest.getValue(); + assertThat(request.getCommand(), is("get_paths")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("noverify"), is("1")); + } + + @Test + public void requestNormalVerify() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, true); + operation.execute(); + + Request request = captorRequest.getValue(); + assertThat(request.getCommand(), is("get_paths")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("noverify"), is("0")); + } + + @Test + public void responseNormal() throws TrackerException, MalformedURLException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, true); + operation.execute(); + + List paths = operation.getPaths(); + assertThat(paths.size(), is(2)); + assertThat(paths.get(0), is(new URL(PATH_1))); + assertThat(paths.get(1), is(new URL(PATH_2))); + } + + @Test + public void zeroPaths() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockEmptyResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, true); + operation.execute(); + + List paths = operation.getPaths(); + assertThat(paths.size(), is(0)); + } + + @Test(expected = UnknownKeyException.class) + public void unknownKey() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockUnknownKeyResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, true); + operation.execute(); + } + + @Test(expected = TrackerException.class) + public void unexpectedError() throws TrackerException { + when(mockRequestHandler.performRequest(captorRequest.capture())).thenReturn(mockFailResponse); + + operation = new GetPathsOperation(mockRequestHandler, DOMAIN, KEY, true); + operation.execute(); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.java b/src/test/java/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.java new file mode 100644 index 0000000..8568076 --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.junit.Assert.assertEquals; + +import java.net.InetSocketAddress; + +import org.junit.Test; + +public class InetSocketAddressFactoryTest { + + @Test + public void numeric1() { + InetSocketAddress address = InetSocketAddressFactory.newAddress("0.0.0.0:80"); + assertEquals("0.0.0.0", address.getHostName()); + assertEquals(80, address.getPort()); + } + + @Test + public void numeric2() { + InetSocketAddress address = InetSocketAddressFactory.newAddress("255.255.255.255:65535"); + assertEquals("255.255.255.255", address.getHostName()); + assertEquals(65535, address.getPort()); + } + + @Test(expected = IllegalArgumentException.class) + public void numericBadPort1() { + InetSocketAddressFactory.newAddress("255.255.255.255:65536"); + } + + @Test(expected = IllegalArgumentException.class) + public void numericBadAddress1() { + InetSocketAddressFactory.newAddress("255.255.255.256:65535"); + } + + @Test(expected = IllegalArgumentException.class) + public void numericBadPort2() { + InetSocketAddressFactory.newAddress("255.255.255.255:-80"); + } + + @Test(expected = IllegalArgumentException.class) + public void numericBadAddress2() { + InetSocketAddressFactory.newAddress("255.-2.255.255:80"); + } + + @Test + public void alpha1() { + InetSocketAddress address = InetSocketAddressFactory.newAddress("www.google.com:80"); + assertEquals("www.google.com", address.getHostName()); + assertEquals(80, address.getPort()); + } + + @Test + public void alpha2() { + InetSocketAddress address = InetSocketAddressFactory.newAddress("localhost:80"); + assertEquals("localhost", address.getHostName()); + assertEquals(80, address.getPort()); + } + + @Test(expected = IllegalArgumentException.class) + public void badAlpha1() { + InetSocketAddressFactory.newAddress("local:host:80"); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/RequestHandlerTest.java b/src/test/java/fm/last/moji/tracker/impl/RequestHandlerTest.java new file mode 100644 index 0000000..0361bfb --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/RequestHandlerTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyChar; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.TrackerException; + +@RunWith(MockitoJUnitRunner.class) +public class RequestHandlerTest { + + @Mock + private Writer mockWriter; + @Mock + private BufferedReader mockReader; + private RequestHandler handler; + + @Test + public void write() throws TrackerException { + BufferedReader reader = new BufferedReader(new StringReader("OK r1=x&r2=y")); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + handler.performRequest(request); + + assertEquals("mock arg1=one&arg2=2\r\n", writer.toString()); + } + + @Test + public void okRead() throws TrackerException { + BufferedReader reader = new BufferedReader(new StringReader("OK r1=x&r2=y")); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + Response response = handler.performRequest(request); + + assertEquals(ResponseStatus.OK, response.getStatus()); + assertEquals("x", response.getValue("r1")); + assertEquals("y", response.getValue("r2")); + assertNull(response.getMessage()); + } + + @Test + public void errorRead() throws TrackerException { + BufferedReader reader = new BufferedReader(new StringReader("ERR problem")); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + Response response = handler.performRequest(request); + + assertEquals(ResponseStatus.ERROR, response.getStatus()); + assertEquals("problem", response.getMessage()); + } + + @Test(expected = TrackerException.class) + public void badResponse() throws TrackerException { + BufferedReader reader = new BufferedReader(new StringReader("ERR")); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + handler.performRequest(request); + } + + @Test(expected = TrackerException.class) + public void badResponse2() throws TrackerException { + BufferedReader reader = new BufferedReader(new StringReader("XXX problem")); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + handler.performRequest(request); + } + + @Test(expected = TrackerException.class) + public void ioExceptionRead() throws IOException { + when(mockReader.readLine()).thenThrow(new IOException()); + StringWriter writer = new StringWriter(); + handler = new RequestHandler(writer, mockReader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + handler.performRequest(request); + } + + @Test(expected = TrackerException.class) + public void ioExceptionWrite() throws IOException { + doThrow(new IOException()).when(mockWriter).write(anyInt()); + doThrow(new IOException()).when(mockWriter).write(anyChar()); + doThrow(new IOException()).when(mockWriter).write(argThat(new TrueMatcher())); + doThrow(new IOException()).when(mockWriter).write(argThat(new TrueMatcher())); + doThrow(new IOException()).when(mockWriter).write(argThat(new TrueMatcher()), anyInt(), anyInt()); + doThrow(new IOException()).when(mockWriter).write(argThat(new TrueMatcher()), anyInt(), anyInt()); + BufferedReader reader = new BufferedReader(new StringReader("ERR problem")); + handler = new RequestHandler(mockWriter, reader); + + Request request = new Request.Builder(2).command("mock").arg("arg1", "one").arg("arg2", 2).build(); + handler.performRequest(request); + } + + @Test + public void close() throws IOException { + handler = new RequestHandler(mockWriter, mockReader); + handler.close(); + verify(mockReader).close(); + verify(mockWriter).close(); + } + + private class TrueMatcher extends BaseMatcher { + + @Override + public boolean matches(Object arg0) { + return true; + } + + @Override + public void describeTo(Description arg0) { + } + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/RequestTest.java b/src/test/java/fm/last/moji/tracker/impl/RequestTest.java new file mode 100644 index 0000000..46b5153 --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/RequestTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.junit.Assert.assertTrue; + +import java.io.StringWriter; +import java.net.URL; + +import org.junit.Test; + +public class RequestTest { + + @Test + public void example() throws Exception { + Request request = new Request.Builder(5).command("mycommand").arg("bool", true).arg("integer", 2).arg("long", 12L) + .arg("string", "/=&URL").arg("url", new URL("http://localhost:80/x.do?what=12&do")).build(); + StringWriter writer = new StringWriter(); + request.writeTo(writer); + String wire = writer.toString(); + assertTrue(wire.startsWith("mycommand ")); + assertTrue(wire.contains("bool=1")); + assertTrue(wire.contains("long=12")); + assertTrue(wire.contains("integer=2")); + assertTrue(wire.contains("url=http%3A%2F%2Flocalhost%3A80%2Fx.do%3Fwhat%3D12%26do")); + assertTrue(wire.contains("string=%2F%3D%26URL")); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/ResponseTest.java b/src/test/java/fm/last/moji/tracker/impl/ResponseTest.java new file mode 100644 index 0000000..fadd8a8 --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/ResponseTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class ResponseTest { + + @Test + public void example() { + Response response = new Response(ResponseStatus.OK, + "path2=http://127.0.0.61:7500/dev469/0/173/153/0173153702.fid&path1=" + + "http://127.0.0.62:7500/dev490/0/173/153/0173153702.fid&paths=2"); + assertThat(response.getStatus(), is(ResponseStatus.OK)); + assertThat(response.getValue("paths"), is("2")); + assertThat(response.getValue("path1"), is("http://127.0.0.62:7500/dev490/0/173/153/0173153702.fid")); + assertThat(response.getValue("path2"), is("http://127.0.0.61:7500/dev469/0/173/153/0173153702.fid")); + assertThat(response.getMessage(), is(nullValue())); + } + + @Test + public void badPair() { + Response response = new Response(ResponseStatus.OK, + "path2=http://127.0.0.61:7500/dev469/0/173/153/0173153702.fid&path1" + + "http://127.0.0.62:7500/dev490/0/173/153/0173153702.fid&paths=2"); + assertThat(response.getStatus(), is(ResponseStatus.OK)); + assertThat(response.getValue("paths"), is("2")); + assertThat(response.getValue("path1"), is(nullValue())); + assertThat(response.getValue("path2"), is("http://127.0.0.61:7500/dev469/0/173/153/0173153702.fid")); + assertThat(response.getMessage(), is(nullValue())); + } + + @Test + public void error() { + Response response = new Response(ResponseStatus.ERROR, "message"); + assertThat(response.getStatus(), is(ResponseStatus.ERROR)); + assertThat(response.getMessage(), is("message")); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/SingleHostTrackerFactoryTest.java b/src/test/java/fm/last/moji/tracker/impl/SingleHostTrackerFactoryTest.java new file mode 100644 index 0000000..61386ed --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/SingleHostTrackerFactoryTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ServerSocket; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; + +public class SingleHostTrackerFactoryTest { + + private SingleHostTrackerFactory factory; + private ServerSocket serverSocket; + private InetSocketAddress address; + + @Before + public void init() throws IOException { + serverSocket = new ServerSocket(0); + address = new InetSocketAddress(serverSocket.getInetAddress(), serverSocket.getLocalPort()); + factory = new SingleHostTrackerFactory(address, Proxy.NO_PROXY); + } + + @After + public void tearDown() throws IOException { + serverSocket.close(); + } + + @Test + public void proxy() { + assertEquals(Proxy.NO_PROXY, factory.getProxy()); + } + + @Test + public void getTracker() throws TrackerException { + Tracker tracker = factory.getTracker(); + assertNotNull(tracker); + } + + @Test + public void getAddresses() { + Set addresses = factory.getAddresses(); + assertEquals(1, addresses.size()); + InetSocketAddress actualAddress = addresses.iterator().next(); + assertEquals(address.getHostName(), actualAddress.getHostName()); + assertEquals(address.getPort(), actualAddress.getPort()); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/impl/TrackerImplTest.java b/src/test/java/fm/last/moji/tracker/impl/TrackerImplTest.java new file mode 100644 index 0000000..d08e377 --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/impl/TrackerImplTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.Socket; +import java.net.URL; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.KeyExistsAlreadyException; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.UnknownKeyException; + +@RunWith(MockitoJUnitRunner.class) +public class TrackerImplTest { + + private static final String STORAGE_CLASS = "storageClass"; + private static final String KEY = "key"; + private static final String DOMAIN = "domain"; + private static final long SIZE = 0; + private static final String NEW_KEY = "newKey"; + + @Mock + private Socket mockSocket; + @Mock + private RequestHandler mockRequestHandler; + @Mock + private Response mockResponse; + + private TrackerImpl tracker; + private final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(Request.class); + + @Before + public void setUp() throws IOException { + tracker = new TrackerImpl(mockSocket, mockRequestHandler); + when(mockResponse.getStatus()).thenReturn(ResponseStatus.OK); + when(mockRequestHandler.performRequest(requestCaptor.capture())).thenReturn(mockResponse); + } + + @Test + public void getPaths() throws Exception { + // See: GetPathsOperationTest + when(mockResponse.getValue("paths")).thenReturn("0"); + List paths = tracker.getPaths(KEY, DOMAIN); + assertThat(paths.size(), is(0)); + } + + @Test + public void createOpen() throws Exception { + // See: CreateOpenOperationTest + when(mockResponse.getValue("dev_count")).thenReturn("0"); + when(mockResponse.getValue("fid")).thenReturn("0"); + List destinations = tracker.createOpen(KEY, DOMAIN, STORAGE_CLASS); + assertThat(destinations.size(), is(0)); + } + + @Test + public void deleteRequest() throws Exception { + tracker.delete(KEY, DOMAIN); + Request request = requestCaptor.getValue(); + assertThat(request.getCommand(), is("delete")); + assertThat(request.getArguments().size(), is(2)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + } + + @Test(expected = UnknownKeyException.class) + public void deleteUnknownKey() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("unknown_key"); + tracker.delete(KEY, DOMAIN); + } + + @Test(expected = TrackerException.class) + public void deleteFails() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("something else"); + try { + tracker.delete(KEY, DOMAIN); + } catch (UnknownKeyException ignored) { + } + } + + @Test + public void updateStorageClassRequest() throws Exception { + tracker.updateStorageClass(KEY, DOMAIN, STORAGE_CLASS); + Request request = requestCaptor.getValue(); + assertThat(request.getCommand(), is("updateclass")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("class"), is(STORAGE_CLASS)); + } + + @Test(expected = UnknownKeyException.class) + public void updateStorageClassUnknownKey() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("unknown_key"); + tracker.updateStorageClass(KEY, DOMAIN, STORAGE_CLASS); + } + + @Test(expected = TrackerException.class) + public void updateStorageClassFails() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("something else"); + try { + tracker.updateStorageClass(KEY, DOMAIN, STORAGE_CLASS); + } catch (UnknownKeyException ignored) { + } + } + + @Test + public void renameRequest() throws Exception { + tracker.rename(KEY, DOMAIN, NEW_KEY); + Request request = requestCaptor.getValue(); + assertThat(request.getCommand(), is("rename")); + assertThat(request.getArguments().size(), is(3)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("from_key"), is(KEY)); + assertThat(request.getArguments().get("to_key"), is(NEW_KEY)); + } + + @Test(expected = UnknownKeyException.class) + public void renameUnknownKey() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("unknown_key"); + tracker.rename(KEY, DOMAIN, NEW_KEY); + } + + @Test(expected = KeyExistsAlreadyException.class) + public void renameKeyExists() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("key_exists"); + tracker.rename(KEY, DOMAIN, NEW_KEY); + } + + @Test(expected = TrackerException.class) + public void renameFails() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("something else"); + try { + tracker.rename(KEY, DOMAIN, NEW_KEY); + } catch (UnknownKeyException ignored) { + } catch (KeyExistsAlreadyException ignored) { + } + } + + @Test + public void noopRequest() throws Exception { + tracker.noop(); + Request request = requestCaptor.getValue(); + assertThat(request.getCommand(), is("noop")); + assertThat(request.getArguments().size(), is(0)); + } + + @Test(expected = TrackerException.class) + public void noopFails() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("unknown_key"); + tracker.noop(); + } + + @Test + public void createCloseRequest() throws Exception { + Destination destination = new Destination(new URL("http://www.last.fm/1/"), 23, 32); + tracker.createClose(KEY, DOMAIN, destination, SIZE); + Request request = requestCaptor.getValue(); + assertThat(request.getCommand(), is("create_close")); + assertThat(request.getArguments().size(), is(6)); + assertThat(request.getArguments().get("domain"), is(DOMAIN)); + assertThat(request.getArguments().get("key"), is(KEY)); + assertThat(request.getArguments().get("size"), is(Long.toString(SIZE))); + assertThat(request.getArguments().get("devid"), is("23")); + assertThat(request.getArguments().get("path"), is("http://www.last.fm/1/")); + assertThat(request.getArguments().get("fid"), is("32")); + } + + @Test(expected = TrackerException.class) + public void createCloseFails() throws Exception { + when(mockResponse.getStatus()).thenReturn(ResponseStatus.ERROR); + when(mockResponse.getMessage()).thenReturn("unknown_key"); + tracker.noop(); + } + + @Test + public void close() throws IOException { + tracker.close(); + verify(mockSocket).close(); + verify(mockRequestHandler).close(); + } + +} diff --git a/src/test/java/fm/last/moji/tracker/pool/BorrowedTrackerTest.java b/src/test/java/fm/last/moji/tracker/pool/BorrowedTrackerTest.java new file mode 100644 index 0000000..327daa6 --- /dev/null +++ b/src/test/java/fm/last/moji/tracker/pool/BorrowedTrackerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2009 Last.fm + * + * 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 fm.last.moji.tracker.pool; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +import org.apache.commons.pool.KeyedObjectPool; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import fm.last.moji.tracker.Destination; +import fm.last.moji.tracker.Tracker; +import fm.last.moji.tracker.TrackerException; +import fm.last.moji.tracker.impl.CommunicationException; + +@RunWith(MockitoJUnitRunner.class) +public class BorrowedTrackerTest { + + private static final String KEY1 = "key1"; + private static final String DOMAIN = "domain"; + private static final String KEY2 = "key2"; + private static final String STORAGE_CLASS = "class"; + private static final int SIZE = 1; + @Mock + private Tracker mockTracker; + @Mock + private KeyedObjectPool mockPool; + @Mock + private Destination mockDestination; + @Mock + private ManagedTrackerHost mockHost; + + private BorrowedTracker borrowedTracker; + + @Before + public void init() { + borrowedTracker = new BorrowedTracker(mockHost, mockTracker, mockPool); + } + + @Test + public void closeReturnsToPool() throws Exception { + borrowedTracker.close(); + verify(mockPool).returnObject(mockHost, borrowedTracker); + verifyZeroInteractions(mockTracker); + } + + @Test + public void closeWithErrorInvalidates() throws Exception { + doThrow(new CommunicationException()).when(mockTracker).noop(); + try { + borrowedTracker.noop(); + } catch (CommunicationException e) { + } + borrowedTracker.close(); + verify(mockPool).invalidateObject(mockHost, borrowedTracker); + } + + @Test + public void reallyCloseDelegates() throws Exception { + borrowedTracker.reallyClose(); + verify(mockTracker).close(); + verifyZeroInteractions(mockPool); + } + + @Test + public void getPathsDelegates() throws TrackerException { + borrowedTracker.getPaths(KEY1, DOMAIN); + verify(mockTracker).getPaths(KEY1, DOMAIN); + } + + @Test + public void createOpenDelegates() throws TrackerException { + borrowedTracker.createOpen(KEY1, DOMAIN, STORAGE_CLASS); + verify(mockTracker).createOpen(KEY1, DOMAIN, STORAGE_CLASS); + } + + @Test + public void createCloseDelegates() throws TrackerException { + borrowedTracker.createClose(KEY1, DOMAIN, mockDestination, SIZE); + verify(mockTracker).createClose(KEY1, DOMAIN, mockDestination, SIZE); + } + + @Test + public void deleteDelegates() throws TrackerException { + borrowedTracker.delete(KEY1, DOMAIN); + verify(mockTracker).delete(KEY1, DOMAIN); + } + + @Test + public void renameDelegates() throws TrackerException { + borrowedTracker.rename(KEY1, DOMAIN, KEY2); + verify(mockTracker).rename(KEY1, DOMAIN, KEY2); + + } + + @Test + public void updateStorageClassDelegates() throws TrackerException { + borrowedTracker.updateStorageClass(KEY1, DOMAIN, STORAGE_CLASS); + verify(mockTracker).updateStorageClass(KEY1, DOMAIN, STORAGE_CLASS); + } + + @Test + public void noopDelegates() throws TrackerException { + borrowedTracker.noop(); + verify(mockTracker).noop(); + } + +} diff --git a/src/test/resources/checkstyle.xml b/src/test/resources/checkstyle.xml new file mode 100644 index 0000000..5a1c437 --- /dev/null +++ b/src/test/resources/checkstyle.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/findbugsExclude.xml b/src/test/resources/findbugsExclude.xml new file mode 100644 index 0000000..17344f8 --- /dev/null +++ b/src/test/resources/findbugsExclude.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..d7f2348 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootLogger=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p %c{2}:%L - %m%n + +log4j.logger.fm.last = DEBUG \ No newline at end of file diff --git a/src/test/resources/moji.properties b/src/test/resources/moji.properties new file mode 100644 index 0000000..daa6075 --- /dev/null +++ b/src/test/resources/moji.properties @@ -0,0 +1,7 @@ +# My local properties +moji.tracker.hosts=localhost:7001 +moji.domain=testdomain + +test.moji.class.a=testclass1 +test.moji.class.b=testclass2 +test.moji.key.prefix=lcmfi-test- \ No newline at end of file diff --git a/src/test/script/init-mogile-test-data.sh b/src/test/script/init-mogile-test-data.sh new file mode 100755 index 0000000..58c04f9 --- /dev/null +++ b/src/test/script/init-mogile-test-data.sh @@ -0,0 +1,53 @@ +#!/bin/bash +TRACKERS=${moji.tracker.hosts} +DOMAIN=${moji.domain} +KEY_PREFIX=${test.moji.key.prefix} +CLASS=${test.moji.class.a} +IFS=$'\n'; + +function clear_test_data { + KEYS=`mogtool listkey $KEY_PREFIX --trackers=$TRACKERS --domain=$DOMAIN` + + for key in $KEYS + do + if [[ $key == *" files found"* ]] + then + break; + fi + echo "Deleting: '$key' ..." + mogtool delete $key --trackers=$TRACKERS --domain=$DOMAIN + done; +} + +function upload_new_random_file { + head /dev/urandom | uuencode -m - | mogupload --trackers=$TRACKERS --domain=$DOMAIN --key="$KEY_PREFIX$1" --class=$CLASS --file="-" + echo "Created mogile file: '$KEY_PREFIX$1'" +} + +function upload_file { + mogupload --trackers=$TRACKERS --domain=$DOMAIN --key="$KEY_PREFIX$1" --class=$CLASS --file="$2" + echo "Created mogile file: '$KEY_PREFIX$1'" +} + +if [ -z "$KEY_PREFIX" ]; then + echo "You MUST declare a key prefix" + exit 1; +fi + +clear_test_data +upload_new_random_file overwriteThenReadBack +upload_new_random_file exists +upload_new_random_file notExistsAfterDelete +upload_new_random_file rename +upload_new_random_file renameExistingKey1 +upload_new_random_file renameExistingKey2 +upload_new_random_file updateStorageClass +upload_new_random_file updateStorageClassToUnknown +upload_new_random_file list1 +upload_new_random_file list2 +upload_new_random_file list3 +upload_new_random_file getPaths + +upload_file fileOfKnownSize data/fileOfKnownSize.dat +upload_file mogileFileCopyToFile data/mogileFileCopyToFile.dat +exit 0 diff --git a/src/test/script/permission-change.sh b/src/test/script/permission-change.sh new file mode 100755 index 0000000..d634156 --- /dev/null +++ b/src/test/script/permission-change.sh @@ -0,0 +1,3 @@ +#!/bin/bash +chmod u+x target/classes/init-mogile-test-data.sh +echo Changed permission \ No newline at end of file diff --git a/target/classes/fm/last/moji/Moji.class b/target/classes/fm/last/moji/Moji.class new file mode 100644 index 0000000000000000000000000000000000000000..02db5b8ecd00d177b9fdd62e5ae3229a34719437 GIT binary patch literal 631 zcmb7?%}T>S6ot>kq%pB-YxNBj8UzR2R=ZOuq;wH+A43ci(o9JvqOaz{2k@c9o2j(W zPDM8}FyFcN+@JaUdj9}$jWZV(L;rcsi?~*NE_1=Bbhxk?dU^a36DTu2d&<*9F}PW( z?nRL@Tn@uEj#MSeY!rlBiCXc9;ntx3Pkc9#i?;_km6& zC1hE<=rRm+)f70Htq6l%h??3vHui)Lct|Sh#*0K&#;U1k4yMjFLvD7|H>O+{ z2FuB20@98DqWjIrk7QFN>2iBsyX32O6deYJJ^BoQLpyiSLA!eguD9U5S;Z@pCE|fAE*}tUOihVC?5QR7-C7OiL_Dl*F5+Eew4WFK?U)$yEC)Q z&OV;EJAgTQ0V;&i{vcABdvS1y5o<1?RNiIR05w8Ck!Q)ejm2h{Xypl=So_H4URtFI z!^w2@N*;X14cFO&5~(SU36dMi9)>q literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/MojiFile.class b/target/classes/fm/last/moji/MojiFile.class new file mode 100644 index 0000000000000000000000000000000000000000..4d8a8e8520f99320d0f2c870580e317fbee0d4f7 GIT binary patch literal 679 zcmZWm%T6jm6s$%a5TBs=xbcyw7>wv$++AO6$0jq~Wau&SYcBi%Kgw7$ zh>&qJJ*TS9sq=Vye|ZIPfImeP1ZoeFAIgFABiqyd6+dS>R7K1RtoG!SBxwS_{lBNW zPQX*odf)~EGxf$lf%55dSM{B?W>Cbuz*?4}t$*JBVwiNVLgiGJ^p|0*jEor8>aB@G z$Hm$N%?1U_fpX`jpEzPlMgp7l#z>h~o)Are6-wKQo6>GgX{Q;A%U#j zOQt|(?1jn%?lEihLZBMid;Ku%I2+4A9dk<@yhO)q=^pu89a|+%hklETp~-wl2S&Oi zrpMkl!Qp5Qqnv+t(`p_u!DGQoH56DMtFw)N?t=x%pV&xh!wEVziMwhv!P59x`{@=v z0rt(q0t>v|96ZKFMzefNM9NsmA&XebAChs=?B~F WtbiT%wTmBYQs8I$vd*uJ`TYSvrjU~W literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/DefaultMojiFactory.class b/target/classes/fm/last/moji/impl/DefaultMojiFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..7b35fd8d71995b0b465ae8d8a6a5497905f3a127 GIT binary patch literal 1277 zcma)5*-qO)6g?B%7!rpiKtk!l5+I2~Xbh7L`-@GnUfnu8ZMcrmkz|;Q<=@L;$ee{hJATXN9z*uvFT#-s zxL4m~7#D%#q0|_*d6-$E9*e*K>uXY?~?Mu_!q%F-({K^@f}a2GTGX zCJwmge(bbP-Qd_hZMw2)zGF9QcEII6GHT+2w;9&r*Ed8wvR6cK-)jroa|m*lIK~bq zRkNw1!Z07RD?sVoCt0Ro;+Z@~mBIPX#`U8O*2Lfr7o*Ekl8*#Ch&e-Q_sVfErOMhm zHt3p_(7A8zAKrOLcW2Q%!R>sl&A2Lh%)+KDGIOu08BGH A5C8xG literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/DeleteCommand.class b/target/classes/fm/last/moji/impl/DeleteCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..4434ca51ed67da7ee63e9e65f10ddaa042bf2f3b GIT binary patch literal 1151 zcmaJ=T~E_c7=F$+Y1!(S6^Fq z+~@1AZCg%TL4qOEu|8T9=Iojq%?@w*3dR`n(bP9oWSGbh-{+qg^!i!yrtb-7ca>qR z?b?=bNO&R~;Xh?4mZEV{OS!?2Sa;hzi8#_4l9(WzQn@h>hASErs8YJBVH_&ML|r)i z?Z9qwZ_8?Sxn$$EtZu{dgw%(|guf^H4D`JK6oaV<*L=P4*OS653JT^JawAO6)zdJKl$>F(CkAEc zXPCX12ZQRn;SLxkOXcW%6%-k!&M)lQKy=&OOJWge6$V3gWGsbu;WpP~KvlyMmWiF! z>v5;eu=JnBMv;dQt19kLZnZ#9XHCTl1A8y4)->G0ZECd{Qel{mkUuR!v)*(EUW>mJ z!Ny0pF3a9fpf?hy?-F9j$VWpzL`FUty5saEks_}~(hfyP%Y<=)%SPo08KZgx{VSPb zn50ujg3cK_b5tOYH0|&jSqfnqvX~-`3Is4LkR(uy0Ip-2?i*xFkTw$lzM{OCjH(!i z1a=7h8}f0S0VV_Y0s-9&fU;ruKR^rQp~1^ztz(+Y&<$ z1l>H*oifokFoz0hRhYDO*>=dLHe{oc=RO_;Y#s)6 G9{mA082zpQ literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/Executor.class b/target/classes/fm/last/moji/impl/Executor.class new file mode 100644 index 0000000000000000000000000000000000000000..cc64b1ee44e095ab055f3f705dae232d963f8cb4 GIT binary patch literal 2146 zcma)7%TgOh6g{oifHYt%0}d}Ej0uv!mV+_G2onS083~d=aGtgYsS!g)5+#is5*NEH zQz@3-rYcpgES#n6!BMVh#=}1J*7V~+dkQKOG$KN@SrlMSrq$*%= zZZ9KuZL?S?YZwu@;KXGX6HBWnJ1Q5WI&NZ&{-o#%MIb_guggxd0q-&^)xgX`F7jwU zgb7S)V7x8$TG4SE6qY|LH!IZg;8}sJ*(IJ&o{>yd_~g)lJCv%c(@6iL{UB!WzJ^&A z;8bBt1zYkt=$OO2fcDU^Dspv8pgp?e7Rh-|t{V%rh-D3wd`}I*`W))`04WO5&HY`| z-ag4rUtRv|-Wfv=VFe#*SY>T%ZmILs@ey<;CSw)LvH@$1dfKv>%!uK>&d8QwS~9EQ z6M^fmgmMB`g|8IMjG^j%QeLJbbF7)(+IrU0W!W@0IDt#GzI8#K zQ}?+X*?{;on$~Rgvb3@5Ja!Eajuq=vZ+*K|{HEF(n#q(S3*;0_k7T z8Xo8Ux4fV+?)w>GWqPm8_Z-uXd1&|$3my#rjYiJW0#7IW9sU)ryQ=Fa2u}n~w>tta zzW0eXpMv}8=&_61s!%^xi(}lYFBn&l4}THs&iFZFTtpgb^g5}$=dg{EQH*@z^cqGh zIroz1AeoMl;cb4WF^&cPg)ZU-mN80C+9AU3Hic)q8W1g7mlr$Niixmv|&$AXEJKD}#U1Sx?t70;I|-R72TKX$hoQbP-BZe!hTds>f;Nwwp31g0v3 z)%6j>Dl&3k*1Ae=cxvZy+pX=%HXZGLPZhmz;tM5>dbGS>lP%>1e#gWmf(rwP+fqGp)ee!*{Xg;|KD)|mI2HE0G=>~(6Vn2xLo~fN z7G{taFuhK>*;4E%Dc;n?7A=fo%*3pK{YHpS{5?PT(SIlpdQ~w_T!6r(w-Y5W)QT!f zpTsIK(cij*)^Yo?C+;ppx-XUUow}KJ&JjxQ&=RtzER138;$#-zHrOJ7x|W}_Vnqu(Q1>JT{-u1%m>8o*8bHb73c<>q08>ttNUz)UDKT1PS3uTgzg;TtBny06FfdFKzz zKYf8xrI1-T!qSH3W;{G(ZXfHJNWXKe7mjduqmX$J>PL9^TQ7HxtpcvH4cFM1Ip)32 j>TZy*c@lDyrxI>qA;dNj<&UwYXsu#}=mb{7c#r-Cm&$xq literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/FileDownloadInputStream.class b/target/classes/fm/last/moji/impl/FileDownloadInputStream.class new file mode 100644 index 0000000000000000000000000000000000000000..83d9a5f6c08ecb62f0b5133f6684509b1a8d7869 GIT binary patch literal 2506 zcmb7GT~`xV6y29Zh9u(`Dq2Or?FrGYMl(1Fff@(Cljr!t3RN> z#FzSDTS42k+V}pbuI_th#59rG@-X+~+`G>{=k9aDpMU-OJAe^H2D$_aOEt^08$bBjEhnhe0>7c4T;;=>2%3K6`qdU-b)B9U%Ym~h zaHy<(HzU{MZQp5zq4Xomqp?AM&Z;^-=6J5}MwiGbUzit2PXvoHi|yEHV28loc7{{7 z69wT@6TR3akg3WjW-f3nAN%ucziLe|MUBxyXMh<9>@l$g-2&Uot}m}QYZV#ZwksYZ zW=0&_o3}$(t&`2}v4r7;#KS+cIr`o(3-ei|aKOYqn9Bb_6B%R;3<_8(zW zRcW&u7De~8i98AdTk;E+3njI4M(tPvq9V|>a9O~hsZ@%M=alg*odiouJVHk)Px|!T zSas_h^3#QB1#;2Eryy5Bo0z5)sB_m-&!()GO?@*~O8FfoJcr25$QnsS!Z`tqehi{g@FqdDBb zEdw_NcC>5P#2juj5sR|YWci(F6)a1ZwUlT{`<<<s(|zt21_8i`&41HxH+M-d+xaCvhpHYj(&YPBTi) zz&7y^Uo+221D;!RBSsR+hKv+HX~Ka?cy(*8S+56SBx9-EQ+C(H5~?|@f^EqY7<^A_ zFySk>?l?#TeEH)oPvWT-<;?`_96`v4BDoAzxW#l?hdbG=p~N4#p&fa;1Iuu*({D=4~}ZQq4+kA;kW{( zPXi}3oj%o6=v}CC<5;9M&DF4`Ji?hG-BbJpN7iuaN6ur$-FiQ-(N%yvlb|aks{`pG zcT|iSg*3c|58I(mX{f&V&<4>Af5AS|Bv{@ZpmU_E0?ujB{)21yxE*#x!xrOX8?d6jjaNUghDUFu z8cY5fWmDcHa}_>^k{??{N4)2>WkZ;Y?YK9{=VlAjx8h_&NyxQjf4M#TciwToOSRGD z{(X*061m~mSP^)J^o&X-*a|WTl4NevNA7WFfpzwvCDdGt8o!}U@Ptld$>41+LoU0} F_#2<1(xLzW literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/FileLengthCommand.class b/target/classes/fm/last/moji/impl/FileLengthCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..da3bb722e96b08249c6ec4594ff490804b604336 GIT binary patch literal 3684 zcma)8Yj+#f72VgCJhD7_i9C>#z_3hWOSYmC%p-DM#E!9p?6`?Z>X;D5(pVZtni0|{ zcAAC)C2fIHUZsVW($WVhZD|R_4lZjo{nXXfU(tWSKd8I!%t*FnlT~?n?##XCefBx$ z{`(kzw=$w9MVqzxWs6QbWz)*>s3mI`b<^TY!Sz5uoDc{d%oI$^ zbPkiZD|SX8&~ImrFt%d5f^7mj8qM={sn~(L1zK~4KJED zCqG`+GZ&4rz6O?sAiB+R5RQ^^-O{h=&yP3hY`1 zmIiq$9>ya~Is07JGcyLPH#5RWtVmNDlD zY^_OHQG7IOSU`1IEyGDZc`6;k69n7P`1qJMbo{XaEpb@8aIy-Szb8O8n8J1>RrK;nlH3TASQ@t>>S-q0d zWUd{V)hf$(9XGxBy1K{nWqVdOA0|{HaaP5gi|D*w871gK__n~lQClm?0@9}Ks+Hxl zta{Lgyu+D?7mX{ccl8O0tfD_-UrhPgX-{7@Lf(7H?u5$*| zd#m6DwrrQgb;c{`V^y<|HOe7;k1_Yn4C8rRlb!E^eEWflAL3<>2ED{^5(|lStX!F) zRe}k@S6MBt-%{FZDt?UDS?Z4M(KQyT-)F*j4%cXs-=qZYS=~w-niqR$&<#3)=*6er zb$GN<&dlYqU?aR!(Xi&^Xs-v3==aE1w^hmwNR<@7ERE&IeL>7`PunY})#XxcVrdQb z9bC~F?M&mtZb_&GilqaqdH7cXq7 zmg{&a+Omja0{_5JD)z-(E0jEY2k#8;FTs*OWXRgsXNtESj#}}A( zDHH#M!o$Lilp@b;Ux0jnHmWS5(u}B*3Xp7vB)g8ysZfn48oDhFxJ1*I=AwbylIgOv zn*W`PxDvpZcy8iLJUvL$DeH3#V1XBmf8lXAokN5fQ&CUnSLA++soI49;BEYb z$!n*ackokMzZ>u2XLy&X?4`c6&iPAnen82uBD6#VDGG#w2NfJp zFsaZtg>wf>7+!OtV8}f?N~%4_6#Qz5U}MXNN-3`kIirKdYzda!e{RMw1IhSUFP4d%N%QD9$$x9$XVlS zewTS@qQU_QxLf#rx;yd_A$C^QlEmHF?#Lf%-x7QL{$uUCV*!8W_g~#_f^u2+Ubi48 w*g?)Q^OHp1Q|u?_nYgEk#S6sSGsKbZ+P2-(??)v>%-z6ev`!FPGKx3<2NPk10RR91 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/FileUploadOutputStream.class b/target/classes/fm/last/moji/impl/FileUploadOutputStream.class new file mode 100644 index 0000000000000000000000000000000000000000..e223922511b2906634b7a3cc2eb0dfaa6faaff4d GIT binary patch literal 5864 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 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/GetOutputStreamCommand.class b/target/classes/fm/last/moji/impl/GetOutputStreamCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..8065d68091e0899b9a119f5dc59974ac528f82c3 GIT binary patch literal 3734 zcmb7GYj+#f72VgCHHthTsRAbA5k%S;OO~wwDFi!lAWobF6uZHW-6W;$Sei)VNE%fd z5vMezg}$Fq`T$yJ`dZ~fKa@7cakEzG>c_4wenEdqSJQp&jO>vkOzXAexsP+tJ&%3% zx&QdjU;YZ<5&X_TkHXN!YOZ26yf@98~D7xFv-|-ffg}&C10COS!yTD%p*3Ze6mk z$kv8lUh33J+=>?6s^!!a%%FtC$U8NAx>a4Y8*|oT#n!ngSe1FJ;mAB# zj2;Rsd}pXLRv!}KPN6LV8nQRY_W!=JMO4%Car9!JiM<#QcONpb9e2nmZDI#@%4pcc zHpFE#Vxr$4}9*q^6Ul2D+kac6t<4sxMV(l ze7RuP1#nFR2NibdS|G$u&8+YAV+@B3@S6L!!n%pWAfn<0)~#pH)NNMJ$uKK_K_=ojj=V(nGbWzHXL*1$(y&NuB%AgD9qCiSI`i;(EE=!~xs5!ucTE&PeRHe-`2!Ywwlu#uSU>W z6!vsxU{l8-51L^HAOb@n)NG}&U7j`TQ6M;ckxpWzrv8v7g}vP`4pjz^D7MT2cJffm zyi#ws%>$cEIrPyQC#ZA#tjA}~X{#P|-yKAB80`wX+kCJ&oI+{)VtA7szI%I0T?ptg zhF_5NvMHas-X=`4tm%!{75VHM-FuE~zUcEWlj!>sVy!k0veV4EtwzC?`Xna1Up^`< zg%t7I%U>U2xJRBLe1qAfenrP!7@)ShIUC_u z{e4$5#IorRA z)(V#29gE7jF9zpi{H0`c4PWj>GAdRq-@sSpmTxM&dF!>2_vxR9x9~Q-jOaX05dgqJ z#`p=ofB6)`IG*8mhVPv-m_Qjvc;YFZIm%O$xQ1i+13T*tOo6WB@09r`p4K^;U^E%A z^_+Zw@8G+km_)Y)^#i!uw{dCkKZC5JNEn|)cRBWj5i6$bNmW!`XpR` z6Z{Jp;>|hUeUQOBf(zQV0ssEMwipxmEBu<) MDg1^ydhpx-0@BdeUH||9 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/GetPathsCommand.class b/target/classes/fm/last/moji/impl/GetPathsCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..f7a1124f934a84fc328a225f7ff11c709511d0c8 GIT binary patch literal 1509 zcma)6YflqF6g|@xb}1`h3l)?{0hPXB1q(h3s2~ty6(a~S#t%z7!a%#*v|9t;{7IVd z@R9ff{87d`vkO8?j31i0b06oPd*^I_{r>(Fz!X+A1Q>=tl+BV=bIr1|D@;+Yl*|?G zzOvk%+M-h~TXs=Hh@orO`e;$2y=|^;?ec=Fp_O5vx%dUa_=OC?J$}Ft$^R{0bF0GM z&M~wWow6lthL(!#MIF4Xx+_X%UesL5XvSir-&Lx(Oh&& zC0d1W?3#go3=m^puDA!1K8>A7HgL{KydeW&v@>+(h0R~q%UiszZwhy3y=oQqc$Ho_ak19*0;ZpklY0m+?H71O zO`;*e5LH78$9%SW7HLNc84YQM{tH|h@C=M&f`Y8LZK&YDGG;*M%Ijsc2! zV@|3DCix|e7J?r9Oh$mX0X@XRPobCahe!emOcU{$b0cMa4mvVe#8YCyutc>1 GEdK!q?^VhG literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/HttpConnectionFactory.class b/target/classes/fm/last/moji/impl/HttpConnectionFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..f1404d8f31e5fe848fb0736f24638e87e313d2f4 GIT binary patch literal 748 zcma)4+iuf96r7W|F)>bCC=^I3cZnuNSmLz^Ayq^mSrl=}yJNOxBWKrg9BBUvBrXyU zh-W?uG3!=wxIAEKt>ZbfvomM?3g1S_#N>gjAUQpL+;lqtd4*rrJ~ObYG3Pe8AH zqo;yi{GUnatYg%>I-%gsg27kM9x@hj5u_`-Pnq zsTycE%I-gJ`L2`2HC{rK{|E=}Zh}>V%f~4qZB_$*J0$3T;oAO$&WDP#!uN8ua|0fO zwy}v-V(Z6joS~k3f=X3bt>GMd=UI8wUa!<2o~Vc0{g3E;#>MZr@}7(aoPLWu&|$3` Q&UHiL*Vz}`sQ5R30i`IeJpcdz literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/ListFilesCommand.class b/target/classes/fm/last/moji/impl/ListFilesCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..491adad831fca90b80e3cca5d78b0b4173dc4525 GIT binary patch literal 2498 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 G@&5xSuz_6w literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/MojiImpl.class b/target/classes/fm/last/moji/impl/MojiImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..35b1b8512b655cf139b3128384acb93089780d01 GIT binary patch literal 4439 zcmbtY`CA*;6+HvO17dJuHE|3kuDnJ<$ZBF}3K%CfF}7M3I}jXVJFO6d7%XX2Y2>AG zn$%s=Hr`B&HWu%(SCpmxeuc zt7L(O)~ub;5Sp-ynM5hOG_aDGurnF6IHF;7|kAcD6d8VZQ+#?HV4Ku=3_qIk#XIXHyGVQ_-7FW#>{wOP<}AyH2_hrX%%WcHp&s z(miPO#p_ZRoeQEB2MruRNFqIKpbNX@?x=xI7;^WRfer+9M4M)p$88W1#Kca&hRFTJ z9i0>N#|=Dy2PuTJY?Yep=VdVU(<#8XCDg_I^m}mXsgQLtd88c!ls<1>55M$sB&y@X z8V=NDY_(8ix)e(gAHlGWA&OSZHzl%x5uDV}#Xx3cnK_lpF`S1Y(e3nh;uN0N@f7Q> zF|Q1K6sI+`Et(5u*4blC(D>|CYIQ{98pJa~^BE1V-FuD8iO-7nrVjf5n^aOntyfKU zfzkVlr=jY78YNjNo<`y8ma`l#v7}QOGn|&{(qOpHN(Vls?ZVAjd(zIRgkaVwsVqC8 zMBgD7B;Mee8)>s39j2sXTEhptg~{pah>iq z+qoPeBse=c{hVWEO9noHPqLPT8J1QpvScmxkt$7fE~3dG7OzvD9ZAZg-QZg6hvYuG4`> zkOZcKsB0-yGpXYxZ$Rz1v&%*MdZqCPvR28_aKxoS4Og3(GG3;hi3!eP*Xx+EGDKZ2 zl9Xmu=C+>dl9M|ybBdktvxF_qBhm0M+o6unX*g7urm7i=X8~6lW0Ixvi$UDR7bN4q zsA0IFze`@bS>*>(q0|3Hav*{)8Tc~3LU}8@w&{9c7!7u-UD=(X`rmDQ)xg*Ab;5A> z0tuB{saHEy76|s6l*$DgKSi{plCA1|(Oj}_uqjs}tS#XFA~i+0l%}4;rle4sy%V;7 z2ftbHcJceyq~R#xra`jN0G*v`k}lik-Anu*vFrH6?kN) z!hNS$QaVUm*2-DT35{DyyJJ@v)$vEKq5E?3blJ)-n#CaA#KCs_g)E4xBscc(rOKIe zRL0&ia2Id0H>C;%Q_>^e1m^Y=fNPzZ_%1E?huzKAJya^oYhro5!JxO_`)I0ws2SWO zyeksgmVrOWi9}zhKw)#;aT&M18P!~+wAtA_tj9%qgn1lm<)@8*J#u8_4~vi-{?wJn zL3QN;QC)dz<_w2 zZy~@xAl$dkPka@J`ubOKq%XFLV|`nQ%*WQzw~BbpVh?t4ejiuG7-T=giZh7gw3tvH zWQ6wk^dVm5PM}DDVL98Xc*1htRiGc|Z>S20D}*kw9(@}a6Z=e zE)Hq@I>yhDb&S2miGF}5c$yth?)O%VRNWuNI9*XSJdU%js|;PWdalkjan*khS3`6) ztXzdFMz*;k27HW8Tj=iNnBaAi&H{XzQgZ#9>x9E0ZE6eWh2dw11O1zLjuW~A@eN$M z7+*u`4)zSUbq6-E#8)fbZSPm_@6wb(K@ZYMtCIN%l_gKn)ziE?O*doArDr(f48mpT!Bx__A@Z3UCnL zQt8meS-0?QhP0nVeFxvA&F|5g!O$yXZ*WXte7^~dA1D}`R8i;~mGFKT>)pVQH}OmA zOj$iBz3ceRa6rIx2Z%<#UY}BM;tEc@5zg~WuX)z?3ncdv%j`1C=n8-Qyy(+wvee P;qQz>gZyVo3*P@9&Zcm% literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/PropertyMojiFactory.class b/target/classes/fm/last/moji/impl/PropertyMojiFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..e8d4fef9344ed7579a9f2a2c26c209709c3a44b7 GIT binary patch literal 3985 zcmai0YgZfB72P9bMu;({!nnjuY;4yCBrK(A>pH=4gLxPnKo|*PoHXtr#xO+EsM5%W zCZy>j=_BbQ=`%?m>C=6DNxF)0%UbnX&077?pVXDrbe}te9!Rtwq?tSS-hKAj=iGDd zzy9;LzXNy#f6@`qaQbF0ku^(hBIm5x2|HKFCT5FH!792N)BG}SW?ZMZp(CiFbIrVE za&dkokz87{GOmtR4Lx-LHG4E1nHx(b=jTSoZp;p+CvMEnC1=Ox(u*1*Q&p&xTeR~l zLmCc_IQf!m=G_G|Ted=I*YJ>#iWfXSwpEH3t&&qNW~_L@bXPTWkB*HG&rhXq)ENq) zgJhLMHG4I5Oe9n3RE0(eA0ZQE!7ZAZ=d5CU)hW3p4TnaP)5DW9H7gHmXjdzjot$as zH5?$R^$nWwJjq+G$hN*gKiciQ?b>G6E?LVOwCnV2*}7?#v+k(Bxx=^Ti_0KP)rsg& zC(<4*f0;n9x^AJeYoLCYiroYkP{%x@V>|ik&W@^xvr4+w3qtcEZTB|@W3N*K3mU>> z>lv#cs3jezG<2yD+D>9JS((ug9C4PdFj{cNz;X0xXz3qV2x9;-9nrcJN^O)}D`#L3 zXX)6A-8jN1$!_BGS89#gri zq18KrLUzu;N6{k+qz%l2FdbRPT=t{Idco>M(Nq}9u3f2YoKd=6Gq4Dr&k%K<@Q!}e z=dpKrlVlUS&lvceAhZ#|JKbU2fT`p28Xl=re^)l8%p~KSa#(WQ;pJu4w32^N4J;v} z!N^;;{R#2mR8wuLUZpH|NCz_{-qd2jak7bNmLFSu^lQp2KZdc46^Sw{>B(BZ1+cLM z=vZT%>P+k(=5}O}*O4Rpx*fYR_%tehJ1BT@VDO}_Mt0*?JrQBpC<UAp1M|Pq;!ixstlzj)axfGbv0q5uv-_-)-SzKRi+_~Ll>owrCfHCX|tf}ijEg{ zYo#h)>D8ihTdIzZ7d0HNvZ!Wl7@IgFHT25{zJjlMx{R<%GZ(scbx~mIBUAZhd|k&Y z^h&{1rwn`p-{dA{=bYa;hf6Gn){$O}uL0HGG=|UQU>tD^G?! zE2Y+d7q9F19-FsQTuGP(GqY+XGEOeX#*t9%VxC8_WZ(^fGcsAHWL+)WmYZeRTT4E< z?xyfMezYrQy-2Hh@=n#XlV0HDte?41ruzcm`7HDl~3ON9*y^r}oko7qU3PGS$N00fbrFb%gld z#tUG8d8i;o@^t0Mx+JvDJ7EqjkZp3 z6y)e6KF+si$*6_T!_jS=jz+?i%;@P zueTSU!lyYRin6AuH{lmhR?jt99p=6OSG7fBf5vCFaiMA|tR^mz^oTMcAY526(dL^F zcP=&nKj8z1qLG8KCb;92{c;U%sDc|(g7TI~+`b0f^TZXm!o+RP_zF2bsgMnCrH`yp zswW!o#+d|y*F)KdcQCpb{R_tVo8a%tHm2U?oF~1fN0QRd)}Z&)peGy9r-?4Ab?-%| zBn{B>gkGqTq`M+XH%&^82INlDVB|v@IY1-VqKi}iAnmJf;WF)|rlW(~cxompkEgeA z%ZbT4cqv_7M%}n;cD#tJ7|r)fB!vN4B{%% zw2>x5c?MNB#?j9A>l{7Jw`b7F&VPiNe4GvcG)$ah@4kWzl3eo?ZYtlRTurXNZ=DQE z2`()h!J{bC%NFj8;udam-8xrF5WO!O{PIP9IfRxEkkp}#>UjR&+!~OJ4*ySZvU;HI z@Qaul*um!FHoi0!y^ELVcKj~BHXR$hi*MaQcxLb(zH|1z{Kwv-Eo81^Q^ybG z`Fty9d0*mcsRFlYS*pOXSfuMNemp(+PlQSJtv7LS2k$IKezlF?+#i&r^cZ^ez~x~o zvo_o!{CW+eW0ed#<}=b+r6iX;Mr{aufQXK_`6Ac8MiQOPNc^rTi5Ey>lO$dwiI@H_ QiQhLPq2UkoEPy}$7bN4U>;M1& literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/impl/RenameCommand.class b/target/classes/fm/last/moji/impl/RenameCommand.class new file mode 100644 index 0000000000000000000000000000000000000000..0ca466a15044058f887e2d0cab8feecb2cf4d49f GIT binary patch literal 1258 zcma)5?M@Rx6g|^cb}1_@Efi6ZqKNIbWd*;$0w#hchAN2=h%x@ywj&I5cbjFa_$EGv zCVWIqd;lNHcxSeNrN)?MckZ3N_ndP-`t#Sf9{^^tq#?pE{?4%)_I_YF-mb8O(`;C; zxNAFn*>fD*t!s!eBzNuiHify{)>?I!*8&Ya3}fA?F9@Wa$PnG*9~n}W^W^ow7w&eM zp{MRSws6UC`G-{**0Ug75iBvx=eiUA=O(|&5L@=@JdP+52KtaB3Ay}cFGz#|4Z5@j z4fKXq#y}i8L!u&F{<7s%xxZmo8(d2EYIbAO_J#D@m$6_+>@!SMx~uMJ7*&_z2fWq_ z_?8HEHhjCb$9+o7b!G}kRqsptHGKP_^T22wE_3K{CukmL>EkK9#X;~_=v^-IIF3~$Fw~78lNx4hvq)nnT zN>3jQin>fz0};gN95heRZx)U)U>1*%HcLks`a<^+4AVP9h`=aCjuF~*^kW=pV!J}O zW|)r=TqTc?6faYjWR56Ha!X~`2qi)&pe!1*(y&Ulj7k^GL*hRq`a=wTMK*?W32_xN zi4<-r+Dw>oMw_PlDS~9Oq4bu;eWDl%yz(9S*C!}c3aR2Trne+dS{bR?Q-2`z@16QX zq5nV~$C&#}IAJrUs0EZmC~TKtbEBu!(O5<4dE7vC2cCK2Nt}nX(_vcB=TZNZJs5 z@CW##jCZCDhz-VvnYnX6&N=ta{QUL(2Y?r-*~l=|KSzGZhsuwVzVu}@2>s(hmn+dy zNy>YoLBU}oi6S0%ZCDJ2KL5h0B<}f#oxTW^jRl5{d6*9bI4fkxo{Db_#pZQ!OQkaI z9WX3(lZeZhVI>w{|74}mD;Z1mn&D-2zVd%OwKjv*NV+12ELI%kQ6MeVT6+nk#(|AR zZIv7>O|7zn92QASQ^w-sDC&sxh<8GvwI=}&+dP%}K66^?L=GADn)B`7Iz?)^D9%JM zQsR?TCr2p{PDRSFUA>8-rU8GJaG-n1y*UfSK*=N?+Sp`p%{*n|zdO8&HXbvq z-%#}zo`W4YBxN-B+6?wAm!a}+O{A!mjM6~7m3l^%Tbth3La{}&&eFO=dqEGMK4L+S z-kdaeIy1ELa4738Ss~pmI!oRKmc9A}tKRboTyJlJwR4kwkM=Sl0_&8yL1+~$V-qg1 z-6yX%t;YjAq#Gd_US=%Ktocg%(T%c4gpwf?P!;uKG`uHUXSsE6O#EY_A7kwjTNbV* cpfafb20)(AcK`qY literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/local/DefaultFileNamingStrategy$KeyPrefixFileNameFilter.class b/target/classes/fm/last/moji/local/DefaultFileNamingStrategy$KeyPrefixFileNameFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..457c78584e050a92c4e874e1b7506e435e929ffe GIT binary patch literal 1867 zcmb_cZBr9x6g^J@Y*<%GBcin}wMsP!2~k^LV`(dtqDDZGwDQ53hvWelHygUyIQ6I4 z{sq5urUa+X_yhW*Iz4w;DiHD^GyRb4^LFn&=iYPg-~ase9>7(sSV$<$t~W}aZiS^r zbJHz(&6@T~vu0hly>QO;%z|#X{>E|`=+JDuns{JdEd^%X{WZQb{Dvm5Frbj$)GsxC z`5UFhwM|nCEet7)MW=4FBo@AI#1;y}FZO&XWGWwQ8H*9i3Pbf~L%Ti~;d8e&aam!i z(hpd1amV%Da9Uv{+n;GSp6-)eRbgPJSvSKVc?UL**+?Sez`;p{WHwion^O*kk&@ME z2NqHaBNf*-3+=|537+UR&j^3XdsPRn?Bl}$p{+327xG>el4R&w&1{7V7qYu#;-Zv~ zptfq`b9`apoI;_WoI5y=3koR-)rx91-Dl==2eH}s5|=DoRB-kS$w3x5QZm1^wZ|l# ziYe_=Upcp$!lZ(lvQb3I!qfqY?#j%;Wn59PTcHlZ)=zHu{E)9$jk#3|U$KIRWqP;m zdUX>>gli72e0dMJ7;rMc>YcC$^tyuZL_o0<_CYa%{Z5c}TbdOm|MKUk^4azW)iNR@|kr@J{(=uhaPa+`@ z`050A&$t`lcQik_jgkBgKFSxixhrmC><^wNaEAYx=*>Jm-J{igoWuis%=2^fMq8Z4 zC;ZAvVkM%LL>DZd;H Xfg}wHJc|Aw6AV}Y_99#32{QizZMWg4 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/local/DefaultFileNamingStrategy.class b/target/classes/fm/last/moji/local/DefaultFileNamingStrategy.class new file mode 100644 index 0000000000000000000000000000000000000000..4003cc301ab2c3addbac38e1311a8bcfb22f89f6 GIT binary patch literal 2440 zcmb_de^VM)6g^K?`Qa*r8jV#Fo1z64ByG~xgh+m9wIoDsO|jNyCW}0DE$lk5r17)# zOSCgBc9NNXfPSb>@7uSqDDa0f8D`mE@1Aq+x%a;P_rG`l0Qds0{1{%_>`tV6w)UFTK3RkJsY8sF0EhIzDWH*}jHo%x|KL{9WKI>ner+3oTPKeYWAV@M6a zDGIcH6oyb)Z}JVRR^bhX$)W>jSXr?&$1wKLFbzA;a4m81RkFmOtXUNvf)5iaf|z9R zC6c8eNR|pe0u14zVe+k3z04ag^>U3fL>(O^y7g;sD)4^CxlRCjYf6 zjUnh2SE1tuxkO&&q~_iy$^J)v++djToRqOoTZZr?gpV=r$J_<4JiApDT4OWn;*A-!J@(T0*Ybuo`Y(Q4ua! z(8;GCLb!TQ#h3Vs{4>l7e=Gb`5`|#i5Khgw5 zIJP%Zbfgm5l_<*P0(K( zso!aVq#2_y;FbfwAOpMbF)SnNypQ1yU5(K7=k&RhM6YSJLa`Aol4;}as^*(-cxlgLF%XUXA6$(Sxj1UZMpPf0$+DsfEGA$Wy#`7YZT-H#I6T!$^P+{UB4Hm|&n z5$ywIR`H0V4v9v@X@bk(1_28H?n#I)`CONLs!KkBrzFEiF+L}0g=DN@-A(#Fokhh) zQ-31t5lR>D_7rKfOET)Z$nXsnTzdBmV+0SV@3Fa`($ef4h0ACwJvC9gXll16DvEAR z=%eD|9+NmBFD{CIju%BOx-!t6jB?K3o2^u#^GwZsojNA{?!~mD(+QXooZIv>;R9zY}hHWZm`KrCN}aRxyx4j2y50ODIy# s2OV#5_cFOQA`o`?dw?OEJ;Vsf^bC7V+;8I;lO{jFA$Jm{{0@-+0NX-~E&u=k literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/local/LocalFileSystemMoji.class b/target/classes/fm/last/moji/local/LocalFileSystemMoji.class new file mode 100644 index 0000000000000000000000000000000000000000..51ddd938597c8d6ac407fe9ebffd227b90f69db8 GIT binary patch literal 4504 zcmb7GYj+dZ72VgCG(rr97z9yjKp~E?E!(1$x`_==07G#xhQvH-2sC49ERT^ijxew3w9}M(R}KM{*eBPEa*OWMzSP}tn`CMcjn%E&OZC>bMF1m zy?@>Xuoo)^+7x=`3hBI6_R83Q4OO$*kG7FQQ@ z>65bycE&RhQFy4qf-F{u&01yqsGHB)C54Vjz0YyeVsn%qidMlX=BB-p<=MFM~BTh@qZjW%U=zf*L)F@?u^8y&A*+;>(XblA<>1|A}c79hs$ zIjfTQ1b8Ei7&c+Ei4E972AUl8osB_ZtITdwIN3D27RLV*y+GbL=@jj$N@3P6owjE4 zwh$pHJ8P93c@I8@ym_ar(EEYrS5rJ1Lp!z$=p72}y?zi$6OT%W4`)g?lXa+;)`__( z+s@kAb&T{e69%F(dfdc%m@?XJVjW@%8*;X{x{Dg>?Q6^m>En_{autTStVKF%%4}TE ztgoYy@jw<^sy-TpVQ`5gQXv(jq<+51pXgZ2)xbv;k`KaCT^7S0d`!68r(k_B<4w>T z*=Qk-)uIa|E)r;P1XnQ<4FPP-@EoMx%FS-1@( zR3jf1II$yFGWMe9xW%%8{R*3_6+dyZHnWZtjhT1~Q5M}CJ;ZPjhYcK3*y)yXX=~BS z%-iXVTPV;>I%pSXJSSf^F^(e&QNibDaAz~NszaSKffOmbl~RVuUZ1tgo>R00ox-+8 zS=FMTGIiS-uV1{YadBN=s@oUJJ7rIyyG1SnsaF_7dXlR|r_)Z3SSlqVYn8s{E&CfG z)clT(uE!}nYv37${)WWVp%gM2$od*HS7)Q|g72_;P#AcwswBKqY*ry&Z_Ga!NBbmddacqMCgBqO#-+DO5)$ab`g~BF0?OB=2C#*$%G8lNV;Vk#tiUF7XyFQel-ApulBC1wuS+nlJ(v{=! zvnGyXQW*Zci7((4rn2n3%-rzknUD-`m$@&S_zGU-M`l%+t~$|K3b;1#HBL3uK($O1 z!g;6QaI2c&IOA4|GG;=fo?DIkJM0QNy>+QSRB`0;62nz&kK+400dyWjL8~Wb^1|vh z=YTBzp@|>i$Lu}U;-X!oGzMGnxmIU^GZxWRye1}o+O)GOsx<{!-5JHtsHB0Ue=#}` z#m{-H4kTGzb@wcO{w0IIC~kgj;@i4fX8k}F5)U*k)%80QZ{YWg%)Y_`q*R%u^D=S6 z#G87ea5?Lg*!};=cD~ByL_TlltUUGS*|n1(ia#qndf2Vxv&o|CCH=caQpilw2rrgy zX=x-WINz2s|7%l?)y_cy@UDr!NhBMokb|>jH(&AWQ>`x8c!KRti3OZL1 zf0v)y@Gze-{sX(Yp%)2`lKyf$f{*Zx8?_syN8+GOo8K0gukby@xkUd8y83B+XMgGz zcJ&Y3LU;e*E%dB9dW1-TelAZD@c7x7<8H+-o;?H*X$kxp|1e z`kN<6hcF+%gFQ2Z-Xrk&>n4eNn~goCjcxTe)EJ86<2u{Ci()u1J00ZvLzx}dX8nxE z*O(N26L=m-t0e)-muN_DrP)Z{ol|@bqfJJG7D}$or=#$m80V zpX833uMQlgh$TB6_#}?eVwfqO;0{SLyDz_-;A|VF#HZRqQLN*8K442&>rUOq$#yK` z%!xa=Ff(v>1sA6VW=4#=!;$vk@C`hfFyi68BO$KsNraa1{B2|yOmwiB`b3o6-$t$t z=Uxq~csL%pe*fP}k`dCivV-1%li13GJIT`8hi7^1o5u4TFW`)ha7@cQUyCr0f=<#_ zHi9BtMv`Pyi+F(%?qeD7vPtXX=+eoIqC^j(SASKRt2}<`Lfd`nE^0s-s2F(3;K`#n zehJ(sOkJrlwWPCik2?kJ@&42@UY_b2yp7K(c1Y77+(4*nI3#?n;EN;S_Tk74Bog7l zc;q&|)Q0!))qsbHibvw1>-Ybg2saaPo}peKwCC_RF0!O9VT}LBNQX3%W4`+u5yt}} z2nk=uHwZU^BOD76T`Y@l@>__OdNesU*~ybY82z z_xSU_(C^Fm)oN+>XeHXr-yFTqOVa zmcAuG`5MEKdKzNFetV$n8g_H=hq3L~u;UKini1@se_Fv``1Kv4A(j>V{a;dV?OL6F zgMEQkN<>y>MR`0NDrDh`#^Bf9P%R-tOpw%Gn62~;+$5F`;(wDPUvuoB@5`~JaEm_L Gu>3zb literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/local/LocalMojiFile.class b/target/classes/fm/last/moji/local/LocalMojiFile.class new file mode 100644 index 0000000000000000000000000000000000000000..a50bf0669793e3a0f16dc227e54abace7ffdd75d GIT binary patch literal 3483 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 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/Destination.class b/target/classes/fm/last/moji/tracker/Destination.class new file mode 100644 index 0000000000000000000000000000000000000000..d2eefd7492401ab8a322ad1ed0e6b6ab093e8133 GIT binary patch literal 1222 zcmah{?M@R>5Iwh_OI=G{Du{v90 zKy=5Ul~{FL$A2O)kiVQ!yxA zXafB;$F*M^AJlE{ja6^i0!FRfuv%M|=SV*c#{50!NMNG&zbQ#Pxoi8c)J&Ot;WAMH zjhE}eV5G;<5)5p}=`_E+>3fd5E08_MDWQUBC5Z`vp$q+j@Uvs5)wI17#*x-=pY*#V z&%Kx0uF1;>I`Sy6oppF!D6A2hTV&Qn<#yPOpUsVm!0OGh8p0PLU~&oWdxMo+A5| z@dyU_A0!GGW#(P3*cj5tVSv4c7%c{P+`=$DR#JV1vSjCovSjZld)~&Ku(QciX2px& zkoiI*Xq8aGNmW`862j6{mlOip-C*PM6>z>M_-;>d_A0pC6U+w+JAWciInw$MjK4d> zy;{kb>|koUB**BWxLq=)&wZl1!gJI9QrZOi5DpIBA03j dCYJGt-&3q8Zi7MpP!$cUyjvkVfyYXS${$$dY*QtquChFoBaETu;+jU;(+NHfS@~<=zNHpOG z_@j*Hwu>KF>C*O`yXWP+^v~a4*8uj>$s%Wl2(>fzPnDwsF*uWf^Io36{-#15 zzH|dAhMWxKL@VFRBSY9p3Hlz|d%Uj`vlKfXOA&^yA?f$xP{c)Bm#smyXQ z<|0CF=#Pc+2zKvOd=*@mH*$`2puAC+kUo>=jCrO!rC$(g^?A_zNuy859QZ?-$6dne zP1#pd>Y!V9EkNYL8)Ejf`ZV_pe7eIJop@>Bj?O3p|NLcPEPkxw=Ph@Z`PF!iG z=nr`Mf_K3A>@cIhsF3iqm$<`Q_s;WQ7)gCNTTqT;#uAE0{xlfKH_8}RZl!n3;IYN! zrg_zAtRagWXL-(2D8M$|aBQ1iA_RXXxWkBL&H`j2UcLDnrBAkfg_YJNR_#apX!CuE zjVo+5TNkJ{+ZWjW5vlG*dKrGMV}pNHR8ZqF*Z5>J) literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/Tracker.class b/target/classes/fm/last/moji/tracker/Tracker.class new file mode 100644 index 0000000000000000000000000000000000000000..7a348a6a2bdad35b77358a0a7a5723bfe42ca190 GIT binary patch literal 1132 zcmb_c%TB^j5Iq->hfl=!b7zE&OEtXqydog~S3qQb*G7c@l z1RJf!73Iv#IWyBUeS3d-1+a&$EK&j+_Z`cVfw4M%+qI0A_9JQQGWyLTEl_C7C&@X= zv6{DSvW-B_A#*OxLm;qUYJA39M!U+Xd_~F;)HAMUHQc~d1SXE3ZR#4=S3wqef$i@a z8RiP)Tdt#|>1h%;`IC4xYDSTa2y$=kmY}A#&ek9rxYeRGKJ}w$P7njQQ+KR4I^_X ijv(_`7(o`lAWK*t$ys5f|7BRkT8#EOXK#f4ZGHedem;2s literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/TrackerException.class b/target/classes/fm/last/moji/tracker/TrackerException.class new file mode 100644 index 0000000000000000000000000000000000000000..2a369c2e2677c7fa67c6125f81f2ce05ceb3ca85 GIT binary patch literal 814 zcma)(%}(1u6ot1mfNO@6Txg(upM|p^l z|7JWB9TR36%|0P};$P_kX7H(q0_F${UE}E=qv53vdg{{EgypX9uz_M5t|!fGbYnuo zZuh_Pw+XFAH_q=WZ(v_Uf$;|I51rmk;QtnWyV;jGWrpmgps8Pr- z%TrO@pW>PrY>GjJu>MhzPVrSlRYKvy9|ev+Gcu2I`tKY_Fx0^_l^LUg9P+$dTxBqe zET4I{ycW50#5?hxYmG7g7)us-EwB{WWN{UXd@3d)EMb`|0hD+ZK!T05Tr{*VAVO6KG VzNA?VW;Or8l3X&M8Yju1{sxWTm1F<_ literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/TrackerFactory.class b/target/classes/fm/last/moji/tracker/TrackerFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..72a2073dcf3877b0fbd54560a4bcfec5ff0eb68e GIT binary patch literal 405 zcmaJ-%Sr=55UfsOHi_|(_yd9ndU3#OM99H_ASkRqFiys0*UgU1&PwuY9{d15O03-& zK@Yi1ch^)`ch~3F`v-taj3cxJC(jF&>oTZ?%Tg7*HZRtz$90@(6P#Z~=m-uo{iaDR z5_SKSSrY_<#D*WH;P~wPZkyJG{F|k*uOW3s8KEyY-Nygr3Wl6BozJ~3OIr#?oYX8+ zg)~>O4V3Q3X;SD=c`H!=|F72c!UlEACw9DXBRIKccaH-eyj!jW<4tqTZm@ds*j3)x wS(;P+=&v~!^(tL~piNtWHoe+GmvzLd!`LG-z%H{Pkrwt?@8f_uU2TxjH?h!fB>(^b literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/UnknownKeyException.class b/target/classes/fm/last/moji/tracker/UnknownKeyException.class new file mode 100644 index 0000000000000000000000000000000000000000..6c96df24d868360780a4dd27f2457ca5d01e47e9 GIT binary patch literal 920 zcmZuv+iuf95IvJPm!>WarlF=oF{pa{oQz_w%Zj}d$|aE}qo>;SwQ}t(IqL5(X z5Aa7B?`#{>*oJ+Wy?5rGbMBlwzyI9)0`L?&S)>SuXT2~Iu?u_VT!qex_Jy?JiN4V0 zi*7n&MOW6jAb#_;E&EOxokg0kH5GO|g#RlgEX2|(5w)a^8G7>WHG$p}@^zzQC$wvc zXdpA(kN=1e#AA|#V&hzV7TlojhD~Rc?j8|lpDV50OTyCbJH&xn%0E;i2le^lmk3Kg)ryLuysOd zcW(m1wKFK)0pVeQ9PSbd4W;FWLGM)BV{sZuZ-Z&GiGr=Im;#)TkEfp#5_;r`DC867Sdb` zlxsgx_*lGmjm7;dEL~ys8f&%vAEV|}-oa5lX$j2C z7{daAgkcUx3z?w}qtS#hJgl3Y0^Yo7oWDS=gsqm-t>{6y!mWm`WG0^4%mAG0x6!5o1_lg+s(RLnz@K;y7(Ic@~nLXLSG)o#XA=nPLq(+Gk2D!lLs%uS?o zdQUMssGCQ$!HjOhPimQd%}mR0chT47BAoAfs0I-3_hTl4Dw^S!mY1kd(7-6Hk#s>| zUE=?;OG^AX{hUj=Kv#{+IQ{k+%#XD*l`?fZzPk2ZZE0)o`J|q=(nhYJ;1P24Ao&T| z2CQIa2&47lrBGPG%IOsCGI71dSu%cv(5@g%YO4ier$R*ps|98=Uwu+b@6od4dqF5% zle9j|Oy`C>XJHN2Du}XRCTCy8V_3(6O6h||R@z6jG4z;}h^gpAmw-a#2MtpedMF(CV;goT*e=ji4K$v! zbQZjdo!CW%vs$L8_YMg(hvKzSx?*;idt^X+1-AUBKltcYc@LlNT5&hd_-Wei))gIJVpo5FNKcrcvgiRw{`{x>VQ6GrY#Bjq(DmrbZ^m` zK<(^H)TdNDjb~T{L|rQ(A-TyRrrI6iJ^@Wa8YCrENcYJ|;Kz}YQmp10;Erb-IO{Z2 z_t@E|=dgaJs28mLS}v8*O+SW^mexiEB2}NRH7Ea5l149nv>_w)vI5&&y=tX*KA;WV z@833M;EaO2z%qIIw7ixa(W6Ntn`IM;%Ea{^wbGe_3R5OaO=gUOeyEt%t;_{K3|Ml5 z9o)cK`F2hoatC+~Ac=T6E=a`BRjb18+($-C5^_66j)ky zFtthYTv*No6L_dT6V-<4W3lrb5Yu#I3;532Wj8U;$cdXE?iK6?&Aw2D4us@<3MC5OCTDeP(q%%yyM$XI z-ieMQLH0mrlm{xOMmm@n=9b|!VFe$uAK2=4PMy_V#dL<;`f(G%27JP7)W2c0p0w`^ zTczMzReT_a zmon%Cz;-aL8Vj?}D4I!qUs~QLmeh}Mjl|0Xz+u9x*bKDD`v~6x^7g=|m%okhbAJ}U zI{1`-jR7$Z^%n$w;>Qdu;IE%gaOgH*B^KJNpfE?8tGbW-Qo`R}!s7PGZ)mCN&as`Z zhKCLsIdY|bvm=eASjI1DUjiZ#cnA-3ua#>ael3^4#AO0ixN`ev1Okr++QzWz9y(@1 zbc-Gl^N5(YjP(QUzoMmtjWMs3+7t^Oe!Y&7iM@)7^KlEavMNM=t0YMe2z w7sn!juk7zJd{edEOzc26qwiq^y~KWym=D#I(hApC1E1gFdwLVNV`KZ_U)az^qW}N^ literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/Charsets.class b/target/classes/fm/last/moji/tracker/impl/Charsets.class new file mode 100644 index 0000000000000000000000000000000000000000..6126f54d0fabeef6146010bfb65a4a7b4c2412e0 GIT binary patch literal 1097 zcma)4T~8WO5IvU#7I0AnORLx~ixpOpwn>c%#M%%{Oi>>Y64Hm}LRVb6yO?D$;a|1- zplMUn=l-asbC*>%ZJP3OKhK#nXJ&r>`g#N46*grA7*?KJG zQCVGIzQX+pF-!n>B*i_;cp$Q(^CFf!dVzLNNrb-?)iiddQQMbsJ zP)=VU)p{qXH?6X;a3E!_@p$9UXzVWgD7Epq7eggQAs$$bR5&d1~Up5AdVJo!zF9 zTJ~XP?wxbbIdk^&>-_`30X7X}2>W-AX^S8<9q&Pzp)agQ>6^-VvdyOFIFYL?5h~9; zeYND1J{ZUnwo|9t$@Bn9Cz69Qcj#+#lSgu)Fw!j5E)?dXpP`6K13 z@R%@Ct#t_5rgtj~7{Npl1&k3UTgsJ}k#i&cwz#n+VY=m6EFf5h>wYsE-m8Fc&`P!W z&*K_|dbRZ+o&|^EcFn8MS8liQqtm|ky%#OjsCBfnIqhtQU^p@eL^pQyYo3s^L=^Dl zTq+g!Q&bayRV}bgSp10?cCn#BYlOm;7x|VvQ#zWJ^dGpV?V$n=S%;2gwJP5u;vG>99SCH V`Y>0OxoUrKXI=i=}I}-jUZ${w&Qla;5^m zz}!RHVU35(!C1$^Lwd>> zZ0?xRWeieb(b5IhUiErIWmRQH#y3$!^#;Q<>`emchRO#V1h2B~T5N7+r7yBuz~7Qf z>mgL3M#VhbN=zFfyJtXPp^BMM<>)mk0;rawMJg0`v_wVF9o4Crflvs`utLFdff^@H z4k|6VOomZpcnd1-z)Aw7XY=+ki54T;5V;DXxYW_0Visl#%t;!i-d@Ze)U9sqV1|f> zlev_Z*{xZIoO_FYd&ppPW7B|Dfo=p^(U0n>qD{G>)}tvs@0h9JPJufra-!R1@WkE_ zZM{pyYq6HhI9N0?X)h=gK+h`5D34t{gl62M;BLBn!_6LHRlE-CNn>WsK+eieC`{Z0 zSK`y#RLC<*I*r~)a@>tv}YWeZs59m(A~{&--oRVT5fx4=B3(2_H4mODM2FeUw& zO%2y(a->fIGj~lzPkPK@Gk3)7Dl3U)l?oF7T^W#RQ1uATYz<;1~#vtWfmHj#1m2wB`sEs1wOXadzUSb0GRwBr>2st<4xY~FS z0V(*Xieq?~)nBs)1$I>sT7ieJN{tf48<`Q#<=iiCQmOV4srJnRn=2^no4#O8bt8ng z;!y=}o6>ou}*&8ef%@ zCn$ZuNXshvUKQ`d`^j7q%E=Fbl@+}7?!c)v;VFDb!3S9pCVQTQY3qZ!rQ*YQnnLrM zRnWP1okUIq@evm0w0^ihl`ERIf{!w1CvWC0mZdq@a0t(U&DY0yhdFOe-H!EiCKY@_ zV8Ntz7fFw*K|D*O@|r!=AHo?~7tY|UicjM+Y+tlIsY+fhB8RV`j3 zr8IGvQo!A=HXiAd_4J!k_P5w*m9wII$jTj&?G3A>f^Q2fab!!qZCXZN`g)r2nJDS( zYk8^7kilq?f#%$x>Y~NsGuuUF+-Am%xF%@REfwJ6L7jL_9;v9#iCj$(KVw%o*;ci< z#i(wd+pKSt;r)dq#4q_7Q0|>~>6M3T3bR;^e$DVV)v>SN7{qT`j+^SryL&GFp2Tdb ztJLE=djBZ({z>wW^<$D1SF8C8{oOB6d)+N8Rb9EFmD0BvviDp#>FvZf%W5f}iIXr* z-EuxYu%PM{lRU<6>XO61eot-B;XXnXIn@^wYe0ge)GoI`0na5L% z7*|g7y*yPP9mV`;;|LZ+n?_I@T{VK+qP6}JERNO$MzAz`3Ac}+{=6e3!vAG#)qx27 zXk@G2L=~$ja5ZX4!lk$qE3pPq?%3pt=dLEss7QVQP`MmQ2Yk**x**-EmmdI>DM!+c z0C%p!t=zSmYktmRj@d79x7_~#&0dp?Uc`dN64tdfl@PyxEze=y1>Da`TM4_K#+)(q z^m5PQQS7;V0e#+!QKgWU3v8CEsY;knC*abXYKKZG@w=$IHJWP^f817X@kqzfdu)>EatG0wA z;Wuz_n;abHU}*_&lDI9tf`#%?!dp&Zc4PP*?&QcG;`u~dleEFbV+m;n+|Sr1h2JF80<2YO6~qEgHoO&*Q5;>^X}$>lMk2T7vkE%a8g+jeqg8 z|2uV_o~*AAXiTP}|>(Ij;z1WIA9Kb&Ad=SU6ACGbU1bvnH(CPX% zkq;*whOjVj*9m-wXw2uC?{ZW{{4#i%BR?fJ<9ql%*?0hp@B{pi7#-)CAK}N0i)7ba z_+R12un_-7mx3!ZeYBQW;RH8)lniuBphoeNWMlZJCH#C(BT*nZI0*l0d`^zY`ESPO zjCD>Ao(&xz8K8gL(HCGe%xiqpe%F8bnuY5 iO}jiif@IT^_$#gU;cr~`;qUkdpYkhVn9}}<;Qs+2Oy21L literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/ErrorCode.class b/target/classes/fm/last/moji/tracker/impl/ErrorCode.class new file mode 100644 index 0000000000000000000000000000000000000000..a20215aaa2c6cee4b6bc1a504bac5030fbcd041a GIT binary patch literal 2173 zcma)7TUQ%Z6#fpmOc(}A1BA9vEG=LXE~%G-7(_6p5(z;Wpbe;T2tzWE%+Q%hEf2o< zFZA8zgFGnJLhWi-pM3C#_%p2goJ=6LbuE$==FGl)XYX%c`0JlPo&k6pwuBaj8#~oR zMRVLl)!H=@uB{dKbUR^G_bZ9CZCiHA+SVob89H{g`x=>=r9|4SRTy*NOwKMfs5vl6hhFMC^l1E!Qx3)C2KA&An(@i6r=ZrTrQ>+-K z;m$JnqOo-bKW8L_fP@}~&NEW#SB20^SJlit)B46N?CA$G7%nTgf4RBFIlS6?EVNC*LsjT&=pL+pF4Eg$NI4 zt)f<0*KC8o3!w}j-sNfDVu-vhyV$yfDTa$@g91sy;VM%9zB5Jd`}U} zgeB_uv-~M{1>(>nhzXTuPP|Rpben$9w}P; z#Bsbi4=?3R(>OIm89u<02xgg}r<>ipg2&Fa7kIlrNfku+g?`;p%G=i4V>`EZxd{}kPMUpS&3RPmtaFj|+6OHT>icZ7a2J;I=x4;&$?=7UEVS05tr zD+SPkx9D$&NUj&FxJ11fq5orC1ANCX5kC1I0pDT#H_SiAB17&c z^o~Eq=M2x$y)-)Z2zSW9e@L=Th96%1?K>oApGaqbY6A>X9j>DtL-ZZS7)CHbrzyll zNY}*m)2AU#<0}*>lpzZ3YdW<+or7K&crlT=&Y8<8U< z@{7Lsv-}L_eaY8%g6*+iNK+>{NcNy#gWOi?^gRzFPBvUChgtjtmFHmp@7DrLVD5W8 GcJE(KjQijK literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/GetPathsOperation.class b/target/classes/fm/last/moji/tracker/impl/GetPathsOperation.class new file mode 100644 index 0000000000000000000000000000000000000000..ef7e2c0f5614c5f28ee94fe9b865a258c01c4430 GIT binary patch literal 4058 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 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/ListKeysOperation.class b/target/classes/fm/last/moji/tracker/impl/ListKeysOperation.class new file mode 100644 index 0000000000000000000000000000000000000000..a148ae68ed4560051ed5734bcdca0ca403a71df7 GIT binary patch literal 3938 zcmbVO`F9)D75<(qdlY3Li4q$pnAAy(Yb}mgLx}B|#3ZqZ7h*e39SUYFPs}9JNQ_3o zre!P8g(VPL_LilPrh7w*<$%v=f9vVtkKiw1``(+8mspkqN9SnXn|bfP?|%2Y_uVhP z`TRcsZo|BWfI$3IHkmOBZZexYZ6#gDNS`sCq?OHQl6_XeJ!;Mr2J@z4xK_^A5ER&W z+Bj=)i#?SboH%W!U4iQz^UX^W1ogTM_s zYEdUgTXkp%$$d1sM07~YH|x-Goxp}Z%Qgp!*$LAbF(xwPEz*}u8<|nVvE7ThYJPZX`pq%SHOpkEbjC`J3r zI>NXecWSsppl!{?zNmHV!CjO(bKz9Z$u1n2x(0o8RL4}vGe#ys-Hi?n`vi7Y1LdvO z(TOgGbjoyxT}qo#NUY(yx6xb8V0YtyhI`gvVCfzm2hk%C7(BXGf-l|0lMdmChQk88 zR(I==X(iE2>C{WvDD$tv{ZY?n32oJ`qL`Anp?m8ly&8AbxbGC@aHSurZ5IjZ9Xe!`S7 zr)52LhfJ0pd(_AjX?Ra{y?E47FpG&SGe{R zI%*?*rr*e`?A9zOI5Eu%bk|Sp3?EOEUdMvu#!^sl(%~Pd-x@A-wzUcMZtvdJOj>HkEe4*+m(g# zS2});-;fQ8&u62xg7((nq~jm2C%;5df5LBlwQ zEgD|q1-%$3ueSKI4dHiQPck0Hc?`?HH+8&)w;69EpEspQn<(cM`tkR(pq5lUfqdi;_;fht`IH}U!qyH zc*^o+Y5Ct*SuQ)XDr+Ik(y<5GF0HzR_rq|m=%mfVmh5aARyep@QcHX(+cupZcE<&i zS*rQq(aaiO!xp+05!st@6p@{oda^H5Pj+Q|24pwI)z_=>2DNr0Ht|h_Z(ij0T0VEg z=CC;ypG89~F^k4n`z*G_wgzX>6uW}$vuOE5ZExlOb{+)mLJ%!%0dJ<^R&2(Pu@!BE z73B!H!!yb^QO;;yffQ;aVFuJl0+mkekPRg50Ws9`Tb!dH0VWj0kGMvz>LA43eX&o` z5HI1j{zM6TFJu1&G+oBMoZMH!z&mK1M`BFQ<}mc%SS&Gz<39Kr!YqSN5KcRR??xk% zbb(zQyRm(^Q-N>xj`P9SvgsPbNyT709>7lt@IhMA5%?OnYIsP)-IDKzNlCzG*-*ff z+#_ACCFYh(90U}EI`wj|I!gBz_(ug45HBzv_+EF;V`8js4wI3o60As;wi-*YBMt}K zIFN)i{STYs5m$~%D3}C2n{6Sens< zXYdQ++eh4z0MG3Z#8W)XAE8&+{@Iu!-&W+9C)q&ui z;6?0gkI&=$Smd{Jc)dPY!W$*L!*!qG5484NP}B#TKK}O0b_qPF$l9j#dYA$^!UK94 zuN1}AOE-=x(4Nq`7lhWWBE;;XYaiib!izA{G6QNTtu|cXyCD8V`;vO^+XjA<5nYeK zH)s?Z-V@vTkws4}0ZTxDKj2=8eGV7<;*md>@YfS@FBlv|{!yMYCUX9-@|+R*g!50; zoZc?#pq1GJc7Gup!v+k|wPD6)gdmSol%v>7e!I!A%%=wLeBaFwBlrwTL>IsuzXosx PpYbUhzX?2q&k_0#Zu|!q literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/Request$Builder.class b/target/classes/fm/last/moji/tracker/impl/Request$Builder.class new file mode 100644 index 0000000000000000000000000000000000000000..c014d5a6fa2c4ed7d6a84897dc8869c6f81d7aaa GIT binary patch literal 2529 zcmbtV?Q#=U7=BKYvS|ou6DdZi5TunPEyN-yn%W{&Br)Zqfil!lXOkY&Ey<>AHudv| zH{b<$1^(3;p(rz6fEOh`XLqyN(sbI$Ow*jt=i`0f^X8v_|N0xiMcmgA63BhsDm3+u zU1+s8je>3I^@q|b7_F^lVMRXb%8osCt!p&brKKSvFutj8>%3q#3b$&TvTh5E>QM=>rC&gND%qy&;o zag5-IvSFC=X17(7)?K~Ur1Vs!UDundx@D-jw-~WE44R&)96E+H6PMp^F>Z3bYJ5*% zQ4(0p?zNR00#r#q9mgnQ^vY}q+!=uQavVo-7SkH01djEC$N+J$>7mEtfuVpzf_8a`(2J61gm6@u#m z$FlOLk^ch|GqZo<%`ooo!EDgihJ%@LPy3h&Wa(UCCXnMu3Ckew zp;tZhq=P>B6f^q}G%enB+#{;wEG6$3nBC!MbMZKm*>08iRClJ%D{ z3WPMA<={E#{WT8M8vmT&e&l<1Q|Ef0eS&KNO~H!zf!==%XxcBhpE}-epyUx;VrW%n zW6w~2kQY0s^lMGM0N^JFRn@$zf2U~2qqv3J9>O%g)!qoBck0EAIm&`xVqOvb|pdj|BFB~X+|#RdFr zDKj3L(G!fmN|TsP|llvrwTP)qj8?eT*hmT4s8+%DD3cy>@vS{Z@ajzZQl65;(? zTJClj@uX&DO-$ zR>~YwF%R?ULR`0;%LuHho?xPxRAuUG3h!JBf~qKiM_$QD>IbreJ-XSc^(1%=k`&Vt zQOz{u+7)^lTn}rj+nyMNs-0OQ(Mxh`qCOO1iHb#-<-;;8S5PTX^V9$;R$wIw(qb_^ zlc`!Ku_?$KTofKkFjsgrSH7zn!*bUsMOgpp2Kr;T6wPpHcyp3b?z!>OvBPUQv!3{ z)H71yZYh(|r-QrB#`Lshq>>p0Td8GN7s&A~4f-DMyovJ!oaBuoIX=9Xfl; zWF{%9FT+0USI{=;EH_jfz(Jmsv}i0{fr?3tx4Loy97czNc2?oI@!YP8PINJ`lKNR% zpB!zc&($mF&T*XcyK0woxC~JoQE^lPc$g6T^dSW=PimD8g{61}uPQhpu;S5W+T5?1 z8QsdL(9k1L($(2=Vv9i4V{$ilq~1pZy()CD*h)@oi7Y!^9EO7aoaag13U{@)?MlYj z22B+MI7z21DTqqjlJK;OQ(#P#el2sr=9X>vs23#J z+re;0D$kuS!)csVaE6Rc?ZzsG@EY&ZtM_C%W7a(0KQ~aaGe+>bit~~Oua$Dx_u&HG zRB%yXR<2N-Vp8!IF42fxZUXbCF(M%^;IfK$FpHXfiEO4{!MhAg4yr4e$)?jO)6#ow zayj7gMM#EiZ1Pa<&^cbn%(*j$oNx;Zh|ZOlG-#L^QYMb_w_PN3)6t)tiY)eu$s=Yc zof|Xt(;ax~W;C(LT281!N>mHRHNk#mGDk2mIK7aY;^t0cbQ`{}%?-{UXB4%P1C9^46%eVY1^@|56 z{~Z-~xtM|H`1J7&PG&E`7ur&}Lr>kKU0g|x`GHX^4g?=y=`gC~d-X5^<1l^{Q)vyK z)wEv2kC}iCSK%B+h(5@_`KU$M_GTT{yC&pX`d1RTkE%QT;dth;g==jX)lta9Y`_cD zaxPnEUvVNxU@FM^X+bt(6G1l9U@_shNEk6nZWT&~|3u`-C>r*Mq*Ehk><*Pzj$lW3 zaT5iBF}x@+hP?ujlG`}8uw)E}1pY=eQoQ6EyhTm7;R}_omIp!$i^uSi+&p#<$43x* zfIbJWF~6NC+`f&Hh)3db8ee~SD^xzy)-LzS*F9ADGodkLix3Qr;M@q_2#(Z>Y{hnV{vGJ&>Jpl9nY*8$1y`{f zH#sMNrN$lX#eL30nIUydJrb0U#W3E(`?T!EZ}7{NE)LH`YVO)AGzXQ`bC2YIhUL_JLI+WD2)!36JQ z3U}FDIO$#Ia^dB>mJEn;kb|49H>>H3ELu+>IEwGaOUB9XH8zg)b_Ra1pSSQM-+r!_ P5#cA=kMb+#XHV&W1iVNy literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/RequestHandler.class b/target/classes/fm/last/moji/tracker/impl/RequestHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..ad5d72a1be1512ec97ec7b00bae77268a9cf6ce2 GIT binary patch literal 2931 zcma)8TUQfT6#h;U7>L8A@rt6x3lb7SywKJXL! zPw0DJ?M0w0BhM z741mQTr?uKr6q6aR>a5^vXN>1c1bVVm$ZB;t6K^@0-cN6lE(Y_Oe8VCs3&a!ch<}Z z_~NFOi4?Qx!No}2%w%*cD$sn#GHl(VSlp2_%*b_TN1j=_mXc?CYG-4mbXvFc)UtdTeD5n2iKT@&z(nCZCYg&K5Q3g&FFT;7lEEY#f;4V3W1#}n$RrJ5jXPsR4F&FTeI4HR(E1dYT0X=WytU9X7g~>!%)@2T97Xa ztnZo^ZtPRB2dcEVUxk7e`F2o6E4+-;UNDLR!T8H2hhViAbSrIIxoXw~Vu7v23BqP3 ziUl)Y)T!Ki;cikd*aklp91}SHVySU6mn-Fsq$Y3I#99y#aNWDV4MFrP2$2)dCr?Ef z0|HGceZE9S_10%>#5OKmfB`)wxmg|;MALfw*(Gv zmgkB=1=H)ARmBv^mCTw&c95&Mu7C|?za*hn(2@&!BuV0u-w_sYEOFH~vPBhdV@^PI zgszkf-OiTRai-d-BxkPTU3q;_!F%jB4LzK#G}jvJE%-oS--TSkF8iP1=}(gk{%RZc z;s!LC)p_DmpVcaI((VjNXiwrdR$f?MK<z5N_nnh7zb6i?-l4lkBJ?rnXZ&wpj^6MzEyf z4(_sH7fbU+r_xy?fmp0TQF-wpK2mURi@{Z40jXn`Xz@Ni7TCsEOL`(LM+i%?VE_%& zR-J|QDx=ctPo>wN$;>o%G=VQT$<_#u3&F4^bp;_ z{)afQroaaekB{(qlmoH{?KtKr@>j&@>mjL;e+;CD(vJ=~NFB3>Dd(abD9y@reEo-V zP27o#Nip>+cKCWjf1uBeQ1EwjFC)U`z%pKSj-p}bC>UPBnMvnvzq9c(I)*%t&@852sh?@ z@2}wI<4~u68ObxxV_Ws{adB`rud5m2Y;kWQIo;$#BU1y{s< zUl11_zsOfaDIm|o<0n7(n|#Fc&LlKl#O6b0-tF9b&pr3P^VdJW-v!WyD+&Sv>rUlT zmR@vIIeWrPxsIMWZ8#}2SFln?jbg#h7Yzk90!t_KGdg$Xv#G&Xru#O7-alF4YZ~*}U$S97AB+%+;O#&@(dN zI%YoGId^8agfF^8AbdRi|LzZXdwEL98O5TWrQOI}SOIls#xnD!yPJ_UH4h8ablGD@ z7!7Dtuw3Aw*>U#h8P|3uHLSoxj4Nxne*6L(o2m?Sk=|lw^IV$hAQ06MLapS<%p3is z+^FFU>Z6wDqn**MVcjw1xI9_Av+UuXriDjd=HW1cSfk+)ER|lY)vyR!9oA!` zf(-&q3uRTqCTIeI!-v9zY0}V)xIq1wks)M3RL;l zs-Yf>!*~pB3bqI|POGTPwk*oeq%t);j;&0bkt?{9RF}ZArsi3=Ortw)QYsr3K$sLs zq2hHtg_Ai=SdV)J9W#o+55rHpuQ2*g>kDO$rc^3sSk{TarvGNBx*8T?FZL;Tk`zx9 zLJu``p_{TL;4Cb^R;j+(cw#e5JoT7gwc*Z4gEOG3b%}W)*bgsu~)^@0%DU~AJ8y}A%Re_V3`bd^L(BiuZp%T zHx(m<*rno_z^Ws*?O2mBBQI4t7AtzKrX%+9tFc8mj*|*bFx9iGUBh#Do`wy_v7Px1 zSDCHwK3oSK83m&c?6h6ux>Gb<3VIAwLuhEQ=lLBEmTcunXP5@E8pd&e2fel%#sn+{ zrv(~3qVtBE8akTp_KMQckVBpX`!y=CV;%;p0-0H^)v}N|qw-z^*$5U&OvVhTG6Pjm z6;9?(so?485yxhy%f&p;*!*Ko`2vzzIV1T$%Z~qGcL&EE`>br3oz24vDDMp(1*V-E z$`?z8g6+7**nXLK8rXj>V-#d3VcD0(z)*6sku>;Ddg?A~lob;y=#I-?(lC$0*l6Uc z_b^F`kY(+o{Eu}4uSm~m_LGH5!*8GK$AT;EJX?%w`w3L=E^n~uCg9KRE1A}q;e>G! zYgBx|%B)DMYrs*>4ERALO+B`0I_nDU~n zuf*Ic=|POwPmmu-HCQFDX1+z_t;E?nz6TKIn3ttD&pV1wxw-^PxnmheEquyfT}0eM z=mTWBM;Y8v#_5|<;+GnF|^H5g!JMW_`#(sJ!P50nH8L!%zmf0}v z^EeZb-zo(5vwBcEC4M1wa(w;{dXL@2p}xeQ2q&hHzKl@vS|V~}3P<}}j&&$^x7P-@ zhpytusL~i}>!^v=M25N6p+?oap(|K+9V;WxFsf&RxQb{pss`JlYE9d9yfB4Rw_r}e z9(g+?8bgh>Ki#{Vyw)!d6kBei6oj}&uT(5Y0q4*N+j~kig5)jX|1PfXVS4s*bsy!^ z#m}T}re{BAdT@x8^s_aOaX!yc0crN=KD@$NDY}EyD^>_nsa`i3Z$OQW*-{wgvn;ewC(V(DL+@p`Gr=E5W6twRH?BJjgjE5)1sHswajU<=J zJAZ^HULk=drPB=0{1g5JW$&$M)F#ZB7hT=md(WOdyL+g`ud^=#9({DG$;N*LR*o?3jqyKR)agAO55s}S^!VL=Ln zSh-%W)d{2DJFdOj`_6vQYglc<_=dzD_)goXT75p5G@b1(hjwj3^=)tQ^;Qxweb?!3 z7l!9hV(e&(BNNgmaR?*j&4<-$?T9vl?QEC!i)U$LK7hu)eQBMO^%SpkWN-DlQTxkEL9* zyvHnO3KuXh!cU5$S2QG$6qIrr^3a*mQ>#6&J;Gux|34BI5Z5#;;yM>rLJ+2N`I9M( z;0Bgd+~m3+b7Ni_KEy|yO@H7M?uO;4ZSB}CzYu~7EMfG(B)Z4x+M9#UmhC>Uw%W3^ zy_VH(Sgs@XfidpMUS<~D)U78>Ksr>6r=Xx91S%3TVIDzyt-mfpNrQ$_F0238VYNnN z&b3FP$b`}<_wvJckI-mU!{;coAlo(zawQD--|46D1=dxp5hjl3)7nWh&rVHY%bH)TUNKnYLsbK>(!mTjCDnA}xxJrRu;VOxzf@dye_6Bat zzVC?gOuhZOTo=R%;kg#Kio>f{Dy-b#ZdCYfi>;g$qXO{C!@-u6=y&#}_&Tsh0+W0q&AVrOJId?U3s5sXrKG10Fuji%xt)!svvPYb_LAMo9%jsDbPwmu zeVl)R3(W&u+Q;QXTy1V1Ei1*JFteCls_fyi{tOHHa~KM~`GF`q_v5R-e&HwzZt-_k z^332SW>7#D70jW@tA#VT&3=u!RhaD^dG0P-CJ=eWg^z+r#mB%jXU5MSZM8oUx)V*0OG{`n1D zTpF*wA<72=m;_dEFSvY%1BlC`LW}44EP_8k?}99ZKt?44aOL=o3P5f%wBVH4!}(@K x(T|!%QHH9LJ;eQM@1l`J6L}@McmE}#2Sie7(hpHRg~jo`2)>eOejW5q{U5C1rLF(~ literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/impl/SingleHostTrackerFactory.class b/target/classes/fm/last/moji/tracker/impl/SingleHostTrackerFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..c920354582b80a01167fbcf8837a8cfffd9c9a64 GIT binary patch literal 1830 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 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/pool/BorrowedTracker.class b/target/classes/fm/last/moji/tracker/pool/BorrowedTracker.class new file mode 100644 index 0000000000000000000000000000000000000000..f790f9728021666039c84192b1225c876c6305ac GIT binary patch literal 5822 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 IyxJ}Q29u#QH2?qr literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/pool/ManagedTrackerHost$ResetTask.class b/target/classes/fm/last/moji/tracker/pool/ManagedTrackerHost$ResetTask.class new file mode 100644 index 0000000000000000000000000000000000000000..8385738555f209c172fc9110c7eeedfdd562855b GIT binary patch literal 1288 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 literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool$BorrowedTrackerObjectPoolFactory.class b/target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool$BorrowedTrackerObjectPoolFactory.class new file mode 100644 index 0000000000000000000000000000000000000000..13502196f8250efbebf7c0cbabdb398a734aeecb GIT binary patch literal 3128 zcmbVOTXPge6#hE9VZt(m5D*9uj2cKbw~7p_V#TUtcBZ?}>F<2!JE!~O zzdra7z!AKrp+Vr>l|srkihio#&RHqnGt7DEr50S*PEC|--#YCU{fpt~6bCy;UC(o` z%Um>eE;}bpU(Jjgrtf-78sY-STrZz877TM%rcAd`aGhf4a?~iwvvNu1l!cMCWoRGS z3T(?sTjmX4Rz{9w>H@S13wCNGTU3C{_#&IMz$SmzDs~wZ3gxSFHN%zfTE)-ufoNb~QR<&2G(+GS6KKLDvKxMY1|J zm7YPI*Kq;UB$M@(;lWlZSAij>sjl1u-Hq{}Dmj*EsH(46F^$i#ot@;V=XJQIS@JyQ z(sjzIR2fX*SzOZa+`65;zIz>)@qE|}CfEtsFG4j{6Bp%Kh4!M3m+-QH#?Vwd;eKmH zv{oKf2QqYI)p?{B3~zpS*gSet2uhwqyq+#1jW42o)!kC+# zqaw;#rs1?tVNVzfQ9p0KX4qDa=lBLQc)1AJZwHy_TJ{KqP9K2 zs{j=$fZVsB4aZ|Q<2Sjug~!WocoD$8N)Y&&roV9JSI+##9R7}d_+tg7?G#kp waC;(fZG`Hh#BdZ$e*H~%aScMlwcwA4HFz;DTjYWUEa6rDD*dq`;RYK10jHm9q5uE@ literal 0 HcmV?d00001 diff --git a/target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool.class b/target/classes/fm/last/moji/tracker/pool/MultiHostTrackerPool.class new file mode 100644 index 0000000000000000000000000000000000000000..06cbf70502b0266f4952dc9e628dc5474892c9bb GIT binary patch literal 5911 zcmbVPXLuCX6+LfRF=90oQ$#h0=BU9c0t{vqKqwSpHA^x@PU5U~2WeroD`rO+IZkYx z#A&wEoMtX~r!s`74;bb)9ga^_G5@E-V#tvF`crcw#hFgb{PGV;|<8-^L9b7aJRG4uf zdN@j{RA0Eg_kb013`|y7?u+EfZjZ*Cv^}gaDVgq5nAwuH`@)&z{ga54?`-bgcY9NNYe!>eV|RO}LVb%bu4X9YI_yN+PB_C&>4Cwh9px>ADjBrXM|j(e z7ELu}IpGdj;Z{&271n76(ZOh}-wMal0|V()##K8pFqjPQuu_(th}~=rTX9!M(VpVw zKr|KYv*L1fCU?P*lSqbJ5*ddlrgSCxQc-8fwiLFw_vf}7CQzo;1@;Uk_>oP7D{Y7+ z6RCu=iMFe(+D!$T(s3(OM0d@u>cFnrq6O*3oERwbm?f%HPP#tNY&+z zm;#0CO&BOOutcHmYRw`S!-a8!qog>k~FanH_~MZ$I_9gP+nP8 z5CYBKdZY{+u+hLxR6Y-o-8B(G1O1$`jyMXlvj_$G7BRqZSdfeD%ak>+MWJ?l&s`pC zA#BE0@miC@63?uXHkUz1s&v6k*H?Ra17XTA)M3gl!ZuJha){gN*mt!YVeZ*#bIsFM%u)3=?3Tq1eD=B4B ze0o86OPbQF-mb!;iPDj|lh(~6*BaJdi18@GI1Dpu$V|0 zkZwR%jd!24wD9FQ9g!d$HgPB3#OUeOsp7_%!VP}e=^AWop?Nd?I1%X(Y}_q@eviVu zLeTDhW~PL-PNRFXg;k^HG*u`oXs8@V{0%yuf{ZrD9ZnP$`-|p%q1bEY6sVXZK|h)F ztJf$bs;|oy-CX-o2p6wiA)pLAK%jhq+!@XEGebhSA4{czu9cGeeiI+S2i>^x?x!Z% zoBRu~be|mp^IT})h=GTxc%Df9M8?FU_&AezC^e9d zC-&KPN~ zqzz8zMTL7pyvM!myophKh2iNG$SSCwLc~ss!)8hlFPZq-Sj7C^*G)WE0OfADfzN3y zYcd+4S&+VE;v4v;YawoD75lD=y*qyV+Qe@JIG9P?X-PKy&cyHW2YQjr zq~|qz0Xw#s{b@`U%2g;<*=Q`rj<%vsVe8e82` zi{}EK2z8~0?3lGZAu&1M?*(tQq&XejoZ<_Ren*gcSaGE}3rpFuCh@O?zYFB$f$x|L zWa8&@rp(s*$x%i>IYaa8b^IRS?p&Uph1p!0!<7ivDJ*}A z%K@z9=QLgh)KIXN09Cj_?|=}?@G72hp(_PYl_5w{!2as9sH(0#jq2)A)Dm*_X@qM= zKp$-k9!JIe;0V?$Jd4+~RX>gJC^qj~bry{ycx?cCWRr>h@GCEkqHQ0MI@*ZRB`0n@ zjlCzaXlBm{UKfCH3~G+mTKNt9?jMfB{O+Q1vk>Ixj+&8^baUA5gBvbXk{%32OA0tqZE6b>u)1q3b|K6H2IPxs+YCVprZKJS7)3;oxJwX|R-YUmY z%7`l#U@||qk^FYIb_##JZH*G2N3Sc6-&A{D$0^qK3QQmps< zvxMRO391yx`NG}H`eL>yGzUS~7p@~D*guI+@l!%R%%}Ldg!HS0j2q;W^pb0X*WeYzwit`pHH=r-Z(&$9MmB6j~vW}=@fhc zk7)(iHa)m{o)Lvg1?~)RLj`n7b2_oH=IBh)#@2L}oWY4=I!8!ncpN$>aZ1x+ z3(mof)$VjL+}(t`XB@b*o_6fj9$dYK&uKUEM3Hv)`D({TJ`SC)7SnlvbRHas&IJ}I zi550`53Y#L#bP=S`O@JqFb(|e^e}g)>ocG4P*WgBSEU_QJs-D@>@X`{IuAv=UEFb_@D*k z&94B>t6pb1d!8v#(xa)q6oxbJKmDm!tu%yX8Acgn3fg>hh&} zo0RH5{uhB6rFa>C_6)kxGlOq+Tdgl)MWM)F@K+y^y~VXGTp;o{{M{2-!KeHL*@}xL ZYO)g4@*g}Cz(4UXKIJ{Tjeq|};QvgCg$n=x literal 0 HcmV?d00001 diff --git a/target/classes/init-mogile-test-data.sh b/target/classes/init-mogile-test-data.sh new file mode 100644 index 0000000..4fca5d5 --- /dev/null +++ b/target/classes/init-mogile-test-data.sh @@ -0,0 +1,53 @@ +#!/bin/bash +TRACKERS=localhost:7001 +DOMAIN=testdomain +KEY_PREFIX=lcmfi-test- +CLASS=testclass1 +IFS=$'\n'; + +function clear_test_data { + KEYS=`mogtool listkey $KEY_PREFIX --trackers=$TRACKERS --domain=$DOMAIN` + + for key in $KEYS + do + if [[ $key == *" files found"* ]] + then + break; + fi + echo "Deleting: '$key' ..." + mogtool delete $key --trackers=$TRACKERS --domain=$DOMAIN + done; +} + +function upload_new_random_file { + head /dev/urandom | uuencode -m - | mogupload --trackers=$TRACKERS --domain=$DOMAIN --key="$KEY_PREFIX$1" --class=$CLASS --file="-" + echo "Created mogile file: '$KEY_PREFIX$1'" +} + +function upload_file { + mogupload --trackers=$TRACKERS --domain=$DOMAIN --key="$KEY_PREFIX$1" --class=$CLASS --file="$2" + echo "Created mogile file: '$KEY_PREFIX$1'" +} + +if [ -z "$KEY_PREFIX" ]; then + echo "You MUST declare a key prefix" + exit 1; +fi + +clear_test_data +upload_new_random_file overwriteThenReadBack +upload_new_random_file exists +upload_new_random_file notExistsAfterDelete +upload_new_random_file rename +upload_new_random_file renameExistingKey1 +upload_new_random_file renameExistingKey2 +upload_new_random_file updateStorageClass +upload_new_random_file updateStorageClassToUnknown +upload_new_random_file list1 +upload_new_random_file list2 +upload_new_random_file list3 +upload_new_random_file getPaths + +upload_file fileOfKnownSize data/fileOfKnownSize.dat +upload_file mogileFileCopyToFile data/mogileFileCopyToFile.dat +exit 0 diff --git a/target/classes/permission-change.sh b/target/classes/permission-change.sh new file mode 100644 index 0000000..d634156 --- /dev/null +++ b/target/classes/permission-change.sh @@ -0,0 +1,3 @@ +#!/bin/bash +chmod u+x target/classes/init-mogile-test-data.sh +echo Changed permission \ No newline at end of file diff --git a/target/classes/spring/moji-context.xml b/target/classes/spring/moji-context.xml new file mode 100644 index 0000000..2f25d06 --- /dev/null +++ b/target/classes/spring/moji-context.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/target/test-classes/checkstyle.xml b/target/test-classes/checkstyle.xml new file mode 100644 index 0000000..5a1c437 --- /dev/null +++ b/target/test-classes/checkstyle.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/target/test-classes/findbugsExclude.xml b/target/test-classes/findbugsExclude.xml new file mode 100644 index 0000000..17344f8 --- /dev/null +++ b/target/test-classes/findbugsExclude.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/target/test-classes/fm/last/moji/FakeMogileFsServer$Builder.class b/target/test-classes/fm/last/moji/FakeMogileFsServer$Builder.class new file mode 100644 index 0000000000000000000000000000000000000000..71564d4236b1fbee123055203f527a6ce7915dc4 GIT binary patch literal 1918 zcmbVNYi|=r6g}fOY`obf#x78rhGzn{1GsJI3x^bNF)#BpGD=%MOq^-5sk2LW*OWex zfDb%A@L!5l6xvAr0sT>kGvhTj56K2u+S!@C_uPBWJ#+p4^MC&X@HwtnNC@ozX|33l zVN_i6SKVSo{;baX%WhLu!kP+hs$l9w%WW>Hz(P_Wy((`?ntRK|i;JtO5eYaA-=kS5 zBiHw6Q8gy5$ZZy@ZWxsWQZ;wklTj;B0!Lo^JXh@(z7|REcUhVzkkx1ff$|tMH>jJy zra9Mhqj`a$T)r-lEc;8!#t_CGjKZ<7Ss>d@`FIe>-*nzK(il%c;6n$)*d#Dsbv<>V zwYI2&t8%f)S~69?A)9p>xOyKSCZiQM6v+2YFs7AdWq(~!-WByrONCL{_afj=lF!cZJK$RW?}$WXH_F#SroZE>;OE~F8YjXjvrnH~_>p6mHq_M1)0krFKB z>$;vp4vP3xU_>?=Dh#J)sl9j7pJM5OjY(*ZUn`T~2&X=ZBbc*5y^Xy}s)M7@*F!Eb@h!cHce?OB`_wL9i{5zm z7A7#fsAq)0-dtaSGQNHCRzuy;y$UUy>|8&C`0|T|Qvy5tlxw4cGg=L21rGFYZs*47 zc6MYzg_PdGvqu>5aI|-JmCf7`!LTqvH1LeyA!k)T+@Yhj1@MJURX!wDY8$D*A_m<4Bj6| z;!lj@Hn#Fid(#Ko@DbO$*p825ydPO)Q9YOg`oN-`KT*Jqt6>Ftv`tGaM}Z zMVkb=_;-kT7dvpz;BReX#P~$RXZ)iP{LSJppE~pFw9%0!H9C>M;F{I%|KOVht`&q1 z+zpdqV!(Zj-~mHC6gcEReF(-2j?v y;BgN>sWxbO1m{qVvz|84?1Ikrb>~~gSr?VSdGox0i(E6@3ySm-TR6e>GVB-JMB(iK literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/FakeMogileFsServer$Stanza.class b/target/test-classes/fm/last/moji/FakeMogileFsServer$Stanza.class new file mode 100644 index 0000000000000000000000000000000000000000..32f7a0f0ccedc2e0f265adba2681876eac6b0193 GIT binary patch literal 1155 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 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/DeleteCommandTest.class b/target/test-classes/fm/last/moji/impl/DeleteCommandTest.class new file mode 100644 index 0000000000000000000000000000000000000000..4992f6960113bb26a5e8bec6d8ffbc23dd114d08 GIT binary patch literal 1225 zcmah|T~8BH5Iwgo6xs^4C<+3#BBd?JDt=o+qNoq1YLr^za_Dx1=m zJFab;PPHsseiRXbWZgV68S2!G*L!to`2umMVASSk%c`5nK}eP0xqd)sz= zWy^|cslBFr>^QD(`pR`$0;!Vg)mVeoP`+!tq-8?X>eL{R8O9zhlhNZ_%2EEdKscAL z2t;<=s*E9w=>+0P2uzohBVV=cJ?WKA@&}}r*{qnJ()X@2;vXm$wDLbjEeu&Bn)F}& z^)@fY6S?ntvKYlBf%K3jqjgVUjs-X5$po&jg{o_t%86kXi@NG1fz@28FZ{0WDW_H( z_2es)5V)GaBw{S4$_ds?U$)9_Z#=PQ$Cf+{g4H!{ukp1kqIrao@;ub|XlAhW>5y%d< z-r2cgexUcRD~j6!3qv}eVUBa#?p8Bq@j3k1uG{u3`CREum>t>q4K0tr{D2-_ z+i|34aEo=@oc-95*=#dAH?+K+uvW0d?CQlk?gVp~T119whWYh{_@kt}MS0V9BJgs2 zv|--4vOLf6X3z@SvBD{)J_cb)o;nFw#Ce`NlmSR#hF0JLJ)s`-Mfx<*HY0RgvhV}5 zA5xc7^QTz&jC3C}#w=h3)5s3sCVRLU;)ZdJF%cpy_ZU{_(F|{b(t&j0Q}ATJAjiMi uuUP+uP0jcXcf#lsP6cWd*zFpYus%SV?vjRZpTHqJ2*~23eaPpLh2meCiy->| literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/ExecutorTest.class b/target/test-classes/fm/last/moji/impl/ExecutorTest.class new file mode 100644 index 0000000000000000000000000000000000000000..1c9a8dacc6a6c1a3df18a27410b1fb633909485d GIT binary patch literal 2796 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 BP<8+S literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/HttpConnectionFactoryTest.class b/target/test-classes/fm/last/moji/impl/HttpConnectionFactoryTest.class new file mode 100644 index 0000000000000000000000000000000000000000..f9a5e4b4f767231ca555f46ee66e2f855f13aa68 GIT binary patch literal 2293 zcma)8X;a%)6g|&22oynxlQapTX-S&KW>XpN{ zHtF_D`Z=9qGi9dz-XGQJeUh=oKr#$7e%oEnJMY{p{{8Q-zX9CBZxTWbgKK5Aq&coy zwu**olq)55&UGs@mT7X`H7s*h(_PEn$#BP&5Mk&kYMUClnCt4&YLRRhq&44$A)Y#6 zxCDdZaC?*6X-nVW6y!q63v6;%O$QFhP}1^wn>!9eU(05m41L$6e@ce3-b8)KR$S=e zN~(1yvTdIbE-`06Wug;S^bnq~8ZgjX5PA4{f;KD;I__&Py|&BwEyV7RJ4LWhJm7&ZMm7b0pVXZYKo8(+W;Ok#P^R67C0S zKB7zo?+f+GdBbUlSx`8y;DOj~(@T~^QtV2*n!d51RXi0+SU8Z5SC*NAZEg9Q?K1gR zOZF0$4;cnD))N5kX;ea*o~+3mm5+pnRHfdImnQAnhPggnHA;DIciojZ_H&_M+JLV<==DqO8n;bxv{ z?zqVQI|h6k_!1o;7d9ahuPIm;f|Mw+(4vTLi0DP?8dMc8iBX~$-t|lcmH^-3T0Rdl zR*(~_u@x+WTEb93HN;*bP$5TNo#a-e<@4_@Gdl2`-Ln*`1OBm2{S?f literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/ListFilesCommandTest$1.class b/target/test-classes/fm/last/moji/impl/ListFilesCommandTest$1.class new file mode 100644 index 0000000000000000000000000000000000000000..d1935b0c13f9ad9cd8b16261ea3fb84c2584be52 GIT binary patch literal 1622 zcmb7EZBr6a6n-us8?YteTV^j*sJy7FR+fSmlBN^rOqhM>(}FixgI#C$qBZ@g`cP>f z8b9}=n$86h7-WWaXJ_xrIp=xKJbP$p$ci+X`+mO!o<>;tuXI zs3k7acBxk7rl2%OqOo_`R{Ta`lM3$9R!7DD@$=&WCKW`8Ry(jvi4>q>3e!{zDF}wi zXj|wEDb1m*yTAenBc>vb1RW%?d&NI73`g57^TH-oB_;N(iaBTmaZ=_c!%90ulYOkC z1{a##HA@yn$u~7hNiKAVx&acvLp)aSh#}IRU#}=CmLwa$AU3bLsM*BH-zqypCz;kx zw;&jNdaE!)mz0HJtfy#J|3`UUHSp8DSyM5BQKFx>YIc!JF*1x_))3~TKhez_B~z!I z$!@E=u9sJ#(HT-b@>nDJ4{n&nisewLa$J_Yy)h zSII}N4q=%7bX_n?7GpH_(CkJ%-e-#Mp_S>x8E!JwoD{f5JVD_NYz2(jc`p+3XCU2nvq=?%Vg^!>oK~6Nw|Y)qLk{Q vG({^iMQ=Ru9jh%wFGZ1Q$kgP%2hZxG=SbK8>&VnsUtkkkG?GUqJ?QxhshFl_ literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/ListFilesCommandTest.class b/target/test-classes/fm/last/moji/impl/ListFilesCommandTest.class new file mode 100644 index 0000000000000000000000000000000000000000..102843f293f4319c256643b750001379b46a6a3c GIT binary patch literal 3306 zcma)8X>-$76g{$?$k<{t5DG16p@C*0xP+Ec9H7BTz~CehXCcs*DvlB>v87a!1EG8A zzR-PNKeRufGaX3EwA0W1Dg67W1p_zOS>ewGoEaA2~O zEULDXESb}KQZLOGlUdz%diA1ar_EAHH715M+mR8Guw`1EQz>msB?rc*wY(#tzI&jr zE7LC_kzGE_IhJlrr6g=jn}+SEhBKlT%UTp0s4!ucRNaseJKr;Q^}=9JZ{|`IiiGft zHZLJsGV?Qi3`N4WY#=nTNt3$<%Z8(uv=QCb$BSB*VVI8U==39EFs&&jlAqBXGug){ z72;w&WU2WX&63amy%F4v?2wG$`OW3*sV+E~flORuN zE(s*GHLo<#63k1~MldE7Lpzc(4zYTH_`H%Rcoc_;>9vAJ0vxZl>+vbo5~!f{^-a4YlcyC8yIFCLV*(JrYqZgB1mZi>M4=6cY9~ zU#Vh9rIK!Ju9^^YNqkYkRwUxcVOYkHga-n(sA)z8C%_KORMW(tELHAS?qm!{o6;QP= zoV~7H*YagY8`YgcWkl_*85%XPkeI^6gaQqd9B+P~dZkD#b^9{zaTG8ug9CL_5Dx`2 zD6-nNeuGnt<1Q0}fms1k>~NWxT z0N2yRd2yYI_F|G-YN<#d^WNpk;S!vst8$O`Lv)f(yec1z+GxD%qUYMa?k{ zFW{zCi@a<>NU^+^@wMo$ZzQB@df=Y>DG!(|gYxU>MA>muX?y5;? zl?_9)lKi|7*)sDwm+ImUub%#%nC*UD@GIloFz;#VP?ai5M!cl?J!NY)uWFKspIQ;@ zMjh(8lDP^Y%5|)29p@hJcAjtMxsco9%5hZI>_xcdR|tQ1P$J2v#AjXW9W?yrs@%t4 zVF0|uckx&81nBU7t~hY19ICk4!#!ar%MgW5W9#o|T);z%I1s`D4&Ft_7;Qhch$liA zUG@;84d5vD;8c~D#)_8{^b*EN$_TrsD*n2-Lw`~`;fV14*ZMzkdMupi&W%M9y}7Zv z#M#^e&Se?Kg+w$FmliP?5rMQVVw4fw#pN*(Vj`Z1-oev-2Zi8mTs?Rj`Tq7rOi2_j z)$?xw_5$YH!;5$&guiif5pQzZcKh3r;C8RST^HOw>urm)!w|r>vT8sYJJC(-dze@+ z&hUR0XK@u7%;6k94!&9gL!r4YhRdkzBcFi_jNl>}Kge&b z9FAj%=nQi;0v)4OXiWQP)MLU&qXWl$G)|$d1`SV2py8bfG{X3n{zCZ9rB$3~-}CQ> QJbuJaJcx18!6*Oy5Am~KhyVZp literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/MojiFileImplTest.class b/target/test-classes/fm/last/moji/impl/MojiFileImplTest.class new file mode 100644 index 0000000000000000000000000000000000000000..13d7b81f8fc5756bdf6c8c885311e17b2a646c89 GIT binary patch literal 9015 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 br%$21Ot!PPOm^t&L@hbue10y7$c6s}Tl^WX literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/impl/RenameCommandTest.class b/target/test-classes/fm/last/moji/impl/RenameCommandTest.class new file mode 100644 index 0000000000000000000000000000000000000000..d1ab010146fb196bf6cb4eba046313d4623ddd9f GIT binary patch literal 1259 zcma)5Yg5xu5Iwgoq_h<97WZtUZZ>?wcB{%z_Cej)mX2x5 zE!Vb9r(Bc`Uqeh_ylNhr40S5T>)oofe1W9xTD78QS~ckj%oX}d`7OWE{+<_@+HN|& zvSmp%)NWlqb{y9?edRh00lnaQ74~4&l`w?g*;AR67#_X3j*tz zLU#!}zNef@{(n5#(g*}Dr!ay9TP@4FteC!R6y4795>JmTc@Qo`!?M6c7pU`v8VI-4 znlhX}sGCh;2*W|2D=A#Vbt1?kX*GTMR{8rKgmk8#(LsG=O9>dbso@6E2gN{9q$t_j zq~%b0YVTNJxtENVBJeKqn3FbZ_-L0Bn9`uZKfEo^Ft2pGGz(J-67VY@YDD0{zQZ0}c0 zUlx7WGpllom(4BLwoRu}l1*PjOkk{L9+-4Z9GnzVd@r0rVul4n|V=?Tmh2EzJn zyV1GN3rxOlIli)GSvA#8Lq2vK*EfCTI!yt+;CfZ&W7U=K8ZRj6pjqu41eOP5pPn*3 z&ul11`I`dKOtvf#+j1*1fha~(NFpULT2PLB*|K+}S2CGPn1E$A%BH6Ry{fP7Wt1#aX{it`4N9c8>%W`^jjJZ$8_{M#rS#j==OlYfd01v4(po+z%MmTYoK$ zK%(fjJWD=P!BEYgx>hRzZ30t$Jb5k0k)FXt3FzP$4IP+`79(W__M`o*8SV+IeZI}{ zq7Pm3NR#C#mp#HCsivk;3uHXBvaOU+>f8jLXQ>&Kf^sN#jFI=@voW556EK5wJcFlv z03G9$0_SOobVFaDEeQ0e0Zj*DEO9fsJ6D R;Ujnu!X*iu=eJ?u;cqz@Od0?H literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/integration/AbstractMojiIT.class b/target/test-classes/fm/last/moji/integration/AbstractMojiIT.class new file mode 100644 index 0000000000000000000000000000000000000000..08a0f85f75c52ec386c3e550643321536703a109 GIT binary patch literal 4946 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 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/integration/MojiIT.class b/target/test-classes/fm/last/moji/integration/MojiIT.class new file mode 100644 index 0000000000000000000000000000000000000000..290d35af1451be68c8dee884db43eb6470f30abe GIT binary patch literal 3419 zcmai1`F9g#6un=YG9jIoMJOUz1PZiiAp&j?L`rF)khV%o*%X~7Lo+3r2{RLD-BD2# z_g%yt_g%5Ha*oGy^e6ud{}J_mGm~`E)8ff#Cg1knefQn>zUg28{Q4V!O_)$nC9vXn zK9SQLH<7nSjD%sj`mn9JhGiytIMF?zAR@4C)=lk0j%#aa*WWAn(KOtIUCilDVnELqEL*e3{4Gg=rTdGfYvlDo!!d?(db?>_uIJwom>)X7pXa%K z&NhSAwhL4@w+sqII;@NyLpADD%tcJWJVx|CPGhJ?-CPJPR8fOD0`pRasrMH1L%Kbn z5d&9JR$9vqYPKQA!DPhE8U(d6H7glD2}G%;t-{!V)ng4C(p;=_G_4mTsg8mh1g;y= zPH73lN_6*?S1O|&Ad5)_HwrBA_U1HmIMMIghB>^mXh?Qr3}A(do3N6knEL5``k26? z=9bj-{mB?sV~v6)dYHbZVlCDQD5tbsQSUo0u(mmLzHewmPrJz(O)f`^WIQggIDEv1 zm~0u0qK(P(2>>be*I)oH`6*9k#2XY=)f+z9oAhRZ|jVBD-#Yr^i)s zvJA-&t53z~TQZhS6)MX3u5!L%UN~t5M8RC}JKfO*^(zO|uq+xok ziB(z2dzlZ~$Qh0+GxdN94Mzp$p3ukcw)Nx2XcWg}Dl#gLfzoLTV=fhr@485(B~mn$ zti>=41zCZ54^(+Y#RyK&ont&mTkMA2G2~$>Fa;J&;qR6WUAJYS7X%ubyTjESEK(pg zTm|HDsiZrr<|hc-lMrJ1)I7r;*>y@Y=QAG@)EaRuPz@Aj?r{KMpcaALAF6Z zX9HQy6{rw?DPtAhOD0+d6+B!D%8P%G=4AVIc9w@wCkc8?#p5``+_EHjd0D)A8cLne zJJ21>lg=k`R>4zD@)Vt=RTWR;8B%KMqXbylO9O-oo@K|IQOC77hx00)_Zl+mn+>@Z zFXCkdFA3C5c_VNYui#ZCP0O$qR`kyjnMWkh*HyfMH`y(+n$zoHVw#Uc@iyC!RM|#p z|ALBlrLkz*GF{Cu9m)86D&EHjQ;ge1Y9!KpME2#6g43I#_$2tbSzbO<2tDC(KZVO= z&kL;(J4+!ddyij24V4$=SfO-T1~Mzlb=xcjvz_)x9;rj%?6^`UH%4ZFqd^6#9Ek`@K zQ}8)Ieg$g$D(Vkk!Teva;786?;W~cTd$yVo!8*)C6Bcn!_0M228aQJ2p}lHc&)*S_ zmX`f3>WL=Z4*ew<%V&z z_wqnzE7!Z*;{5KN!2T&@jh>%YG@^}f6dTaYJAETRTX`{e;1&;Q-1i$GZDiLugu}$R znk*f`QO3BDQQnLDJRXi+I}TycS`Fj)Q^NA%xQzRQIEG4bq*JYv;H%37a&!1Mfs+$B zeH9Op*++V~^r*nr$mdwLG%|@N1b)Z4-nR9VcursvFI3^6oPSN=!oPpFO>^;B}Epd->zaWyxd2%W#}g19yO>mDQ82*UL8_*Csv?~|AFlG;*)r3Hi&Np=lu9& zEK>RYjEqI#hyMZT1R&RxKrZ}|qUYfL3c36&<#KU4tbJI96icor1ZzzIYZ)C^!t!T) LSmT6Rg)9F7>&aG| literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/local/DefaultFileNamingStrategyTest.class b/target/test-classes/fm/last/moji/local/DefaultFileNamingStrategyTest.class new file mode 100644 index 0000000000000000000000000000000000000000..0311ddcd0a6d36ef0bca5a68dd44169d0c6cc6c1 GIT binary patch literal 2274 zcmb7FYgZdp6y29shA^d&7HqXh3sOmd3=~>WkQS&|H9Q0w1)oE>NromfI5SbZmi{Q$ zl1Ftde}I3&f8uiAnIXwks`7!kIrrXk_Bnf>bNS=1Uw;R14{ic20+Yq6Q87K=s5*z1 zQE>`p#ds`>X076{S{1o&RxP`hXVT9u85{eDvfv9OeIBkl zl>_Mt+{rm^$vCXpmT$PViu8;fSv_)G(|sOZnHRXZRkMAoD)W|S?N{WIZ9Bf{TaN89 zev|(e{dv8k7{%_+1>lxrs04Y^ZO4)n$mT9nl#0DzG1EnX*3oocpl!uDkV&+nSH~4} zCvZ(*@IMMlB8A>g2=wb{M~A@GoMp@PT6JH#J0|NBNaYl}dDFF&9c{MxWs3#hyHv_b z2d3TbN&o3l5V+3vvMf5ToY(LsB?;u5P|B+GZtJ*%A%RX?et$01&}hhJASGp{*z@T) z!K$dYbaWz_z+Hjy%W#{-2u2l&X@TAd>Rh;VUPT_$(FL8kK9g(>S4kR|Kd4oMA<(D# zv{;>(o0*-So0*%Pp3NSZzJi$2+0WDLghmiHE(6_tb$QJdYq-x08gY}D#XAYiu}PKe zWwTmvNx88?YS&D^P?oNz<6TgL7NuUM873SEvKVuX+~$*b4-XSyuk|!KYRU7Y>+5)g zCFWrU)=t^vRE=E_`QpH;aa&oxYKO*jMv@9#IdH0`Wk(b%(pGhRph)YDohaTke8`yy z+2)V%oa`3E2SHLKU%DFB=Lx*?ETV~4ek?xph@%;s#SnzB)j1sw;R3ix1g9~zC-`|xMvk( zd?lzL|3|cPwTA=*gV&?>D4&PjUSieLn{t zukqU*#2!Q&hUhVf8{E_BO&{FE>s$ri;H{QA>Mic6*cTmC*%M6W#!@q9cyNY=m*X2G zV-nXe6~ylk{i66uo-JdANeevwe@O2W>B+@N_ZjU0k!D|k^ihQL@r6i#l6EEPPXTv1 za+eFLdy-l|!RFeyVlcJy0{K>);fv7xCNlR!Q^w6w+_d86v${D}54+zWFBA08JiiO* x=ZuVC5i@wm5+3o%Sb|C7j%fWz;#7y+4wJ7F>!C_tju1+);gEL0`>H#?{TqUQ78d{j literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/spring/SpringMojiBeanInstantiationTest.class b/target/test-classes/fm/last/moji/spring/SpringMojiBeanInstantiationTest.class new file mode 100644 index 0000000000000000000000000000000000000000..245e3d3cd39c4115f1f52121edc094742afa4f8c GIT binary patch literal 2021 zcmbtVYgZdp6x}x@OcUTf+i`0E5K{^E&YiY7F_c8mPv(LHvoc#XhPrm}V4M#&n;Ldh4-7q~r-E`_! z+S_$4yP7TpL!LWJ(zLU-=bN@~nZD)No6_?&L)6=_eyMS;O%#{aw#7~W_xwx-;&Jc}F_ZQJpKNJ(KJ=eX5$y~U`~ zDsWoY6xM0^G3JmT?GFua3rvQnu9!PA?^LaZT=5Fh-IMObQp;*oq^n~HeHyNY(pT_R z(&KoS);rus;TizJsF z+b53nA10sWj$`h)$)v(*{dcaJI3{_O>#HNmQ_8>;rUm-_z>~M@*cE|=yGvIMa_uSkdrU;BqL^BY=VP=v8h$HIT&<)&||M60;TMiRGHrk;9sXyuiidV6(O_ ztI{>Fj?YMK&up~h`nJGZ$?WSn>B2HPHn6Foz@k!hQ!|@orqWL@JFd)|ez_)H&%hU8 zZ-}T0pL#vQP7|R#Lr1&EX!=WhrJ=~eIWEaX&y%ii;2FMVL}swsteNaYq1<&XpKds{ zrBw2bfp1lZij^CV$BgvSFw0h8^sH)8Eg_JuLcei6Cv|qz2Y4!4Vrvzc2s=1c#|ph2 zXksXgKDu54&J}#KypuO~gC?q>9_n|SHP?C3X}NmAX}M+D(b&Wpo&J{6f`E~=ZRsxa zf5MZzH?7;vFbaw%=z|f?*yH)sxK48v9Ai@lh`;0{0-aCw8n}XfK8+55<2kN`(kD$_ z;5^FJ01}+_b2iA|r6;(o?27Y&4`4^+e#TJo0K@s2-d}K~*pqmtP>d$76^gOMNTH3f z#LYI6PiNYgA#$!b-Nx<22M3s6n@udV@zH#AIQk090<&#=a){L^e~0+|5Kn)EF&`uR z`EcwvHN2$%2%hr`@>5aV4;WwL?g-bTn8X-nFphb);w9W*ecdE_l7dR2j1;yx*4aE& z)OQ&G>35@!z`}Ph=|M*UC88s=GL8x)eJ$ZC&sLd_8qW+O@)ylUGzg7?<->{-zQb1$ PG|=Rz)6fv#+35QlN|h6g literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/spring/SpringMojiBeanTest.class b/target/test-classes/fm/last/moji/spring/SpringMojiBeanTest.class new file mode 100644 index 0000000000000000000000000000000000000000..ec87d0a895f42cf0468b5b321014f92f4ec049e7 GIT binary patch literal 2197 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 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/tracker/impl/GetPathsOperationTest.class b/target/test-classes/fm/last/moji/tracker/impl/GetPathsOperationTest.class new file mode 100644 index 0000000000000000000000000000000000000000..71a99dc0f113e93c5236d41d4f270375bf6f64f4 GIT binary patch literal 5197 zcmc&%>3s@YSyfvm7JTQ`j%k+Van zQcyRo&1#%xq+$c((;{grs5vmu*U{atAlh4eIcQtDk%}wW*l8L$TQlqtEu9x(Y~X|m zGo$H-g36w*u`pCV%!qjfk)s_$hmUs-4EGO(u~~uAMqkIaCyc0qYo}~G+a8O}&CTtS zf$w4hdtwEjwlJ<;*{6*@(Ttg#Nr(sXOysa;Or(XSprv+osJW?+c2o749(74jId)3 z*L`b7@4y=0FV`~*lu>Z|8gt4R#?xgRvggh0F8R#}s&m2~&XS2;CzB#8iJuE$kAj<5 zhk~7Jh3S$}MJEpMm{YnS@;kKscHJ~mq)Af!=a1e(>IHcg3V4i*CF-u29H;(Y+=cWYZp*@(Kt2S3;m$UQZ zXD|9g=u^lX zs|drp_TuFSS#CMjJR*^OLdA`^NpAFE6;DbH4vCW@nYXFSci?GB)zcB|#j`4&D{!XM z%w%}MhVc8DspyxY1X_!hns z!ne6fv8cKYTcm`g;=6c}IM2#Ji2wHODY#g{4oIE%4rc+^f8 zO4iG-<(zWP%bugoH9JS=L_qv|&0vo)pDpwmjaT^YbI0CpZZ~vFSf-a=#1lt0a7?=( zhO6E+#dgn^KQ<_vi!$2fs_3mNpUaiK3Fk%z&AgQq2X)ywZ7q`%DW|(6UVIr)Su1ZC z{9C~0cT~5hWRK)9lhRqzXSi-Tr}yPWeq@*}5a)1e96P8amHgu&yNznpa1`RG0%49L zfpIOz8w2A_9M=WL^&DRl7&mYY3P0C-3)hqDUhk~So~F#k`>)K#E3nMQ`>M>wZi%)X z&e}VZ0Ml^ssu>t$pn%u?g z@5TXi;6ApOGkiM3Ht{?T;dwTMi#UQe&=bJ$vX5cT$FPQI?S}26ILq}Vijr&zFi{7+!2`oF+5zA{Yr6Q&b{ zNh3^2!Zbmc1Yw#aOew-NMVRz;VEW;DFyY@Go6btAxWrhOO=U2VRIl~6F5@Q-cdCor zsa(d-C2uZW#HF!l{UR=VM5!uy(cr(3T&aWg0|AZ5?dl|I*LiXpi1N+NW1!lhKz@B1Y&oHoO8Q60x1Xh8+ UGW-hs&Cykf)Zh8}M-u=1FU5#nuK)l5 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.class b/target/test-classes/fm/last/moji/tracker/impl/InetSocketAddressFactoryTest.class new file mode 100644 index 0000000000000000000000000000000000000000..99cbb744d17f6e56879ae8562a9f1c220404f670 GIT binary patch literal 2151 zcmbVM+gcM>7+o8Ii6K^u-~mrXQ8|=Mh=$U{s?mZ#BT}L9!mG_>Hw;c@=**$xgl5y*v(3<# z);r~!c5~k`*IX5Dc^rq8dfit+up+I{^MBq}L1=Q6|Qs;8_u*(2$& zvgZO9|C4@cekq>>&FB?@3p#qf@KFldmp4*6`@53VYg|7#ILI|Tui>bibeq-}K3oJ}5bD zXH0_Lh>VkFk6#SF4USE=g6+`k?@0a1&H&PU>t)~yM)=OO1K6J9h(&RA0O#YVQMMZB zEe1IYT+3cK#Kmg%>fCR*c8JL_+-Gm*mqW~V;j;_|{X2{s1iFa{-YZRYsSOcR4PzX6 zERd4lh(4qbBGO`q^vmN&?+|E;q<2YruMg>09nuk|Fv_Q7p&XT4jIv_s*+*o4(=9C> zl`zYlb3N)A9rYfc2J=Z0rpcZ8M9j)*FrOu1KIG0~BIY-z!CX$l)T)&dF<*2rwZD!! zXO(c(?0V-Ak9Cd~@d*#{DG#v{Q62A`Y9G><5$UV`q{{?aA?XW}uJ$4Qu0wi)b6zK@ Qr_+6zsNOa&$N;|o2S#s-jsO4v literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/tracker/impl/RequestHandlerTest$TrueMatcher.class b/target/test-classes/fm/last/moji/tracker/impl/RequestHandlerTest$TrueMatcher.class new file mode 100644 index 0000000000000000000000000000000000000000..ed1c026a510e7e117bd157ac875871434841db1c GIT binary patch literal 1255 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 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/tracker/impl/TrackerImplTest.class b/target/test-classes/fm/last/moji/tracker/impl/TrackerImplTest.class new file mode 100644 index 0000000000000000000000000000000000000000..a3cd7e0a6a16d9e7f7680322376369b713b1f59f GIT binary patch literal 7735 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 literal 0 HcmV?d00001 diff --git a/target/test-classes/fm/last/moji/tracker/pool/BorrowedTrackerTest.class b/target/test-classes/fm/last/moji/tracker/pool/BorrowedTrackerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..c7c5b890c27c1ec507a813c73c1138858d154d82 GIT binary patch literal 4093 zcmb7HYj+dZ72Veo$dLvI6bb~=5JC+WjZq;@LhF>o*do~27?6=`oP>_X7kg}JMjed| zPSWPtG-=aElD2vMfPT(et-;M|S3dVgb#HrnRcQe9d($`3tV+xp(y9Y;$u~*ZmZd3VT<~HIqut zQhuhqqAgz`dATruMj<=Z{#5im+gTb>*fZujb>DRSl38i!emq3+RlQE}-QzRUqZ3#9 zVeoy?t(vyeGzUGQolGmbA9xs!mY8X(PFV5S%qB|U9MB2IB~O}pe7QGNGW8h zuC*GvtMK?#&)CB6^z6~=4ac{ux@6bwaz&3ij_aGgO?M>}*IObf)~fBh`Ds3>&@bz& z_jGUh zAWhSzV=hHm5_y3vcP3BahYv8ZCDYX3{k-ki{&|JOP_9Ix$J|AoMgk`cJc;A9KV>`m zN~2oV-mFRP0>Uh_QZhYT*5PK-U$#kRe&^UyWyDVkd@Bnp4KDH*^ljJEBPpCF`#m$; zt7;#D!jB9*j-&E+*1!QAl(!!nIE*7{4C7e|={beFLl5w>2g$vqTh+DW^2n9Mou3$Z z0>>DzRdMV3y7n8MQ=fI4wU#d2wR9~g%oKj6@JPGMqAzUA0J6zdz@h~AqJdG#7dwV_ z(-=b`g>jnLTHUO20~hfUQ@^G?`}VrRiJ@3KHw$_s*S(d(gu+R&w}WoT;1Vtyn8LJz z;RRu~X2{7NaNTIRk_0hh;3}>u9BBG_Q+w`&<7*D8uze2VEXlZSU`dj(*N&KIC1g+0=V>r)*1~4q?`gAASs#;18rK!QB&>;{RRa~V zoQVkt;)@FVmb8D>^q1?ALJBp7!3Q}T<8~OHfjT$|`dfw+o{jSw9y76IhdW`VHhjC1 zpRzer#3PTDZ_!I%&(yRN`6NNyHL#A?6w>YQ3YT^SwL|c`3j3SJJ1La@|Fwa~@O|;) zEdy`k9g-OnRhZnFBRy%2pDL5NlJM{rC-@>$=yxXaeFMM62XYt(-`LhWf^FJ-pEf=1 znAJ|39~t-q{z#k6Zya?yve|>v!1Iq3jx=i0utneXc<_zMRiGo2pBVU55VPHK68k5- zQb-F#I?Q>|ZFrWxXv;x%Xv=9Rv-Gq~T!q825$iP^hc_i&A!L~F$lAq8(xCoOM}IfP z>cI_yGj{R@k22~|bp*$lc)7Wxc@*u!Zmv>X^`W2ZbZnjBdQWUU!1dnPdLP%1V1K0l zJ=)+g$MqrF6T6QFc6s1+?Xl{*_IOx!?eTP?4TJkU|M~j^uJU{;K6jnEhrzD{h?D#k zKu~CizoI|^L1AS?+pK*zK3W2iF0k+X=(${ zF}QOv`-dX?Kc)QyMktdc+Vc_8`4Gtp_JLsa9}Jf6Y}F*k-IH?D8v#^-cW0x5ih;`}|_xQ~*6ZQ^DhzQwIg zu-L?z4J`g0$*JaRPiJ)kUo+ApOv@mTq68!Zhsh^+CLKY6yBBeSgMJtj7{_GH-9qT@ zGG+O{fKE}LrtEEeUkE82Y|*tKn7=i*TdNOs=_ z?rZ_h?%Txc$!_Qc4B%x({7MYE9q}96Kre*dk!wZF=daQmc(XI=V0RPm?q)&A6p=z` zMs9LilnHIc;B!&r?`;ErEhPMC3w{H?=|tYdhf;-|pi9VLnfGox26{9C{oOX8H$u9P zw?MN48~A-EY<6H1e@b=%S2-&kwnHrjd^`gF^LGYjoiVEW%)W0!2Gd|Gy|1y6Ugwm5BL;RbjIs}Z3zG3!&^#HM{hWVaSore)thS-e literal 0 HcmV?d00001 diff --git a/target/test-classes/log4j.properties b/target/test-classes/log4j.properties new file mode 100644 index 0000000..d7f2348 --- /dev/null +++ b/target/test-classes/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootLogger=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p %c{2}:%L - %m%n + +log4j.logger.fm.last = DEBUG \ No newline at end of file diff --git a/target/test-classes/moji.properties b/target/test-classes/moji.properties new file mode 100644 index 0000000..daa6075 --- /dev/null +++ b/target/test-classes/moji.properties @@ -0,0 +1,7 @@ +# My local properties +moji.tracker.hosts=localhost:7001 +moji.domain=testdomain + +test.moji.class.a=testclass1 +test.moji.class.b=testclass2 +test.moji.key.prefix=lcmfi-test- \ No newline at end of file