From 5491dee81bccd980936d7ed28f6032255e8d5a63 Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Sat, 1 Nov 2008 21:32:06 +0000 Subject: [PATCH] Damn it. Weird symlink-in-checkout bug. There goes our commit history. Sorry, all. Guess I should go back to svk --- src/com/beetstra/jutf7/Base64Util.java | 117 + src/com/beetstra/jutf7/CharsetProvider.java | 90 + .../beetstra/jutf7/ModifiedUTF7Charset.java | 57 + src/com/beetstra/jutf7/UTF7Charset.java | 75 + src/com/beetstra/jutf7/UTF7StyleCharset.java | 117 + .../jutf7/UTF7StyleCharsetDecoder.java | 195 ++ .../jutf7/UTF7StyleCharsetEncoder.java | 217 ++ src/com/fsck/k9/Account.java | 372 ++++ src/com/fsck/k9/EmailAddressAdapter.java | 88 + src/com/fsck/k9/EmailAddressValidator.java | 18 + src/com/fsck/k9/FixedLengthInputStream.java | 60 + src/com/fsck/k9/Manifest.java | 14 + src/com/fsck/k9/MessagingController.java | 1476 +++++++++++++ src/com/fsck/k9/MessagingListener.java | 132 ++ src/com/fsck/k9/PeekableInputStream.java | 64 + src/com/fsck/k9/Preferences.java | 123 ++ src/com/fsck/k9/R.java | 452 ++++ src/com/fsck/k9/Utility.java | 176 ++ src/com/fsck/k9/activity/Accounts.java | 296 +++ src/com/fsck/k9/activity/Debug.java | 73 + .../fsck/k9/activity/FolderMessageList.java | 1283 +++++++++++ src/com/fsck/k9/activity/MessageCompose.java | 1053 +++++++++ src/com/fsck/k9/activity/MessageView.java | 891 ++++++++ .../fsck/k9/activity/ProgressListener.java | 36 + src/com/fsck/k9/activity/Welcome.java | 36 + .../k9/activity/setup/AccountSettings.java | 170 ++ .../setup/AccountSetupAccountType.java | 90 + .../k9/activity/setup/AccountSetupBasics.java | 388 ++++ .../setup/AccountSetupCheckSettings.java | 188 ++ .../setup/AccountSetupComposition.java | 108 + .../activity/setup/AccountSetupIncoming.java | 325 +++ .../k9/activity/setup/AccountSetupNames.java | 103 + .../activity/setup/AccountSetupOptions.java | 103 + .../activity/setup/AccountSetupOutgoing.java | 266 +++ .../fsck/k9/activity/setup/SpinnerOption.java | 33 + src/com/fsck/k9/codec/binary/Base64.java | 788 +++++++ .../k9/codec/binary/Base64OutputStream.java | 179 ++ src/com/fsck/k9/k9.java | 169 ++ src/com/fsck/k9/mail/Address.java | 215 ++ .../mail/AuthenticationFailedException.java | 14 + src/com/fsck/k9/mail/Body.java | 11 + src/com/fsck/k9/mail/BodyPart.java | 10 + .../mail/CertificateValidationException.java | 14 + src/com/fsck/k9/mail/FetchProfile.java | 57 + src/com/fsck/k9/mail/Flag.java | 48 + src/com/fsck/k9/mail/Folder.java | 95 + src/com/fsck/k9/mail/Message.java | 118 + .../fsck/k9/mail/MessageDateComparator.java | 19 + .../k9/mail/MessageRetrievalListener.java | 8 + src/com/fsck/k9/mail/MessagingException.java | 14 + src/com/fsck/k9/mail/Multipart.java | 48 + .../fsck/k9/mail/NoSuchProviderException.java | 14 + src/com/fsck/k9/mail/Part.java | 31 + src/com/fsck/k9/mail/Store.java | 76 + src/com/fsck/k9/mail/Transport.java | 22 + .../k9/mail/internet/BinaryTempFileBody.java | 77 + .../fsck/k9/mail/internet/MimeBodyPart.java | 121 ++ src/com/fsck/k9/mail/internet/MimeHeader.java | 105 + .../fsck/k9/mail/internet/MimeMessage.java | 424 ++++ .../fsck/k9/mail/internet/MimeMultipart.java | 95 + .../fsck/k9/mail/internet/MimeUtility.java | 304 +++ src/com/fsck/k9/mail/internet/TextBody.java | 47 + .../k9/mail/store/ImapResponseParser.java | 356 ++++ src/com/fsck/k9/mail/store/ImapStore.java | 1283 +++++++++++ src/com/fsck/k9/mail/store/LocalStore.java | 1187 +++++++++++ src/com/fsck/k9/mail/store/Pop3Store.java | 880 ++++++++ .../k9/mail/store/TrustManagerFactory.java | 95 + .../mail/transport/CountingOutputStream.java | 24 + .../transport/EOLConvertingOutputStream.java | 33 + .../fsck/k9/mail/transport/SmtpTransport.java | 376 ++++ .../k9/mail/transport/StatusOutputStream.java | 29 + .../fsck/k9/provider/AttachmentProvider.java | 272 +++ src/com/fsck/k9/service/BootReceiver.java | 22 + src/com/fsck/k9/service/MailService.java | 193 ++ src/org/apache/commons/io/CopyUtils.java | 332 +++ .../apache/commons/io/DirectoryWalker.java | 620 ++++++ src/org/apache/commons/io/EndianUtils.java | 489 +++++ src/org/apache/commons/io/FileCleaner.java | 154 ++ .../commons/io/FileCleaningTracker.java | 258 +++ .../apache/commons/io/FileDeleteStrategy.java | 156 ++ .../apache/commons/io/FileSystemUtils.java | 457 ++++ src/org/apache/commons/io/FileUtils.java | 1890 +++++++++++++++++ src/org/apache/commons/io/FilenameUtils.java | 1260 +++++++++++ src/org/apache/commons/io/HexDump.java | 149 ++ src/org/apache/commons/io/IOCase.java | 238 +++ .../commons/io/IOExceptionWithCause.java | 69 + src/org/apache/commons/io/IOUtils.java | 1274 +++++++++++ src/org/apache/commons/io/LineIterator.java | 181 ++ .../io/comparator/DefaultFileComparator.java | 68 + .../comparator/ExtensionFileComparator.java | 112 + .../LastModifiedFileComparator.java | 79 + .../io/comparator/NameFileComparator.java | 106 + .../io/comparator/PathFileComparator.java | 107 + .../io/comparator/ReverseComparator.java | 57 + .../io/comparator/SizeFileComparator.java | 132 ++ .../apache/commons/io/comparator/package.html | 25 + .../io/filefilter/AbstractFileFilter.java | 67 + .../commons/io/filefilter/AgeFileFilter.java | 150 ++ .../commons/io/filefilter/AndFileFilter.java | 167 ++ .../io/filefilter/CanReadFileFilter.java | 92 + .../io/filefilter/CanWriteFileFilter.java | 80 + .../io/filefilter/ConditionalFileFilter.java | 67 + .../io/filefilter/DelegateFileFilter.java | 104 + .../io/filefilter/DirectoryFileFilter.java | 73 + .../io/filefilter/EmptyFileFilter.java | 84 + .../io/filefilter/FalseFileFilter.java | 72 + .../commons/io/filefilter/FileFileFilter.java | 60 + .../io/filefilter/FileFilterUtils.java | 361 ++++ .../io/filefilter/HiddenFileFilter.java | 76 + .../commons/io/filefilter/IOFileFilter.java | 55 + .../commons/io/filefilter/NameFileFilter.java | 191 ++ .../commons/io/filefilter/NotFileFilter.java | 78 + .../commons/io/filefilter/OrFileFilter.java | 161 ++ .../io/filefilter/PrefixFileFilter.java | 197 ++ .../io/filefilter/RegexFileFilter.java | 122 ++ .../commons/io/filefilter/SizeFileFilter.java | 103 + .../io/filefilter/SuffixFileFilter.java | 198 ++ .../commons/io/filefilter/TrueFileFilter.java | 72 + .../io/filefilter/WildcardFileFilter.java | 196 ++ .../commons/io/filefilter/WildcardFilter.java | 140 ++ .../apache/commons/io/filefilter/package.html | 143 ++ .../io/input/AutoCloseInputStream.java | 129 ++ .../commons/io/input/CharSequenceReader.java | 155 ++ .../input/ClassLoaderObjectInputStream.java | 77 + .../io/input/CloseShieldInputStream.java | 52 + .../commons/io/input/ClosedInputStream.java | 48 + .../commons/io/input/CountingInputStream.java | 175 ++ .../commons/io/input/DemuxInputStream.java | 91 + .../commons/io/input/NullInputStream.java | 329 +++ .../apache/commons/io/input/NullReader.java | 313 +++ .../commons/io/input/ProxyInputStream.java | 129 ++ .../apache/commons/io/input/ProxyReader.java | 130 ++ .../io/input/SwappedDataInputStream.java | 251 +++ .../commons/io/input/TeeInputStream.java | 147 ++ src/org/apache/commons/io/input/package.html | 25 + .../io/output/ByteArrayOutputStream.java | 308 +++ .../io/output/CloseShieldOutputStream.java | 52 + .../commons/io/output/ClosedOutputStream.java | 50 + .../io/output/CountingOutputStream.java | 154 ++ .../io/output/DeferredFileOutputStream.java | 269 +++ .../commons/io/output/DemuxOutputStream.java | 102 + .../io/output/FileWriterWithEncoding.java | 324 +++ .../commons/io/output/LockableFileWriter.java | 333 +++ .../commons/io/output/NullOutputStream.java | 65 + .../apache/commons/io/output/NullWriter.java | 96 + .../commons/io/output/ProxyOutputStream.java | 89 + .../apache/commons/io/output/ProxyWriter.java | 111 + .../commons/io/output/TeeOutputStream.java | 94 + .../io/output/ThresholdingOutputStream.java | 257 +++ src/org/apache/commons/io/output/package.html | 25 + src/org/apache/commons/io/overview.html | 32 + src/org/apache/commons/io/package.html | 47 + .../james/mime4j/AbstractContentHandler.java | 113 + .../apache/james/mime4j/BodyDescriptor.java | 410 ++++ .../james/mime4j/CloseShieldInputStream.java | 129 ++ .../apache/james/mime4j/ContentHandler.java | 177 ++ .../mime4j/EOLConvertingInputStream.java | 108 + .../james/mime4j/MimeBoundaryInputStream.java | 184 ++ .../apache/james/mime4j/MimeStreamParser.java | 320 +++ .../apache/james/mime4j/RootInputStream.java | 111 + .../james/mime4j/SimpleContentHandler.java | 100 + .../mime4j/decoder/Base64InputStream.java | 146 ++ .../james/mime4j/decoder/ByteQueue.java | 62 + .../james/mime4j/decoder/DecoderUtil.java | 276 +++ .../decoder/QuotedPrintableInputStream.java | 227 ++ .../decoder/UnboundedFifoByteBuffer.java | 272 +++ .../james/mime4j/field/AddressListField.java | 63 + .../field/ContentTransferEncodingField.java | 88 + .../james/mime4j/field/ContentTypeField.java | 256 +++ .../james/mime4j/field/DateTimeField.java | 65 + .../mime4j/field/DefaultFieldParser.java | 45 + .../mime4j/field/DelegatingFieldParser.java | 47 + src/org/apache/james/mime4j/field/Field.java | 192 ++ .../james/mime4j/field/FieldParser.java | 21 + .../james/mime4j/field/MailboxField.java | 68 + .../james/mime4j/field/MailboxListField.java | 65 + .../james/mime4j/field/UnstructuredField.java | 49 + .../james/mime4j/field/address/Address.java | 52 + .../mime4j/field/address/AddressList.java | 140 ++ .../james/mime4j/field/address/Builder.java | 244 +++ .../mime4j/field/address/DomainList.java | 76 + .../james/mime4j/field/address/Group.java | 73 + .../james/mime4j/field/address/Mailbox.java | 119 ++ .../mime4j/field/address/MailboxList.java | 71 + .../mime4j/field/address/NamedMailbox.java | 70 + .../field/address/parser/ASTaddr_spec.java | 19 + .../field/address/parser/ASTaddress.java | 19 + .../field/address/parser/ASTaddress_list.java | 19 + .../field/address/parser/ASTangle_addr.java | 19 + .../field/address/parser/ASTdomain.java | 19 + .../field/address/parser/ASTgroup_body.java | 19 + .../field/address/parser/ASTlocal_part.java | 19 + .../field/address/parser/ASTmailbox.java | 19 + .../field/address/parser/ASTname_addr.java | 19 + .../field/address/parser/ASTphrase.java | 19 + .../mime4j/field/address/parser/ASTroute.java | 19 + .../address/parser/AddressListParser.java | 977 +++++++++ .../field/address/parser/AddressListParser.jj | 595 ++++++ .../parser/AddressListParserConstants.java | 76 + .../parser/AddressListParserTokenManager.java | 1009 +++++++++ .../AddressListParserTreeConstants.java | 35 + .../parser/AddressListParserVisitor.java | 19 + .../mime4j/field/address/parser/BaseNode.java | 30 + .../parser/JJTAddressListParserState.java | 123 ++ .../mime4j/field/address/parser/Node.java | 37 + .../field/address/parser/ParseException.java | 207 ++ .../address/parser/SimpleCharStream.java | 454 ++++ .../field/address/parser/SimpleNode.java | 87 + .../mime4j/field/address/parser/Token.java | 96 + .../field/address/parser/TokenMgrError.java | 148 ++ .../contenttype/parser/ContentTypeParser.java | 267 +++ .../parser/ContentTypeParserConstants.java | 62 + .../parser/ContentTypeParserTokenManager.java | 877 ++++++++ .../contenttype/parser/ParseException.java | 207 ++ .../contenttype/parser/SimpleCharStream.java | 454 ++++ .../field/contenttype/parser/Token.java | 96 + .../contenttype/parser/TokenMgrError.java | 148 ++ .../james/mime4j/field/datetime/DateTime.java | 127 ++ .../field/datetime/parser/DateTimeParser.java | 570 +++++ .../parser/DateTimeParserConstants.java | 86 + .../parser/DateTimeParserTokenManager.java | 882 ++++++++ .../field/datetime/parser/ParseException.java | 207 ++ .../datetime/parser/SimpleCharStream.java | 454 ++++ .../mime4j/field/datetime/parser/Token.java | 96 + .../field/datetime/parser/TokenMgrError.java | 148 ++ .../james/mime4j/message/AbstractBody.java | 47 + .../james/mime4j/message/BinaryBody.java | 42 + src/org/apache/james/mime4j/message/Body.java | 54 + .../apache/james/mime4j/message/BodyPart.java | 42 + .../apache/james/mime4j/message/Entity.java | 170 ++ .../apache/james/mime4j/message/Header.java | 155 ++ .../mime4j/message/MemoryBinaryBody.java | 90 + .../james/mime4j/message/MemoryTextBody.java | 116 + .../apache/james/mime4j/message/Message.java | 256 +++ .../james/mime4j/message/Multipart.java | 203 ++ .../mime4j/message/TempFileBinaryBody.java | 89 + .../mime4j/message/TempFileTextBody.java | 115 + .../apache/james/mime4j/message/TextBody.java | 42 + .../apache/james/mime4j/util/CharsetUtil.java | 1178 ++++++++++ .../james/mime4j/util/PartialInputStream.java | 63 + .../mime4j/util/PositionInputStream.java | 87 + .../james/mime4j/util/SimpleTempStorage.java | 236 ++ .../apache/james/mime4j/util/TempFile.java | 84 + .../apache/james/mime4j/util/TempPath.java | 73 + .../apache/james/mime4j/util/TempStorage.java | 70 + 245 files changed, 49373 insertions(+) create mode 100644 src/com/beetstra/jutf7/Base64Util.java create mode 100644 src/com/beetstra/jutf7/CharsetProvider.java create mode 100644 src/com/beetstra/jutf7/ModifiedUTF7Charset.java create mode 100644 src/com/beetstra/jutf7/UTF7Charset.java create mode 100644 src/com/beetstra/jutf7/UTF7StyleCharset.java create mode 100644 src/com/beetstra/jutf7/UTF7StyleCharsetDecoder.java create mode 100644 src/com/beetstra/jutf7/UTF7StyleCharsetEncoder.java create mode 100644 src/com/fsck/k9/Account.java create mode 100644 src/com/fsck/k9/EmailAddressAdapter.java create mode 100644 src/com/fsck/k9/EmailAddressValidator.java create mode 100644 src/com/fsck/k9/FixedLengthInputStream.java create mode 100644 src/com/fsck/k9/Manifest.java create mode 100644 src/com/fsck/k9/MessagingController.java create mode 100644 src/com/fsck/k9/MessagingListener.java create mode 100644 src/com/fsck/k9/PeekableInputStream.java create mode 100644 src/com/fsck/k9/Preferences.java create mode 100644 src/com/fsck/k9/R.java create mode 100644 src/com/fsck/k9/Utility.java create mode 100644 src/com/fsck/k9/activity/Accounts.java create mode 100644 src/com/fsck/k9/activity/Debug.java create mode 100644 src/com/fsck/k9/activity/FolderMessageList.java create mode 100644 src/com/fsck/k9/activity/MessageCompose.java create mode 100644 src/com/fsck/k9/activity/MessageView.java create mode 100644 src/com/fsck/k9/activity/ProgressListener.java create mode 100644 src/com/fsck/k9/activity/Welcome.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSettings.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupAccountType.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupBasics.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupComposition.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupIncoming.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupNames.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupOptions.java create mode 100644 src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java create mode 100644 src/com/fsck/k9/activity/setup/SpinnerOption.java create mode 100644 src/com/fsck/k9/codec/binary/Base64.java create mode 100644 src/com/fsck/k9/codec/binary/Base64OutputStream.java create mode 100644 src/com/fsck/k9/k9.java create mode 100644 src/com/fsck/k9/mail/Address.java create mode 100644 src/com/fsck/k9/mail/AuthenticationFailedException.java create mode 100644 src/com/fsck/k9/mail/Body.java create mode 100644 src/com/fsck/k9/mail/BodyPart.java create mode 100644 src/com/fsck/k9/mail/CertificateValidationException.java create mode 100644 src/com/fsck/k9/mail/FetchProfile.java create mode 100644 src/com/fsck/k9/mail/Flag.java create mode 100644 src/com/fsck/k9/mail/Folder.java create mode 100644 src/com/fsck/k9/mail/Message.java create mode 100644 src/com/fsck/k9/mail/MessageDateComparator.java create mode 100644 src/com/fsck/k9/mail/MessageRetrievalListener.java create mode 100644 src/com/fsck/k9/mail/MessagingException.java create mode 100644 src/com/fsck/k9/mail/Multipart.java create mode 100644 src/com/fsck/k9/mail/NoSuchProviderException.java create mode 100644 src/com/fsck/k9/mail/Part.java create mode 100644 src/com/fsck/k9/mail/Store.java create mode 100644 src/com/fsck/k9/mail/Transport.java create mode 100644 src/com/fsck/k9/mail/internet/BinaryTempFileBody.java create mode 100644 src/com/fsck/k9/mail/internet/MimeBodyPart.java create mode 100644 src/com/fsck/k9/mail/internet/MimeHeader.java create mode 100644 src/com/fsck/k9/mail/internet/MimeMessage.java create mode 100644 src/com/fsck/k9/mail/internet/MimeMultipart.java create mode 100644 src/com/fsck/k9/mail/internet/MimeUtility.java create mode 100644 src/com/fsck/k9/mail/internet/TextBody.java create mode 100644 src/com/fsck/k9/mail/store/ImapResponseParser.java create mode 100644 src/com/fsck/k9/mail/store/ImapStore.java create mode 100644 src/com/fsck/k9/mail/store/LocalStore.java create mode 100644 src/com/fsck/k9/mail/store/Pop3Store.java create mode 100644 src/com/fsck/k9/mail/store/TrustManagerFactory.java create mode 100644 src/com/fsck/k9/mail/transport/CountingOutputStream.java create mode 100644 src/com/fsck/k9/mail/transport/EOLConvertingOutputStream.java create mode 100644 src/com/fsck/k9/mail/transport/SmtpTransport.java create mode 100644 src/com/fsck/k9/mail/transport/StatusOutputStream.java create mode 100644 src/com/fsck/k9/provider/AttachmentProvider.java create mode 100644 src/com/fsck/k9/service/BootReceiver.java create mode 100644 src/com/fsck/k9/service/MailService.java create mode 100644 src/org/apache/commons/io/CopyUtils.java create mode 100644 src/org/apache/commons/io/DirectoryWalker.java create mode 100644 src/org/apache/commons/io/EndianUtils.java create mode 100644 src/org/apache/commons/io/FileCleaner.java create mode 100644 src/org/apache/commons/io/FileCleaningTracker.java create mode 100644 src/org/apache/commons/io/FileDeleteStrategy.java create mode 100644 src/org/apache/commons/io/FileSystemUtils.java create mode 100644 src/org/apache/commons/io/FileUtils.java create mode 100644 src/org/apache/commons/io/FilenameUtils.java create mode 100644 src/org/apache/commons/io/HexDump.java create mode 100644 src/org/apache/commons/io/IOCase.java create mode 100644 src/org/apache/commons/io/IOExceptionWithCause.java create mode 100644 src/org/apache/commons/io/IOUtils.java create mode 100644 src/org/apache/commons/io/LineIterator.java create mode 100644 src/org/apache/commons/io/comparator/DefaultFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/ExtensionFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/LastModifiedFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/NameFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/PathFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/ReverseComparator.java create mode 100644 src/org/apache/commons/io/comparator/SizeFileComparator.java create mode 100644 src/org/apache/commons/io/comparator/package.html create mode 100644 src/org/apache/commons/io/filefilter/AbstractFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/AgeFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/AndFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/CanReadFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/CanWriteFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/ConditionalFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/DelegateFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/DirectoryFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/EmptyFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/FalseFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/FileFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/FileFilterUtils.java create mode 100644 src/org/apache/commons/io/filefilter/HiddenFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/IOFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/NameFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/NotFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/OrFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/PrefixFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/RegexFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/SizeFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/SuffixFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/TrueFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/WildcardFileFilter.java create mode 100644 src/org/apache/commons/io/filefilter/WildcardFilter.java create mode 100644 src/org/apache/commons/io/filefilter/package.html create mode 100644 src/org/apache/commons/io/input/AutoCloseInputStream.java create mode 100644 src/org/apache/commons/io/input/CharSequenceReader.java create mode 100644 src/org/apache/commons/io/input/ClassLoaderObjectInputStream.java create mode 100644 src/org/apache/commons/io/input/CloseShieldInputStream.java create mode 100644 src/org/apache/commons/io/input/ClosedInputStream.java create mode 100644 src/org/apache/commons/io/input/CountingInputStream.java create mode 100644 src/org/apache/commons/io/input/DemuxInputStream.java create mode 100644 src/org/apache/commons/io/input/NullInputStream.java create mode 100644 src/org/apache/commons/io/input/NullReader.java create mode 100644 src/org/apache/commons/io/input/ProxyInputStream.java create mode 100644 src/org/apache/commons/io/input/ProxyReader.java create mode 100644 src/org/apache/commons/io/input/SwappedDataInputStream.java create mode 100644 src/org/apache/commons/io/input/TeeInputStream.java create mode 100644 src/org/apache/commons/io/input/package.html create mode 100644 src/org/apache/commons/io/output/ByteArrayOutputStream.java create mode 100644 src/org/apache/commons/io/output/CloseShieldOutputStream.java create mode 100644 src/org/apache/commons/io/output/ClosedOutputStream.java create mode 100644 src/org/apache/commons/io/output/CountingOutputStream.java create mode 100644 src/org/apache/commons/io/output/DeferredFileOutputStream.java create mode 100644 src/org/apache/commons/io/output/DemuxOutputStream.java create mode 100644 src/org/apache/commons/io/output/FileWriterWithEncoding.java create mode 100644 src/org/apache/commons/io/output/LockableFileWriter.java create mode 100644 src/org/apache/commons/io/output/NullOutputStream.java create mode 100644 src/org/apache/commons/io/output/NullWriter.java create mode 100644 src/org/apache/commons/io/output/ProxyOutputStream.java create mode 100644 src/org/apache/commons/io/output/ProxyWriter.java create mode 100644 src/org/apache/commons/io/output/TeeOutputStream.java create mode 100644 src/org/apache/commons/io/output/ThresholdingOutputStream.java create mode 100644 src/org/apache/commons/io/output/package.html create mode 100644 src/org/apache/commons/io/overview.html create mode 100644 src/org/apache/commons/io/package.html create mode 100644 src/org/apache/james/mime4j/AbstractContentHandler.java create mode 100644 src/org/apache/james/mime4j/BodyDescriptor.java create mode 100644 src/org/apache/james/mime4j/CloseShieldInputStream.java create mode 100644 src/org/apache/james/mime4j/ContentHandler.java create mode 100644 src/org/apache/james/mime4j/EOLConvertingInputStream.java create mode 100644 src/org/apache/james/mime4j/MimeBoundaryInputStream.java create mode 100644 src/org/apache/james/mime4j/MimeStreamParser.java create mode 100644 src/org/apache/james/mime4j/RootInputStream.java create mode 100644 src/org/apache/james/mime4j/SimpleContentHandler.java create mode 100644 src/org/apache/james/mime4j/decoder/Base64InputStream.java create mode 100644 src/org/apache/james/mime4j/decoder/ByteQueue.java create mode 100644 src/org/apache/james/mime4j/decoder/DecoderUtil.java create mode 100644 src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java create mode 100644 src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java create mode 100644 src/org/apache/james/mime4j/field/AddressListField.java create mode 100644 src/org/apache/james/mime4j/field/ContentTransferEncodingField.java create mode 100644 src/org/apache/james/mime4j/field/ContentTypeField.java create mode 100644 src/org/apache/james/mime4j/field/DateTimeField.java create mode 100644 src/org/apache/james/mime4j/field/DefaultFieldParser.java create mode 100644 src/org/apache/james/mime4j/field/DelegatingFieldParser.java create mode 100644 src/org/apache/james/mime4j/field/Field.java create mode 100644 src/org/apache/james/mime4j/field/FieldParser.java create mode 100644 src/org/apache/james/mime4j/field/MailboxField.java create mode 100644 src/org/apache/james/mime4j/field/MailboxListField.java create mode 100644 src/org/apache/james/mime4j/field/UnstructuredField.java create mode 100644 src/org/apache/james/mime4j/field/address/Address.java create mode 100644 src/org/apache/james/mime4j/field/address/AddressList.java create mode 100644 src/org/apache/james/mime4j/field/address/Builder.java create mode 100644 src/org/apache/james/mime4j/field/address/DomainList.java create mode 100644 src/org/apache/james/mime4j/field/address/Group.java create mode 100644 src/org/apache/james/mime4j/field/address/Mailbox.java create mode 100644 src/org/apache/james/mime4j/field/address/MailboxList.java create mode 100644 src/org/apache/james/mime4j/field/address/NamedMailbox.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTaddress.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTdomain.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTphrase.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ASTroute.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParser.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/BaseNode.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/Node.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/ParseException.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/SimpleNode.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/Token.java create mode 100644 src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/Token.java create mode 100644 src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java create mode 100644 src/org/apache/james/mime4j/field/datetime/DateTime.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/ParseException.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/Token.java create mode 100644 src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java create mode 100644 src/org/apache/james/mime4j/message/AbstractBody.java create mode 100644 src/org/apache/james/mime4j/message/BinaryBody.java create mode 100644 src/org/apache/james/mime4j/message/Body.java create mode 100644 src/org/apache/james/mime4j/message/BodyPart.java create mode 100644 src/org/apache/james/mime4j/message/Entity.java create mode 100644 src/org/apache/james/mime4j/message/Header.java create mode 100644 src/org/apache/james/mime4j/message/MemoryBinaryBody.java create mode 100644 src/org/apache/james/mime4j/message/MemoryTextBody.java create mode 100644 src/org/apache/james/mime4j/message/Message.java create mode 100644 src/org/apache/james/mime4j/message/Multipart.java create mode 100644 src/org/apache/james/mime4j/message/TempFileBinaryBody.java create mode 100644 src/org/apache/james/mime4j/message/TempFileTextBody.java create mode 100644 src/org/apache/james/mime4j/message/TextBody.java create mode 100644 src/org/apache/james/mime4j/util/CharsetUtil.java create mode 100644 src/org/apache/james/mime4j/util/PartialInputStream.java create mode 100644 src/org/apache/james/mime4j/util/PositionInputStream.java create mode 100644 src/org/apache/james/mime4j/util/SimpleTempStorage.java create mode 100644 src/org/apache/james/mime4j/util/TempFile.java create mode 100644 src/org/apache/james/mime4j/util/TempPath.java create mode 100644 src/org/apache/james/mime4j/util/TempStorage.java diff --git a/src/com/beetstra/jutf7/Base64Util.java b/src/com/beetstra/jutf7/Base64Util.java new file mode 100644 index 000000000..6dffb32c5 --- /dev/null +++ b/src/com/beetstra/jutf7/Base64Util.java @@ -0,0 +1,117 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +import java.util.Arrays; + +/** + *

+ * Represent a base 64 mapping. The 64 characters used in the encoding can be + * specified, since modified-UTF-7 uses other characters than UTF-7 (',' instead + * of '/'). + *

+ *

+ * The exact type of the arguments and result values is adapted to the needs of + * the encoder and decoder, as opposed to following a strict interpretation of + * base 64. + *

+ *

+ * Base 64, as specified in RFC 2045, is an encoding used to encode bytes as + * characters. In (modified-)UTF-7 however, it is used to encode characters as + * bytes, using some intermediate steps: + *

+ *
    + *
  1. Encode all characters as a 16-bit (UTF-16) integer value
  2. + *
  3. Write this as stream of bytes (most-significant first)
  4. + *
  5. Encode these bytes using (modified) base 64 encoding
  6. + *
  7. Write the thus formed stream of characters as a stream of bytes, using + * ASCII encoding
  8. + *
+ * + * @author Jaap Beetstra + */ +class Base64Util { + private static final int ALPHABET_LENGTH = 64; + private final char[] alphabet; + private final int[] inverseAlphabet; + + /** + * Initializes the class with the specified encoding/decoding alphabet. + * + * @param alphabet + * @throws IllegalArgumentException if alphabet is not 64 characters long or + * contains characters which are not 7-bit ASCII + */ + Base64Util(final String alphabet) { + this.alphabet = alphabet.toCharArray(); + if (alphabet.length() != ALPHABET_LENGTH) + throw new IllegalArgumentException("alphabet has incorrect length (should be 64, not " + + alphabet.length() + ")"); + inverseAlphabet = new int[128]; + Arrays.fill(inverseAlphabet, -1); + for (int i = 0; i < this.alphabet.length; i++) { + final char ch = this.alphabet[i]; + if (ch >= 128) + throw new IllegalArgumentException("invalid character in alphabet: " + ch); + inverseAlphabet[ch] = i; + } + } + + /** + * Returns the integer value of the six bits represented by the specified + * character. + * + * @param ch The character, as a ASCII encoded byte + * @return The six bits, as an integer value, or -1 if the byte is not in + * the alphabet + */ + int getSextet(final byte ch) { + if (ch >= 128) + return -1; + return inverseAlphabet[ch]; + } + + /** + * Tells whether the alphabet contains the specified character. + * + * @param ch The character + * @return true if the alphabet contains ch, false otherwise + */ + boolean contains(final char ch) { + if (ch >= 128) + return false; + return inverseAlphabet[ch] >= 0; + } + + /** + * Encodes the six bit group as a character. + * + * @param sextet The six bit group to be encoded + * @return The ASCII value of the character + */ + byte getChar(final int sextet) { + return (byte)alphabet[sextet]; + } +} diff --git a/src/com/beetstra/jutf7/CharsetProvider.java b/src/com/beetstra/jutf7/CharsetProvider.java new file mode 100644 index 000000000..f0cabe562 --- /dev/null +++ b/src/com/beetstra/jutf7/CharsetProvider.java @@ -0,0 +1,90 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + *

+ * Charset service-provider class used for both variants of the UTF-7 charset + * and the modified-UTF-7 charset. + *

+ * + * @author Jaap Beetstra + */ +public class CharsetProvider extends java.nio.charset.spi.CharsetProvider { + private static final String UTF7_NAME = "UTF-7"; + private static final String UTF7_O_NAME = "X-UTF-7-OPTIONAL"; + private static final String UTF7_M_NAME = "X-MODIFIED-UTF-7"; + private static final String[] UTF7_ALIASES = new String[] { + "UNICODE-1-1-UTF-7", "CSUNICODE11UTF7", "X-RFC2152", "X-RFC-2152" + }; + private static final String[] UTF7_O_ALIASES = new String[] { + "X-RFC2152-OPTIONAL", "X-RFC-2152-OPTIONAL" + }; + private static final String[] UTF7_M_ALIASES = new String[] { + "X-IMAP-MODIFIED-UTF-7", "X-IMAP4-MODIFIED-UTF7", "X-IMAP4-MODIFIED-UTF-7", + "X-RFC3501", "X-RFC-3501" + }; + private Charset utf7charset = new UTF7Charset(UTF7_NAME, UTF7_ALIASES, false); + private Charset utf7oCharset = new UTF7Charset(UTF7_O_NAME, UTF7_O_ALIASES, true); + private Charset imap4charset = new ModifiedUTF7Charset(UTF7_M_NAME, UTF7_M_ALIASES); + private List charsets; + + public CharsetProvider() { + charsets = Arrays.asList(new Object[] { + utf7charset, imap4charset, utf7oCharset + }); + } + + /** + * {@inheritDoc} + */ + public Charset charsetForName(String charsetName) { + charsetName = charsetName.toUpperCase(); + for (Iterator iter = charsets.iterator(); iter.hasNext();) { + Charset charset = (Charset)iter.next(); + if (charset.name().equals(charsetName)) + return charset; + } + for (Iterator iter = charsets.iterator(); iter.hasNext();) { + Charset charset = (Charset)iter.next(); + if (charset.aliases().contains(charsetName)) + return charset; + } + return null; + } + + /** + * {@inheritDoc} + */ + public Iterator charsets() { + return charsets.iterator(); + } +} diff --git a/src/com/beetstra/jutf7/ModifiedUTF7Charset.java b/src/com/beetstra/jutf7/ModifiedUTF7Charset.java new file mode 100644 index 000000000..603a19ee9 --- /dev/null +++ b/src/com/beetstra/jutf7/ModifiedUTF7Charset.java @@ -0,0 +1,57 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +/** + *

+ * The character set specified in RFC 3501 to use for IMAP4rev1 mailbox name + * encoding. + *

+ * + * @see RFC 3501< /a> + * @author Jaap Beetstra + */ +class ModifiedUTF7Charset extends UTF7StyleCharset { + private static final String MODIFIED_BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + "0123456789+,"; + + ModifiedUTF7Charset(String name, String[] aliases) { + super(name, aliases, MODIFIED_BASE64_ALPHABET, true); + } + + boolean canEncodeDirectly(char ch) { + if (ch == shift()) + return false; + return ch >= 0x20 && ch <= 0x7E; + } + + byte shift() { + return '&'; + } + + byte unshift() { + return '-'; + } +} diff --git a/src/com/beetstra/jutf7/UTF7Charset.java b/src/com/beetstra/jutf7/UTF7Charset.java new file mode 100644 index 000000000..8c85472cf --- /dev/null +++ b/src/com/beetstra/jutf7/UTF7Charset.java @@ -0,0 +1,75 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +/** + *

+ * The character set specified in RFC 2152. Two variants are supported using the + * encodeOptional constructor flag + *

+ * + * @see
RFC 2152< /a> + * @author Jaap Beetstra + */ +class UTF7Charset extends UTF7StyleCharset { + private static final String BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + private static final String SET_D = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'(),-./:?"; + private static final String SET_O = "!\"#$%&*;<=>@[]^_`{|}"; + private static final String RULE_3 = " \t\r\n"; + final String directlyEncoded; + + UTF7Charset(String name, String[] aliases, boolean includeOptional) { + super(name, aliases, BASE64_ALPHABET, false); + if (includeOptional) + this.directlyEncoded = SET_D + SET_O + RULE_3; + else + this.directlyEncoded = SET_D + RULE_3; + } + + /* + * (non-Javadoc) + * @see com.beetstra.jutf7.UTF7StyleCharset#canEncodeDirectly(char) + */ + boolean canEncodeDirectly(char ch) { + return directlyEncoded.indexOf(ch) >= 0; + } + + /* + * (non-Javadoc) + * @see com.beetstra.jutf7.UTF7StyleCharset#shift() + */ + byte shift() { + return '+'; + } + + /* + * (non-Javadoc) + * @see com.beetstra.jutf7.UTF7StyleCharset#unshift() + */ + byte unshift() { + return '-'; + } +} diff --git a/src/com/beetstra/jutf7/UTF7StyleCharset.java b/src/com/beetstra/jutf7/UTF7StyleCharset.java new file mode 100644 index 000000000..0878af603 --- /dev/null +++ b/src/com/beetstra/jutf7/UTF7StyleCharset.java @@ -0,0 +1,117 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.Arrays; +import java.util.List; + +/** + *

+ * Abstract base class for UTF-7 style encoding and decoding. + *

+ * + * @author Jaap Beetstra + */ +abstract class UTF7StyleCharset extends Charset { + private static final List CONTAINED = Arrays.asList(new String[] { + "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16", "UTF-16LE", "UTF-16BE" + }); + final boolean strict; + Base64Util base64; + + /** + *

+ * Besides the name and aliases, two additional parameters are required. + * First the base 64 alphabet used; in modified UTF-7 a slightly different + * alphabet is used. Additionally, it should be specified if encoders and + * decoders should be strict about the interpretation of malformed encoded + * sequences. This is used since modified UTF-7 specifically disallows some + * constructs which are allowed (or not specifically disallowed) in UTF-7 + * (RFC 2152). + *

+ * + * @param canonicalName The name as defined in java.nio.charset.Charset + * @param aliases The aliases as defined in java.nio.charset.Charset + * @param alphabet The base 64 alphabet used + * @param strict True if strict handling of sequences is requested + */ + protected UTF7StyleCharset(String canonicalName, String[] aliases, String alphabet, + boolean strict) { + super(canonicalName, aliases); + this.base64 = new Base64Util(alphabet); + this.strict = strict; + } + + /* + * (non-Javadoc) + * @see java.nio.charset.Charset#contains(java.nio.charset.Charset) + */ + public boolean contains(final Charset cs) { + return CONTAINED.contains(cs.name()); + } + + /* + * (non-Javadoc) + * @see java.nio.charset.Charset#newDecoder() + */ + public CharsetDecoder newDecoder() { + return new UTF7StyleCharsetDecoder(this, base64, strict); + } + + /* + * (non-Javadoc) + * @see java.nio.charset.Charset#newEncoder() + */ + public CharsetEncoder newEncoder() { + return new UTF7StyleCharsetEncoder(this, base64, strict); + } + + /** + * Tells if a character can be encoded using simple (US-ASCII) encoding or + * requires base 64 encoding. + * + * @param ch The character + * @return True if the character can be encoded directly, false otherwise + */ + abstract boolean canEncodeDirectly(char ch); + + /** + * Returns character used to switch to base 64 encoding. + * + * @return The shift character + */ + abstract byte shift(); + + /** + * Returns character used to switch from base 64 encoding to simple + * encoding. + * + * @return The unshift character + */ + abstract byte unshift(); +} diff --git a/src/com/beetstra/jutf7/UTF7StyleCharsetDecoder.java b/src/com/beetstra/jutf7/UTF7StyleCharsetDecoder.java new file mode 100644 index 000000000..2fa9d3435 --- /dev/null +++ b/src/com/beetstra/jutf7/UTF7StyleCharsetDecoder.java @@ -0,0 +1,195 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; + +/** + *

+ * The CharsetDecoder used to decode both variants of the UTF-7 charset and the + * modified-UTF-7 charset. + *

+ * + * @author Jaap Beetstra + */ +class UTF7StyleCharsetDecoder extends CharsetDecoder { + private final Base64Util base64; + private final byte shift; + private final byte unshift; + private final boolean strict; + private boolean base64mode; + private int bitsRead; + private int tempChar; + private boolean justShifted; + private boolean justUnshifted; + + UTF7StyleCharsetDecoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) { + super(cs, 0.6f, 1.0f); + this.base64 = base64; + this.strict = strict; + this.shift = cs.shift(); + this.unshift = cs.unshift(); + } + + /* + * (non-Javadoc) + * @see java.nio.charset.CharsetDecoder#decodeLoop(java.nio.ByteBuffer, + * java.nio.CharBuffer) + */ + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { + while (in.hasRemaining()) { + byte b = in.get(); + if (base64mode) { + if (b == unshift) { + if (base64bitsWaiting()) + return malformed(in); + if (justShifted) { + if (!out.hasRemaining()) + return overflow(in); + out.put((char)shift); + } else + justUnshifted = true; + setUnshifted(); + } else { + if (!out.hasRemaining()) + return overflow(in); + CoderResult result = handleBase64(in, out, b); + if (result != null) + return result; + } + justShifted = false; + } else { + if (b == shift) { + base64mode = true; + if (justUnshifted && strict) + return malformed(in); + justShifted = true; + continue; + } + if (!out.hasRemaining()) + return overflow(in); + out.put((char)b); + justUnshifted = false; + } + } + return CoderResult.UNDERFLOW; + } + + private CoderResult overflow(ByteBuffer in) { + in.position(in.position() - 1); + return CoderResult.OVERFLOW; + } + + /** + *

+ * Decodes a byte in base 64 mode. Will directly write a character to + * the output buffer if completed. + *

+ * + * @param in The input buffer + * @param out The output buffer + * @param lastRead Last byte read from the input buffer + * @return CoderResult.malformed if a non-base 64 character was encountered + * in strict mode, null otherwise + */ + private CoderResult handleBase64(ByteBuffer in, CharBuffer out, byte lastRead) { + CoderResult result = null; + int sextet = base64.getSextet(lastRead); + if (sextet >= 0) { + bitsRead += 6; + if (bitsRead < 16) { + tempChar += sextet << (16 - bitsRead); + } else { + bitsRead -= 16; + tempChar += sextet >> (bitsRead); + out.put((char)tempChar); + tempChar = (sextet << (16 - bitsRead)) & 0xFFFF; + } + } else { + if (strict) + return malformed(in); + out.put((char)lastRead); + if (base64bitsWaiting()) + result = malformed(in); + setUnshifted(); + } + return result; + } + + /* + * (non-Javadoc) + * @see java.nio.charset.CharsetDecoder#implFlush(java.nio.CharBuffer) + */ + protected CoderResult implFlush(CharBuffer out) { + if ((base64mode && strict) || base64bitsWaiting()) + return CoderResult.malformedForLength(1); + return CoderResult.UNDERFLOW; + } + + /* + * (non-Javadoc) + * @see java.nio.charset.CharsetDecoder#implReset() + */ + protected void implReset() { + setUnshifted(); + justUnshifted = false; + } + + /** + *

+ * Resets the input buffer position to just before the last byte read, and + * returns a result indicating to skip the last byte. + *

+ * + * @param in The input buffer + * @return CoderResult.malformedForLength(1); + */ + private CoderResult malformed(ByteBuffer in) { + in.position(in.position() - 1); + return CoderResult.malformedForLength(1); + } + + /** + * @return True if there are base64 encoded characters waiting to be written + */ + private boolean base64bitsWaiting() { + return tempChar != 0 || bitsRead >= 6; + } + + /** + *

+ * Updates internal state to reflect the decoder is no longer in base 64 + * mode + *

+ */ + private void setUnshifted() { + base64mode = false; + bitsRead = 0; + tempChar = 0; + } +} diff --git a/src/com/beetstra/jutf7/UTF7StyleCharsetEncoder.java b/src/com/beetstra/jutf7/UTF7StyleCharsetEncoder.java new file mode 100644 index 000000000..de8239713 --- /dev/null +++ b/src/com/beetstra/jutf7/UTF7StyleCharsetEncoder.java @@ -0,0 +1,217 @@ +/* ==================================================================== + * Copyright (c) 2006 J.T. Beetstra + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ==================================================================== + */ + +package com.beetstra.jutf7; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; + +/** + *

+ * The CharsetEncoder used to encode both variants of the UTF-7 charset and the + * modified-UTF-7 charset. + *

+ *

+ * Please note this class does not behave strictly according to the + * specification in Sun Java VMs before 1.6. This is done to get around + * a bug in the implementation of + * {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)}. Unfortunately, + * that method cannot be overridden. + *

+ * + * @see
JDK + * bug 6221056< /a> + * @author Jaap Beetstra + */ +class UTF7StyleCharsetEncoder extends CharsetEncoder { + private static final float AVG_BYTES_PER_CHAR = 1.5f; + private static final float MAX_BYTES_PER_CHAR = 5.0f; + private final UTF7StyleCharset cs; + private final Base64Util base64; + private final byte shift; + private final byte unshift; + private final boolean strict; + private boolean base64mode; + private int bitsToOutput; + private int sextet; + static boolean useUglyHackToForceCallToFlushInJava5; + static { + String version = System.getProperty("java.specification.version"); + String vendor = System.getProperty("java.vm.vendor"); + useUglyHackToForceCallToFlushInJava5 = "1.4".equals(version) || "1.5".equals(version); + useUglyHackToForceCallToFlushInJava5 &= "Sun Microsystems Inc.".equals(vendor); + } + + UTF7StyleCharsetEncoder(UTF7StyleCharset cs, Base64Util base64, boolean strict) { + super(cs, AVG_BYTES_PER_CHAR, MAX_BYTES_PER_CHAR); + this.cs = cs; + this.base64 = base64; + this.strict = strict; + this.shift = cs.shift(); + this.unshift = cs.unshift(); + } + + /* + * (non-Javadoc) + * @see java.nio.charset.CharsetEncoder#implReset() + */ + protected void implReset() { + base64mode = false; + sextet = 0; + bitsToOutput = 0; + } + + /** + * {@inheritDoc} + *

+ * Note that this method might return CoderResult.OVERFLOW (as + * is required by the specification) if insufficient space is available in + * the output buffer. However, calling it again on JDKs before Java 6 + * triggers a bug in + * {@link java.nio.charset.CharsetEncoder#flush(ByteBuffer)} causing it to + * throw an IllegalStateException (the buggy method is final, + * thus cannot be overridden). + *

+ * + * @see
+ * JDK bug 6227608< /a> + * @param out The output byte buffer + * @return A coder-result object describing the reason for termination + */ + protected CoderResult implFlush(ByteBuffer out) { + if (base64mode) { + if (out.remaining() < 2) + return CoderResult.OVERFLOW; + if (bitsToOutput != 0) + out.put(base64.getChar(sextet)); + out.put(unshift); + } + return CoderResult.UNDERFLOW; + } + + /** + * {@inheritDoc} + *

+ * Note that this method might return CoderResult.OVERFLOW, + * even though there is sufficient space available in the output buffer. + * This is done to force the broken implementation of + * {@link java.nio.charset.CharsetEncoder#encode(CharBuffer)} to call flush + * (the buggy method is final, thus cannot be overridden). + *

+ *

+ * However, String.getBytes() fails if CoderResult.OVERFLOW is returned, + * since this assumes it always allocates sufficient bytes (maxBytesPerChar + * * nr_of_chars). Thus, as an extra check, the size of the input buffer is + * compared against the size of the output buffer. A static variable is used + * to indicate if a broken java version is used. + *

+ *

+ * It is not possible to directly write the last few bytes, since more bytes + * might be waiting to be encoded then those available in the input buffer. + *

+ * + * @see
+ * JDK bug 6221056< /a> + * @param in The input character buffer + * @param out The output byte buffer + * @return A coder-result object describing the reason for termination + */ + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { + while (in.hasRemaining()) { + if (out.remaining() < 4) + return CoderResult.OVERFLOW; + char ch = in.get(); + if (cs.canEncodeDirectly(ch)) { + unshift(out, ch); + out.put((byte)ch); + } else if (!base64mode && ch == shift) { + out.put(shift); + out.put(unshift); + } else + encodeBase64(ch, out); + } + /* + * These lines are required to trick JDK 1.5 and + * earlier into flushing when using Charset.encode(String), + * Charset.encode(CharBuffer) or CharsetEncoder.encode(CharBuffer) + * Without them, the last few bytes may be missing. + */ + if (base64mode && useUglyHackToForceCallToFlushInJava5 + && out.limit() != MAX_BYTES_PER_CHAR * in.limit()) + return CoderResult.OVERFLOW; + /* */ + return CoderResult.UNDERFLOW; + } + + /** + *

+ * Writes the bytes necessary to leave base 64 mode. This might + * include an unshift character. + *

+ * + * @param out + * @param ch + */ + private void unshift(ByteBuffer out, char ch) { + if (!base64mode) + return; + if (bitsToOutput != 0) + out.put(base64.getChar(sextet)); + if (base64.contains(ch) || ch == unshift || strict) + out.put(unshift); + base64mode = false; + sextet = 0; + bitsToOutput = 0; + } + + /** + *

+ * Writes the bytes necessary to encode a character in base 64 mode. + * All bytes which are fully determined will be written. The fields + * bitsToOutput and sextet are used to remember + * the bytes not yet fully determined. + *

+ * + * @param out + * @param ch + */ + private void encodeBase64(char ch, ByteBuffer out) { + if (!base64mode) + out.put(shift); + base64mode = true; + bitsToOutput += 16; + while (bitsToOutput >= 6) { + bitsToOutput -= 6; + sextet += (ch >> bitsToOutput); + sextet &= 0x3F; + out.put(base64.getChar(sextet)); + sextet = 0; + } + sextet = (ch << (6 - bitsToOutput)) & 0x3F; + } +} diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java new file mode 100644 index 000000000..013489b9a --- /dev/null +++ b/src/com/fsck/k9/Account.java @@ -0,0 +1,372 @@ + +package com.fsck.k9; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.UUID; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; + +/** + * Account stores all of the settings for a single account defined by the user. It is able to save + * and delete itself given a Preferences to work with. Each account is defined by a UUID. + */ +public class Account implements Serializable { + public static final int DELETE_POLICY_NEVER = 0; + public static final int DELETE_POLICY_7DAYS = 1; + public static final int DELETE_POLICY_ON_DELETE = 2; + + private static final long serialVersionUID = 2975156672298625121L; + + String mUuid; + String mStoreUri; + String mLocalStoreUri; + String mTransportUri; + String mDescription; + String mName; + String mEmail; + String mSignature; + String mAlwaysBcc; + int mAutomaticCheckIntervalMinutes; + long mLastAutomaticCheckTime; + boolean mNotifyNewMail; + String mDraftsFolderName; + String mSentFolderName; + String mTrashFolderName; + String mOutboxFolderName; + int mAccountNumber; + boolean mVibrate; + String mRingtoneUri; + + /** + *
+     * 0 Never 
+     * 1 After 7 days 
+     * 2 When I delete from inbox
+     * 
+ */ + int mDeletePolicy; + + public Account(Context context) { + // TODO Change local store path to something readable / recognizable + mUuid = UUID.randomUUID().toString(); + mLocalStoreUri = "local://localhost/" + context.getDatabasePath(mUuid + ".db"); + mAutomaticCheckIntervalMinutes = -1; + mAccountNumber = -1; + mNotifyNewMail = true; + mSignature = "Sent from my Android phone with K-9. Please excuse my brevity."; + mVibrate = false; + mRingtoneUri = "content://settings/system/notification_sound"; + } + + Account(Preferences preferences, String uuid) { + this.mUuid = uuid; + refresh(preferences); + } + + /** + * Refresh the account from the stored settings. + */ + public void refresh(Preferences preferences) { + mStoreUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid + + ".storeUri", null)); + mLocalStoreUri = preferences.mSharedPreferences.getString(mUuid + ".localStoreUri", null); + mTransportUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid + + ".transportUri", null)); + mDescription = preferences.mSharedPreferences.getString(mUuid + ".description", null); + mAlwaysBcc = preferences.mSharedPreferences.getString(mUuid + ".alwaysBcc", mAlwaysBcc); + mName = preferences.mSharedPreferences.getString(mUuid + ".name", mName); + mEmail = preferences.mSharedPreferences.getString(mUuid + ".email", mEmail); + mSignature = preferences.mSharedPreferences.getString(mUuid + ".signature", mSignature); + mAutomaticCheckIntervalMinutes = preferences.mSharedPreferences.getInt(mUuid + + ".automaticCheckIntervalMinutes", -1); + mLastAutomaticCheckTime = preferences.mSharedPreferences.getLong(mUuid + + ".lastAutomaticCheckTime", 0); + mNotifyNewMail = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyNewMail", + false); + mDeletePolicy = preferences.mSharedPreferences.getInt(mUuid + ".deletePolicy", 0); + mDraftsFolderName = preferences.mSharedPreferences.getString(mUuid + ".draftsFolderName", + "Drafts"); + mSentFolderName = preferences.mSharedPreferences.getString(mUuid + ".sentFolderName", + "Sent"); + mTrashFolderName = preferences.mSharedPreferences.getString(mUuid + ".trashFolderName", + "Trash"); + mOutboxFolderName = preferences.mSharedPreferences.getString(mUuid + ".outboxFolderName", + "Outbox"); + mAccountNumber = preferences.mSharedPreferences.getInt(mUuid + ".accountNumber", 0); + mVibrate = preferences.mSharedPreferences.getBoolean(mUuid + ".vibrate", false); + mRingtoneUri = preferences.mSharedPreferences.getString(mUuid + ".ringtone", + "content://settings/system/notification_sound"); + } + + public String getUuid() { + return mUuid; + } + + public String getStoreUri() { + return mStoreUri; + } + + public void setStoreUri(String storeUri) { + this.mStoreUri = storeUri; + } + + public String getTransportUri() { + return mTransportUri; + } + + public void setTransportUri(String transportUri) { + this.mTransportUri = transportUri; + } + + public String getDescription() { + return mDescription; + } + + public void setDescription(String description) { + this.mDescription = description; + } + + public String getName() { + return mName; + } + + public void setName(String name) { + this.mName = name; + } + + public String getSignature() { + return mSignature; + } + + public void setSignature(String signature) { + this.mSignature = signature; + } + + public String getEmail() { + return mEmail; + } + + public void setEmail(String email) { + this.mEmail = email; + } + + public String getAlwaysBcc() { + return mAlwaysBcc; + } + + public void setAlwaysBcc(String alwaysBcc) { + this.mAlwaysBcc = alwaysBcc; + } + + + public boolean isVibrate() { + return mVibrate; + } + + public void setVibrate(boolean vibrate) { + mVibrate = vibrate; + } + + public String getRingtone() { + return mRingtoneUri; + } + + public void setRingtone(String ringtoneUri) { + mRingtoneUri = ringtoneUri; + } + + public void delete(Preferences preferences) { + String[] uuids = preferences.mSharedPreferences.getString("accountUuids", "").split(","); + StringBuffer sb = new StringBuffer(); + for (int i = 0, length = uuids.length; i < length; i++) { + if (!uuids[i].equals(mUuid)) { + if (sb.length() > 0) { + sb.append(','); + } + sb.append(uuids[i]); + } + } + String accountUuids = sb.toString(); + SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + editor.putString("accountUuids", accountUuids); + + editor.remove(mUuid + ".storeUri"); + editor.remove(mUuid + ".localStoreUri"); + editor.remove(mUuid + ".transportUri"); + editor.remove(mUuid + ".description"); + editor.remove(mUuid + ".name"); + editor.remove(mUuid + ".email"); + editor.remove(mUuid + ".alwaysBcc"); + editor.remove(mUuid + ".automaticCheckIntervalMinutes"); + editor.remove(mUuid + ".lastAutomaticCheckTime"); + editor.remove(mUuid + ".notifyNewMail"); + editor.remove(mUuid + ".deletePolicy"); + editor.remove(mUuid + ".draftsFolderName"); + editor.remove(mUuid + ".sentFolderName"); + editor.remove(mUuid + ".trashFolderName"); + editor.remove(mUuid + ".outboxFolderName"); + editor.remove(mUuid + ".accountNumber"); + editor.remove(mUuid + ".vibrate"); + editor.remove(mUuid + ".ringtone"); + editor.commit(); + } + + public void save(Preferences preferences) { + if (!preferences.mSharedPreferences.getString("accountUuids", "").contains(mUuid)) { + /* + * When the account is first created we assign it a unique account number. The + * account number will be unique to that account for the lifetime of the account. + * So, we get all the existing account numbers, sort them ascending, loop through + * the list and check if the number is greater than 1 + the previous number. If so + * we use the previous number + 1 as the account number. This refills gaps. + * mAccountNumber starts as -1 on a newly created account. It must be -1 for this + * algorithm to work. + * + * I bet there is a much smarter way to do this. Anyone like to suggest it? + */ + Account[] accounts = preferences.getAccounts(); + int[] accountNumbers = new int[accounts.length]; + for (int i = 0; i < accounts.length; i++) { + accountNumbers[i] = accounts[i].getAccountNumber(); + } + Arrays.sort(accountNumbers); + for (int accountNumber : accountNumbers) { + if (accountNumber > mAccountNumber + 1) { + break; + } + mAccountNumber = accountNumber; + } + mAccountNumber++; + + String accountUuids = preferences.mSharedPreferences.getString("accountUuids", ""); + accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid; + SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + editor.putString("accountUuids", accountUuids); + editor.commit(); + } + + SharedPreferences.Editor editor = preferences.mSharedPreferences.edit(); + + editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri)); + editor.putString(mUuid + ".localStoreUri", mLocalStoreUri); + editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri)); + editor.putString(mUuid + ".description", mDescription); + editor.putString(mUuid + ".name", mName); + editor.putString(mUuid + ".email", mEmail); + editor.putString(mUuid + ".signature", mSignature); + editor.putString(mUuid + ".alwaysBcc", mAlwaysBcc); + editor.putInt(mUuid + ".automaticCheckIntervalMinutes", mAutomaticCheckIntervalMinutes); + editor.putLong(mUuid + ".lastAutomaticCheckTime", mLastAutomaticCheckTime); + editor.putBoolean(mUuid + ".notifyNewMail", mNotifyNewMail); + editor.putInt(mUuid + ".deletePolicy", mDeletePolicy); + editor.putString(mUuid + ".draftsFolderName", mDraftsFolderName); + editor.putString(mUuid + ".sentFolderName", mSentFolderName); + editor.putString(mUuid + ".trashFolderName", mTrashFolderName); + editor.putString(mUuid + ".outboxFolderName", mOutboxFolderName); + editor.putInt(mUuid + ".accountNumber", mAccountNumber); + editor.putBoolean(mUuid + ".vibrate", mVibrate); + editor.putString(mUuid + ".ringtone", mRingtoneUri); + editor.commit(); + } + + public String toString() { + return mDescription; + } + + public Uri getContentUri() { + return Uri.parse("content://accounts/" + getUuid()); + } + + public String getLocalStoreUri() { + return mLocalStoreUri; + } + + public void setLocalStoreUri(String localStoreUri) { + this.mLocalStoreUri = localStoreUri; + } + + /** + * Returns -1 for never. + */ + public int getAutomaticCheckIntervalMinutes() { + return mAutomaticCheckIntervalMinutes; + } + + /** + * @param automaticCheckIntervalMinutes or -1 for never. + */ + public void setAutomaticCheckIntervalMinutes(int automaticCheckIntervalMinutes) { + this.mAutomaticCheckIntervalMinutes = automaticCheckIntervalMinutes; + } + + public long getLastAutomaticCheckTime() { + return mLastAutomaticCheckTime; + } + + public void setLastAutomaticCheckTime(long lastAutomaticCheckTime) { + this.mLastAutomaticCheckTime = lastAutomaticCheckTime; + } + + public boolean isNotifyNewMail() { + return mNotifyNewMail; + } + + public void setNotifyNewMail(boolean notifyNewMail) { + this.mNotifyNewMail = notifyNewMail; + } + + public int getDeletePolicy() { + return mDeletePolicy; + } + + public void setDeletePolicy(int deletePolicy) { + this.mDeletePolicy = deletePolicy; + } + + public String getDraftsFolderName() { + return mDraftsFolderName; + } + + public void setDraftsFolderName(String draftsFolderName) { + mDraftsFolderName = draftsFolderName; + } + + public String getSentFolderName() { + return mSentFolderName; + } + + public void setSentFolderName(String sentFolderName) { + mSentFolderName = sentFolderName; + } + + public String getTrashFolderName() { + return mTrashFolderName; + } + + public void setTrashFolderName(String trashFolderName) { + mTrashFolderName = trashFolderName; + } + + public String getOutboxFolderName() { + return mOutboxFolderName; + } + + public void setOutboxFolderName(String outboxFolderName) { + mOutboxFolderName = outboxFolderName; + } + + public int getAccountNumber() { + return mAccountNumber; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Account) { + return ((Account)o).mUuid.equals(mUuid); + } + return super.equals(o); + } +} diff --git a/src/com/fsck/k9/EmailAddressAdapter.java b/src/com/fsck/k9/EmailAddressAdapter.java new file mode 100644 index 000000000..f547bafe7 --- /dev/null +++ b/src/com/fsck/k9/EmailAddressAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * 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 com.fsck.k9; + +import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.provider.Contacts.ContactMethods; +import android.provider.Contacts.People; +import android.view.View; +import android.widget.ResourceCursorAdapter; +import android.widget.TextView; + +import com.fsck.k9.mail.Address; + +public class EmailAddressAdapter extends ResourceCursorAdapter { + public static final int NAME_INDEX = 1; + + public static final int DATA_INDEX = 2; + + private static final String SORT_ORDER = People.TIMES_CONTACTED + " DESC, " + People.NAME; + + private ContentResolver mContentResolver; + + private static final String[] PROJECTION = { + ContactMethods._ID, // 0 + ContactMethods.NAME, // 1 + ContactMethods.DATA + // 2 + }; + + public EmailAddressAdapter(Context context) { + super(context, R.layout.recipient_dropdown_item, null); + mContentResolver = context.getContentResolver(); + } + + @Override + public final String convertToString(Cursor cursor) { + String name = cursor.getString(NAME_INDEX); + String address = cursor.getString(DATA_INDEX); + + return new Address(address, name).toString(); + } + + @Override + public final void bindView(View view, Context context, Cursor cursor) { + TextView text1 = (TextView)view.findViewById(R.id.text1); + TextView text2 = (TextView)view.findViewById(R.id.text2); + text1.setText(cursor.getString(NAME_INDEX)); + text2.setText(cursor.getString(DATA_INDEX)); + } + + @Override + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + String where = null; + + if (constraint != null) { + String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%'); + + StringBuilder s = new StringBuilder(); + s.append("(people.name LIKE "); + s.append(filter); + s.append(") OR (contact_methods.data LIKE "); + s.append(filter); + s.append(")"); + + where = s.toString(); + } + + return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER); + } +} diff --git a/src/com/fsck/k9/EmailAddressValidator.java b/src/com/fsck/k9/EmailAddressValidator.java new file mode 100644 index 000000000..9a7ca21c3 --- /dev/null +++ b/src/com/fsck/k9/EmailAddressValidator.java @@ -0,0 +1,18 @@ + +package com.fsck.k9; + +import com.fsck.k9.mail.Address; + +import android.util.Config; +import android.util.Log; +import android.widget.AutoCompleteTextView.Validator; + +public class EmailAddressValidator implements Validator { + public CharSequence fixText(CharSequence invalidText) { + return ""; + } + + public boolean isValid(CharSequence text) { + return Address.parse(text.toString()).length > 0; + } +} diff --git a/src/com/fsck/k9/FixedLengthInputStream.java b/src/com/fsck/k9/FixedLengthInputStream.java new file mode 100644 index 000000000..457e7c3ca --- /dev/null +++ b/src/com/fsck/k9/FixedLengthInputStream.java @@ -0,0 +1,60 @@ + +package com.fsck.k9; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A filtering InputStream that stops allowing reads after the given length has been read. This + * is used to allow a client to read directly from an underlying protocol stream without reading + * past where the protocol handler intended the client to read. + */ +public class FixedLengthInputStream extends InputStream { + private InputStream mIn; + private int mLength; + private int mCount; + + public FixedLengthInputStream(InputStream in, int length) { + this.mIn = in; + this.mLength = length; + } + + @Override + public int available() throws IOException { + return mLength - mCount; + } + + @Override + public int read() throws IOException { + if (mCount < mLength) { + mCount++; + return mIn.read(); + } else { + return -1; + } + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if (mCount < mLength) { + int d = mIn.read(b, offset, Math.min(mLength - mCount, length)); + if (d == -1) { + return -1; + } else { + mCount += d; + return d; + } + } else { + return -1; + } + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public String toString() { + return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength); + } +} diff --git a/src/com/fsck/k9/Manifest.java b/src/com/fsck/k9/Manifest.java new file mode 100644 index 000000000..80158aa5a --- /dev/null +++ b/src/com/fsck/k9/Manifest.java @@ -0,0 +1,14 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.fsck.k9; + +public final class Manifest { + public static final class permission { + public static final String READ_ATTACHMENT="com.fsck.k9.permission.READ_ATTACHMENT"; + } +} diff --git a/src/com/fsck/k9/MessagingController.java b/src/com/fsck/k9/MessagingController.java new file mode 100644 index 000000000..0c98225ce --- /dev/null +++ b/src/com/fsck/k9/MessagingController.java @@ -0,0 +1,1476 @@ + +package com.fsck.k9; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import android.app.Application; +import android.content.Context; +import android.os.Process; +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.Folder.FolderType; +import com.fsck.k9.mail.Folder.OpenMode; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.LocalStore.PendingCommand; + +/** + * Starts a long running (application) Thread that will run through commands + * that require remote mailbox access. This class is used to serialize and + * prioritize these commands. Each method that will submit a command requires a + * MessagingListener instance to be provided. It is expected that that listener + * has also been added as a registered listener using addListener(). When a + * command is to be executed, if the listener that was provided with the command + * is no longer registered the command is skipped. The design idea for the above + * is that when an Activity starts it registers as a listener. When it is paused + * it removes itself. Thus, any commands that that activity submitted are + * removed from the queue once the activity is no longer active. + */ +public class MessagingController implements Runnable { + /** + * The maximum message size that we'll consider to be "small". A small message is downloaded + * in full immediately instead of in pieces. Anything over this size will be downloaded in + * pieces with attachments being left off completely and downloaded on demand. + * + * + * 25k for a "small" message was picked by educated trial and error. + * http://answers.google.com/answers/threadview?id=312463 claims that the + * average size of an email is 59k, which I feel is too large for our + * blind download. The following tests were performed on a download of + * 25 random messages. + *
+     * 5k - 61 seconds,
+     * 25k - 51 seconds,
+     * 55k - 53 seconds,
+     * 
+ * So 25k gives good performance and a reasonable data footprint. Sounds good to me. + */ + private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); + + private static final String PENDING_COMMAND_TRASH = + "com.fsck.k9.MessagingController.trash"; + private static final String PENDING_COMMAND_MARK_READ = + "com.fsck.k9.MessagingController.markRead"; + private static final String PENDING_COMMAND_APPEND = + "com.fsck.k9.MessagingController.append"; + + private static MessagingController inst = null; + private BlockingQueue mCommands = new LinkedBlockingQueue(); + private Thread mThread; + private HashSet mListeners = new HashSet(); + private boolean mBusy; + private Application mApplication; + + private MessagingController(Application application) { + mApplication = application; + mThread = new Thread(this); + mThread.start(); + } + + /** + * Gets or creates the singleton instance of MessagingController. Application is used to + * provide a Context to classes that need it. + * @param application + * @return + */ + public synchronized static MessagingController getInstance(Application application) { + if (inst == null) { + inst = new MessagingController(application); + } + return inst; + } + + public boolean isBusy() { + return mBusy; + } + + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + while (true) { + try { + Command command = mCommands.take(); + if (command.listener == null || mListeners.contains(command.listener)) { + mBusy = true; + command.runnable.run(); + for (MessagingListener l : mListeners) { + l.controllerCommandCompleted(mCommands.size() > 0); + } + } + } + catch (Exception e) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "Error running command", e); + } + } + mBusy = false; + } + } + + private void put(String description, MessagingListener listener, Runnable runnable) { + try { + Command command = new Command(); + command.listener = listener; + command.runnable = runnable; + command.description = description; + mCommands.put(command); + } + catch (InterruptedException ie) { + throw new Error(ie); + } + } + + public void addListener(MessagingListener listener) { + mListeners.add(listener); + } + + public void removeListener(MessagingListener listener) { + mListeners.remove(listener); + } + + /** + * Lists folders that are available locally and remotely. This method calls + * listFoldersCallback for local folders before it returns, and then for + * remote folders at some later point. If there are no local folders + * includeRemote is forced by this method. This method should be called from + * a Thread as it may take several seconds to list the local folders. TODO + * this needs to cache the remote folder list + * + * @param account + * @param includeRemote + * @param listener + * @throws MessagingException + */ + public void listFolders( + final Account account, + boolean refreshRemote, + MessagingListener listener) { + for (MessagingListener l : mListeners) { + l.listFoldersStarted(account); + } + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder[] localFolders = localStore.getPersonalNamespaces(); + + if (localFolders == null || localFolders.length == 0) { + refreshRemote = true; + } else { + for (MessagingListener l : mListeners) { + l.listFolders(account, localFolders); + } + } + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + l.listFoldersFailed(account, e.getMessage()); + return; + } + } + if (refreshRemote) { + put("listFolders", listener, new Runnable() { + public void run() { + try { + Store store = Store.getInstance(account.getStoreUri(), mApplication); + + Folder[] remoteFolders = store.getPersonalNamespaces(); + + Store localStore = Store.getInstance( + account.getLocalStoreUri(), + mApplication); + HashSet remoteFolderNames = new HashSet(); + for (int i = 0, count = remoteFolders.length; i < count; i++) { + Folder localFolder = localStore.getFolder(remoteFolders[i].getName()); + if (!localFolder.exists()) { + localFolder.create(FolderType.HOLDS_MESSAGES); + } + remoteFolderNames.add(remoteFolders[i].getName()); + } + + Folder[] localFolders = localStore.getPersonalNamespaces(); + + /* + * Clear out any folders that are no longer on the remote store. + */ + for (Folder localFolder : localFolders) { + String localFolderName = localFolder.getName(); + if (localFolderName.equalsIgnoreCase(k9.INBOX) || + localFolderName.equals(account.getTrashFolderName()) || + localFolderName.equals(account.getOutboxFolderName()) || + localFolderName.equals(account.getDraftsFolderName()) || + localFolderName.equals(account.getSentFolderName())) { + continue; + } + if (!remoteFolderNames.contains(localFolder.getName())) { + localFolder.delete(false); + } + } + + localFolders = localStore.getPersonalNamespaces(); + + for (MessagingListener l : mListeners) { + l.listFolders(account, localFolders); + } + for (MessagingListener l : mListeners) { + l.listFoldersFinished(account); + } + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + l.listFoldersFailed(account, ""); + } + } + } + }); + } else { + for (MessagingListener l : mListeners) { + l.listFoldersFinished(account); + } + } + } + + /** + * List the local message store for the given folder. This work is done + * synchronously. + * + * @param account + * @param folder + * @param listener + * @throws MessagingException + */ + public void listLocalMessages(final Account account, final String folder, + MessagingListener listener) { + for (MessagingListener l : mListeners) { + l.listLocalMessagesStarted(account, folder); + } + + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder localFolder = localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + Message[] localMessages = localFolder.getMessages(null); + ArrayList messages = new ArrayList(); + for (Message message : localMessages) { + if (!message.isSet(Flag.DELETED)) { + messages.add(message); + } + } + for (MessagingListener l : mListeners) { + l.listLocalMessages(account, folder, messages.toArray(new Message[0])); + } + for (MessagingListener l : mListeners) { + l.listLocalMessagesFinished(account, folder); + } + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + l.listLocalMessagesFailed(account, folder, e.getMessage()); + } + } + } + + public void loadMoreMessages(Account account, String folder, MessagingListener listener) { + try { + LocalStore localStore = (LocalStore) Store.getInstance( + account.getLocalStoreUri(), + mApplication); + LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); + localFolder.setVisibleLimit(localFolder.getVisibleLimit() + + k9.VISIBLE_LIMIT_INCREMENT); + synchronizeMailbox(account, folder, listener); + } + catch (MessagingException me) { + throw new RuntimeException("Unable to set visible limit on folder", me); + } + } + + public void resetVisibleLimits(Account[] accounts) { + for (Account account : accounts) { + try { + LocalStore localStore = + (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); + localStore.resetVisibleLimits(); + } + catch (MessagingException e) { + Log.e(k9.LOG_TAG, "Unable to reset visible limits", e); + } + } + } + + /** + * Start background synchronization of the specified folder. + * @param account + * @param folder + * @param numNewestMessagesToKeep Specifies the number of messages that should be + * considered as part of the window of available messages. This number effectively limits + * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. + * @param listener + */ + public void synchronizeMailbox(final Account account, final String folder, + MessagingListener listener) { + /* + * We don't ever sync the Outbox. + */ + if (folder.equals(account.getOutboxFolderName())) { + return; + } + for (MessagingListener l : mListeners) { + l.synchronizeMailboxStarted(account, folder); + } + put("synchronizeMailbox", listener, new Runnable() { + public void run() { + synchronizeMailboxSynchronous(account, folder); + } + }); + } + + /** + * Start foreground synchronization of the specified folder. This is generally only called + * by synchronizeMailbox. + * @param account + * @param folder + * @param numNewestMessagesToKeep Specifies the number of messages that should be + * considered as part of the window of available messages. This number effectively limits + * the user's view into the mailbox to the newest (numNewestMessagesToKeep) messages. + * @param listener + * + * TODO Break this method up into smaller chunks. + */ + public void synchronizeMailboxSynchronous(final Account account, final String folder) { + for (MessagingListener l : mListeners) { + l.synchronizeMailboxStarted(account, folder); + } + try { + processPendingCommandsSynchronous(account); + + /* + * Get the message list from the local store and create an index of + * the uids within the list. + */ + final LocalStore localStore = + (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); + final LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + Message[] localMessages = localFolder.getMessages(null); + HashMap localUidMap = new HashMap(); + for (Message message : localMessages) { + localUidMap.put(message.getUid(), message); + } + + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + Folder remoteFolder = remoteStore.getFolder(folder); + + /* + * If the folder is a "special" folder we need to see if it exists + * on the remote server. It if does not exist we'll try to create it. If we + * can't create we'll abort. This will happen on every single Pop3 folder as + * designed and on Imap folders during error conditions. This allows us + * to treat Pop3 and Imap the same in this code. + */ + if (folder.equals(account.getTrashFolderName()) || + folder.equals(account.getSentFolderName()) || + folder.equals(account.getDraftsFolderName())) { + if (!remoteFolder.exists()) { + if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { + for (MessagingListener l : mListeners) { + l.synchronizeMailboxFinished(account, folder, 0, 0); + } + return; + } + } + } + + /* + * Synchronization process: + Open the folder + Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) + Get the message count + Get the list of the newest k9.DEFAULT_VISIBLE_LIMIT messages + getMessages(messageCount - k9.DEFAULT_VISIBLE_LIMIT, messageCount) + See if we have each message locally, if not fetch it's flags and envelope + Get and update the unread count for the folder + Update the remote flags of any messages we have locally with an internal date + newer than the remote message. + Get the current flags for any messages we have locally but did not just download + Update local flags + For any message we have locally but not remotely, delete the local message to keep + cache clean. + Download larger parts of any new messages. + (Optional) Download small attachments in the background. + */ + + /* + * Open the remote folder. This pre-loads certain metadata like message count. + */ + remoteFolder.open(OpenMode.READ_WRITE); + + + /* + * Get the remote message count. + */ + int remoteMessageCount = remoteFolder.getMessageCount(); + + int visibleLimit = localFolder.getVisibleLimit(); + + Message[] remoteMessages = new Message[0]; + final ArrayList unsyncedMessages = new ArrayList(); + HashMap remoteUidMap = new HashMap(); + + if (remoteMessageCount > 0) { + /* + * Message numbers start at 1. + */ + int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; + int remoteEnd = remoteMessageCount; + remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); + for (Message message : remoteMessages) { + remoteUidMap.put(message.getUid(), message); + } + + + + + + + + /* + * Get a list of the messages that are in the remote list but not on the + * local store, or messages that are in the local store but failed to download + * on the last sync. These are the new messages that we will download. + */ + for (Message message : remoteMessages) { + Message localMessage = localUidMap.get(message.getUid()); + if (localMessage == null || + (!localMessage.isSet(Flag.DELETED) && + !localMessage.isSet(Flag.X_DOWNLOADED_FULL) && + !localMessage.isSet(Flag.X_DOWNLOADED_PARTIAL))) { + unsyncedMessages.add(message); + } + } + } + + + /* + * Trash any remote messages that are marked as trashed locally. + */ + for (Message message : localMessages) { + Message remoteMessage = remoteUidMap.get(message.getUid()); + // skip things deleted on the server side + if (remoteMessage != null && message.isSet(Flag.DELETED)) { + remoteMessage.setFlag(Flag.DELETED, true); + } + + } + + + /* + * A list of messages that were downloaded and which did not have the Seen flag set. + * This will serve to indicate the true "new" message count that will be reported to + * the user via notification. + */ + final ArrayList newMessages = new ArrayList(); + + /* + * Fetch the flags and envelope only of the new messages. This is intended to get us +s * critical data as fast as possible, and then we'll fill in the details. + */ + if (unsyncedMessages.size() > 0) { + + /* + * Reverse the order of the messages. Depending on the server this may get us + * fetch results for newest to oldest. If not, no harm done. + */ + Collections.reverse(unsyncedMessages); + + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + fp.add(FetchProfile.Item.ENVELOPE); + remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, + new MessageRetrievalListener() { + public void messageFinished(Message message, int number, int ofTotal) { + try { + // Store the new message locally + localFolder.appendMessages(new Message[] { + message + }); + + // And include it in the view + if (message.getSubject() != null && + message.getFrom() != null) { + /* + * We check to make sure that we got something worth + * showing (subject and from) because some protocols + * (POP) may not be able to give us headers for + * ENVELOPE, only size. + */ + for (MessagingListener l : mListeners) { + l.synchronizeMailboxNewMessage(account, folder, + localFolder.getMessage(message.getUid())); + } + } + + if (!message.isSet(Flag.SEEN)) { + newMessages.add(message); + } + } + catch (Exception e) { + Log.e(k9.LOG_TAG, + "Error while storing downloaded message.", + e); + } + } + + public void messageStarted(String uid, int number, int ofTotal) { + } + }); + } + + /* + * Refresh the flags for any messages in the local store that we didn't just + * download. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + remoteFolder.fetch(remoteMessages, fp, null); + for (Message remoteMessage : remoteMessages) { + Message localMessage = localFolder.getMessage(remoteMessage.getUid()); + if (localMessage == null) { + continue; + } + if (remoteMessage.isSet(Flag.SEEN) != localMessage.isSet(Flag.SEEN)) { + localMessage.setFlag(Flag.SEEN, remoteMessage.isSet(Flag.SEEN)); + for (MessagingListener l : mListeners) { + l.synchronizeMailboxNewMessage(account, folder, localMessage); + } + } + } + + /* + * Get and store the unread message count. + */ + int remoteUnreadMessageCount = remoteFolder.getUnreadMessageCount(); + if (remoteUnreadMessageCount == -1) { + localFolder.setUnreadMessageCount(localFolder.getUnreadMessageCount() + + newMessages.size()); + } + else { + localFolder.setUnreadMessageCount(remoteUnreadMessageCount); + } + + /* + * Remove any messages that are in the local store but no longer on the remote store. + */ + for (Message localMessage : localMessages) { + if (remoteUidMap.get(localMessage.getUid()) == null) { + localMessage.setFlag(Flag.X_DESTROYED, true); + for (MessagingListener l : mListeners) { + l.synchronizeMailboxRemovedMessage(account, folder, localMessage); + } + } + } + + /* + * Now we download the actual content of messages. + */ + ArrayList largeMessages = new ArrayList(); + ArrayList smallMessages = new ArrayList(); + for (Message message : unsyncedMessages) { + /* + * Sort the messages into two buckets, small and large. Small messages will be + * downloaded fully and large messages will be downloaded in parts. By sorting + * into two buckets we can pipeline the commands for each set of messages + * into a single command to the server saving lots of round trips. + */ + if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { + largeMessages.add(message); + } else { + smallMessages.add(message); + } + } + /* + * Grab the content of the small messages first. This is going to + * be very fast and at very worst will be a single up of a few bytes and a single + * download of 625k. + */ + fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), + fp, new MessageRetrievalListener() { + public void messageFinished(Message message, int number, int ofTotal) { + try { + // Store the updated message locally + localFolder.appendMessages(new Message[] { + message + }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + // Set a flag indicating this message has now be fully downloaded + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + + // Update the listener with what we've found + for (MessagingListener l : mListeners) { + l.synchronizeMailboxNewMessage( + account, + folder, + localMessage); + } + } + catch (MessagingException me) { + + } + } + + public void messageStarted(String uid, int number, int ofTotal) { + } + }); + + /* + * Now do the large messages that require more round trips. + */ + fp.clear(); + fp.add(FetchProfile.Item.STRUCTURE); + remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), + fp, null); + for (Message message : largeMessages) { + if (message.getBody() == null) { + /* + * The provider was unable to get the structure of the message, so + * we'll download a reasonable portion of the messge and mark it as + * incomplete so the entire thing can be downloaded later if the user + * wishes to download it. + */ + fp.clear(); + fp.add(FetchProfile.Item.BODY_SANE); + /* + * TODO a good optimization here would be to make sure that all Stores set + * the proper size after this fetch and compare the before and after size. If + * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED + */ + + remoteFolder.fetch(new Message[] { message }, fp, null); + // Store the updated message locally + localFolder.appendMessages(new Message[] { + message + }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + // Set a flag indicating that the message has been partially downloaded and + // is ready for view. + localMessage.setFlag(Flag.X_DOWNLOADED_PARTIAL, true); + } else { + /* + * We have a structure to deal with, from which + * we can pull down the parts we want to actually store. + * Build a list of parts we are interested in. Text parts will be downloaded + * right now, attachments will be left for later. + */ + + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + /* + * Now download the parts we're interested in storing. + */ + for (Part part : viewables) { + fp.clear(); + fp.add(part); + // TODO what happens if the network connection dies? We've got partial + // messages with incorrect status stored. + remoteFolder.fetch(new Message[] { message }, fp, null); + } + // Store the updated message locally + localFolder.appendMessages(new Message[] { + message + }); + + Message localMessage = localFolder.getMessage(message.getUid()); + + // Set a flag indicating this message has been fully downloaded and can be + // viewed. + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + } + + // Update the listener with what we've found + for (MessagingListener l : mListeners) { + l.synchronizeMailboxNewMessage( + account, + folder, + localFolder.getMessage(message.getUid())); + } + } + + + /* + * Notify listeners that we're finally done. + */ + for (MessagingListener l : mListeners) { + l.synchronizeMailboxFinished( + account, + folder, + remoteFolder.getMessageCount(), newMessages.size()); + } + + remoteFolder.close(false); + localFolder.close(false); + } + catch (Exception e) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "synchronizeMailbox", e); + } + for (MessagingListener l : mListeners) { + l.synchronizeMailboxFailed( + account, + folder, + e.getMessage()); + } + } + } + + private void queuePendingCommand(Account account, PendingCommand command) { + try { + LocalStore localStore = (LocalStore) Store.getInstance( + account.getLocalStoreUri(), + mApplication); + localStore.addPendingCommand(command); + } + catch (Exception e) { + throw new RuntimeException("Unable to enqueue pending command", e); + } + } + + private void processPendingCommands(final Account account) { + put("processPendingCommands", null, new Runnable() { + public void run() { + try { + processPendingCommandsSynchronous(account); + } + catch (MessagingException me) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "processPendingCommands", me); + } + /* + * Ignore any exceptions from the commands. Commands will be processed + * on the next round. + */ + } + } + }); + } + + private void processPendingCommandsSynchronous(Account account) throws MessagingException { + LocalStore localStore = (LocalStore) Store.getInstance( + account.getLocalStoreUri(), + mApplication); + ArrayList commands = localStore.getPendingCommands(); + for (PendingCommand command : commands) { + /* + * We specifically do not catch any exceptions here. If a command fails it is + * most likely due to a server or IO error and it must be retried before any + * other command processes. This maintains the order of the commands. + */ + if (PENDING_COMMAND_APPEND.equals(command.command)) { + processPendingAppend(command, account); + } + else if (PENDING_COMMAND_MARK_READ.equals(command.command)) { + processPendingMarkRead(command, account); + } + else if (PENDING_COMMAND_TRASH.equals(command.command)) { + processPendingTrash(command, account); + } + localStore.removePendingCommand(command); + } + } + + /** + * Process a pending append message command. This command uploads a local message to the + * server, first checking to be sure that the server message is not newer than + * the local message. Once the local message is successfully processed it is deleted so + * that the server message will be synchronized down without an additional copy being + * created. + * TODO update the local message UID instead of deleteing it + * + * @param command arguments = (String folder, String uid) + * @param account + * @throws MessagingException + */ + private void processPendingAppend(PendingCommand command, Account account) + throws MessagingException { + String folder = command.arguments[0]; + String uid = command.arguments[1]; + + LocalStore localStore = (LocalStore) Store.getInstance( + account.getLocalStoreUri(), + mApplication); + LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); + LocalMessage localMessage = (LocalMessage) localFolder.getMessage(uid); + + if (localMessage == null) { + return; + } + + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + Folder remoteFolder = remoteStore.getFolder(folder); + if (!remoteFolder.exists()) { + if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { + return; + } + } + remoteFolder.open(OpenMode.READ_WRITE); + if (remoteFolder.getMode() != OpenMode.READ_WRITE) { + return; + } + + Message remoteMessage = null; + if (!localMessage.getUid().startsWith("Local") + && !localMessage.getUid().contains("-")) { + remoteMessage = remoteFolder.getMessage(localMessage.getUid()); + } + + if (remoteMessage == null) { + /* + * If the message does not exist remotely we just upload it and then + * update our local copy with the new uid. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + localFolder.fetch(new Message[] { localMessage }, fp, null); + String oldUid = localMessage.getUid(); + remoteFolder.appendMessages(new Message[] { localMessage }); + localFolder.changeUid(localMessage); + for (MessagingListener l : mListeners) { + l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); + } + } + else { + /* + * If the remote message exists we need to determine which copy to keep. + */ + /* + * See if the remote message is newer than ours. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); + Date localDate = localMessage.getInternalDate(); + Date remoteDate = remoteMessage.getInternalDate(); + if (remoteDate.compareTo(localDate) > 0) { + /* + * If the remote message is newer than ours we'll just + * delete ours and move on. A sync will get the server message + * if we need to be able to see it. + */ + localMessage.setFlag(Flag.DELETED, true); + } + else { + /* + * Otherwise we'll upload our message and then delete the remote message. + */ + fp.clear(); + fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + localFolder.fetch(new Message[] { localMessage }, fp, null); + String oldUid = localMessage.getUid(); + remoteFolder.appendMessages(new Message[] { localMessage }); + localFolder.changeUid(localMessage); + for (MessagingListener l : mListeners) { + l.messageUidChanged(account, folder, oldUid, localMessage.getUid()); + } + remoteMessage.setFlag(Flag.DELETED, true); + } + } + } + + /** + * Process a pending trash message command. + * + * @param command arguments = (String folder, String uid) + * @param account + * @throws MessagingException + */ + private void processPendingTrash(PendingCommand command, Account account) + throws MessagingException { + String folder = command.arguments[0]; + String uid = command.arguments[1]; + + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + Folder remoteFolder = remoteStore.getFolder(folder); + if (!remoteFolder.exists()) { + return; + } + remoteFolder.open(OpenMode.READ_WRITE); + if (remoteFolder.getMode() != OpenMode.READ_WRITE) { + return; + } + + Message remoteMessage = null; + if (!uid.startsWith("Local") + && !uid.contains("-")) { + remoteMessage = remoteFolder.getMessage(uid); + } + if (remoteMessage == null) { + return; + } + + Folder remoteTrashFolder = remoteStore.getFolder(account.getTrashFolderName()); + /* + * Attempt to copy the remote message to the remote trash folder. + */ + if (!remoteTrashFolder.exists()) { + /* + * If the remote trash folder doesn't exist we try to create it. + */ + remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); + } + + if (remoteTrashFolder.exists()) { + remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder); + } + + remoteMessage.setFlag(Flag.DELETED, true); + remoteFolder.expunge(); + } + + /** + * Processes a pending mark read or unread command. + * + * @param command arguments = (String folder, String uid, boolean read) + * @param account + */ + private void processPendingMarkRead(PendingCommand command, Account account) + throws MessagingException { + String folder = command.arguments[0]; + String uid = command.arguments[1]; + boolean read = Boolean.parseBoolean(command.arguments[2]); + + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + Folder remoteFolder = remoteStore.getFolder(folder); + if (!remoteFolder.exists()) { + return; + } + remoteFolder.open(OpenMode.READ_WRITE); + if (remoteFolder.getMode() != OpenMode.READ_WRITE) { + return; + } + Message remoteMessage = null; + if (!uid.startsWith("Local") + && !uid.contains("-")) { + remoteMessage = remoteFolder.getMessage(uid); + } + if (remoteMessage == null) { + return; + } + remoteMessage.setFlag(Flag.SEEN, read); + } + + /** + * Mark the message with the given account, folder and uid either Seen or not Seen. + * @param account + * @param folder + * @param uid + * @param seen + */ + public void markMessageRead( + final Account account, + final String folder, + final String uid, + final boolean seen) { + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder localFolder = localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + + Message message = localFolder.getMessage(uid); + message.setFlag(Flag.SEEN, seen); + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_MARK_READ; + command.arguments = new String[] { folder, uid, Boolean.toString(seen) }; + queuePendingCommand(account, command); + processPendingCommands(account); + } + catch (MessagingException me) { + throw new RuntimeException(me); + } + } + + private void loadMessageForViewRemote(final Account account, final String folder, + final String uid, MessagingListener listener) { + put("loadMessageForViewRemote", listener, new Runnable() { + public void run() { + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + + Message message = localFolder.getMessage(uid); + + if (message.isSet(Flag.X_DOWNLOADED_FULL)) { + /* + * If the message has been synchronized since we were called we'll + * just hand it back cause it's ready to go. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.BODY); + localFolder.fetch(new Message[] { message }, fp, null); + + for (MessagingListener l : mListeners) { + l.loadMessageForViewBodyAvailable(account, folder, uid, message); + } + for (MessagingListener l : mListeners) { + l.loadMessageForViewFinished(account, folder, uid, message); + } + localFolder.close(false); + return; + } + + /* + * At this point the message is not available, so we need to download it + * fully if possible. + */ + + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + Folder remoteFolder = remoteStore.getFolder(folder); + remoteFolder.open(OpenMode.READ_WRITE); + + // Get the remote message and fully download it + Message remoteMessage = remoteFolder.getMessage(uid); + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); + + // Store the message locally and load the stored message into memory + localFolder.appendMessages(new Message[] { remoteMessage }); + message = localFolder.getMessage(uid); + localFolder.fetch(new Message[] { message }, fp, null); + + // This is a view message request, so mark it read + if (!message.isSet(Flag.SEEN)) { + markMessageRead(account, folder, uid, true); + } + + // Mark that this message is now fully synched + message.setFlag(Flag.X_DOWNLOADED_FULL, true); + + for (MessagingListener l : mListeners) { + l.loadMessageForViewBodyAvailable(account, folder, uid, message); + } + for (MessagingListener l : mListeners) { + l.loadMessageForViewFinished(account, folder, uid, message); + } + remoteFolder.close(false); + localFolder.close(false); + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); + } + } + } + }); + } + + public void loadMessageForView(final Account account, final String folder, final String uid, + MessagingListener listener) { + for (MessagingListener l : mListeners) { + l.loadMessageForViewStarted(account, folder, uid); + } + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + LocalFolder localFolder = (LocalFolder) localStore.getFolder(folder); + localFolder.open(OpenMode.READ_WRITE); + + Message message = localFolder.getMessage(uid); + + for (MessagingListener l : mListeners) { + l.loadMessageForViewHeadersAvailable(account, folder, uid, message); + } + + if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { + loadMessageForViewRemote(account, folder, uid, listener); + localFolder.close(false); + return; + } + + if (!message.isSet(Flag.SEEN)) { + markMessageRead(account, folder, uid, true); + } + + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.BODY); + localFolder.fetch(new Message[] { + message + }, fp, null); + + for (MessagingListener l : mListeners) { + l.loadMessageForViewBodyAvailable(account, folder, uid, message); + } + + for (MessagingListener l : mListeners) { + l.loadMessageForViewFinished(account, folder, uid, message); + } + localFolder.close(false); + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + l.loadMessageForViewFailed(account, folder, uid, e.getMessage()); + } + } + } + + /** + * Attempts to load the attachment specified by part from the given account and message. + * @param account + * @param message + * @param part + * @param listener + */ + public void loadAttachment( + final Account account, + final Message message, + final Part part, + final Object tag, + MessagingListener listener) { + /* + * Check if the attachment has already been downloaded. If it has there's no reason to + * download it, so we just tell the listener that it's ready to go. + */ + try { + if (part.getBody() != null) { + for (MessagingListener l : mListeners) { + l.loadAttachmentStarted(account, message, part, tag, false); + } + + for (MessagingListener l : mListeners) { + l.loadAttachmentFinished(account, message, part, tag); + } + return; + } + } + catch (MessagingException me) { + /* + * If the header isn't there the attachment isn't downloaded yet, so just continue + * on. + */ + } + + for (MessagingListener l : mListeners) { + l.loadAttachmentStarted(account, message, part, tag, true); + } + + put("loadAttachment", listener, new Runnable() { + public void run() { + try { + LocalStore localStore = + (LocalStore) Store.getInstance(account.getLocalStoreUri(), mApplication); + /* + * We clear out any attachments already cached in the entire store and then + * we update the passed in message to reflect that there are no cached + * attachments. This is in support of limiting the account to having one + * attachment downloaded at a time. + */ + localStore.pruneCachedAttachments(); + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + for (Part attachment : attachments) { + attachment.setBody(null); + } + Store remoteStore = Store.getInstance(account.getStoreUri(), mApplication); + LocalFolder localFolder = + (LocalFolder) localStore.getFolder(message.getFolder().getName()); + Folder remoteFolder = remoteStore.getFolder(message.getFolder().getName()); + remoteFolder.open(OpenMode.READ_WRITE); + + FetchProfile fp = new FetchProfile(); + fp.add(part); + remoteFolder.fetch(new Message[] { message }, fp, null); + localFolder.updateMessage((LocalMessage)message); + localFolder.close(false); + for (MessagingListener l : mListeners) { + l.loadAttachmentFinished(account, message, part, tag); + } + } + catch (MessagingException me) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "", me); + } + for (MessagingListener l : mListeners) { + l.loadAttachmentFailed(account, message, part, tag, me.getMessage()); + } + } + } + }); + } + + /** + * Stores the given message in the Outbox and starts a sendPendingMessages command to + * attempt to send the message. + * @param account + * @param message + * @param listener + */ + public void sendMessage(final Account account, + final Message message, + MessagingListener listener) { + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + LocalFolder localFolder = + (LocalFolder) localStore.getFolder(account.getOutboxFolderName()); + localFolder.open(OpenMode.READ_WRITE); + localFolder.appendMessages(new Message[] { + message + }); + Message localMessage = localFolder.getMessage(message.getUid()); + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + localFolder.close(false); + sendPendingMessages(account, null); + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + // TODO general failed + } + } + } + + /** + * Attempt to send any messages that are sitting in the Outbox. + * @param account + * @param listener + */ + public void sendPendingMessages(final Account account, + MessagingListener listener) { + put("sendPendingMessages", listener, new Runnable() { + public void run() { + sendPendingMessagesSynchronous(account); + } + }); + } + + /** + * Attempt to send any messages that are sitting in the Outbox. + * @param account + * @param listener + */ + public void sendPendingMessagesSynchronous(final Account account) { + try { + Store localStore = Store.getInstance( + account.getLocalStoreUri(), + mApplication); + Folder localFolder = localStore.getFolder( + account.getOutboxFolderName()); + if (!localFolder.exists()) { + return; + } + localFolder.open(OpenMode.READ_WRITE); + + Message[] localMessages = localFolder.getMessages(null); + + /* + * The profile we will use to pull all of the content + * for a given local message into memory for sending. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.BODY); + + LocalFolder localSentFolder = + (LocalFolder) localStore.getFolder( + account.getSentFolderName()); + + Transport transport = Transport.getInstance(account.getTransportUri()); + for (Message message : localMessages) { + try { + localFolder.fetch(new Message[] { message }, fp, null); + try { + message.setFlag(Flag.X_SEND_IN_PROGRESS, true); + transport.sendMessage(message); + message.setFlag(Flag.X_SEND_IN_PROGRESS, false); + localFolder.copyMessages( + new Message[] { message }, + localSentFolder); + + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_APPEND; + command.arguments = + new String[] { + localSentFolder.getName(), + message.getUid() }; + queuePendingCommand(account, command); + processPendingCommands(account); + message.setFlag(Flag.X_DESTROYED, true); + } + catch (Exception e) { + message.setFlag(Flag.X_SEND_FAILED, true); + } + } + catch (Exception e) { + /* + * We ignore this exception because a future refresh will retry this + * message. + */ + } + } + localFolder.expunge(); + if (localFolder.getMessageCount() == 0) { + localFolder.delete(false); + } + for (MessagingListener l : mListeners) { + l.sendPendingMessagesCompleted(account); + } + } + catch (Exception e) { + for (MessagingListener l : mListeners) { + // TODO general failed + } + } + } + + /** + * We do the local portion of this synchronously because other activities may have to make + * updates based on what happens here + * @param account + * @param folder + * @param message + * @param listener + */ + public void deleteMessage(final Account account, final String folder, final Message message, + MessagingListener listener) { + if (folder.equals(account.getTrashFolderName())) { + return; + } + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder localFolder = localStore.getFolder(folder); + Folder localTrashFolder = localStore.getFolder(account.getTrashFolderName()); + + localFolder.copyMessages(new Message[] { message }, localTrashFolder); + message.setFlag(Flag.DELETED, true); + + if (account.getDeletePolicy() == Account.DELETE_POLICY_ON_DELETE) { + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_TRASH; + command.arguments = new String[] { folder, message.getUid() }; + queuePendingCommand(account, command); + processPendingCommands(account); + } + } + catch (MessagingException me) { + throw new RuntimeException("Error deleting message from local store.", me); + } + } + + public void emptyTrash(final Account account, MessagingListener listener) { + put("emptyTrash", listener, new Runnable() { + public void run() { + // TODO IMAP + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + Folder localFolder = localStore.getFolder(account.getTrashFolderName()); + localFolder.open(OpenMode.READ_WRITE); + Message[] messages = localFolder.getMessages(null); + localFolder.setFlags(messages, new Flag[] { + Flag.DELETED + }, true); + localFolder.close(true); + for (MessagingListener l : mListeners) { + l.emptyTrashCompleted(account); + } + } + catch (Exception e) { + // TODO + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "emptyTrash"); + } + } + } + }); + } + + /** + * Checks mail for one or multiple accounts. If account is null all accounts + * are checked. + * + * @param context + * @param account + * @param listener + */ + public void checkMail(final Context context, final Account account, + final MessagingListener listener) { + for (MessagingListener l : mListeners) { + l.checkMailStarted(context, account); + } + put("checkMail", listener, new Runnable() { + public void run() { + Account[] accounts; + if (account != null) { + accounts = new Account[] { + account + }; + } else { + accounts = Preferences.getPreferences(context).getAccounts(); + } + for (Account account : accounts) { + sendPendingMessagesSynchronous(account); + synchronizeMailboxSynchronous(account, k9.INBOX); + } + for (MessagingListener l : mListeners) { + l.checkMailFinished(context, account); + } + } + }); + } + + public void saveDraft(final Account account, final Message message) { + try { + Store localStore = Store.getInstance(account.getLocalStoreUri(), mApplication); + LocalFolder localFolder = + (LocalFolder) localStore.getFolder(account.getDraftsFolderName()); + localFolder.open(OpenMode.READ_WRITE); + localFolder.appendMessages(new Message[] { + message + }); + Message localMessage = localFolder.getMessage(message.getUid()); + localMessage.setFlag(Flag.X_DOWNLOADED_FULL, true); + + PendingCommand command = new PendingCommand(); + command.command = PENDING_COMMAND_APPEND; + command.arguments = new String[] { + localFolder.getName(), + localMessage.getUid() }; + queuePendingCommand(account, command); + processPendingCommands(account); + } + catch (MessagingException e) { + Log.e(k9.LOG_TAG, "Unable to save message as draft.", e); + } + } + + class Command { + public Runnable runnable; + + public MessagingListener listener; + + public String description; + } +} diff --git a/src/com/fsck/k9/MessagingListener.java b/src/com/fsck/k9/MessagingListener.java new file mode 100644 index 000000000..17780e778 --- /dev/null +++ b/src/com/fsck/k9/MessagingListener.java @@ -0,0 +1,132 @@ + +package com.fsck.k9; + +import android.content.Context; + +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + +/** + * Defines the interface that MessagingController will use to callback to requesters. This class + * is defined as non-abstract so that someone who wants to receive only a few messages can + * do so without implementing the entire interface. It is highly recommended that users of + * this interface use the @Override annotation in their implementations to avoid being caught by + * changes in this class. + */ +public class MessagingListener { + public void listFoldersStarted(Account account) { + } + + public void listFolders(Account account, Folder[] folders) { + } + + public void listFoldersFailed(Account account, String message) { + } + + public void listFoldersFinished(Account account) { + } + + public void listLocalMessagesStarted(Account account, String folder) { + } + + public void listLocalMessages(Account account, String folder, Message[] messages) { + } + + public void listLocalMessagesFailed(Account account, String folder, String message) { + } + + public void listLocalMessagesFinished(Account account, String folder) { + } + + public void synchronizeMailboxStarted(Account account, String folder) { + } + + public void synchronizeMailboxNewMessage(Account account, String folder, Message message) { + } + + public void synchronizeMailboxRemovedMessage(Account account, String folder,Message message) { + } + + public void synchronizeMailboxFinished(Account account, String folder, + int totalMessagesInMailbox, int numNewMessages) { + } + + public void synchronizeMailboxFailed(Account account, String folder, + String message) { + } + + public void loadMessageForViewStarted(Account account, String folder, String uid) { + } + + public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid, + Message message) { + } + + public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, + Message message) { + } + + public void loadMessageForViewFinished(Account account, String folder, String uid, + Message message) { + } + + public void loadMessageForViewFailed(Account account, String folder, String uid, String message) { + } + + public void checkMailStarted(Context context, Account account) { + } + + public void checkMailFinished(Context context, Account account) { + } + + public void checkMailFailed(Context context, Account account, String reason) { + } + + public void sendPendingMessagesCompleted(Account account) { + } + + public void emptyTrashCompleted(Account account) { + } + + public void messageUidChanged(Account account, String folder, String oldUid, String newUid) { + + } + + public void loadAttachmentStarted( + Account account, + Message message, + Part part, + Object tag, + boolean requiresDownload) + { + } + + public void loadAttachmentFinished( + Account account, + Message message, + Part part, + Object tag) + { + } + + public void loadAttachmentFailed( + Account account, + Message message, + Part part, + Object tag, + String reason) + { + } + + /** + * General notification messages subclasses can override to be notified that the controller + * has completed a command. This is useful for turning off progress indicators that may have + * been left over from previous commands. + * @param moreCommandsToRun True if the controller will continue on to another command + * immediately. + */ + public void controllerCommandCompleted(boolean moreCommandsToRun) { + + } +} diff --git a/src/com/fsck/k9/PeekableInputStream.java b/src/com/fsck/k9/PeekableInputStream.java new file mode 100644 index 000000000..2039c7ed0 --- /dev/null +++ b/src/com/fsck/k9/PeekableInputStream.java @@ -0,0 +1,64 @@ + +package com.fsck.k9; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A filtering InputStream that allows single byte "peeks" without consuming the byte. The + * client of this stream can call peek() to see the next available byte in the stream + * and a subsequent read will still return the peeked byte. + */ +public class PeekableInputStream extends InputStream { + private InputStream mIn; + private boolean mPeeked; + private int mPeekedByte; + + public PeekableInputStream(InputStream in) { + this.mIn = in; + } + + @Override + public int read() throws IOException { + if (!mPeeked) { + return mIn.read(); + } else { + mPeeked = false; + return mPeekedByte; + } + } + + public int peek() throws IOException { + if (!mPeeked) { + mPeekedByte = read(); + mPeeked = true; + } + return mPeekedByte; + } + + @Override + public int read(byte[] b, int offset, int length) throws IOException { + if (!mPeeked) { + return mIn.read(b, offset, length); + } else { + b[0] = (byte)mPeekedByte; + mPeeked = false; + int r = mIn.read(b, offset + 1, length - 1); + if (r == -1) { + return 1; + } else { + return r + 1; + } + } + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public String toString() { + return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)", + mIn.toString(), mPeeked, mPeekedByte); + } +} diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java new file mode 100644 index 000000000..75b75364a --- /dev/null +++ b/src/com/fsck/k9/Preferences.java @@ -0,0 +1,123 @@ + +package com.fsck.k9; + +import java.util.Arrays; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.util.Config; +import android.util.Log; + +public class Preferences { + private static Preferences preferences; + + SharedPreferences mSharedPreferences; + + private Preferences(Context context) { + mSharedPreferences = context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE); + } + + /** + * TODO need to think about what happens if this gets GCed along with the + * Activity that initialized it. Do we lose ability to read Preferences in + * further Activities? Maybe this should be stored in the Application + * context. + * + * @return + */ + public static synchronized Preferences getPreferences(Context context) { + if (preferences == null) { + preferences = new Preferences(context); + } + return preferences; + } + + /** + * Returns an array of the accounts on the system. If no accounts are + * registered the method returns an empty array. + * + * @return + */ + public Account[] getAccounts() { + String accountUuids = mSharedPreferences.getString("accountUuids", null); + if (accountUuids == null || accountUuids.length() == 0) { + return new Account[] {}; + } + String[] uuids = accountUuids.split(","); + Account[] accounts = new Account[uuids.length]; + for (int i = 0, length = uuids.length; i < length; i++) { + accounts[i] = new Account(this, uuids[i]); + } + return accounts; + } + + public Account getAccountByContentUri(Uri uri) { + return new Account(this, uri.getPath().substring(1)); + } + + /** + * Returns the Account marked as default. If no account is marked as default + * the first account in the list is marked as default and then returned. If + * there are no accounts on the system the method returns null. + * + * @return + */ + public Account getDefaultAccount() { + String defaultAccountUuid = mSharedPreferences.getString("defaultAccountUuid", null); + Account defaultAccount = null; + Account[] accounts = getAccounts(); + if (defaultAccountUuid != null) { + for (Account account : accounts) { + if (account.getUuid().equals(defaultAccountUuid)) { + defaultAccount = account; + break; + } + } + } + + if (defaultAccount == null) { + if (accounts.length > 0) { + defaultAccount = accounts[0]; + setDefaultAccount(defaultAccount); + } + } + + return defaultAccount; + } + + public void setDefaultAccount(Account account) { + mSharedPreferences.edit().putString("defaultAccountUuid", account.getUuid()).commit(); + } + + public void setEnableDebugLogging(boolean value) { + mSharedPreferences.edit().putBoolean("enableDebugLogging", value).commit(); + } + + public boolean geteEnableDebugLogging() { + return mSharedPreferences.getBoolean("enableDebugLogging", false); + } + + public void setEnableSensitiveLogging(boolean value) { + mSharedPreferences.edit().putBoolean("enableSensitiveLogging", value).commit(); + } + + public boolean getEnableSensitiveLogging() { + return mSharedPreferences.getBoolean("enableSensitiveLogging", false); + } + + public void save() { + } + + public void clear() { + mSharedPreferences.edit().clear().commit(); + } + + public void dump() { + if (Config.LOGV) { + for (String key : mSharedPreferences.getAll().keySet()) { + Log.v(k9.LOG_TAG, key + " = " + mSharedPreferences.getAll().get(key)); + } + } + } +} diff --git a/src/com/fsck/k9/R.java b/src/com/fsck/k9/R.java new file mode 100644 index 000000000..2f543cc9d --- /dev/null +++ b/src/com/fsck/k9/R.java @@ -0,0 +1,452 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.fsck.k9; + +public final class R { + public static final class array { + public static final int account_settings_check_frequency_entries=0x7f050000; + public static final int account_settings_check_frequency_values=0x7f050001; + } + public static final class attr { + } + public static final class color { + public static final int folder_message_list_child_background=0x7f070000; + } + public static final class dimen { + public static final int button_minWidth=0x7f080000; + } + public static final class drawable { + public static final int appointment_indicator_leftside_1=0x7f020000; + public static final int appointment_indicator_leftside_10=0x7f020001; + public static final int appointment_indicator_leftside_11=0x7f020002; + public static final int appointment_indicator_leftside_12=0x7f020003; + public static final int appointment_indicator_leftside_13=0x7f020004; + public static final int appointment_indicator_leftside_14=0x7f020005; + public static final int appointment_indicator_leftside_15=0x7f020006; + public static final int appointment_indicator_leftside_16=0x7f020007; + public static final int appointment_indicator_leftside_17=0x7f020008; + public static final int appointment_indicator_leftside_18=0x7f020009; + public static final int appointment_indicator_leftside_19=0x7f02000a; + public static final int appointment_indicator_leftside_2=0x7f02000b; + public static final int appointment_indicator_leftside_20=0x7f02000c; + public static final int appointment_indicator_leftside_21=0x7f02000d; + public static final int appointment_indicator_leftside_3=0x7f02000e; + public static final int appointment_indicator_leftside_4=0x7f02000f; + public static final int appointment_indicator_leftside_5=0x7f020010; + public static final int appointment_indicator_leftside_6=0x7f020011; + public static final int appointment_indicator_leftside_7=0x7f020012; + public static final int appointment_indicator_leftside_8=0x7f020013; + public static final int appointment_indicator_leftside_9=0x7f020014; + public static final int attached_image_placeholder=0x7f020015; + public static final int bottombar_565=0x7f020016; + public static final int btn_dialog=0x7f020017; + public static final int btn_dialog_disable=0x7f020018; + public static final int btn_dialog_disable_focused=0x7f020019; + public static final int btn_dialog_normal=0x7f02001a; + public static final int btn_dialog_pressed=0x7f02001b; + public static final int btn_dialog_selected=0x7f02001c; + public static final int button_indicator_next=0x7f02001d; + public static final int divider_horizontal_email=0x7f02001e; + public static final int email_quoted_bar=0x7f02001f; + public static final int expander_ic_folder=0x7f020020; + public static final int expander_ic_folder_maximized=0x7f020021; + public static final int expander_ic_folder_minimized=0x7f020022; + public static final int folder_message_list_child_background=0x7f020023; + public static final int ic_delete=0x7f020024; + public static final int ic_email_attachment=0x7f020025; + public static final int ic_email_attachment_small=0x7f020026; + public static final int ic_email_caret_double_light=0x7f020027; + public static final int ic_email_caret_single_light=0x7f020028; + public static final int ic_email_thread_open_bottom_default=0x7f020029; + public static final int ic_email_thread_open_top_default=0x7f02002a; + public static final int ic_menu_account_list=0x7f02002b; + public static final int ic_menu_add=0x7f02002c; + public static final int ic_menu_archive=0x7f02002d; + public static final int ic_menu_attachment=0x7f02002e; + public static final int ic_menu_cc=0x7f02002f; + public static final int ic_menu_close_clear_cancel=0x7f020030; + public static final int ic_menu_compose=0x7f020031; + public static final int ic_menu_delete=0x7f020032; + public static final int ic_menu_edit=0x7f020033; + public static final int ic_menu_forward_mail=0x7f020034; + public static final int ic_menu_inbox=0x7f020035; + public static final int ic_menu_mark=0x7f020036; + public static final int ic_menu_navigate=0x7f020037; + public static final int ic_menu_preferences=0x7f020038; + public static final int ic_menu_refresh=0x7f020039; + public static final int ic_menu_reply=0x7f02003a; + public static final int ic_menu_reply_all=0x7f02003b; + public static final int ic_menu_save_draft=0x7f02003c; + public static final int ic_menu_search=0x7f02003d; + public static final int ic_menu_send=0x7f02003e; + public static final int ic_mms_attachment_small=0x7f02003f; + public static final int icon=0x7f020040; + public static final int stat_notify_email_generic=0x7f020041; + public static final int text_box=0x7f020042; + public static final int text_box_light=0x7f020043; + } + public static final class id { + public static final int account_always_bcc=0x7f0a000b; + public static final int account_check_frequency=0x7f0a0018; + public static final int account_default=0x7f0a0005; + public static final int account_delete_policy=0x7f0a0013; + public static final int account_delete_policy_label=0x7f0a0012; + public static final int account_description=0x7f0a0016; + public static final int account_email=0x7f0a0002; + public static final int account_name=0x7f0a000a; + public static final int account_notify=0x7f0a0019; + public static final int account_password=0x7f0a0003; + public static final int account_port=0x7f0a0010; + public static final int account_require_login=0x7f0a001a; + public static final int account_require_login_settings=0x7f0a001b; + public static final int account_security_type=0x7f0a0011; + public static final int account_server=0x7f0a000f; + public static final int account_server_label=0x7f0a000e; + public static final int account_settings=0x7f0a004e; + public static final int account_signature=0x7f0a000c; + public static final int account_username=0x7f0a000d; + public static final int accounts=0x7f0a004d; + public static final int add_attachment=0x7f0a0053; + public static final int add_cc_bcc=0x7f0a004f; + public static final int add_new_account=0x7f0a001d; + public static final int attachment=0x7f0a003d; + public static final int attachment_delete=0x7f0a0033; + public static final int attachment_icon=0x7f0a0039; + public static final int attachment_info=0x7f0a003a; + public static final int attachment_name=0x7f0a0034; + public static final int attachments=0x7f0a002e; + public static final int bcc=0x7f0a002d; + public static final int cancel=0x7f0a0009; + public static final int cc=0x7f0a002c; + public static final int check_mail=0x7f0a0047; + public static final int chip=0x7f0a0024; + public static final int compose=0x7f0a0048; + public static final int date=0x7f0a0026; + public static final int debug_logging=0x7f0a0022; + public static final int delete=0x7f0a0038; + public static final int delete_account=0x7f0a0046; + public static final int description=0x7f0a001e; + public static final int discard=0x7f0a0052; + public static final int done=0x7f0a0017; + public static final int download=0x7f0a003b; + public static final int dump_settings=0x7f0a0049; + public static final int edit_account=0x7f0a0045; + public static final int email=0x7f0a001f; + public static final int empty=0x7f0a001c; + public static final int folder_name=0x7f0a0029; + public static final int folder_status=0x7f0a002a; + public static final int forward=0x7f0a004a; + public static final int from=0x7f0a0025; + public static final int imap=0x7f0a0001; + public static final int imap_path_prefix=0x7f0a0015; + public static final int imap_path_prefix_section=0x7f0a0014; + public static final int main_text=0x7f0a0028; + public static final int manual_setup=0x7f0a0006; + public static final int mark_as_read=0x7f0a004b; + public static final int mark_as_unread=0x7f0a0054; + public static final int message=0x7f0a0007; + public static final int message_content=0x7f0a002f; + public static final int new_message_count=0x7f0a0020; + public static final int next=0x7f0a0004; + public static final int open=0x7f0a0044; + public static final int pop=0x7f0a0000; + public static final int previous=0x7f0a0035; + public static final int progress=0x7f0a0008; + public static final int quoted_text=0x7f0a0032; + public static final int quoted_text_bar=0x7f0a0030; + public static final int quoted_text_delete=0x7f0a0031; + public static final int refresh=0x7f0a004c; + public static final int reply=0x7f0a0036; + public static final int reply_all=0x7f0a0037; + public static final int save=0x7f0a0051; + public static final int send=0x7f0a0050; + public static final int sensitive_logging=0x7f0a0023; + public static final int show_pictures=0x7f0a0041; + public static final int show_pictures_section=0x7f0a0040; + public static final int subject=0x7f0a0027; + public static final int text1=0x7f0a0042; + public static final int text2=0x7f0a0043; + public static final int to=0x7f0a002b; + public static final int to_container=0x7f0a003e; + public static final int to_label=0x7f0a003f; + public static final int version=0x7f0a0021; + public static final int view=0x7f0a003c; + } + public static final class layout { + public static final int account_setup_account_type=0x7f030000; + public static final int account_setup_basics=0x7f030001; + public static final int account_setup_check_settings=0x7f030002; + public static final int account_setup_composition=0x7f030003; + public static final int account_setup_incoming=0x7f030004; + public static final int account_setup_names=0x7f030005; + public static final int account_setup_options=0x7f030006; + public static final int account_setup_outgoing=0x7f030007; + public static final int accounts=0x7f030008; + public static final int accounts_item=0x7f030009; + public static final int debug=0x7f03000a; + public static final int folder_message_list_child=0x7f03000b; + public static final int folder_message_list_child_footer=0x7f03000c; + public static final int folder_message_list_group=0x7f03000d; + public static final int message_compose=0x7f03000e; + public static final int message_compose_attachment=0x7f03000f; + public static final int message_view=0x7f030010; + public static final int message_view_attachment=0x7f030011; + public static final int message_view_header=0x7f030012; + public static final int recipient_dropdown_item=0x7f030013; + } + public static final class menu { + public static final int accounts_context=0x7f090000; + public static final int accounts_option=0x7f090001; + public static final int debug_option=0x7f090002; + public static final int folder_message_list_context=0x7f090003; + public static final int folder_message_list_option=0x7f090004; + public static final int message_compose_option=0x7f090005; + public static final int message_view_option=0x7f090006; + } + public static final class string { + public static final int account_delete_dlg_instructions_fmt=0x7f0600c8; + public static final int account_delete_dlg_title=0x7f0600c7; + public static final int account_settings_action=0x7f06001b; + public static final int account_settings_add_account_label=0x7f0600b9; + public static final int account_settings_always_bcc_label=0x7f0600c3; + public static final int account_settings_always_bcc_summary=0x7f0600c4; + public static final int account_settings_composition_label=0x7f0600c2; + public static final int account_settings_composition_title=0x7f0600c1; + public static final int account_settings_default=0x7f0600ad; + public static final int account_settings_default_label=0x7f0600ae; + public static final int account_settings_default_summary=0x7f0600af; + public static final int account_settings_description_label=0x7f0600ba; + public static final int account_settings_email_label=0x7f0600b1; + public static final int account_settings_incoming_label=0x7f0600b5; + public static final int account_settings_incoming_summary=0x7f0600b6; + public static final int account_settings_mail_check_frequency_label=0x7f0600b4; + public static final int account_settings_name_label=0x7f0600bb; + public static final int account_settings_notifications=0x7f0600bc; + public static final int account_settings_notify_label=0x7f0600b0; + public static final int account_settings_notify_summary=0x7f0600b2; + public static final int account_settings_outgoing_label=0x7f0600b7; + public static final int account_settings_outgoing_summary=0x7f0600b8; + public static final int account_settings_ringtone=0x7f0600bf; + public static final int account_settings_servers=0x7f0600c0; + public static final int account_settings_show_combined_label=0x7f0600b3; + public static final int account_settings_signature_label=0x7f0600c5; + public static final int account_settings_signature_summary=0x7f0600c6; + public static final int account_settings_title_fmt=0x7f0600ac; + public static final int account_settings_vibrate_enable=0x7f0600bd; + public static final int account_settings_vibrate_summary=0x7f0600be; + public static final int account_setup_account_type_imap_action=0x7f060079; + public static final int account_setup_account_type_instructions=0x7f060077; + public static final int account_setup_account_type_pop_action=0x7f060078; + public static final int account_setup_account_type_title=0x7f060076; + public static final int account_setup_basics_default_label=0x7f060069; + public static final int account_setup_basics_email_error_duplicate_fmt=0x7f060067; + public static final int account_setup_basics_email_error_invalid_fmt=0x7f060066; + public static final int account_setup_basics_email_hint=0x7f060065; + public static final int account_setup_basics_instructions=0x7f060063; + public static final int account_setup_basics_instructions2_fmt=0x7f060064; + public static final int account_setup_basics_manual_setup_action=0x7f06006a; + public static final int account_setup_basics_password_hint=0x7f060068; + public static final int account_setup_basics_title=0x7f060062; + public static final int account_setup_check_settings_canceling_msg=0x7f060070; + public static final int account_setup_check_settings_check_incoming_msg=0x7f06006d; + public static final int account_setup_check_settings_check_outgoing_msg=0x7f06006e; + public static final int account_setup_check_settings_finishing_msg=0x7f06006f; + public static final int account_setup_check_settings_retr_info_msg=0x7f06006c; + public static final int account_setup_check_settings_title=0x7f06006b; + public static final int account_setup_failed_dlg_auth_message_fmt=0x7f0600a8; + /** Username or password incorrect\n(ERR01 Account does not exist) + */ + public static final int account_setup_failed_dlg_certificate_message_fmt=0x7f0600a9; + /** Cannot connect to server\n(Connection timed out) + */ + public static final int account_setup_failed_dlg_edit_details_action=0x7f0600ab; + /** Cannot safely connect to server\n(Invalid certificate) + */ + public static final int account_setup_failed_dlg_server_message_fmt=0x7f0600aa; + public static final int account_setup_failed_dlg_title=0x7f0600a7; + public static final int account_setup_finished_toast=0x7f060075; + public static final int account_setup_incoming_delete_policy_7days_label=0x7f060088; + public static final int account_setup_incoming_delete_policy_delete_label=0x7f060089; + public static final int account_setup_incoming_delete_policy_label=0x7f060086; + public static final int account_setup_incoming_delete_policy_never_label=0x7f060087; + public static final int account_setup_incoming_imap_path_prefix_hint=0x7f06008b; + public static final int account_setup_incoming_imap_path_prefix_label=0x7f06008a; + public static final int account_setup_incoming_imap_server_label=0x7f06007e; + public static final int account_setup_incoming_password_label=0x7f06007c; + public static final int account_setup_incoming_pop_server_label=0x7f06007d; + public static final int account_setup_incoming_port_label=0x7f06007f; + public static final int account_setup_incoming_security_label=0x7f060080; + public static final int account_setup_incoming_security_none_label=0x7f060081; + public static final int account_setup_incoming_security_ssl_label=0x7f060083; + public static final int account_setup_incoming_security_ssl_optional_label=0x7f060082; + public static final int account_setup_incoming_security_tls_label=0x7f060085; + public static final int account_setup_incoming_security_tls_optional_label=0x7f060084; + public static final int account_setup_incoming_title=0x7f06007a; + public static final int account_setup_incoming_username_label=0x7f06007b; + public static final int account_setup_names_account_name_label=0x7f060073; + public static final int account_setup_names_instructions=0x7f060072; + public static final int account_setup_names_title=0x7f060071; + public static final int account_setup_names_user_name_label=0x7f060074; + public static final int account_setup_options_default_label=0x7f0600a5; + public static final int account_setup_options_mail_check_frequency_10min=0x7f0600a1; + public static final int account_setup_options_mail_check_frequency_15min=0x7f0600a2; + public static final int account_setup_options_mail_check_frequency_1hour=0x7f0600a4; + public static final int account_setup_options_mail_check_frequency_30min=0x7f0600a3; + public static final int account_setup_options_mail_check_frequency_5min=0x7f0600a0; + public static final int account_setup_options_mail_check_frequency_label=0x7f06009e; + /** Frequency also used in account_settings_* + */ + public static final int account_setup_options_mail_check_frequency_never=0x7f06009f; + public static final int account_setup_options_notify_label=0x7f0600a6; + public static final int account_setup_options_title=0x7f06009d; + public static final int account_setup_outgoing_authentication_basic_label=0x7f060098; + public static final int account_setup_outgoing_authentication_basic_password_label=0x7f06009a; + public static final int account_setup_outgoing_authentication_basic_username_label=0x7f060099; + public static final int account_setup_outgoing_authentication_imap_before_smtp_label=0x7f06009c; + /** The authentication strings below are for a planned (hopefully) change to the above username and password options + */ + public static final int account_setup_outgoing_authentication_label=0x7f060097; + public static final int account_setup_outgoing_authentication_pop_before_smtp_label=0x7f06009b; + public static final int account_setup_outgoing_password_label=0x7f060096; + public static final int account_setup_outgoing_port_label=0x7f06008e; + public static final int account_setup_outgoing_require_login_label=0x7f060094; + public static final int account_setup_outgoing_security_label=0x7f06008f; + public static final int account_setup_outgoing_security_none_label=0x7f060090; + public static final int account_setup_outgoing_security_ssl_label=0x7f060091; + public static final int account_setup_outgoing_security_tls_label=0x7f060093; + public static final int account_setup_outgoing_security_tls_optional_label=0x7f060092; + public static final int account_setup_outgoing_smtp_server_label=0x7f06008d; + public static final int account_setup_outgoing_title=0x7f06008c; + public static final int account_setup_outgoing_username_label=0x7f060095; + public static final int accounts_action=0x7f06001d; + public static final int accounts_context_menu_title=0x7f060029; + public static final int accounts_title=0x7f060004; + public static final int accounts_welcome=0x7f06003b; + public static final int add_account_action=0x7f060016; + public static final int add_attachment_action=0x7f060026; + public static final int add_cc_bcc_action=0x7f060024; + public static final int app_name=0x7f060003; + public static final int build_number=0x7f060000; + /** User to confirm acceptance of dialog boxes, warnings, errors, etc. + */ + public static final int cancel_action=0x7f060009; + public static final int combined_inbox_label=0x7f060041; + public static final int combined_inbox_list_title=0x7f060042; + public static final int combined_inbox_title=0x7f060040; + public static final int compose_action=0x7f060017; + public static final int compose_title=0x7f060005; + public static final int continue_action=0x7f06000f; + public static final int debug_enable_debug_logging_label=0x7f06003d; + public static final int debug_enable_sensitive_logging_label=0x7f06003e; + public static final int debug_title=0x7f060006; + public static final int debug_version_fmt=0x7f06003c; + public static final int delete_action=0x7f06000d; + public static final int discard_action=0x7f060012; + public static final int done_action=0x7f060010; + public static final int dump_settings_action=0x7f060027; + public static final int edit_subject_action=0x7f060025; + public static final int empty_trash_action=0x7f060028; + public static final int folders_action=0x7f060022; + public static final int forward_action=0x7f06000e; + public static final int general_no_subject=0x7f06002a; + public static final int mailbox_select_dlg_new_mailbox_action=0x7f06005b; + public static final int mailbox_select_dlg_title=0x7f06005a; + public static final int mark_as_read_action=0x7f06001f; + public static final int mark_as_unread_action=0x7f060020; + public static final int message_compose_attachments_skipped_toast=0x7f06004e; + public static final int message_compose_bcc_hint=0x7f060047; + public static final int message_compose_cc_hint=0x7f060046; + public static final int message_compose_downloading_attachments_toast=0x7f06004d; + public static final int message_compose_error_no_recipients=0x7f06004c; + public static final int message_compose_fwd_header_fmt=0x7f060049; + public static final int message_compose_quoted_text_label=0x7f06004b; + public static final int message_compose_reply_header_fmt=0x7f06004a; + public static final int message_compose_subject_hint=0x7f060048; + public static final int message_compose_to_hint=0x7f060045; + public static final int message_copied_toast=0x7f06005d; + public static final int message_deleted_toast=0x7f06005f; + public static final int message_discarded_toast=0x7f060060; + public static final int message_header_mua=0x7f06003f; + /** Inbox (12) + */ + public static final int message_list_load_more_messages_action=0x7f060044; + /** Inbox here should be the same as mailbox_name_inbox + */ + public static final int message_list_title_fmt=0x7f060043; + public static final int message_moved_toast=0x7f06005e; + public static final int message_saved_toast=0x7f060061; + public static final int message_view_attachment_download_action=0x7f060051; + public static final int message_view_attachment_view_action=0x7f060050; + public static final int message_view_datetime_fmt=0x7f060054; + public static final int message_view_fetching_attachment_toast=0x7f060059; + public static final int message_view_next_action=0x7f060053; + public static final int message_view_prev_action=0x7f060052; + public static final int message_view_show_pictures_action=0x7f060058; + public static final int message_view_show_pictures_instructions=0x7f060057; + public static final int message_view_status_attachment_not_saved=0x7f060056; + public static final int message_view_status_attachment_saved=0x7f060055; + public static final int message_view_to_label=0x7f06004f; + public static final int move_to_action=0x7f060021; + public static final int new_mailbox_dlg_title=0x7f06005c; + /** Actions will be used as buttons and in menu items + */ + public static final int next_action=0x7f060007; + /** 279 Unread (someone@google.com) + */ + public static final int notification_new_multi_account_fmt=0x7f060034; + public static final int notification_new_one_account_fmt=0x7f060033; + public static final int notification_new_scrolling=0x7f060032; + public static final int notification_new_title=0x7f060031; + public static final int notification_unsent_title=0x7f060035; + /** Used as part of a multi-step process + */ + public static final int okay_action=0x7f060008; + public static final int open_action=0x7f06001a; + public static final int preferences_action=0x7f060019; + public static final int provider_note_live=0x7f0600ca; + public static final int provider_note_yahoo=0x7f0600c9; + public static final int read_action=0x7f06001e; + public static final int read_attachment_desc=0x7f060002; + public static final int read_attachment_label=0x7f060001; + public static final int refresh_action=0x7f060015; + public static final int remove_account_action=0x7f06001c; + /** Used to complete a multi-step process + */ + public static final int remove_action=0x7f060011; + public static final int reply_action=0x7f06000b; + public static final int reply_all_action=0x7f06000c; + public static final int retry_action=0x7f060014; + public static final int save_draft_action=0x7f060013; + public static final int search_action=0x7f060018; + public static final int send_action=0x7f06000a; + /** The following mailbox names will be used if the user has not specified one from the server + */ + public static final int special_mailbox_name_drafts=0x7f060038; + public static final int special_mailbox_name_inbox=0x7f060036; + public static final int special_mailbox_name_outbox=0x7f060037; + public static final int special_mailbox_name_sent=0x7f06003a; + public static final int special_mailbox_name_trash=0x7f060039; + public static final int status_error=0x7f06002e; + /** Shown in place of the subject when a message has no subject. Showing this in parentheses is customary. + */ + public static final int status_loading=0x7f06002b; + public static final int status_loading_more=0x7f06002c; + /** Used in Outbox when a message is currently sending + */ + public static final int status_loading_more_failed=0x7f060030; + public static final int status_network_error=0x7f06002d; + /** Used in Outbox when a message has failed to send + */ + public static final int status_sending=0x7f06002f; + public static final int view_hide_details_action=0x7f060023; + } + public static final class xml { + public static final int account_settings_preferences=0x7f040000; + public static final int providers=0x7f040001; + } +} diff --git a/src/com/fsck/k9/Utility.java b/src/com/fsck/k9/Utility.java new file mode 100644 index 000000000..08f643f3b --- /dev/null +++ b/src/com/fsck/k9/Utility.java @@ -0,0 +1,176 @@ + +package com.fsck.k9; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.util.Date; + +import com.fsck.k9.codec.binary.Base64; + +import android.text.Editable; +import android.widget.TextView; + +public class Utility { + public final static String readInputStream(InputStream in, String encoding) throws IOException { + InputStreamReader reader = new InputStreamReader(in, encoding); + StringBuffer sb = new StringBuffer(); + int count; + char[] buf = new char[512]; + while ((count = reader.read(buf)) != -1) { + sb.append(buf, 0, count); + } + return sb.toString(); + } + + public final static boolean arrayContains(Object[] a, Object o) { + for (int i = 0, count = a.length; i < count; i++) { + if (a[i].equals(o)) { + return true; + } + } + return false; + } + + /** + * Combines the given array of Objects into a single string using the + * seperator character and each Object's toString() method. between each + * part. + * + * @param parts + * @param seperator + * @return + */ + public static String combine(Object[] parts, char seperator) { + if (parts == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < parts.length; i++) { + sb.append(parts[i].toString()); + if (i < parts.length - 1) { + sb.append(seperator); + } + } + return sb.toString(); + } + + public static String base64Decode(String encoded) { + if (encoded == null) { + return null; + } + byte[] decoded = new Base64().decode(encoded.getBytes()); + return new String(decoded); + } + + public static String base64Encode(String s) { + if (s == null) { + return s; + } + byte[] encoded = new Base64().encode(s.getBytes()); + return new String(encoded); + } + + public static boolean requiredFieldValid(TextView view) { + return view.getText() != null && view.getText().length() > 0; + } + + public static boolean requiredFieldValid(Editable s) { + return s != null && s.length() > 0; + } + + /** + * Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the + * double quote character to start and end if it's not already there. + * sample -> "sample" + * "sample" -> "sample" + * ""sample"" -> "sample" + * "sample"" -> "sample" + * sa"mp"le -> "sa"mp"le" + * "sa"mp"le" -> "sa"mp"le" + * (empty string) -> "" + * " -> "" + * @param s + * @return + */ + public static String quoteString(String s) { + if (s == null) { + return null; + } + if (!s.matches("^\".*\"$")) { + return "\"" + s + "\""; + } + else { + return s; + } + } + + /** + * A fast version of URLDecoder.decode() that works only with UTF-8 and does only two + * allocations. This version is around 3x as fast as the standard one and I'm using it + * hundreds of times in places that slow down the UI, so it helps. + */ + public static String fastUrlDecode(String s) { + try { + byte[] bytes = s.getBytes("UTF-8"); + byte ch; + int length = 0; + for (int i = 0, count = bytes.length; i < count; i++) { + ch = bytes[i]; + if (ch == '%') { + int h = (bytes[i + 1] - '0'); + int l = (bytes[i + 2] - '0'); + if (h > 9) { + h -= 7; + } + if (l > 9) { + l -= 7; + } + bytes[length] = (byte) ((h << 4) | l); + i += 2; + } + else if (ch == '+') { + bytes[length] = ' '; + } + else { + bytes[length] = bytes[i]; + } + length++; + } + return new String(bytes, 0, length, "UTF-8"); + } + catch (UnsupportedEncodingException uee) { + return null; + } + } + + /** + * Returns true if the specified date is within today. Returns false otherwise. + * @param date + * @return + */ + public static boolean isDateToday(Date date) { + // TODO But Calendar is so slowwwwwww.... + Date today = new Date(); + if (date.getYear() == today.getYear() && + date.getMonth() == today.getMonth() && + date.getDate() == today.getDate()) { + return true; + } + return false; + } + + /* + * TODO disabled this method globally. It is used in all the settings screens but I just + * noticed that an unrelated icon was dimmed. Android must share drawables internally. + */ + public static void setCompoundDrawablesAlpha(TextView view, int alpha) { +// Drawable[] drawables = view.getCompoundDrawables(); +// for (Drawable drawable : drawables) { +// if (drawable != null) { +// drawable.setAlpha(alpha); +// } +// } + } +} diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java new file mode 100644 index 000000000..3010eaf20 --- /dev/null +++ b/src/com/fsck/k9/activity/Accounts.java @@ -0,0 +1,296 @@ + +package com.fsck.k9.activity; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.app.NotificationManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.MessagingController; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.activity.setup.AccountSettings; +import com.fsck.k9.activity.setup.AccountSetupBasics; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.LocalStore.LocalFolder; + +public class Accounts extends ListActivity implements OnItemClickListener, OnClickListener { + private static final int DIALOG_REMOVE_ACCOUNT = 1; + /** + * Key codes used to open a debug settings screen. + */ + private static int[] secretKeyCodes = { + KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U, + KeyEvent.KEYCODE_G + }; + + private int mSecretKeyCodeIndex = 0; + private Account mSelectedContextAccount; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.accounts); + ListView listView = getListView(); + listView.setOnItemClickListener(this); + listView.setItemsCanFocus(false); + listView.setEmptyView(findViewById(R.id.empty)); + findViewById(R.id.add_new_account).setOnClickListener(this); + registerForContextMenu(listView); + + if (icicle != null && icicle.containsKey("selectedContextAccount")) { + mSelectedContextAccount = (Account) icicle.getSerializable("selectedContextAccount"); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mSelectedContextAccount != null) { + outState.putSerializable("selectedContextAccount", mSelectedContextAccount); + } + } + + @Override + public void onResume() { + super.onResume(); + + NotificationManager notifMgr = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + notifMgr.cancel(1); + + refresh(); + } + + private void refresh() { + Account[] accounts = Preferences.getPreferences(this).getAccounts(); + getListView().setAdapter(new AccountsAdapter(accounts)); + } + + private void onAddNewAccount() { + AccountSetupBasics.actionNewAccount(this); + } + + private void onEditAccount(Account account) { + AccountSettings.actionSettings(this, account); + } + + private void onRefresh() { + MessagingController.getInstance(getApplication()).checkMail(this, null, null); + } + + private void onCompose() { + Account defaultAccount = + Preferences.getPreferences(this).getDefaultAccount(); + if (defaultAccount != null) { + MessageCompose.actionCompose(this, defaultAccount); + } + else { + onAddNewAccount(); + } + } + + private void onOpenAccount(Account account) { + FolderMessageList.actionHandleAccount(this, account); + } + + public void onClick(View view) { + if (view.getId() == R.id.add_new_account) { + onAddNewAccount(); + } + } + + private void onDeleteAccount(Account account) { + mSelectedContextAccount = account; + showDialog(DIALOG_REMOVE_ACCOUNT); + } + + @Override + public Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_REMOVE_ACCOUNT: + return createRemoveAccountDialog(); + } + return super.onCreateDialog(id); + } + + private Dialog createRemoveAccountDialog() { + return new AlertDialog.Builder(this) + .setTitle(R.string.account_delete_dlg_title) + .setMessage(getString(R.string.account_delete_dlg_instructions_fmt, + mSelectedContextAccount.getDescription())) + .setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dismissDialog(DIALOG_REMOVE_ACCOUNT); + try { + ((LocalStore)Store.getInstance( + mSelectedContextAccount.getLocalStoreUri(), + getApplication())).delete(); + } catch (Exception e) { + // Ignore + } + mSelectedContextAccount.delete(Preferences.getPreferences(Accounts.this)); + k9.setServicesEnabled(Accounts.this); + refresh(); + } + }) + .setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dismissDialog(DIALOG_REMOVE_ACCOUNT); + } + }) + .create(); + } + + public boolean onContextItemSelected(MenuItem item) { + AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo)item.getMenuInfo(); + Account account = (Account)getListView().getItemAtPosition(menuInfo.position); + switch (item.getItemId()) { + case R.id.delete_account: + onDeleteAccount(account); + break; + case R.id.edit_account: + onEditAccount(account); + break; + case R.id.open: + onOpenAccount(account); + break; + } + return true; + } + + public void onItemClick(AdapterView parent, View view, int position, long id) { + Account account = (Account)parent.getItemAtPosition(position); + onOpenAccount(account); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_new_account: + onAddNewAccount(); + break; + case R.id.check_mail: + onRefresh(); + break; + case R.id.compose: + onCompose(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + return true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.accounts_option, menu); + return true; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + menu.setHeaderTitle(R.string.accounts_context_menu_title); + getMenuInflater().inflate(R.menu.accounts_context, menu); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event.getKeyCode() == secretKeyCodes[mSecretKeyCodeIndex]) { + mSecretKeyCodeIndex++; + if (mSecretKeyCodeIndex == secretKeyCodes.length) { + mSecretKeyCodeIndex = 0; + startActivity(new Intent(this, Debug.class)); + } + } else { + mSecretKeyCodeIndex = 0; + } + return super.onKeyDown(keyCode, event); + } + + class AccountsAdapter extends ArrayAdapter { + public AccountsAdapter(Account[] accounts) { + super(Accounts.this, 0, accounts); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Account account = getItem(position); + View view; + if (convertView != null) { + view = convertView; + } + else { + view = getLayoutInflater().inflate(R.layout.accounts_item, parent, false); + } + AccountViewHolder holder = (AccountViewHolder) view.getTag(); + if (holder == null) { + holder = new AccountViewHolder(); + holder.description = (TextView) view.findViewById(R.id.description); + holder.email = (TextView) view.findViewById(R.id.email); + holder.newMessageCount = (TextView) view.findViewById(R.id.new_message_count); + view.setTag(holder); + } + holder.description.setText(account.getDescription()); + holder.email.setText(account.getEmail()); + if (account.getEmail().equals(account.getDescription())) { + holder.email.setVisibility(View.GONE); + } + int unreadMessageCount = 0; + try { + LocalStore localStore = (LocalStore) Store.getInstance( + account.getLocalStoreUri(), + getApplication()); + LocalFolder localFolder = (LocalFolder) localStore.getFolder(k9.INBOX); + if (localFolder.exists()) { + unreadMessageCount = localFolder.getUnreadMessageCount(); + } + } + catch (MessagingException me) { + /* + * This is not expected to fail under normal circumstances. + */ + throw new RuntimeException("Unable to get unread count from local store.", me); + } + holder.newMessageCount.setText(Integer.toString(unreadMessageCount)); + holder.newMessageCount.setVisibility(unreadMessageCount > 0 ? View.VISIBLE : View.GONE); + return view; + } + + class AccountViewHolder { + public TextView description; + public TextView email; + public TextView newMessageCount; + } + } +} + + diff --git a/src/com/fsck/k9/activity/Debug.java b/src/com/fsck/k9/activity/Debug.java new file mode 100644 index 000000000..8c4c51809 --- /dev/null +++ b/src/com/fsck/k9/activity/Debug.java @@ -0,0 +1,73 @@ + +package com.fsck.k9.activity; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; +import android.widget.CompoundButton.OnCheckedChangeListener; + +import com.fsck.k9.k9; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; + +public class Debug extends Activity implements OnCheckedChangeListener { + private TextView mVersionView; + private CheckBox mEnableDebugLoggingView; + private CheckBox mEnableSensitiveLoggingView; + + private Preferences mPreferences; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.debug); + + mPreferences = Preferences.getPreferences(this); + + mVersionView = (TextView)findViewById(R.id.version); + mEnableDebugLoggingView = (CheckBox)findViewById(R.id.debug_logging); + mEnableSensitiveLoggingView = (CheckBox)findViewById(R.id.sensitive_logging); + + mEnableDebugLoggingView.setOnCheckedChangeListener(this); + mEnableSensitiveLoggingView.setOnCheckedChangeListener(this); + + mVersionView.setText(String.format(getString(R.string.debug_version_fmt).toString(), + getString(R.string.build_number))); + + mEnableDebugLoggingView.setChecked(k9.DEBUG); + mEnableSensitiveLoggingView.setChecked(k9.DEBUG_SENSITIVE); + } + + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.getId() == R.id.debug_logging) { + k9.DEBUG = isChecked; + mPreferences.setEnableDebugLogging(k9.DEBUG); + } else if (buttonView.getId() == R.id.sensitive_logging) { + k9.DEBUG_SENSITIVE = isChecked; + mPreferences.setEnableSensitiveLogging(k9.DEBUG_SENSITIVE); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.dump_settings) { + Preferences.getPreferences(this).dump(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.debug_option, menu); + return true; + } + +} diff --git a/src/com/fsck/k9/activity/FolderMessageList.java b/src/com/fsck/k9/activity/FolderMessageList.java new file mode 100644 index 000000000..5642bedce --- /dev/null +++ b/src/com/fsck/k9/activity/FolderMessageList.java @@ -0,0 +1,1283 @@ +package com.fsck.k9.activity; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; + +import android.app.ExpandableListActivity; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Process; +import android.util.Config; +import android.util.Log; +import android.view.ContextMenu; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ExpandableListView.ExpandableListContextMenuInfo; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.MessagingController; +import com.fsck.k9.MessagingListener; +import com.fsck.k9.R; +import com.fsck.k9.Utility; +import com.fsck.k9.Preferences; +import com.fsck.k9.activity.FolderMessageList.FolderMessageListAdapter.FolderInfoHolder; +import com.fsck.k9.activity.FolderMessageList.FolderMessageListAdapter.MessageInfoHolder; +import com.fsck.k9.activity.setup.AccountSettings; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.mail.store.LocalStore; + +/** + * FolderMessageList is the primary user interface for the program. This Activity shows + * a two level list of the Account's folders and each folder's messages. From this + * Activity the user can perform all standard message operations. + * + * + * TODO some things that are slowing us down: + * Need a way to remove state such as progress bar and per folder progress on + * resume if the command has completed. + * + * TODO + * Break out seperate functions for: + * refresh local folders + * refresh remote folders + * refresh open folder local messages + * refresh open folder remote messages + * + * And don't refresh remote folders ever unless the user runs a refresh. Maybe not even then. + */ +public class FolderMessageList extends ExpandableListActivity { + private static final String EXTRA_ACCOUNT = "account"; + private static final String EXTRA_CLEAR_NOTIFICATION = "clearNotification"; + private static final String EXTRA_INITIAL_FOLDER = "initialFolder"; + + private static final String STATE_KEY_LIST = + "com.fsck.k9.activity.folderlist_expandableListState"; + private static final String STATE_KEY_EXPANDED_GROUP = + "com.fsck.k9.activity.folderlist_expandedGroup"; + private static final String STATE_KEY_EXPANDED_GROUP_SELECTION = + "com.fsck.k9.activity.folderlist_expandedGroupSelection"; + + private static final int UPDATE_FOLDER_ON_EXPAND_INTERVAL_MS = (1000 * 60 * 3); + + private static final int[] colorChipResIds = new int[] { + R.drawable.appointment_indicator_leftside_1, + R.drawable.appointment_indicator_leftside_2, + R.drawable.appointment_indicator_leftside_3, + R.drawable.appointment_indicator_leftside_4, + R.drawable.appointment_indicator_leftside_5, + R.drawable.appointment_indicator_leftside_6, + R.drawable.appointment_indicator_leftside_7, + R.drawable.appointment_indicator_leftside_8, + R.drawable.appointment_indicator_leftside_9, + R.drawable.appointment_indicator_leftside_10, + R.drawable.appointment_indicator_leftside_11, + R.drawable.appointment_indicator_leftside_12, + R.drawable.appointment_indicator_leftside_13, + R.drawable.appointment_indicator_leftside_14, + R.drawable.appointment_indicator_leftside_15, + R.drawable.appointment_indicator_leftside_16, + R.drawable.appointment_indicator_leftside_17, + R.drawable.appointment_indicator_leftside_18, + R.drawable.appointment_indicator_leftside_19, + R.drawable.appointment_indicator_leftside_20, + R.drawable.appointment_indicator_leftside_21, + }; + + private ExpandableListView mListView; + private int colorChipResId; + + private FolderMessageListAdapter mAdapter; + private LayoutInflater mInflater; + private Account mAccount; + /** + * Stores the name of the folder that we want to open as soon as possible after load. It is + * set to null once the folder has been opened once. + */ + private String mInitialFolder; + + private DateFormat mDateFormat = DateFormat.getDateInstance(DateFormat.SHORT); + private DateFormat mTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT); + + private int mExpandedGroup = -1; + private boolean mRestoringState; + + private boolean mRefreshRemote; + + private FolderMessageListHandler mHandler = new FolderMessageListHandler(); + + class FolderMessageListHandler extends Handler { + private static final int MSG_PROGRESS = 2; + private static final int MSG_DATA_CHANGED = 3; + private static final int MSG_EXPAND_GROUP = 5; + private static final int MSG_FOLDER_LOADING = 7; + private static final int MSG_REMOVE_MESSAGE = 11; + private static final int MSG_SYNC_MESSAGES = 13; + private static final int MSG_FOLDER_STATUS = 17; + + @Override + public void handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_PROGRESS: + setProgressBarIndeterminateVisibility(msg.arg1 != 0); + break; + case MSG_DATA_CHANGED: + mAdapter.notifyDataSetChanged(); + break; + case MSG_EXPAND_GROUP: + mListView.expandGroup(msg.arg1); + break; + /* + * The following functions modify the state of the adapter's underlying list and + * must be run here, in the main thread, so that notifyDataSetChanged is run + * before any further requests are made to the adapter. + */ + case MSG_FOLDER_LOADING: { + FolderInfoHolder folder = mAdapter.getFolder((String) msg.obj); + if (folder != null) { + folder.loading = msg.arg1 != 0; + mAdapter.notifyDataSetChanged(); + } + break; + } + case MSG_REMOVE_MESSAGE: { + FolderInfoHolder folder = (FolderInfoHolder) ((Object[]) msg.obj)[0]; + MessageInfoHolder message = (MessageInfoHolder) ((Object[]) msg.obj)[1]; + folder.messages.remove(message); + mAdapter.notifyDataSetChanged(); + break; + } + case MSG_SYNC_MESSAGES: { + FolderInfoHolder folder = (FolderInfoHolder) ((Object[]) msg.obj)[0]; + Message[] messages = (Message[]) ((Object[]) msg.obj)[1]; + folder.messages.clear(); + for (Message message : messages) { + mAdapter.addOrUpdateMessage(folder, message, false, false); + } + Collections.sort(folder.messages); + mAdapter.notifyDataSetChanged(); + break; + } + case MSG_FOLDER_STATUS: { + String folderName = (String) ((Object[]) msg.obj)[0]; + String status = (String) ((Object[]) msg.obj)[1]; + FolderInfoHolder folder = mAdapter.getFolder(folderName); + if (folder != null) { + folder.status = status; + mAdapter.notifyDataSetChanged(); + } + break; + } + default: + super.handleMessage(msg); + } + } + + public void synchronizeMessages(FolderInfoHolder folder, Message[] messages) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_SYNC_MESSAGES; + msg.obj = new Object[] { folder, messages }; + sendMessage(msg); + } + + public void removeMessage(FolderInfoHolder folder, MessageInfoHolder message) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_REMOVE_MESSAGE; + msg.obj = new Object[] { folder, message }; + sendMessage(msg); + } + + public void folderLoading(String folder, boolean loading) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_FOLDER_LOADING; + msg.arg1 = loading ? 1 : 0; + msg.obj = folder; + sendMessage(msg); + } + + public void progress(boolean progress) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_PROGRESS; + msg.arg1 = progress ? 1 : 0; + sendMessage(msg); + } + + public void dataChanged() { + sendEmptyMessage(MSG_DATA_CHANGED); + } + + public void expandGroup(int groupPosition) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_EXPAND_GROUP; + msg.arg1 = groupPosition; + sendMessage(msg); + } + + public void folderStatus(String folder, String status) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_FOLDER_STATUS; + msg.obj = new String[] { folder, status }; + sendMessage(msg); + } + } + + /** + * This class is responsible for reloading the list of local messages for a given folder, + * notifying the adapter that the message have been loaded and queueing up a remote + * update of the folder. + */ + class FolderUpdateWorker implements Runnable { + String mFolder; + boolean mSynchronizeRemote; + + /** + * Create a worker for the given folder and specifying whether the + * worker should synchronize the remote folder or just the local one. + * @param folder + * @param synchronizeRemote + */ + public FolderUpdateWorker(String folder, boolean synchronizeRemote) { + mFolder = folder; + mSynchronizeRemote = synchronizeRemote; + } + + public void run() { + // Lower our priority + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + // Synchronously load the list of local messages + MessagingController.getInstance(getApplication()).listLocalMessages( + mAccount, + mFolder, + mAdapter.mListener); + if (mSynchronizeRemote) { + // Tell the MessagingController to run a remote update of this folder + // at it's leisure + MessagingController.getInstance(getApplication()).synchronizeMailbox( + mAccount, + mFolder, + mAdapter.mListener); + } + } + } + + public static void actionHandleAccount(Context context, Account account, String initialFolder) { + Intent intent = new Intent(context, FolderMessageList.class); + intent.putExtra(EXTRA_ACCOUNT, account); + if (initialFolder != null) { + intent.putExtra(EXTRA_INITIAL_FOLDER, initialFolder); + } + context.startActivity(intent); + } + + public static void actionHandleAccount(Context context, Account account) { + actionHandleAccount(context, account, null); + } + + public static Intent actionHandleAccountIntent(Context context, Account account, String initialFolder) { + Intent intent = new Intent(context, FolderMessageList.class); + intent.putExtra(EXTRA_ACCOUNT, account); + intent.putExtra(EXTRA_CLEAR_NOTIFICATION, true); + if (initialFolder != null) { + intent.putExtra(EXTRA_INITIAL_FOLDER, initialFolder); + } + return intent; + } + + public static Intent actionHandleAccountIntent(Context context, Account account) { + return actionHandleAccountIntent(context, account, null); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + mListView = getExpandableListView(); + mListView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); + mListView.setLongClickable(true); + registerForContextMenu(mListView); + + /* + * We manually save and restore the list's state because our adapter is slow. + */ + mListView.setSaveEnabled(false); + + getExpandableListView().setGroupIndicator( + getResources().getDrawable(R.drawable.expander_ic_folder)); + + mInflater = getLayoutInflater(); + + Intent intent = getIntent(); + mAccount = (Account)intent.getSerializableExtra(EXTRA_ACCOUNT); + + // Take the initial folder into account only if we are *not* restoring the activity already + if (savedInstanceState == null) { + mInitialFolder = intent.getStringExtra(EXTRA_INITIAL_FOLDER); + } + + /* + * Since the color chip is always the same color for a given account we just cache the id + * of the chip right here. + */ + colorChipResId = colorChipResIds[mAccount.getAccountNumber() % colorChipResIds.length]; + + mAdapter = new FolderMessageListAdapter(); + + final Object previousData = getLastNonConfigurationInstance(); + if (previousData != null) { + //noinspection unchecked + mAdapter.mFolders = (ArrayList) previousData; + } + + setListAdapter(mAdapter); + + if (savedInstanceState != null) { + mRestoringState = true; + onRestoreListState(savedInstanceState); + mRestoringState = false; + } + + setTitle(mAccount.getDescription()); + } + + private void onRestoreListState(Bundle savedInstanceState) { + final int expandedGroup = savedInstanceState.getInt(STATE_KEY_EXPANDED_GROUP, -1); + if (expandedGroup >= 0 && mAdapter.getGroupCount() > expandedGroup) { + mListView.expandGroup(expandedGroup); + long selectedChild = savedInstanceState.getLong(STATE_KEY_EXPANDED_GROUP_SELECTION, -1); + if (selectedChild != ExpandableListView.PACKED_POSITION_VALUE_NULL) { + mListView.setSelection(mListView.getFlatListPosition(selectedChild)); + } + } + mListView.onRestoreInstanceState(savedInstanceState.getParcelable(STATE_KEY_LIST)); + } + + @Override + public Object onRetainNonConfigurationInstance() { + return mAdapter.mFolders; + } + + @Override + public void onPause() { + super.onPause(); + MessagingController.getInstance(getApplication()).removeListener(mAdapter.mListener); + } + + /** + * On resume we refresh the folder list (in the background) and we refresh the messages + * for any folder that is currently open. This guarantees that things like unread message + * count and read status are updated. + */ + @Override + public void onResume() { + super.onResume(); + + NotificationManager notifMgr = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + notifMgr.cancel(1); + + MessagingController.getInstance(getApplication()).addListener(mAdapter.mListener); + mAccount.refresh(Preferences.getPreferences(this)); + onRefresh(false); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_KEY_LIST, mListView.onSaveInstanceState()); + outState.putInt(STATE_KEY_EXPANDED_GROUP, mExpandedGroup); + outState.putLong(STATE_KEY_EXPANDED_GROUP_SELECTION, mListView.getSelectedPosition()); + } + + @Override + public void onGroupCollapse(int groupPosition) { + super.onGroupCollapse(groupPosition); + mExpandedGroup = -1; + } + + @Override + public void onGroupExpand(int groupPosition) { + super.onGroupExpand(groupPosition); + if (mExpandedGroup != -1) { + mListView.collapseGroup(mExpandedGroup); + } + mExpandedGroup = groupPosition; + + if (!mRestoringState) { + /* + * Scroll the selected item to the top of the screen. + */ + int position = mListView.getFlatListPosition( + ExpandableListView.getPackedPositionForGroup(groupPosition)); + mListView.setSelectionFromTop(position, 0); + } + + final FolderInfoHolder folder = (FolderInfoHolder) mAdapter.getGroup(groupPosition); + /* + * We'll only do a hard refresh of a particular folder every 3 minutes or if the user + * specifically asks for a refresh. + */ + if (System.currentTimeMillis() - folder.lastChecked + > UPDATE_FOLDER_ON_EXPAND_INTERVAL_MS) { + folder.lastChecked = System.currentTimeMillis(); + // TODO: If the previous thread is already running, we should cancel it + new Thread(new FolderUpdateWorker(folder.name, true)).start(); + } + } + + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + int group = mListView.getPackedPositionGroup(mListView.getSelectedId()); + int item =(mListView.getSelectedItemPosition() -1 ); + // Guard against hitting delete on group names + // + try { + MessageInfoHolder message = (MessageInfoHolder) mAdapter.getChild(group, item); + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: { onDelete(message); return true;} + case KeyEvent.KEYCODE_C: { onCompose(); return true;} + case KeyEvent.KEYCODE_Q: { onAccounts(); return true; } + case KeyEvent.KEYCODE_F: { onForward(message); return true;} + case KeyEvent.KEYCODE_A: { onReplyAll(message); return true; } + case KeyEvent.KEYCODE_R: { onReply(message); return true; } + } + } + finally { + return super.onKeyDown(keyCode, event); + } + } + + + @Override + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { + FolderInfoHolder folder = (FolderInfoHolder) mAdapter.getGroup(groupPosition); + if (folder.outbox) { + return false; + } + if (childPosition == folder.messages.size() && !folder.loading) { + if (folder.status == null) { + MessagingController.getInstance(getApplication()).loadMoreMessages( + mAccount, + folder.name, + mAdapter.mListener); + return false; + } + else { + MessagingController.getInstance(getApplication()).synchronizeMailbox( + mAccount, + folder.name, + mAdapter.mListener); + return false; + } + } + else if (childPosition >= folder.messages.size()) { + return false; + } + MessageInfoHolder message = (MessageInfoHolder) mAdapter.getChild(groupPosition, childPosition); + + onOpenMessage(folder, message); + + return true; + } + + private void onRefresh(final boolean forceRemote) { + if (forceRemote) { + mRefreshRemote = true; + } + new Thread() { + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + MessagingController.getInstance(getApplication()).listFolders( + mAccount, + forceRemote, + mAdapter.mListener); + if (forceRemote) { + MessagingController.getInstance(getApplication()).sendPendingMessages( + mAccount, + null); + } + } + }.start(); + } + + private void onOpenMessage(FolderInfoHolder folder, MessageInfoHolder message) { + /* + * We set read=true here for UI performance reasons. The actual value will get picked up + * on the refresh when the Activity is resumed but that may take a second or so and we + * don't want this to show and then go away. + * I've gone back and forth on this, and this gives a better UI experience, so I am + * putting it back in. + */ + if (!message.read) { + message.read = true; + mHandler.dataChanged(); + } + + if (folder.name.equals(mAccount.getDraftsFolderName())) { + MessageCompose.actionEditDraft(this, mAccount, message.message); + } + else { + ArrayList folderUids = new ArrayList(); + for (MessageInfoHolder holder : folder.messages) { + folderUids.add(holder.uid); + } + MessageView.actionView(this, mAccount, folder.name, message.uid, folderUids); + } + } + + private void onEditAccount() { + AccountSettings.actionSettings(this, mAccount); + } + + private void onAccounts() { + startActivity(new Intent(this, Accounts.class)); + finish(); + } + + private void onCompose() { + MessageCompose.actionCompose(this, mAccount); + } + + private void onDelete(MessageInfoHolder holder) { + MessagingController.getInstance(getApplication()).deleteMessage( + mAccount, + holder.message.getFolder().getName(), + holder.message, + null); + mAdapter.removeMessage(holder.message.getFolder().getName(), holder.uid); + Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); + } + + private void onReply(MessageInfoHolder holder) { + MessageCompose.actionReply(this, mAccount, holder.message, false); + } + + private void onReplyAll(MessageInfoHolder holder) { + MessageCompose.actionReply(this, mAccount, holder.message, true); + } + + private void onForward(MessageInfoHolder holder) { + MessageCompose.actionForward(this, mAccount, holder.message); + } + + private void onToggleRead(MessageInfoHolder holder) { + MessagingController.getInstance(getApplication()).markMessageRead( + mAccount, + holder.message.getFolder().getName(), + holder.uid, + !holder.read); + holder.read = !holder.read; + onRefresh(false); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.refresh: + onRefresh(true); + return true; + case R.id.accounts: + onAccounts(); + return true; + case R.id.compose: + onCompose(); + return true; + case R.id.account_settings: + onEditAccount(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.folder_message_list_option, menu); + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + ExpandableListContextMenuInfo info = + (ExpandableListContextMenuInfo) item.getMenuInfo(); + int groupPosition = + ExpandableListView.getPackedPositionGroup(info.packedPosition); + int childPosition = + ExpandableListView.getPackedPositionChild(info.packedPosition); + FolderInfoHolder folder = (FolderInfoHolder) mAdapter.getGroup(groupPosition); + if (childPosition < mAdapter.getChildrenCount(groupPosition)) { + MessageInfoHolder holder = + (MessageInfoHolder) mAdapter.getChild(groupPosition, childPosition); + switch (item.getItemId()) { + case R.id.open: + onOpenMessage(folder, holder); + break; + case R.id.delete: + onDelete(holder); + break; + case R.id.reply: + onReply(holder); + break; + case R.id.reply_all: + onReplyAll(holder); + break; + case R.id.forward: + onForward(holder); + break; + case R.id.mark_as_read: + onToggleRead(holder); + break; + } + } + return super.onContextItemSelected(item); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo; + if (ExpandableListView.getPackedPositionType(info.packedPosition) == + ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + long packedPosition = info.packedPosition; + int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition); + int childPosition = ExpandableListView.getPackedPositionChild(packedPosition); + FolderInfoHolder folder = (FolderInfoHolder) mAdapter.getGroup(groupPosition); + if (folder.outbox) { + return; + } + if (childPosition < folder.messages.size()) { + getMenuInflater().inflate(R.menu.folder_message_list_context, menu); + MessageInfoHolder message = + (MessageInfoHolder) mAdapter.getChild(groupPosition, childPosition); + if (message.read) { + menu.findItem(R.id.mark_as_read).setTitle(R.string.mark_as_unread_action); + } + } + } + } + + class FolderMessageListAdapter extends BaseExpandableListAdapter { + private ArrayList mFolders = new ArrayList(); + + private MessagingListener mListener = new MessagingListener() { + @Override + public void listFoldersStarted(Account account) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(true); + } + + @Override + public void listFoldersFailed(Account account, String message) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "listFoldersFailed " + message); + } + } + + @Override + public void listFoldersFinished(Account account) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + if (mInitialFolder != null) { + int groupPosition = getFolderPosition(mInitialFolder); + mInitialFolder = null; + if (groupPosition != -1) { + mHandler.expandGroup(groupPosition); + } + } + } + + @Override + public void listFolders(Account account, Folder[] folders) { + if (!account.equals(mAccount)) { + return; + } + for (Folder folder : folders) { + FolderInfoHolder holder = getFolder(folder.getName()); + if (holder == null) { + holder = new FolderInfoHolder(); + mFolders.add(holder); + } + holder.name = folder.getName(); + if (holder.name.equalsIgnoreCase(k9.INBOX)) { + holder.displayName = getString(R.string.special_mailbox_name_inbox); + } + else { + holder.displayName = folder.getName(); + } + if (holder.name.equals(mAccount.getOutboxFolderName())) { + holder.outbox = true; + } + if (holder.messages == null) { + holder.messages = new ArrayList(); + } + try { + folder.open(Folder.OpenMode.READ_WRITE); + holder.unreadMessageCount = folder.getUnreadMessageCount(); + folder.close(false); + } + catch (MessagingException me) { + Log.e(k9.LOG_TAG, "Folder.getUnreadMessageCount() failed", me); + } + } + + Collections.sort(mFolders); + mHandler.dataChanged(); + + + /* + * We will do this eventually. This restores the state of the list in the + * case of a killed Activity but we have some message sync issues to take care of. + */ +// if (mRestoredState != null) { +// if (Config.LOGV) { +// Log.v(k9.LOG_TAG, "Attempting to restore list state"); +// } +// Parcelable listViewState = +// mListView.onRestoreInstanceState(mListViewState); +// mListViewState = null; +// } + + /* + * Now we need to refresh any folders that are currently expanded. We do this + * in case the status or amount of messages has changed. + */ + for (int i = 0, count = getGroupCount(); i < count; i++) { + if (mListView.isGroupExpanded(i)) { + final FolderInfoHolder folder = (FolderInfoHolder) mAdapter.getGroup(i); + new Thread(new FolderUpdateWorker(folder.name, mRefreshRemote)).start(); + } + } + mRefreshRemote = false; + } + + @Override + public void listLocalMessagesStarted(Account account, String folder) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(true); + mHandler.folderLoading(folder, true); + } + + @Override + public void listLocalMessagesFailed(Account account, String folder, String message) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + mHandler.folderLoading(folder, false); + } + + @Override + public void listLocalMessagesFinished(Account account, String folder) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + mHandler.folderLoading(folder, false); + } + + @Override + public void listLocalMessages(Account account, String folder, Message[] messages) { + if (!account.equals(mAccount)) { + return; + } + synchronizeMessages(folder, messages); + } + + @Override + public void synchronizeMailboxStarted( + Account account, + String folder) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(true); + mHandler.folderLoading(folder, true); + mHandler.folderStatus(folder, null); + } + + @Override + public void synchronizeMailboxFinished( + Account account, + String folder, + int totalMessagesInMailbox, + int numNewMessages) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + mHandler.folderLoading(folder, false); + mHandler.folderStatus(folder, null); + onRefresh(false); + } + + @Override + public void synchronizeMailboxFailed( + Account account, + String folder, + String message) { + if (!account.equals(mAccount)) { + return; + } + mHandler.progress(false); + mHandler.folderLoading(folder, false); + mHandler.folderStatus(folder, getString(R.string.status_network_error)); + FolderInfoHolder holder = getFolder(folder); + if (holder != null) { + /* + * Reset the last checked time to 0 so that the next expand will attempt to + * refresh this folder. + */ + holder.lastChecked = 0; + } + } + + @Override + public void synchronizeMailboxNewMessage( + Account account, + String folder, + Message message) { + if (!account.equals(mAccount)) { + return; + } + addOrUpdateMessage(folder, message); + } + + @Override + public void synchronizeMailboxRemovedMessage( + Account account, + String folder, + Message message) { + if (!account.equals(mAccount)) { + return; + } + removeMessage(folder, message.getUid()); + } + + @Override + public void emptyTrashCompleted(Account account) { + if (!account.equals(mAccount)) { + return; + } + onRefresh(false); + } + + @Override + public void sendPendingMessagesCompleted(Account account) { + if (!account.equals(mAccount)) { + return; + } + onRefresh(false); + } + + @Override + public void messageUidChanged( + Account account, + String folder, + String oldUid, + String newUid) { + if (mAccount.equals(account)) { + FolderInfoHolder holder = getFolder(folder); + if (folder != null) { + for (MessageInfoHolder message : holder.messages) { + if (message.uid.equals(oldUid)) { + message.uid = newUid; + message.message.setUid(newUid); + } + } + } + } + } + }; + + private Drawable mAttachmentIcon; + + FolderMessageListAdapter() { + mAttachmentIcon = getResources().getDrawable(R.drawable.ic_mms_attachment_small); + } + + public void removeMessage(String folder, String messageUid) { + FolderInfoHolder f = getFolder(folder); + if (f == null) { + return; + } + MessageInfoHolder m = getMessage(f, messageUid); + if (m == null) { + return; + } + mHandler.removeMessage(f, m); + } + + public void synchronizeMessages(String folder, Message[] messages) { + FolderInfoHolder f = getFolder(folder); + if (f == null) { + return; + } + mHandler.synchronizeMessages(f, messages); + } + + public void addOrUpdateMessage(String folder, Message message) { + addOrUpdateMessage(folder, message, true, true); + } + + private void addOrUpdateMessage(FolderInfoHolder folder, Message message, + boolean sort, boolean notify) { + MessageInfoHolder m = getMessage(folder, message.getUid()); + if (m == null) { + m = new MessageInfoHolder(message, folder); + folder.messages.add(m); + } + else { + m.populate(message, folder); + } + if (sort) { + Collections.sort(folder.messages); + } + if (notify) { + mHandler.dataChanged(); + } + } + + private void addOrUpdateMessage(String folder, Message message, + boolean sort, boolean notify) { + FolderInfoHolder f = getFolder(folder); + if (f == null) { + return; + } + addOrUpdateMessage(f, message, sort, notify); + } + + public MessageInfoHolder getMessage(FolderInfoHolder folder, String messageUid) { + for (MessageInfoHolder message : folder.messages) { + if (message.uid.equals(messageUid)) { + return message; + } + } + return null; + } + + public int getGroupCount() { + return mFolders.size(); + } + + public long getGroupId(int groupPosition) { + return groupPosition; + } + + public Object getGroup(int groupPosition) { + return mFolders.get(groupPosition); + } + + public FolderInfoHolder getFolder(String folder) { + FolderInfoHolder folderHolder = null; + for (int i = 0, count = getGroupCount(); i < count; i++) { + FolderInfoHolder holder = (FolderInfoHolder) getGroup(i); + if (holder.name.equals(folder)) { + folderHolder = holder; + } + } + return folderHolder; + } + + /** + * Gets the group position of the given folder or returns -1 if the folder is not + * found. + * @param folder + * @return + */ + public int getFolderPosition(String folder) { + for (int i = 0, count = getGroupCount(); i < count; i++) { + FolderInfoHolder holder = (FolderInfoHolder) getGroup(i); + if (holder.name.equals(folder)) { + return i; + } + } + return -1; + } + + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + FolderInfoHolder folder = (FolderInfoHolder) getGroup(groupPosition); + View view; + if (convertView != null) { + view = convertView; + } else { + view = mInflater.inflate(R.layout.folder_message_list_group, parent, false); + } + FolderViewHolder holder = (FolderViewHolder) view.getTag(); + if (holder == null) { + holder = new FolderViewHolder(); + holder.folderName = (TextView) view.findViewById(R.id.folder_name); + holder.newMessageCount = (TextView) view.findViewById(R.id.new_message_count); + holder.folderStatus = (TextView) view.findViewById(R.id.folder_status); + view.setTag(holder); + } + holder.folderName.setText(folder.displayName); + + if (folder.status == null) { + holder.folderStatus.setVisibility(View.GONE); + } + else { + holder.folderStatus.setText(folder.status); + holder.folderStatus.setVisibility(View.VISIBLE); + } + + if (folder.unreadMessageCount != 0) { + holder.newMessageCount.setText(Integer.toString(folder.unreadMessageCount)); + holder.newMessageCount.setVisibility(View.VISIBLE); + } + else { + holder.newMessageCount.setVisibility(View.GONE); + } + return view; + } + + public int getChildrenCount(int groupPosition) { + FolderInfoHolder folder = (FolderInfoHolder) getGroup(groupPosition); + return folder.messages.size() + 1; + } + + public long getChildId(int groupPosition, int childPosition) { + FolderInfoHolder folder = (FolderInfoHolder) getGroup(groupPosition); + if (childPosition < folder.messages.size()) { + MessageInfoHolder holder = folder.messages.get(childPosition); + return ((LocalStore.LocalMessage) holder.message).getId(); + } else { + return -1; + } + } + + public Object getChild(int groupPosition, int childPosition) { + FolderInfoHolder folder = (FolderInfoHolder) getGroup(groupPosition); + return folder.messages.get(childPosition); + } + + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + FolderInfoHolder folder = (FolderInfoHolder) getGroup(groupPosition); + if (isLastChild) { + View view; + if ((convertView != null) + && (convertView.getId() + == R.layout.folder_message_list_child_footer)) { + view = convertView; + } + else { + view = mInflater.inflate(R.layout.folder_message_list_child_footer, + parent, false); + view.setId(R.layout.folder_message_list_child_footer); + } + FooterViewHolder holder = (FooterViewHolder) view.getTag(); + if (holder == null) { + holder = new FooterViewHolder(); + holder.progress = (ProgressBar) view.findViewById(R.id.progress); + holder.main = (TextView) view.findViewById(R.id.main_text); + view.setTag(holder); + } + if (folder.loading) { + holder.main.setText(getString(R.string.status_loading_more)); + holder.progress.setVisibility(View.VISIBLE); + } + else { + if (folder.status == null) { + holder.main.setText(getString(R.string.message_list_load_more_messages_action)); + } + else { + holder.main.setText(getString(R.string.status_loading_more_failed)); + } + holder.progress.setVisibility(View.GONE); + } + return view; + } + else { + MessageInfoHolder message = + (MessageInfoHolder) getChild(groupPosition, childPosition); + View view; + if ((convertView != null) + && (convertView.getId() != R.layout.folder_message_list_child_footer)) { + view = convertView; + } else { + view = mInflater.inflate(R.layout.folder_message_list_child, parent, false); + } + MessageViewHolder holder = (MessageViewHolder) view.getTag(); + if (holder == null) { + holder = new MessageViewHolder(); + holder.subject = (TextView) view.findViewById(R.id.subject); + holder.from = (TextView) view.findViewById(R.id.from); + holder.date = (TextView) view.findViewById(R.id.date); + holder.chip = view.findViewById(R.id.chip); + /* + * TODO + * The line below and the commented lines a bit further down are work + * in progress for outbox status. They should not be removed. + */ +// holder.status = (TextView) view.findViewById(R.id.status); + + /* + * This will need to move to below if we ever convert this whole thing + * to a combined inbox. + */ + holder.chip.setBackgroundResource(colorChipResId); + + view.setTag(holder); + } + holder.chip.getBackground().setAlpha(message.read ? 0 : 255); + holder.subject.setText(message.subject); + holder.subject.setTypeface(null, message.read ? Typeface.NORMAL : Typeface.BOLD); + holder.from.setText(message.sender); + holder.from.setTypeface(null, message.read ? Typeface.NORMAL : Typeface.BOLD); + holder.date.setText(message.date); + holder.from.setCompoundDrawablesWithIntrinsicBounds(null, null, + message.hasAttachments ? mAttachmentIcon : null, null); +// if (folder.outbox) { +// holder.status.setText("Sending"); +// } +// else { +// holder.status.setText(""); +// } + return view; + } + } + + public boolean hasStableIds() { + return true; + } + + public boolean isChildSelectable(int groupPosition, int childPosition) { + return childPosition < getChildrenCount(groupPosition); + } + + public class FolderInfoHolder implements Comparable { + public String name; + public String displayName; + public ArrayList messages; + public long lastChecked; + public int unreadMessageCount; + public boolean loading; + public String status; + public boolean lastCheckFailed; + + /** + * Outbox is handled differently from any other folder. + */ + public boolean outbox; + + public int compareTo(FolderInfoHolder o) { + String s1 = this.name; + String s2 = o.name; + if (k9.INBOX.equalsIgnoreCase(s1)) { + return -1; + } else if (k9.INBOX.equalsIgnoreCase(s2)) { + return 1; + } else + return s1.toUpperCase().compareTo(s2.toUpperCase()); + } + } + + public class MessageInfoHolder implements Comparable { + public String subject; + public String date; + public Date compareDate; + public String sender; + public boolean hasAttachments; + public String uid; + public boolean read; + public Message message; + + public MessageInfoHolder(Message m, FolderInfoHolder folder) { + populate(m, folder); + } + + public void populate(Message m, FolderInfoHolder folder) { + try { + LocalMessage message = (LocalMessage) m; + Date date = message.getSentDate(); + this.compareDate = date; + if (Utility.isDateToday(date)) { + this.date = mTimeFormat.format(date); + } + else { + this.date = mDateFormat.format(date); + } + this.hasAttachments = message.getAttachmentCount() > 0; + this.read = message.isSet(Flag.SEEN); + if (folder.outbox) { + this.sender = Address.toFriendly( + message.getRecipients(RecipientType.TO)); + } + else { + this.sender = Address.toFriendly(message.getFrom()); + } + this.subject = message.getSubject(); + this.uid = message.getUid(); + this.message = m; + } + catch (MessagingException me) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "Unable to load message info", me); + } + } + } + + public int compareTo(MessageInfoHolder o) { + return this.compareDate.compareTo(o.compareDate) * -1; + } + } + + class FolderViewHolder { + public TextView folderName; + public TextView folderStatus; + public TextView newMessageCount; + } + + class MessageViewHolder { + public TextView subject; + public TextView preview; + public TextView from; + public TextView date; + public View chip; + } + + class FooterViewHolder { + public ProgressBar progress; + public TextView main; + } + } +} diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java new file mode 100644 index 000000000..5a4cf273a --- /dev/null +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -0,0 +1,1053 @@ + +package com.fsck.k9.activity; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.provider.OpenableColumns; +import android.text.TextWatcher; +import android.text.util.Rfc822Tokenizer; +import android.util.Config; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.view.View.OnFocusChangeListener; +import android.webkit.WebView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.MultiAutoCompleteTextView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AutoCompleteTextView.Validator; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.EmailAddressAdapter; +import com.fsck.k9.EmailAddressValidator; +import com.fsck.k9.MessagingController; +import com.fsck.k9.MessagingListener; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; + +public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener { + private static final String ACTION_REPLY = "com.fsck.k9.intent.action.REPLY"; + private static final String ACTION_REPLY_ALL = "com.fsck.k9.intent.action.REPLY_ALL"; + private static final String ACTION_FORWARD = "com.fsck.k9.intent.action.FORWARD"; + private static final String ACTION_EDIT_DRAFT = "com.fsck.k9.intent.action.EDIT_DRAFT"; + + private static final String EXTRA_ACCOUNT = "account"; + private static final String EXTRA_FOLDER = "folder"; + private static final String EXTRA_MESSAGE = "message"; + + private static final String STATE_KEY_ATTACHMENTS = + "com.fsck.k9.activity.MessageCompose.attachments"; + private static final String STATE_KEY_CC_SHOWN = + "com.fsck.k9.activity.MessageCompose.ccShown"; + private static final String STATE_KEY_BCC_SHOWN = + "com.fsck.k9.activity.MessageCompose.bccShown"; + private static final String STATE_KEY_QUOTED_TEXT_SHOWN = + "com.fsck.k9.activity.MessageCompose.quotedTextShown"; + private static final String STATE_KEY_SOURCE_MESSAGE_PROCED = + "com.fsck.k9.activity.MessageCompose.stateKeySourceMessageProced"; + private static final String STATE_KEY_DRAFT_UID = + "com.fsck.k9.activity.MessageCompose.draftUid"; + + private static final int MSG_PROGRESS_ON = 1; + private static final int MSG_PROGRESS_OFF = 2; + private static final int MSG_UPDATE_TITLE = 3; + private static final int MSG_SKIPPED_ATTACHMENTS = 4; + private static final int MSG_SAVED_DRAFT = 5; + private static final int MSG_DISCARDED_DRAFT = 6; + + private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1; + + private Account mAccount; + private String mFolder; + private String mSourceMessageUid; + private Message mSourceMessage; + /** + * Indicates that the source message has been processed at least once and should not + * be processed on any subsequent loads. This protects us from adding attachments that + * have already been added from the restore of the view state. + */ + private boolean mSourceMessageProcessed = false; + + private MultiAutoCompleteTextView mToView; + private MultiAutoCompleteTextView mCcView; + private MultiAutoCompleteTextView mBccView; + private EditText mSubjectView; + private EditText mMessageContentView; + private Button mSendButton; + private Button mDiscardButton; + private Button mSaveButton; + private LinearLayout mAttachments; + private View mQuotedTextBar; + private ImageButton mQuotedTextDelete; + private WebView mQuotedText; + + private boolean mDraftNeedsSaving = false; + + /** + * The draft uid of this message. This is used when saving drafts so that the same draft is + * overwritten instead of being created anew. This property is null until the first save. + */ + private String mDraftUid; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_PROGRESS_ON: + setProgressBarIndeterminateVisibility(true); + break; + case MSG_PROGRESS_OFF: + setProgressBarIndeterminateVisibility(false); + break; + case MSG_UPDATE_TITLE: + updateTitle(); + break; + case MSG_SKIPPED_ATTACHMENTS: + Toast.makeText( + MessageCompose.this, + getString(R.string.message_compose_attachments_skipped_toast), + Toast.LENGTH_LONG).show(); + break; + case MSG_SAVED_DRAFT: + Toast.makeText( + MessageCompose.this, + getString(R.string.message_saved_toast), + Toast.LENGTH_LONG).show(); + break; + case MSG_DISCARDED_DRAFT: + Toast.makeText( + MessageCompose.this, + getString(R.string.message_discarded_toast), + Toast.LENGTH_LONG).show(); + break; + default: + super.handleMessage(msg); + break; + } + } + }; + + private Listener mListener = new Listener(); + private EmailAddressAdapter mAddressAdapter; + private Validator mAddressValidator; + + + class Attachment implements Serializable { + public String name; + public String contentType; + public long size; + public Uri uri; + } + + /** + * Compose a new message using the given account. If account is null the default account + * will be used. + * @param context + * @param account + */ + public static void actionCompose(Context context, Account account) { + Intent i = new Intent(context, MessageCompose.class); + i.putExtra(EXTRA_ACCOUNT, account); + context.startActivity(i); + } + + /** + * Compose a new message as a reply to the given message. If replyAll is true the function + * is reply all instead of simply reply. + * @param context + * @param account + * @param message + * @param replyAll + */ + public static void actionReply( + Context context, + Account account, + Message message, + boolean replyAll) { + Intent i = new Intent(context, MessageCompose.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); + i.putExtra(EXTRA_MESSAGE, message.getUid()); + if (replyAll) { + i.setAction(ACTION_REPLY_ALL); + } + else { + i.setAction(ACTION_REPLY); + } + context.startActivity(i); + } + + /** + * Compose a new message as a forward of the given message. + * @param context + * @param account + * @param message + */ + public static void actionForward(Context context, Account account, Message message) { + Intent i = new Intent(context, MessageCompose.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); + i.putExtra(EXTRA_MESSAGE, message.getUid()); + i.setAction(ACTION_FORWARD); + context.startActivity(i); + } + + /** + * Continue composition of the given message. This action modifies the way this Activity + * handles certain actions. + * Save will attempt to replace the message in the given folder with the updated version. + * Discard will delete the message from the given folder. + * @param context + * @param account + * @param folder + * @param message + */ + public static void actionEditDraft(Context context, Account account, Message message) { + Intent i = new Intent(context, MessageCompose.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_FOLDER, message.getFolder().getName()); + i.putExtra(EXTRA_MESSAGE, message.getUid()); + i.setAction(ACTION_EDIT_DRAFT); + context.startActivity(i); + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.message_compose); + + mAddressAdapter = new EmailAddressAdapter(this); + mAddressValidator = new EmailAddressValidator(); + + mToView = (MultiAutoCompleteTextView)findViewById(R.id.to); + mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc); + mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc); + mSubjectView = (EditText)findViewById(R.id.subject); + mMessageContentView = (EditText)findViewById(R.id.message_content); + mAttachments = (LinearLayout)findViewById(R.id.attachments); + mQuotedTextBar = findViewById(R.id.quoted_text_bar); + mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete); + mQuotedText = (WebView)findViewById(R.id.quoted_text); + + TextWatcher watcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, + int before, int after) { } + + public void onTextChanged(CharSequence s, int start, + int before, int count) { + mDraftNeedsSaving = true; + } + + public void afterTextChanged(android.text.Editable s) { } + }; + + mToView.addTextChangedListener(watcher); + mCcView.addTextChangedListener(watcher); + mBccView.addTextChangedListener(watcher); + mSubjectView.addTextChangedListener(watcher); + mMessageContentView.addTextChangedListener(watcher); + + /* + * We set this to invisible by default. Other methods will turn it back on if it's + * needed. + */ + mQuotedTextBar.setVisibility(View.GONE); + mQuotedText.setVisibility(View.GONE); + + mQuotedTextDelete.setOnClickListener(this); + + mToView.setAdapter(mAddressAdapter); + mToView.setTokenizer(new Rfc822Tokenizer()); + mToView.setValidator(mAddressValidator); + + mCcView.setAdapter(mAddressAdapter); + mCcView.setTokenizer(new Rfc822Tokenizer()); + mCcView.setValidator(mAddressValidator); + + mBccView.setAdapter(mAddressAdapter); + mBccView.setTokenizer(new Rfc822Tokenizer()); + mBccView.setValidator(mAddressValidator); + + + mSubjectView.setOnFocusChangeListener(this); + + if (savedInstanceState != null) { + /* + * This data gets used in onCreate, so grab it here instead of onRestoreIntstanceState + */ + mSourceMessageProcessed = + savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false); + } + + Intent intent = getIntent(); + + String action = intent.getAction(); + + if (Intent.ACTION_VIEW.equals(action) || Intent.ACTION_SENDTO.equals(action)) { + /* + * Someone has clicked a mailto: link. The address is in the URI. + */ + mAccount = Preferences.getPreferences(this).getDefaultAccount(); + if (mAccount == null) { + /* + * There are no accounts set up. This should not have happened. Prompt the + * user to set up an account as an acceptable bailout. + */ + startActivity(new Intent(this, Accounts.class)); + mDraftNeedsSaving = false; + finish(); + return; + } + if (intent.getData() != null) { + Uri uri = intent.getData(); + try { + if (uri.getScheme().equalsIgnoreCase("mailto")) { + Address[] addresses = Address.parse(uri.getSchemeSpecificPart()); + addAddresses(mToView, addresses); + } + } + catch (Exception e) { + /* + * If we can't extract any information from the URI it's okay. They can + * still compose a message. + */ + } + } + } + else if (Intent.ACTION_SEND.equals(action)) { + /* + * Someone is trying to compose an email with an attachment, probably Pictures. + * The Intent should contain an EXTRA_STREAM with the data to attach. + */ + + mAccount = Preferences.getPreferences(this).getDefaultAccount(); + if (mAccount == null) { + /* + * There are no accounts set up. This should not have happened. Prompt the + * user to set up an account as an acceptable bailout. + */ + startActivity(new Intent(this, Accounts.class)); + mDraftNeedsSaving = false; + finish(); + return; + } + + String type = intent.getType(); + Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (stream != null && type != null) { + if (MimeUtility.mimeTypeMatches(type, k9.ACCEPTABLE_ATTACHMENT_SEND_TYPES)) { + addAttachment(stream); + } + } + } + else { + mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT); + mFolder = (String) intent.getStringExtra(EXTRA_FOLDER); + mSourceMessageUid = (String) intent.getStringExtra(EXTRA_MESSAGE); + } + + if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action) || + ACTION_FORWARD.equals(action) || ACTION_EDIT_DRAFT.equals(action)) { + /* + * If we need to load the message we add ourself as a message listener here + * so we can kick it off. Normally we add in onResume but we don't + * want to reload the message every time the activity is resumed. + * There is no harm in adding twice. + */ + MessagingController.getInstance(getApplication()).addListener(mListener); + MessagingController.getInstance(getApplication()).loadMessageForView( + mAccount, + mFolder, + mSourceMessageUid, + mListener); + } + + addAddress(mBccView, new Address(mAccount.getAlwaysBcc(), "")); + updateTitle(); + } + + public void onResume() { + super.onResume(); + MessagingController.getInstance(getApplication()).addListener(mListener); + } + + public void onPause() { + super.onPause(); + saveIfNeeded(); + MessagingController.getInstance(getApplication()).removeListener(mListener); + } + + /** + * The framework handles most of the fields, but we need to handle stuff that we + * dynamically show and hide: + * Attachment list, + * Cc field, + * Bcc field, + * Quoted text, + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + saveIfNeeded(); + ArrayList attachments = new ArrayList(); + for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { + View view = mAttachments.getChildAt(i); + Attachment attachment = (Attachment) view.getTag(); + attachments.add(attachment.uri); + } + outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments); + outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE); + outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE); + outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN, + mQuotedTextBar.getVisibility() == View.VISIBLE); + outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed); + outState.putString(STATE_KEY_DRAFT_UID, mDraftUid); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + ArrayList attachments = (ArrayList) + savedInstanceState.getParcelableArrayList(STATE_KEY_ATTACHMENTS); + mAttachments.removeAllViews(); + for (Parcelable p : attachments) { + Uri uri = (Uri) p; + addAttachment(uri); + } + + mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ? + View.VISIBLE : View.GONE); + mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ? + View.VISIBLE : View.GONE); + mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? + View.VISIBLE : View.GONE); + mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ? + View.VISIBLE : View.GONE); + mDraftUid = savedInstanceState.getString(STATE_KEY_DRAFT_UID); + mDraftNeedsSaving = false; + } + + private void updateTitle() { + if (mSubjectView.getText().length() == 0) { + setTitle(R.string.compose_title); + } else { + setTitle(mSubjectView.getText().toString()); + } + } + + public void onFocusChange(View view, boolean focused) { + if (!focused) { + updateTitle(); + } + } + + private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) { + if (addresses == null) { + return; + } + for (Address address : addresses) { + addAddress(view, address); + } + } + + private void addAddress(MultiAutoCompleteTextView view, Address address) { + view.append(address + ", "); + } + + private Address[] getAddresses(MultiAutoCompleteTextView view) { + Address[] addresses = Address.parse(view.getText().toString().trim()); + return addresses; + } + + private MimeMessage createMessage() throws MessagingException { + MimeMessage message = new MimeMessage(); + message.setSentDate(new Date()); + Address from = new Address(mAccount.getEmail(), mAccount.getName()); + message.setFrom(from); + message.setRecipients(RecipientType.TO, getAddresses(mToView)); + message.setRecipients(RecipientType.CC, getAddresses(mCcView)); + message.setRecipients(RecipientType.BCC, getAddresses(mBccView)); + message.setSubject(mSubjectView.getText().toString()); + // XXX TODO - not sure why this won't add header + // message.setHeader("X-User-Agent", getString(R.string.message_header_mua)); + + + + /* + * Build the Body that will contain the text of the message. We'll decide where to + * include it later. + */ + + String text = mMessageContentView.getText().toString(); + + if (mQuotedTextBar.getVisibility() == View.VISIBLE) { + String action = getIntent().getAction(); + String quotedText = null; + Part part = MimeUtility.findFirstPartByMimeType(mSourceMessage, + "text/plain"); + if (part != null) { + quotedText = MimeUtility.getTextFromPart(part); + } + if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) { + text += String.format( + getString(R.string.message_compose_reply_header_fmt), + Address.toString(mSourceMessage.getFrom())); + if (quotedText != null) { + text += quotedText.replaceAll("(?m)^", ">"); + } + } + else if (ACTION_FORWARD.equals(action)) { + text += String.format( + getString(R.string.message_compose_fwd_header_fmt), + mSourceMessage.getSubject(), + Address.toString(mSourceMessage.getFrom()), + Address.toString( + mSourceMessage.getRecipients(RecipientType.TO)), + Address.toString( + mSourceMessage.getRecipients(RecipientType.CC))); + if (quotedText != null) { + text += quotedText; + } + } + } + + + + text = appendSignature(text); + + + TextBody body = new TextBody(text); + + if (mAttachments.getChildCount() > 0) { + /* + * The message has attachments that need to be included. First we add the part + * containing the text that will be sent and then we include each attachment. + */ + + MimeMultipart mp; + + mp = new MimeMultipart(); + mp.addBodyPart(new MimeBodyPart(body, "text/plain")); + + for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { + Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); + MimeBodyPart bp = new MimeBodyPart( + new LocalStore.LocalAttachmentBody(attachment.uri, getApplication())); + bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", + attachment.contentType, + attachment.name)); + bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + String.format("attachment;\n filename=\"%s\"", + attachment.name)); + mp.addBodyPart(bp); + } + + message.setBody(mp); + } + else { + /* + * No attachments to include, just stick the text body in the message and call + * it good. + */ + message.setBody(body); + } + + return message; + } + + private String appendSignature (String text) { + String mSignature; + mSignature = mAccount.getSignature(); + + if (mSignature != null && ! mSignature.contentEquals("")){ + text += "\n-- \n" + mAccount.getSignature(); + } + + return text; + } + + + private void sendOrSaveMessage(boolean save) { + /* + * Create the message from all the data the user has entered. + */ + MimeMessage message; + try { + message = createMessage(); + } + catch (MessagingException me) { + Log.e(k9.LOG_TAG, "Failed to create new message for send or save.", me); + throw new RuntimeException("Failed to create a new message for send or save.", me); + } + + if (save) { + /* + * Save a draft + */ + if (mDraftUid != null) { + message.setUid(mDraftUid); + } + else if (ACTION_EDIT_DRAFT.equals(getIntent().getAction())) { + /* + * We're saving a previously saved draft, so update the new message's uid + * to the old message's uid. + */ + message.setUid(mSourceMessageUid); + } + MessagingController.getInstance(getApplication()).saveDraft(mAccount, message); + mDraftUid = message.getUid(); + + // Don't display the toast if the user is just changing the orientation + if ((getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) { + mHandler.sendEmptyMessage(MSG_SAVED_DRAFT); + } + } + else { + /* + * Send the message + * TODO Is it possible for us to be editing a draft with a null source message? Don't + * think so. Could probably remove below check. + */ + if (ACTION_EDIT_DRAFT.equals(getIntent().getAction()) + && mSourceMessageUid != null) { + /* + * We're sending a previously saved draft, so delete the old draft first. + */ + MessagingController.getInstance(getApplication()).deleteMessage( + mAccount, + mFolder, + mSourceMessage, + null); + } + MessagingController.getInstance(getApplication()).sendMessage(mAccount, message, null); + } + } + + private void saveIfNeeded() { + if (!mDraftNeedsSaving) { + return; + } + mDraftNeedsSaving = false; + sendOrSaveMessage(true); + } + + private void onSend() { + if (getAddresses(mToView).length == 0 && + getAddresses(mCcView).length == 0 && + getAddresses(mBccView).length == 0) { + mToView.setError(getString(R.string.message_compose_error_no_recipients)); + Toast.makeText(this, getString(R.string.message_compose_error_no_recipients), + Toast.LENGTH_LONG).show(); + return; + } + sendOrSaveMessage(false); + mDraftNeedsSaving = false; + finish(); + } + + private void onDiscard() { + if (mSourceMessageUid != null) { + if (ACTION_EDIT_DRAFT.equals(getIntent().getAction()) && mSourceMessageUid != null) { + MessagingController.getInstance(getApplication()).deleteMessage( + mAccount, + mFolder, + mSourceMessage, + null); + } + } + mHandler.sendEmptyMessage(MSG_DISCARDED_DRAFT); + mDraftNeedsSaving = false; + finish(); + } + + private void onSave() { + saveIfNeeded(); + finish(); + } + + private void onAddCcBcc() { + mCcView.setVisibility(View.VISIBLE); + mBccView.setVisibility(View.VISIBLE); + } + + /** + * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over. + */ + private void onAddAttachment() { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType(k9.ACCEPTABLE_ATTACHMENT_SEND_TYPES[0]); + startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_ATTACHMENT); + } + + private void addAttachment(Uri uri) { + addAttachment(uri, -1, null); + } + + private void addAttachment(Uri uri, int size, String name) { + ContentResolver contentResolver = getContentResolver(); + + String contentType = contentResolver.getType(uri); + + if (contentType == null) { + contentType = ""; + } + + Attachment attachment = new Attachment(); + attachment.name = name; + attachment.contentType = contentType; + attachment.size = size; + attachment.uri = uri; + + if (attachment.size == -1 || attachment.name == null) { + Cursor metadataCursor = contentResolver.query( + uri, + new String[]{ OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, + null, + null, + null); + if (metadataCursor != null) { + try { + if (metadataCursor.moveToFirst()) { + if (attachment.name == null) { + attachment.name = metadataCursor.getString(0); + } + if (attachment.size == -1) { + attachment.size = metadataCursor.getInt(1); + } + } + } finally { + metadataCursor.close(); + } + } + } + + if (attachment.name == null) { + attachment.name = uri.getLastPathSegment(); + } + + View view = getLayoutInflater().inflate( + R.layout.message_compose_attachment, + mAttachments, + false); + TextView nameView = (TextView)view.findViewById(R.id.attachment_name); + ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete); + nameView.setText(attachment.name); + delete.setOnClickListener(this); + delete.setTag(view); + view.setTag(attachment); + mAttachments.addView(view); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data == null) { + return; + } + addAttachment(data.getData()); + mDraftNeedsSaving = true; + } + + public void onClick(View view) { + switch (view.getId()) { + case R.id.attachment_delete: + /* + * The view is the delete button, and we have previously set the tag of + * the delete button to the view that owns it. We don't use parent because the + * view is very complex and could change in the future. + */ + mAttachments.removeView((View) view.getTag()); + mDraftNeedsSaving = true; + break; + case R.id.quoted_text_delete: + mQuotedTextBar.setVisibility(View.GONE); + mQuotedText.setVisibility(View.GONE); + mDraftNeedsSaving = true; + break; + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.send: + onSend(); + break; + case R.id.save: + onSave(); + break; + case R.id.discard: + onDiscard(); + break; + case R.id.add_cc_bcc: + onAddCcBcc(); + break; + case R.id.add_attachment: + onAddAttachment(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.message_compose_option, menu); + return true; + } + + /** + * Returns true if all attachments were able to be attached, otherwise returns false. + */ + private boolean loadAttachments(Part part, int depth) throws MessagingException { + if (part.getBody() instanceof Multipart) { + Multipart mp = (Multipart) part.getBody(); + boolean ret = true; + for (int i = 0, count = mp.getCount(); i < count; i++) { + if (!loadAttachments(mp.getBodyPart(i), depth + 1)) { + ret = false; + } + } + return ret; + } else { + String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); + String name = MimeUtility.getHeaderParameter(contentType, "name"); + if (name != null) { + Body body = part.getBody(); + if (body != null && body instanceof LocalAttachmentBody) { + final Uri uri = ((LocalAttachmentBody) body).getContentUri(); + mHandler.post(new Runnable() { + public void run() { + addAttachment(uri); + } + }); + } + else { + return false; + } + } + return true; + } + } + + /** + * Pull out the parts of the now loaded source message and apply them to the new message + * depending on the type of message being composed. + * @param message + */ + private void processSourceMessage(Message message) { + String action = getIntent().getAction(); + if (ACTION_REPLY.equals(action) || ACTION_REPLY_ALL.equals(action)) { + try { + if (message.getSubject() != null && + !message.getSubject().toLowerCase().startsWith("re:")) { + mSubjectView.setText("Re: " + message.getSubject()); + } + else { + mSubjectView.setText(message.getSubject()); + } + /* + * If a reply-to was included with the message use that, otherwise use the from + * or sender address. + */ + Address[] replyToAddresses; + if (message.getReplyTo().length > 0) { + addAddresses(mToView, replyToAddresses = message.getReplyTo()); + } + else { + addAddresses(mToView, replyToAddresses = message.getFrom()); + } + if (ACTION_REPLY_ALL.equals(action)) { + for (Address address : message.getRecipients(RecipientType.TO)) { + if (!address.getAddress().equalsIgnoreCase(mAccount.getEmail())) { + addAddress(mToView, address); + } + } + if (message.getRecipients(RecipientType.CC).length > 0) { + for (Address address : message.getRecipients(RecipientType.CC)) { + if (!Utility.arrayContains(replyToAddresses, address)) { + addAddress(mCcView, address); + } + } + mCcView.setVisibility(View.VISIBLE); + } + } + + Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + if (part == null) { + part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + } + if (part != null) { + String text = MimeUtility.getTextFromPart(part); + if (text != null) { + mQuotedTextBar.setVisibility(View.VISIBLE); + mQuotedText.setVisibility(View.VISIBLE); + mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(), + "utf-8", null); + } + } + } + catch (MessagingException me) { + /* + * This really should not happen at this point but if it does it's okay. + * The user can continue composing their message. + */ + } + } + else if (ACTION_FORWARD.equals(action)) { + try { + if (message.getSubject() != null && + !message.getSubject().toLowerCase().startsWith("fwd:")) { + mSubjectView.setText("Fwd: " + message.getSubject()); + } + else { + mSubjectView.setText(message.getSubject()); + } + + Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + if (part == null) { + part = MimeUtility.findFirstPartByMimeType(message, "text/html"); + } + if (part != null) { + String text = MimeUtility.getTextFromPart(part); + if (text != null) { + mQuotedTextBar.setVisibility(View.VISIBLE); + mQuotedText.setVisibility(View.VISIBLE); + mQuotedText.loadDataWithBaseURL("email://", text, part.getMimeType(), + "utf-8", null); + } + } + if (!mSourceMessageProcessed) { + if (!loadAttachments(message, 0)) { + mHandler.sendEmptyMessage(MSG_SKIPPED_ATTACHMENTS); + } + } + } + catch (MessagingException me) { + /* + * This really should not happen at this point but if it does it's okay. + * The user can continue composing their message. + */ + } + } + else if (ACTION_EDIT_DRAFT.equals(action)) { + try { + mSubjectView.setText(message.getSubject()); + addAddresses(mToView, message.getRecipients(RecipientType.TO)); + if (message.getRecipients(RecipientType.CC).length > 0) { + addAddresses(mCcView, message.getRecipients(RecipientType.CC)); + mCcView.setVisibility(View.VISIBLE); + } + if (message.getRecipients(RecipientType.BCC).length > 0) { + addAddresses(mBccView, message.getRecipients(RecipientType.BCC)); + mBccView.setVisibility(View.VISIBLE); + } + Part part = MimeUtility.findFirstPartByMimeType(message, "text/plain"); + if (part != null) { + String text = MimeUtility.getTextFromPart(part); + mMessageContentView.setText(text); + } + if (!mSourceMessageProcessed) { + loadAttachments(message, 0); + } + } + catch (MessagingException me) { + // TODO + } + } + mSourceMessageProcessed = true; + mDraftNeedsSaving = false; + } + + class Listener extends MessagingListener { + @Override + public void loadMessageForViewStarted(Account account, String folder, String uid) { + mHandler.sendEmptyMessage(MSG_PROGRESS_ON); + } + + @Override + public void loadMessageForViewFinished(Account account, String folder, String uid, + Message message) { + mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); + } + + @Override + public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, + final Message message) { + mSourceMessage = message; + runOnUiThread(new Runnable() { + public void run() { + processSourceMessage(message); + } + }); + } + + @Override + public void loadMessageForViewFailed(Account account, String folder, String uid, + final String message) { + mHandler.sendEmptyMessage(MSG_PROGRESS_OFF); + // TODO show network error + } + + @Override + public void messageUidChanged( + Account account, + String folder, + String oldUid, + String newUid) { + if (account.equals(mAccount) + && (folder.equals(mFolder) + || (mFolder == null + && folder.equals(mAccount.getDraftsFolderName())))) { + if (oldUid.equals(mDraftUid)) { + mDraftUid = newUid; + } + if (oldUid.equals(mSourceMessageUid)) { + mSourceMessageUid = newUid; + } + if (mSourceMessage != null && (oldUid.equals(mSourceMessage.getUid()))) { + mSourceMessage.setUid(newUid); + } + } + } + } +} diff --git a/src/com/fsck/k9/activity/MessageView.java b/src/com/fsck/k9/activity/MessageView.java new file mode 100644 index 000000000..532096648 --- /dev/null +++ b/src/com/fsck/k9/activity/MessageView.java @@ -0,0 +1,891 @@ + +package com.fsck.k9.activity; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; + +import org.apache.commons.io.IOUtils; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Process; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.util.Regex; +import android.text.util.Linkify; +import android.util.Config; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.view.View.OnClickListener; +import android.webkit.CacheManager; +import android.webkit.UrlInterceptHandler; +import android.webkit.WebView; +import android.webkit.CacheManager.CacheResult; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.MessagingController; +import com.fsck.k9.MessagingListener; +import com.fsck.k9.R; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody; +import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; +import com.fsck.k9.provider.AttachmentProvider; + +public class MessageView extends Activity + implements UrlInterceptHandler, OnClickListener { + private static final String EXTRA_ACCOUNT = "com.fsck.k9.MessageView_account"; + private static final String EXTRA_FOLDER = "com.fsck.k9.MessageView_folder"; + private static final String EXTRA_MESSAGE = "com.fsck.k9.MessageView_message"; + private static final String EXTRA_FOLDER_UIDS = "com.fsck.k9.MessageView_folderUids"; + private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next"; + + private TextView mFromView; + private TextView mDateView; + private TextView mToView; + private TextView mSubjectView; + private WebView mMessageContentView; + private LinearLayout mAttachments; + private View mAttachmentIcon; + private View mShowPicturesSection; + + private Account mAccount; + private String mFolder; + private String mMessageUid; + private ArrayList mFolderUids; + + private Message mMessage; + private String mNextMessageUid = null; + private String mPreviousMessageUid = null; + + private DateFormat mDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); + private DateFormat mTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT); + + private Listener mListener = new Listener(); + private MessageViewHandler mHandler = new MessageViewHandler(); + + + + + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: { onDelete(); return true;} + case KeyEvent.KEYCODE_F: { onForward(); return true;} + case KeyEvent.KEYCODE_A: { onReplyAll(); return true; } + case KeyEvent.KEYCODE_R: { onReply(); return true; } + case KeyEvent.KEYCODE_J: { onPrevious(); return true; } + case KeyEvent.KEYCODE_K: { onNext(); return true; } + } + return super.onKeyDown(keyCode, event); + } + + + + class MessageViewHandler extends Handler { + private static final int MSG_PROGRESS = 2; + private static final int MSG_ADD_ATTACHMENT = 3; + private static final int MSG_SET_ATTACHMENTS_ENABLED = 4; + private static final int MSG_SET_HEADERS = 5; + private static final int MSG_NETWORK_ERROR = 6; + private static final int MSG_ATTACHMENT_SAVED = 7; + private static final int MSG_ATTACHMENT_NOT_SAVED = 8; + private static final int MSG_SHOW_SHOW_PICTURES = 9; + private static final int MSG_FETCHING_ATTACHMENT = 10; + + @Override + public void handleMessage(android.os.Message msg) { + switch (msg.what) { + case MSG_PROGRESS: + setProgressBarIndeterminateVisibility(msg.arg1 != 0); + break; + case MSG_ADD_ATTACHMENT: + mAttachments.addView((View) msg.obj); + mAttachments.setVisibility(View.VISIBLE); + break; + case MSG_SET_ATTACHMENTS_ENABLED: + for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { + Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag(); + attachment.viewButton.setEnabled(msg.arg1 == 1); + attachment.downloadButton.setEnabled(msg.arg1 == 1); + } + break; + case MSG_SET_HEADERS: + String[] values = (String[]) msg.obj; + setTitle(values[0]); + mSubjectView.setText(values[0]); + mFromView.setText(values[1]); + mDateView.setText(values[2]); + mToView.setText(values[3]); + mAttachmentIcon.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE); + break; + case MSG_NETWORK_ERROR: + Toast.makeText(MessageView.this, + R.string.status_network_error, Toast.LENGTH_LONG).show(); + break; + case MSG_ATTACHMENT_SAVED: + Toast.makeText(MessageView.this, String.format( + getString(R.string.message_view_status_attachment_saved), msg.obj), + Toast.LENGTH_LONG).show(); + break; + case MSG_ATTACHMENT_NOT_SAVED: + Toast.makeText(MessageView.this, + getString(R.string.message_view_status_attachment_not_saved), + Toast.LENGTH_LONG).show(); + break; + case MSG_SHOW_SHOW_PICTURES: + mShowPicturesSection.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE); + break; + case MSG_FETCHING_ATTACHMENT: + Toast.makeText(MessageView.this, + getString(R.string.message_view_fetching_attachment_toast), + Toast.LENGTH_SHORT).show(); + break; + default: + super.handleMessage(msg); + } + } + + public void progress(boolean progress) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_PROGRESS; + msg.arg1 = progress ? 1 : 0; + sendMessage(msg); + } + + public void addAttachment(View attachmentView) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_ADD_ATTACHMENT; + msg.obj = attachmentView; + sendMessage(msg); + } + + public void setAttachmentsEnabled(boolean enabled) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_SET_ATTACHMENTS_ENABLED; + msg.arg1 = enabled ? 1 : 0; + sendMessage(msg); + } + + public void setHeaders( + String subject, + String from, + String date, + String to, + boolean hasAttachments) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_SET_HEADERS; + msg.arg1 = hasAttachments ? 1 : 0; + msg.obj = new String[] { subject, from, date, to }; + sendMessage(msg); + } + + public void networkError() { + sendEmptyMessage(MSG_NETWORK_ERROR); + } + + public void attachmentSaved(String filename) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_ATTACHMENT_SAVED; + msg.obj = filename; + sendMessage(msg); + } + + public void attachmentNotSaved() { + sendEmptyMessage(MSG_ATTACHMENT_NOT_SAVED); + } + + public void fetchingAttachment() { + sendEmptyMessage(MSG_FETCHING_ATTACHMENT); + } + + public void showShowPictures(boolean show) { + android.os.Message msg = new android.os.Message(); + msg.what = MSG_SHOW_SHOW_PICTURES; + msg.arg1 = show ? 1 : 0; + sendMessage(msg); + } + + + + } + + class Attachment { + public String name; + public String contentType; + public long size; + public LocalAttachmentBodyPart part; + public Button viewButton; + public Button downloadButton; + public ImageView iconView; + } + + public static void actionView(Context context, Account account, + String folder, String messageUid, ArrayList folderUids) { + actionView(context, account, folder, messageUid, folderUids, null); + } + + public static void actionView(Context context, Account account, + String folder, String messageUid, ArrayList folderUids, Bundle extras) { + Intent i = new Intent(context, MessageView.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_FOLDER, folder); + i.putExtra(EXTRA_MESSAGE, messageUid); + i.putExtra(EXTRA_FOLDER_UIDS, folderUids); + if (extras != null) { + i.putExtras(extras); + } + context.startActivity(i); + } + + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + setContentView(R.layout.message_view); + + mFromView = (TextView)findViewById(R.id.from); + mToView = (TextView)findViewById(R.id.to); + mSubjectView = (TextView)findViewById(R.id.subject); + mDateView = (TextView)findViewById(R.id.date); + mMessageContentView = (WebView)findViewById(R.id.message_content); + mAttachments = (LinearLayout)findViewById(R.id.attachments); + mAttachmentIcon = findViewById(R.id.attachment); + mShowPicturesSection = findViewById(R.id.show_pictures_section); + + mMessageContentView.setVerticalScrollBarEnabled(false); + mAttachments.setVisibility(View.GONE); + mAttachmentIcon.setVisibility(View.GONE); + + findViewById(R.id.reply).setOnClickListener(this); + findViewById(R.id.reply_all).setOnClickListener(this); + findViewById(R.id.delete).setOnClickListener(this); + findViewById(R.id.show_pictures).setOnClickListener(this); + + // UrlInterceptRegistry.registerHandler(this); + + mMessageContentView.getSettings().setBlockNetworkImage(true); + mMessageContentView.getSettings().setSupportZoom(false); + + setTitle(""); + + Intent intent = getIntent(); + mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT); + mFolder = intent.getStringExtra(EXTRA_FOLDER); + mMessageUid = intent.getStringExtra(EXTRA_MESSAGE); + mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS); + + View next = findViewById(R.id.next); + View previous = findViewById(R.id.previous); + /* + * Next and Previous Message are not shown in landscape mode, so + * we need to check before we use them. + */ + if (next != null && previous != null) { + next.setOnClickListener(this); + previous.setOnClickListener(this); + + findSurroundingMessagesUid(); + + previous.setVisibility(mPreviousMessageUid != null ? View.VISIBLE : View.GONE); + next.setVisibility(mNextMessageUid != null ? View.VISIBLE : View.GONE); + + boolean goNext = intent.getBooleanExtra(EXTRA_NEXT, false); + if (goNext) { + next.requestFocus(); + } + } + + MessagingController.getInstance(getApplication()).addListener(mListener); + new Thread() { + public void run() { + // TODO this is a spot that should be eventually handled by a MessagingController + // thread pool. We want it in a thread but it can't be blocked by the normal + // synchronization stuff in MC. + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + MessagingController.getInstance(getApplication()).loadMessageForView( + mAccount, + mFolder, + mMessageUid, + mListener); + } + }.start(); + } + + private void findSurroundingMessagesUid() { + for (int i = 0, count = mFolderUids.size(); i < count; i++) { + String messageUid = mFolderUids.get(i); + if (messageUid.equals(mMessageUid)) { + if (i != 0) { + mPreviousMessageUid = mFolderUids.get(i - 1); + } + + if (i != count - 1) { + mNextMessageUid = mFolderUids.get(i + 1); + } + break; + } + } + } + + public void onResume() { + super.onResume(); + MessagingController.getInstance(getApplication()).addListener(mListener); + } + + public void onPause() { + super.onPause(); + MessagingController.getInstance(getApplication()).removeListener(mListener); + } + + private void onDelete() { + if (mMessage != null) { + MessagingController.getInstance(getApplication()).deleteMessage( + mAccount, + mFolder, + mMessage, + null); + Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show(); + + // Remove this message's Uid locally + mFolderUids.remove(mMessage.getUid()); + // Check if we have previous/next messages available before choosing + // which one to display + findSurroundingMessagesUid(); + + if (mPreviousMessageUid != null) { + onPrevious(); + } else if (mNextMessageUid != null) { + onNext(); + } else { + finish(); + } + } + } + + private void onReply() { + if (mMessage != null) { + MessageCompose.actionReply(this, mAccount, mMessage, false); + finish(); + } + } + + private void onReplyAll() { + if (mMessage != null) { + MessageCompose.actionReply(this, mAccount, mMessage, true); + finish(); + } + } + + private void onForward() { + if (mMessage != null) { + MessageCompose.actionForward(this, mAccount, mMessage); + finish(); + } + } + + private void onNext() { + Bundle extras = new Bundle(1); + extras.putBoolean(EXTRA_NEXT, true); + MessageView.actionView(this, mAccount, mFolder, mNextMessageUid, mFolderUids, extras); + finish(); + } + + private void onPrevious() { + MessageView.actionView(this, mAccount, mFolder, mPreviousMessageUid, mFolderUids); + finish(); + } + + private void onMarkAsUnread() { + MessagingController.getInstance(getApplication()).markMessageRead( + mAccount, + mFolder, + mMessage.getUid(), + false); + } + + /** + * Creates a unique file in the given directory by appending a hyphen + * and a number to the given filename. + * @param directory + * @param filename + * @return + */ + private File createUniqueFile(File directory, String filename) { + File file = new File(directory, filename); + if (!file.exists()) { + return file; + } + // Get the extension of the file, if any. + int index = filename.lastIndexOf('.'); + String format; + if (index != -1) { + String name = filename.substring(0, index); + String extension = filename.substring(index); + format = name + "-%d" + extension; + } + else { + format = filename + "-%d"; + } + for (int i = 2; i < Integer.MAX_VALUE; i++) { + file = new File(directory, String.format(format, i)); + if (!file.exists()) { + return file; + } + } + return null; + } + + private void onDownloadAttachment(Attachment attachment) { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + /* + * Abort early if there's no place to save the attachment. We don't want to spend + * the time downloading it and then abort. + */ + Toast.makeText(this, + getString(R.string.message_view_status_attachment_not_saved), + Toast.LENGTH_SHORT).show(); + return; + } + MessagingController.getInstance(getApplication()).loadAttachment( + mAccount, + mMessage, + attachment.part, + new Object[] { true, attachment }, + mListener); + } + + private void onViewAttachment(Attachment attachment) { + MessagingController.getInstance(getApplication()).loadAttachment( + mAccount, + mMessage, + attachment.part, + new Object[] { false, attachment }, + mListener); + } + + private void onShowPictures() { + mMessageContentView.getSettings().setBlockNetworkImage(false); + mShowPicturesSection.setVisibility(View.GONE); + } + + public void onClick(View view) { + switch (view.getId()) { + case R.id.reply: + onReply(); + break; + case R.id.reply_all: + onReplyAll(); + break; + case R.id.delete: + onDelete(); + break; + case R.id.next: + onNext(); + break; + case R.id.previous: + onPrevious(); + break; + case R.id.download: + onDownloadAttachment((Attachment) view.getTag()); + break; + case R.id.view: + onViewAttachment((Attachment) view.getTag()); + break; + case R.id.show_pictures: + onShowPictures(); + break; + } + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.delete: + onDelete(); + break; + case R.id.reply: + onReply(); + break; + case R.id.reply_all: + onReplyAll(); + break; + case R.id.forward: + onForward(); + break; + case R.id.mark_as_unread: + onMarkAsUnread(); + break; + default: + return super.onOptionsItemSelected(item); + } + return true; + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.message_view_option, menu); + return true; + } + + public CacheResult service(String url, Map headers) { + String prefix = "http://cid/"; + if (url.startsWith(prefix)) { + try { + String contentId = url.substring(prefix.length()); + final Part part = MimeUtility.findPartByContentId(mMessage, "<" + contentId + ">"); + if (part != null) { + CacheResult cr = new CacheManager.CacheResult(); + // TODO looks fixed in Mainline, cr.setInputStream + // part.getBody().writeTo(cr.getStream()); + return cr; + } + } + catch (Exception e) { + // TODO + } + } + return null; + } + + private Bitmap getPreviewIcon(Attachment attachment) throws MessagingException { + try { + return BitmapFactory.decodeStream( + getContentResolver().openInputStream( + AttachmentProvider.getAttachmentThumbnailUri(mAccount, + attachment.part.getAttachmentId(), + 62, + 62))); + } + catch (Exception e) { + /* + * We don't care what happened, we just return null for the preview icon. + */ + return null; + } + } + + /* + * Formats the given size as a String in bytes, kB, MB or GB with a single digit + * of precision. Ex: 12,315,000 = 12.3 MB + */ + public static String formatSize(float size) { + long kb = 1024; + long mb = (kb * 1024); + long gb = (mb * 1024); + if (size < kb) { + return String.format("%d bytes", (int) size); + } + else if (size < mb) { + return String.format("%.1f kB", size / kb); + } + else if (size < gb) { + return String.format("%.1f MB", size / mb); + } + else { + return String.format("%.1f GB", size / gb); + } + } + + private void renderAttachments(Part part, int depth) throws MessagingException { + String contentType = MimeUtility.unfoldAndDecode(part.getContentType()); + String name = MimeUtility.getHeaderParameter(contentType, "name"); + if (name != null) { + /* + * We're guaranteed size because LocalStore.fetch puts it there. + */ + String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition()); + int size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size")); + + Attachment attachment = new Attachment(); + attachment.size = size; + attachment.contentType = part.getMimeType(); + attachment.name = name; + attachment.part = (LocalAttachmentBodyPart) part; + + LayoutInflater inflater = getLayoutInflater(); + View view = inflater.inflate(R.layout.message_view_attachment, null); + + TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name); + TextView attachmentInfo = (TextView)view.findViewById(R.id.attachment_info); + ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon); + Button attachmentView = (Button)view.findViewById(R.id.view); + Button attachmentDownload = (Button)view.findViewById(R.id.download); + + if ((!MimeUtility.mimeTypeMatches(attachment.contentType, + k9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES)) + || (MimeUtility.mimeTypeMatches(attachment.contentType, + k9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) { + attachmentView.setVisibility(View.GONE); + } + if ((!MimeUtility.mimeTypeMatches(attachment.contentType, + k9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES)) + || (MimeUtility.mimeTypeMatches(attachment.contentType, + k9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) { + attachmentDownload.setVisibility(View.GONE); + } + + if (attachment.size > k9.MAX_ATTACHMENT_DOWNLOAD_SIZE) { + attachmentView.setVisibility(View.GONE); + attachmentDownload.setVisibility(View.GONE); + } + + attachment.viewButton = attachmentView; + attachment.downloadButton = attachmentDownload; + attachment.iconView = attachmentIcon; + + view.setTag(attachment); + attachmentView.setOnClickListener(this); + attachmentView.setTag(attachment); + attachmentDownload.setOnClickListener(this); + attachmentDownload.setTag(attachment); + + attachmentName.setText(name); + attachmentInfo.setText(formatSize(size)); + + Bitmap previewIcon = getPreviewIcon(attachment); + if (previewIcon != null) { + attachmentIcon.setImageBitmap(previewIcon); + } + + mHandler.addAttachment(view); + } + + if (part.getBody() instanceof Multipart) { + Multipart mp = (Multipart)part.getBody(); + for (int i = 0; i < mp.getCount(); i++) { + renderAttachments(mp.getBodyPart(i), depth + 1); + } + } + } + + class Listener extends MessagingListener { + + @Override + public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid, + final Message message) { + MessageView.this.mMessage = message; + try { + String subjectText = message.getSubject(); + String fromText = Address.toFriendly(message.getFrom()); + String dateText = Utility.isDateToday(message.getSentDate()) ? + mTimeFormat.format(message.getSentDate()) : + mDateTimeFormat.format(message.getSentDate()); + String toText = Address.toFriendly(message.getRecipients(RecipientType.TO)); + boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0; + mHandler.setHeaders(subjectText, + fromText, + dateText, + toText, + hasAttachments); + } + catch (MessagingException me) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "loadMessageForViewHeadersAvailable", me); + } + } + } + + @Override + public void loadMessageForViewBodyAvailable(Account account, String folder, String uid, + Message message) { + SpannableString markup; + MessageView.this.mMessage = message; + try { + Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html"); + if (part == null) { + part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain"); + } + if (part != null) { + String text = MimeUtility.getTextFromPart(part); + if (part.getMimeType().equalsIgnoreCase("text/html")) { + text = text.replaceAll("cid:", "http://cid/"); + } else { + /* + * Convert plain text to HTML by replacing + * \r?\n with
and adding a html/body wrapper. + */ + text = text.replaceAll("\r?\n", "
"); + text = "" + text + ""; + } + + + + /* + * TODO this should be smarter, change to regex for img, but consider how to + * get backgroung images and a million other things that HTML allows. + */ + if (text.contains(" 0) { + mDefaultView.setVisibility(View.VISIBLE); + } + + if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { + mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT); + } + + if (savedInstanceState != null && savedInstanceState.containsKey(STATE_KEY_PROVIDER)) { + mProvider = (Provider)savedInstanceState.getSerializable(STATE_KEY_PROVIDER); + } + } + + @Override + public void onResume() { + super.onResume(); + validateFields(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(EXTRA_ACCOUNT, mAccount); + if (mProvider != null) { + outState.putSerializable(STATE_KEY_PROVIDER, mProvider); + } + } + + public void afterTextChanged(Editable s) { + validateFields(); + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + private void validateFields() { + boolean valid = Utility.requiredFieldValid(mEmailView) + && Utility.requiredFieldValid(mPasswordView) + && mEmailValidator.isValid(mEmailView.getText().toString()); + mNextButton.setEnabled(valid); + mManualSetupButton.setEnabled(valid); + /* + * Dim the next button's icon to 50% if the button is disabled. + * TODO this can probably be done with a stateful drawable. Check into it. + * android:state_enabled + */ + Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); + } + + private String getOwnerName() { + String name = null; + String projection[] = { + ContactMethods.NAME + }; + Cursor c = getContentResolver().query( + Uri.withAppendedPath(Contacts.People.CONTENT_URI, "owner"), projection, null, null, + null); + if (c.getCount() > 0) { + c.moveToFirst(); + name = c.getString(0); + c.close(); + } + + if (name == null || name.length() == 0) { + Account account = Preferences.getPreferences(this).getDefaultAccount(); + if (account != null) { + name = account.getName(); + } + } + return name; + } + + @Override + public Dialog onCreateDialog(int id) { + if (id == DIALOG_NOTE) { + if (mProvider != null && mProvider.note != null) { + return new AlertDialog.Builder(this) + .setMessage(mProvider.note) + .setPositiveButton( + getString(R.string.okay_action), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + finishAutoSetup(); + } + }) + .setNegativeButton( + getString(R.string.cancel_action), + null) + .create(); + } + } + return null; + } + + private void finishAutoSetup() { + String email = mEmailView.getText().toString(); + String password = mPasswordView.getText().toString(); + String[] emailParts = email.split("@"); + String user = emailParts[0]; + String domain = emailParts[1]; + URI incomingUri = null; + URI outgoingUri = null; + try { + String incomingUsername = mProvider.incomingUsernameTemplate; + incomingUsername = incomingUsername.replaceAll("\\$email", email); + incomingUsername = incomingUsername.replaceAll("\\$user", user); + incomingUsername = incomingUsername.replaceAll("\\$domain", domain); + + URI incomingUriTemplate = mProvider.incomingUriTemplate; + incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":" + + password, incomingUriTemplate.getHost(), incomingUriTemplate.getPort(), null, + null, null); + + String outgoingUsername = mProvider.outgoingUsernameTemplate; + outgoingUsername = outgoingUsername.replaceAll("\\$email", email); + outgoingUsername = outgoingUsername.replaceAll("\\$user", user); + outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain); + + URI outgoingUriTemplate = mProvider.outgoingUriTemplate; + outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":" + + password, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null, + null, null); + } catch (URISyntaxException use) { + /* + * If there is some problem with the URI we give up and go on to + * manual setup. + */ + onManualSetup(); + return; + } + + mAccount = new Account(this); + mAccount.setName(getOwnerName()); + mAccount.setEmail(email); + mAccount.setStoreUri(incomingUri.toString()); + mAccount.setTransportUri(outgoingUri.toString()); + mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts)); + mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash)); + mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox)); + mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent)); + if (incomingUri.toString().startsWith("imap")) { + mAccount.setDeletePolicy(Account.DELETE_POLICY_ON_DELETE); + } + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true); + } + + private void onNext() { + String email = mEmailView.getText().toString(); + String password = mPasswordView.getText().toString(); + String[] emailParts = email.split("@"); + String user = emailParts[0]; + String domain = emailParts[1]; + mProvider = findProviderForDomain(domain); + if (mProvider == null) { + /* + * We don't have default settings for this account, start the manual + * setup process. + */ + onManualSetup(); + return; + } + + if (mProvider.note != null) { + showDialog(DIALOG_NOTE); + } + else { + finishAutoSetup(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + mAccount.setDescription(mAccount.getEmail()); + mAccount.save(Preferences.getPreferences(this)); + if (mDefaultView.isChecked()) { + Preferences.getPreferences(this).setDefaultAccount(mAccount); + } + k9.setServicesEnabled(this); + AccountSetupNames.actionSetNames(this, mAccount); + finish(); + } + } + + private void onManualSetup() { + String email = mEmailView.getText().toString(); + String password = mPasswordView.getText().toString(); + String[] emailParts = email.split("@"); + String user = emailParts[0]; + String domain = emailParts[1]; + + mAccount = new Account(this); + mAccount.setName(getOwnerName()); + mAccount.setEmail(email); + try { + URI uri = new URI("placeholder", user + ":" + password, "mail." + domain, -1, null, + null, null); + mAccount.setStoreUri(uri.toString()); + mAccount.setTransportUri(uri.toString()); + } catch (URISyntaxException use) { + /* + * If we can't set up the URL we just continue. It's only for + * convenience. + */ + } + mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts)); + mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash)); + mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox)); + mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent)); + + AccountSetupAccountType.actionSelectAccountType(this, mAccount, mDefaultView.isChecked()); + finish(); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.next: + onNext(); + break; + case R.id.manual_setup: + onManualSetup(); + break; + } + } + + /** + * Attempts to get the given attribute as a String resource first, and if it fails + * returns the attribute as a simple String value. + * @param xml + * @param name + * @return + */ + private String getXmlAttribute(XmlResourceParser xml, String name) { + int resId = xml.getAttributeResourceValue(null, name, 0); + if (resId == 0) { + return xml.getAttributeValue(null, name); + } + else { + return getString(resId); + } + } + + private Provider findProviderForDomain(String domain) { + try { + XmlResourceParser xml = getResources().getXml(R.xml.providers); + int xmlEventType; + Provider provider = null; + while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) { + if (xmlEventType == XmlResourceParser.START_TAG + && "provider".equals(xml.getName()) + && domain.equalsIgnoreCase(getXmlAttribute(xml, "domain"))) { + provider = new Provider(); + provider.id = getXmlAttribute(xml, "id"); + provider.label = getXmlAttribute(xml, "label"); + provider.domain = getXmlAttribute(xml, "domain"); + provider.note = getXmlAttribute(xml, "note"); + } + else if (xmlEventType == XmlResourceParser.START_TAG + && "incoming".equals(xml.getName()) + && provider != null) { + provider.incomingUriTemplate = new URI(getXmlAttribute(xml, "uri")); + provider.incomingUsernameTemplate = getXmlAttribute(xml, "username"); + } + else if (xmlEventType == XmlResourceParser.START_TAG + && "outgoing".equals(xml.getName()) + && provider != null) { + provider.outgoingUriTemplate = new URI(getXmlAttribute(xml, "uri")); + provider.outgoingUsernameTemplate = getXmlAttribute(xml, "username"); + } + else if (xmlEventType == XmlResourceParser.END_TAG + && "provider".equals(xml.getName()) + && provider != null) { + return provider; + } + } + } + catch (Exception e) { + Log.e(k9.LOG_TAG, "Error while trying to load provider settings.", e); + } + return null; + } + + static class Provider implements Serializable { + private static final long serialVersionUID = 8511656164616538989L; + + public String id; + + public String label; + + public String domain; + + public URI incomingUriTemplate; + + public String incomingUsernameTemplate; + + public URI outgoingUriTemplate; + + public String outgoingUsernameTemplate; + + public String note; + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java b/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java new file mode 100644 index 000000000..ebc8a4b0a --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupCheckSettings.java @@ -0,0 +1,188 @@ + +package com.fsck.k9.activity.setup; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Process; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.fsck.k9.Account; +import com.fsck.k9.R; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.CertificateValidationException; + +/** + * Checks the given settings to make sure that they can be used to send and + * receive mail. + * + * XXX NOTE: The manifest for this app has it ignore config changes, because + * it doesn't correctly deal with restarting while its thread is running. + */ +public class AccountSetupCheckSettings extends Activity implements OnClickListener { + private static final String EXTRA_ACCOUNT = "account"; + + private static final String EXTRA_CHECK_INCOMING = "checkIncoming"; + + private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing"; + + private Handler mHandler = new Handler(); + + private ProgressBar mProgressBar; + + private TextView mMessageView; + + private Account mAccount; + + private boolean mCheckIncoming; + + private boolean mCheckOutgoing; + + private boolean mCanceled; + + private boolean mDestroyed; + + public static void actionCheckSettings(Activity context, Account account, + boolean checkIncoming, boolean checkOutgoing) { + Intent i = new Intent(context, AccountSetupCheckSettings.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming); + i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing); + context.startActivityForResult(i, 1); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.account_setup_check_settings); + mMessageView = (TextView)findViewById(R.id.message); + mProgressBar = (ProgressBar)findViewById(R.id.progress); + ((Button)findViewById(R.id.cancel)).setOnClickListener(this); + + setMessage(R.string.account_setup_check_settings_retr_info_msg); + mProgressBar.setIndeterminate(true); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + mCheckIncoming = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_INCOMING, false); + mCheckOutgoing = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_OUTGOING, false); + + new Thread() { + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + try { + if (mDestroyed) { + return; + } + if (mCanceled) { + finish(); + return; + } + if (mCheckIncoming) { + setMessage(R.string.account_setup_check_settings_check_incoming_msg); + Store store = Store.getInstance(mAccount.getStoreUri(), getApplication()); + store.checkSettings(); + } + if (mDestroyed) { + return; + } + if (mCanceled) { + finish(); + return; + } + if (mCheckOutgoing) { + setMessage(R.string.account_setup_check_settings_check_outgoing_msg); + Transport transport = Transport.getInstance(mAccount.getTransportUri()); + transport.close(); + transport.open(); + transport.close(); + } + if (mDestroyed) { + return; + } + if (mCanceled) { + finish(); + return; + } + setResult(RESULT_OK); + finish(); + } catch (final AuthenticationFailedException afe) { + showErrorDialog( + R.string.account_setup_failed_dlg_auth_message_fmt, + afe.getMessage() == null ? "" : afe.getMessage()); + } catch (final CertificateValidationException cve) { + showErrorDialog( + R.string.account_setup_failed_dlg_certificate_message_fmt, + cve.getMessage() == null ? "" : cve.getMessage()); + } catch (final MessagingException me) { + showErrorDialog( + R.string.account_setup_failed_dlg_server_message_fmt, + me.getMessage() == null ? "" : me.getMessage()); + } + } + }.start(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mDestroyed = true; + mCanceled = true; + } + + private void setMessage(final int resId) { + mHandler.post(new Runnable() { + public void run() { + if (mDestroyed) { + return; + } + mMessageView.setText(getString(resId)); + } + }); + } + + private void showErrorDialog(final int msgResId, final Object... args) { + mHandler.post(new Runnable() { + public void run() { + if (mDestroyed) { + return; + } + mProgressBar.setIndeterminate(false); + new AlertDialog.Builder(AccountSetupCheckSettings.this) + .setTitle(getString(R.string.account_setup_failed_dlg_title)) + .setMessage(getString(msgResId, args)) + .setCancelable(true) + .setPositiveButton( + getString(R.string.account_setup_failed_dlg_edit_details_action), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }) + .show(); + } + }); + } + + private void onCancel() { + mCanceled = true; + setMessage(R.string.account_setup_check_settings_canceling_msg); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.cancel: + onCancel(); + break; + } + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupComposition.java b/src/com/fsck/k9/activity/setup/AccountSetupComposition.java new file mode 100644 index 000000000..cb450b0a9 --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupComposition.java @@ -0,0 +1,108 @@ +package com.fsck.k9.activity.setup; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.KeyEvent; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.TextView; + +import com.fsck.k9.Account; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.k9; +import com.fsck.k9.Utility; + +public class AccountSetupComposition extends Activity { + + private static final String EXTRA_ACCOUNT = "account"; + + private Account mAccount; + + private EditText mAccountSignature; + private EditText mAccountEmail; + private EditText mAccountAlwaysBcc; + private EditText mAccountName; + + + + public static void actionEditCompositionSettings(Activity context, Account account) { + Intent i = new Intent(context, AccountSetupComposition.class); + i.setAction(Intent.ACTION_EDIT); + i.putExtra(EXTRA_ACCOUNT, account); + context.startActivity(i); + } + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + + setContentView(R.layout.account_setup_composition); + + /* + * If we're being reloaded we override the original account with the one + * we saved + */ + if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { + mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT); + } + + mAccountName = (EditText)findViewById(R.id.account_name); + mAccountName.setText(mAccount.getName()); + + mAccountEmail = (EditText)findViewById(R.id.account_email); + mAccountEmail.setText(mAccount.getEmail()); + + mAccountAlwaysBcc = (EditText)findViewById(R.id.account_always_bcc); + mAccountAlwaysBcc.setText(mAccount.getAlwaysBcc()); + + mAccountSignature = (EditText)findViewById(R.id.account_signature); + mAccountSignature.setText(mAccount.getSignature()); + + } + + @Override + public void onResume() { + super.onResume(); + mAccount.refresh(Preferences.getPreferences(this)); + } + + private void saveSettings() { + mAccount.setEmail(mAccountEmail.getText().toString()); + mAccount.setAlwaysBcc(mAccountAlwaysBcc.getText().toString()); + mAccount.setName(mAccountName.getText().toString()); + mAccount.setSignature(mAccountSignature.getText().toString()); + + mAccount.save(Preferences.getPreferences(this)); + + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + saveSettings(); + } + return super.onKeyDown(keyCode, event); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(EXTRA_ACCOUNT, mAccount); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + mAccount.save(Preferences.getPreferences(this)); + finish(); + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java new file mode 100644 index 000000000..d5ee5b029 --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -0,0 +1,325 @@ + +package com.fsck.k9.activity.setup; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; + +import com.fsck.k9.Account; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.Utility; + +public class AccountSetupIncoming extends Activity implements OnClickListener { + private static final String EXTRA_ACCOUNT = "account"; + private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; + + private static final int popPorts[] = { + 110, 995, 995, 110, 110 + }; + private static final String popSchemes[] = { + "pop3", "pop3+ssl", "pop3+ssl+", "pop3+tls", "pop3+tls+" + }; + private static final int imapPorts[] = { + 143, 993, 993, 143, 143 + }; + private static final String imapSchemes[] = { + "imap", "imap+ssl", "imap+ssl+", "imap+tls", "imap+tls+" + }; + + private int mAccountPorts[]; + private String mAccountSchemes[]; + private EditText mUsernameView; + private EditText mPasswordView; + private EditText mServerView; + private EditText mPortView; + private Spinner mSecurityTypeView; + private Spinner mDeletePolicyView; + private EditText mImapPathPrefixView; + private Button mNextButton; + private Account mAccount; + private boolean mMakeDefault; + + public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) { + Intent i = new Intent(context, AccountSetupIncoming.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault); + context.startActivity(i); + } + + public static void actionEditIncomingSettings(Activity context, Account account) { + Intent i = new Intent(context, AccountSetupIncoming.class); + i.setAction(Intent.ACTION_EDIT); + i.putExtra(EXTRA_ACCOUNT, account); + context.startActivity(i); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.account_setup_incoming); + + mUsernameView = (EditText)findViewById(R.id.account_username); + mPasswordView = (EditText)findViewById(R.id.account_password); + TextView serverLabelView = (TextView) findViewById(R.id.account_server_label); + mServerView = (EditText)findViewById(R.id.account_server); + mPortView = (EditText)findViewById(R.id.account_port); + mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type); + mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy); + mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix); + mNextButton = (Button)findViewById(R.id.next); + + mNextButton.setOnClickListener(this); + + SpinnerOption securityTypes[] = { + new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)), + new SpinnerOption(1, + getString(R.string.account_setup_incoming_security_ssl_optional_label)), + new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)), + new SpinnerOption(3, + getString(R.string.account_setup_incoming_security_tls_optional_label)), + new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)), + }; + + SpinnerOption deletePolicies[] = { + new SpinnerOption(0, + getString(R.string.account_setup_incoming_delete_policy_never_label)), + new SpinnerOption(1, + getString(R.string.account_setup_incoming_delete_policy_7days_label)), + new SpinnerOption(2, + getString(R.string.account_setup_incoming_delete_policy_delete_label)), + }; + + ArrayAdapter securityTypesAdapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, securityTypes); + securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSecurityTypeView.setAdapter(securityTypesAdapter); + + ArrayAdapter deletePoliciesAdapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, deletePolicies); + deletePoliciesAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mDeletePolicyView.setAdapter(deletePoliciesAdapter); + + /* + * Updates the port when the user changes the security type. This allows + * us to show a reasonable default which the user can change. + */ + mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + updatePortFromSecurityType(); + } + + public void onNothingSelected(AdapterView arg0) { + } + }); + + /* + * Calls validateFields() which enables or disables the Next button + * based on the fields' validity. + */ + TextWatcher validationTextWatcher = new TextWatcher() { + public void afterTextChanged(Editable s) { + validateFields(); + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + mUsernameView.addTextChangedListener(validationTextWatcher); + mPasswordView.addTextChangedListener(validationTextWatcher); + mServerView.addTextChangedListener(validationTextWatcher); + mPortView.addTextChangedListener(validationTextWatcher); + + /* + * Only allow digits in the port field. + */ + mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false); + + /* + * If we're being reloaded we override the original account with the one + * we saved + */ + if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { + mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT); + } + + try { + URI uri = new URI(mAccount.getStoreUri()); + String username = null; + String password = null; + if (uri.getUserInfo() != null) { + String[] userInfoParts = uri.getUserInfo().split(":", 2); + username = userInfoParts[0]; + if (userInfoParts.length > 1) { + password = userInfoParts[1]; + } + } + + if (username != null) { + mUsernameView.setText(username); + } + + if (password != null) { + mPasswordView.setText(password); + } + + if (uri.getScheme().startsWith("pop3")) { + serverLabelView.setText(R.string.account_setup_incoming_pop_server_label); + mAccountPorts = popPorts; + mAccountSchemes = popSchemes; + + findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE); + } else if (uri.getScheme().startsWith("imap")) { + serverLabelView.setText(R.string.account_setup_incoming_imap_server_label); + mAccountPorts = imapPorts; + mAccountSchemes = imapSchemes; + + findViewById(R.id.account_delete_policy_label).setVisibility(View.GONE); + mDeletePolicyView.setVisibility(View.GONE); + if (uri.getPath() != null && uri.getPath().length() > 0) { + mImapPathPrefixView.setText(uri.getPath().substring(1)); + } + } else { + throw new Error("Unknown account type: " + mAccount.getStoreUri()); + } + + for (int i = 0; i < mAccountSchemes.length; i++) { + if (mAccountSchemes[i].equals(uri.getScheme())) { + SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i); + } + } + + SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mAccount.getDeletePolicy()); + + if (uri.getHost() != null) { + mServerView.setText(uri.getHost()); + } + + if (uri.getPort() != -1) { + mPortView.setText(Integer.toString(uri.getPort())); + } else { + updatePortFromSecurityType(); + } + } catch (URISyntaxException use) { + /* + * We should always be able to parse our own settings. + */ + throw new Error(use); + } + + validateFields(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(EXTRA_ACCOUNT, mAccount); + } + + private void validateFields() { + mNextButton + .setEnabled(Utility.requiredFieldValid(mUsernameView) + && Utility.requiredFieldValid(mPasswordView) + && Utility.requiredFieldValid(mServerView) + && Utility.requiredFieldValid(mPortView)); + Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); + } + + private void updatePortFromSecurityType() { + int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; + mPortView.setText(Integer.toString(mAccountPorts[securityType])); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { + mAccount.save(Preferences.getPreferences(this)); + finish(); + } else { + /* + * Set the username and password for the outgoing settings to the username and + * password the user just set for incoming. + */ + try { + URI oldUri = new URI(mAccount.getTransportUri()); + URI uri = new URI( + oldUri.getScheme(), + mUsernameView.getText() + ":" + mPasswordView.getText(), + oldUri.getHost(), + oldUri.getPort(), + null, + null, + null); + mAccount.setTransportUri(uri.toString()); + } catch (URISyntaxException use) { + /* + * If we can't set up the URL we just continue. It's only for + * convenience. + */ + } + + + AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault); + finish(); + } + } + } + + private void onNext() { + int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; + try { + String path = null; + if (mAccountSchemes[securityType].startsWith("imap")) { + path = "/" + mImapPathPrefixView.getText(); + } + URI uri = new URI( + mAccountSchemes[securityType], + mUsernameView.getText() + ":" + mPasswordView.getText(), + mServerView.getText().toString(), + Integer.parseInt(mPortView.getText().toString()), + path, // path + null, // query + null); + mAccount.setStoreUri(uri.toString()); + } catch (URISyntaxException use) { + /* + * It's unrecoverable if we cannot create a URI from components that + * we validated to be safe. + */ + throw new Error(use); + } + + mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value); + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.next: + onNext(); + break; + } + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupNames.java b/src/com/fsck/k9/activity/setup/AccountSetupNames.java new file mode 100644 index 000000000..65706aaba --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupNames.java @@ -0,0 +1,103 @@ + +package com.fsck.k9.activity.setup; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.TextKeyListener; +import android.text.method.TextKeyListener.Capitalize; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.Utility; +import com.fsck.k9.activity.FolderMessageList; + +public class AccountSetupNames extends Activity implements OnClickListener { + private static final String EXTRA_ACCOUNT = "account"; + + private EditText mDescription; + + private EditText mName; + + private Account mAccount; + + private Button mDoneButton; + + public static void actionSetNames(Context context, Account account) { + Intent i = new Intent(context, AccountSetupNames.class); + i.putExtra(EXTRA_ACCOUNT, account); + context.startActivity(i); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.account_setup_names); + mDescription = (EditText)findViewById(R.id.account_description); + mName = (EditText)findViewById(R.id.account_name); + mDoneButton = (Button)findViewById(R.id.done); + mDoneButton.setOnClickListener(this); + + TextWatcher validationTextWatcher = new TextWatcher() { + public void afterTextChanged(Editable s) { + validateFields(); + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + mName.addTextChangedListener(validationTextWatcher); + + mName.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS)); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + + /* + * Since this field is considered optional, we don't set this here. If + * the user fills in a value we'll reset the current value, otherwise we + * just leave the saved value alone. + */ + // mDescription.setText(mAccount.getDescription()); + if (mAccount.getName() != null) { + mName.setText(mAccount.getName()); + } + if (!Utility.requiredFieldValid(mName)) { + mDoneButton.setEnabled(false); + } + } + + private void validateFields() { + mDoneButton.setEnabled(Utility.requiredFieldValid(mName)); + Utility.setCompoundDrawablesAlpha(mDoneButton, mDoneButton.isEnabled() ? 255 : 128); + } + + private void onNext() { + if (Utility.requiredFieldValid(mDescription)) { + mAccount.setDescription(mDescription.getText().toString()); + } + mAccount.setName(mName.getText().toString()); + mAccount.save(Preferences.getPreferences(this)); + FolderMessageList.actionHandleAccount(this, mAccount, k9.INBOX); + finish(); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.done: + onNext(); + break; + } + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupOptions.java b/src/com/fsck/k9/activity/setup/AccountSetupOptions.java new file mode 100644 index 000000000..ebaa74830 --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupOptions.java @@ -0,0 +1,103 @@ + +package com.fsck.k9.activity.setup; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.Spinner; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; + +public class AccountSetupOptions extends Activity implements OnClickListener { + private static final String EXTRA_ACCOUNT = "account"; + + private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; + + private Spinner mCheckFrequencyView; + + private CheckBox mDefaultView; + + private CheckBox mNotifyView; + + private Account mAccount; + + public static void actionOptions(Context context, Account account, boolean makeDefault) { + Intent i = new Intent(context, AccountSetupOptions.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault); + context.startActivity(i); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.account_setup_options); + + mCheckFrequencyView = (Spinner)findViewById(R.id.account_check_frequency); + mDefaultView = (CheckBox)findViewById(R.id.account_default); + mNotifyView = (CheckBox)findViewById(R.id.account_notify); + + findViewById(R.id.next).setOnClickListener(this); + + SpinnerOption checkFrequencies[] = { + new SpinnerOption(-1, + getString(R.string.account_setup_options_mail_check_frequency_never)), + new SpinnerOption(5, + getString(R.string.account_setup_options_mail_check_frequency_5min)), + new SpinnerOption(10, + getString(R.string.account_setup_options_mail_check_frequency_10min)), + new SpinnerOption(15, + getString(R.string.account_setup_options_mail_check_frequency_15min)), + new SpinnerOption(30, + getString(R.string.account_setup_options_mail_check_frequency_30min)), + new SpinnerOption(60, + getString(R.string.account_setup_options_mail_check_frequency_1hour)), + }; + + ArrayAdapter checkFrequenciesAdapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, checkFrequencies); + checkFrequenciesAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mCheckFrequencyView.setAdapter(checkFrequenciesAdapter); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + boolean makeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false); + + if (mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()) || makeDefault) { + mDefaultView.setChecked(true); + } + mNotifyView.setChecked(mAccount.isNotifyNewMail()); + SpinnerOption.setSpinnerOptionValue(mCheckFrequencyView, mAccount + .getAutomaticCheckIntervalMinutes()); + } + + private void onDone() { + mAccount.setDescription(mAccount.getEmail()); + mAccount.setNotifyNewMail(mNotifyView.isChecked()); + mAccount.setAutomaticCheckIntervalMinutes((Integer)((SpinnerOption)mCheckFrequencyView + .getSelectedItem()).value); + mAccount.save(Preferences.getPreferences(this)); + if (mDefaultView.isChecked()) { + Preferences.getPreferences(this).setDefaultAccount(mAccount); + } + k9.setServicesEnabled(this); + AccountSetupNames.actionSetNames(this, mAccount); + finish(); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.next: + onDone(); + break; + } + } +} diff --git a/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java b/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java new file mode 100644 index 000000000..dfc547070 --- /dev/null +++ b/src/com/fsck/k9/activity/setup/AccountSetupOutgoing.java @@ -0,0 +1,266 @@ + +package com.fsck.k9.activity.setup; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.CompoundButton.OnCheckedChangeListener; + +import com.fsck.k9.Account; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.Utility; + +public class AccountSetupOutgoing extends Activity implements OnClickListener, + OnCheckedChangeListener { + private static final String EXTRA_ACCOUNT = "account"; + + private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; + + private static final int smtpPorts[] = { + 25, 465, 465, 25, 25 + }; + + private static final String smtpSchemes[] = { + "smtp", "smtp+ssl", "smtp+ssl+", "smtp+tls", "smtp+tls+" + }; + + private EditText mUsernameView; + private EditText mPasswordView; + private EditText mServerView; + private EditText mPortView; + private CheckBox mRequireLoginView; + private ViewGroup mRequireLoginSettingsView; + private Spinner mSecurityTypeView; + private Button mNextButton; + private Account mAccount; + private boolean mMakeDefault; + + public static void actionOutgoingSettings(Context context, Account account, boolean makeDefault) { + Intent i = new Intent(context, AccountSetupOutgoing.class); + i.putExtra(EXTRA_ACCOUNT, account); + i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault); + context.startActivity(i); + } + + public static void actionEditOutgoingSettings(Context context, Account account) { + Intent i = new Intent(context, AccountSetupOutgoing.class); + i.setAction(Intent.ACTION_EDIT); + i.putExtra(EXTRA_ACCOUNT, account); + context.startActivity(i); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.account_setup_outgoing); + + mUsernameView = (EditText)findViewById(R.id.account_username); + mPasswordView = (EditText)findViewById(R.id.account_password); + mServerView = (EditText)findViewById(R.id.account_server); + mPortView = (EditText)findViewById(R.id.account_port); + mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login); + mRequireLoginSettingsView = (ViewGroup)findViewById(R.id.account_require_login_settings); + mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type); + mNextButton = (Button)findViewById(R.id.next); + + mNextButton.setOnClickListener(this); + mRequireLoginView.setOnCheckedChangeListener(this); + + SpinnerOption securityTypes[] = { + new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)), + new SpinnerOption(1, + getString(R.string.account_setup_incoming_security_ssl_optional_label)), + new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)), + new SpinnerOption(3, + getString(R.string.account_setup_incoming_security_tls_optional_label)), + new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)), + }; + + ArrayAdapter securityTypesAdapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, securityTypes); + securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSecurityTypeView.setAdapter(securityTypesAdapter); + + /* + * Updates the port when the user changes the security type. This allows + * us to show a reasonable default which the user can change. + */ + mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { + updatePortFromSecurityType(); + } + + public void onNothingSelected(AdapterView arg0) { + } + }); + + /* + * Calls validateFields() which enables or disables the Next button + * based on the fields' validity. + */ + TextWatcher validationTextWatcher = new TextWatcher() { + public void afterTextChanged(Editable s) { + validateFields(); + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + mUsernameView.addTextChangedListener(validationTextWatcher); + mPasswordView.addTextChangedListener(validationTextWatcher); + mServerView.addTextChangedListener(validationTextWatcher); + mPortView.addTextChangedListener(validationTextWatcher); + + /* + * Only allow digits in the port field. + */ + mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); + + mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT); + mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false); + + /* + * If we're being reloaded we override the original account with the one + * we saved + */ + if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { + mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT); + } + + try { + URI uri = new URI(mAccount.getTransportUri()); + String username = null; + String password = null; + if (uri.getUserInfo() != null) { + String[] userInfoParts = uri.getUserInfo().split(":", 2); + username = userInfoParts[0]; + if (userInfoParts.length > 1) { + password = userInfoParts[1]; + } + } + + if (username != null) { + mUsernameView.setText(username); + mRequireLoginView.setChecked(true); + } + + if (password != null) { + mPasswordView.setText(password); + } + + for (int i = 0; i < smtpSchemes.length; i++) { + if (smtpSchemes[i].equals(uri.getScheme())) { + SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i); + } + } + + if (uri.getHost() != null) { + mServerView.setText(uri.getHost()); + } + + if (uri.getPort() != -1) { + mPortView.setText(Integer.toString(uri.getPort())); + } else { + updatePortFromSecurityType(); + } + } catch (URISyntaxException use) { + /* + * We should always be able to parse our own settings. + */ + throw new Error(use); + } + + validateFields(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putSerializable(EXTRA_ACCOUNT, mAccount); + } + + private void validateFields() { + mNextButton + .setEnabled( + Utility.requiredFieldValid(mServerView) && + Utility.requiredFieldValid(mPortView) && + (!mRequireLoginView.isChecked() || + (Utility.requiredFieldValid(mUsernameView) && + Utility.requiredFieldValid(mPasswordView)))); + Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128); + } + + private void updatePortFromSecurityType() { + int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; + mPortView.setText(Integer.toString(smtpPorts[securityType])); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { + mAccount.save(Preferences.getPreferences(this)); + finish(); + } else { + AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault); + finish(); + } + } + } + + private void onNext() { + int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; + URI uri; + try { + String userInfo = null; + if (mRequireLoginView.isChecked()) { + userInfo = mUsernameView.getText().toString() + ":" + + mPasswordView.getText().toString(); + } + uri = new URI(smtpSchemes[securityType], userInfo, mServerView.getText().toString(), + Integer.parseInt(mPortView.getText().toString()), null, null, null); + mAccount.setTransportUri(uri.toString()); + } catch (URISyntaxException use) { + /* + * It's unrecoverable if we cannot create a URI from components that + * we validated to be safe. + */ + throw new Error(use); + } + AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true); + } + + public void onClick(View v) { + switch (v.getId()) { + case R.id.next: + onNext(); + break; + } + } + + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mRequireLoginSettingsView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + validateFields(); + } +} diff --git a/src/com/fsck/k9/activity/setup/SpinnerOption.java b/src/com/fsck/k9/activity/setup/SpinnerOption.java new file mode 100644 index 000000000..f97a95249 --- /dev/null +++ b/src/com/fsck/k9/activity/setup/SpinnerOption.java @@ -0,0 +1,33 @@ +/** + * + */ + +package com.fsck.k9.activity.setup; + +import android.widget.Spinner; + +public class SpinnerOption { + public Object value; + + public String label; + + public static void setSpinnerOptionValue(Spinner spinner, Object value) { + for (int i = 0, count = spinner.getCount(); i < count; i++) { + SpinnerOption so = (SpinnerOption)spinner.getItemAtPosition(i); + if (so.value.equals(value)) { + spinner.setSelection(i, true); + return; + } + } + } + + public SpinnerOption(Object value, String label) { + this.value = value; + this.label = label; + } + + @Override + public String toString() { + return label; + } +} diff --git a/src/com/fsck/k9/codec/binary/Base64.java b/src/com/fsck/k9/codec/binary/Base64.java new file mode 100644 index 000000000..ba5b03126 --- /dev/null +++ b/src/com/fsck/k9/codec/binary/Base64.java @@ -0,0 +1,788 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fsck.k9.codec.binary; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; + +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ * + * @see
RFC 2045 + * @author Apache Software Foundation + * @since 1.0-dev + * @version $Id$ + */ +public class Base64 implements BinaryEncoder, BinaryDecoder { + /** + * Chunk size per RFC 2045 section 6.8. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 2045 section 6.8 + */ + static final int CHUNK_SIZE = 76; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = {'\r','\n'}; + + /** + * This array is a lookup table that translates 6-bit positive integer + * index values into their "Base64 Alphabet" equivalents as specified + * in Table 1 of RFC 2045. + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] intToBase64 = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + /** + * Byte used to pad output. + */ + private static final byte PAD = '='; + + /** + * This array is a lookup table that translates unicode characters + * drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045) + * into their 6-bit positive integer equivalents. Characters that + * are not in the Base64 alphabet but fall within the bounds of the + * array are translated to -1. + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] base64ToInt = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }; + + /** Mask used to extract 6 bits, used when encoding */ + private static final int MASK_6BITS = 0x3f; + + /** Mask used to extract 8 bits, used in decoding base64 bytes */ + private static final int MASK_8BITS = 0xff; + + // The static final fields above are used for the original static byte[] methods on Base64. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + + /** + * Line length for encoding. Not used when decoding. A value of zero or less implies + * no chunking of the base64 encoded data. + */ + private final int lineLength; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run out of + * room and needs resizing. decodeSize = 3 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run out of + * room and needs resizing. encodeSize = 4 + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Buffer for streaming. + */ + private byte[] buf; + + /** + * Position where next character should be written in the buffer. + */ + private int pos; + + /** + * Position where next character should be read from the buffer. + */ + private int readPos; + + /** + * Variable tracks how many characters have been written to the current line. + * Only used when encoding. We use it to make sure each encoded line never + * goes beyond lineLength (if lineLength > 0). + */ + private int currentLinePos; + + /** + * Writes to the buffer only occur after every 3 reads when encoding, an + * every 4 reads when decoding. This variable helps track that. + */ + private int modulus; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been + * reached, this Base64 object becomes useless, and must be thrown away. + */ + private boolean eof; + + /** + * Place holder for the 3 bytes we're dealing with for our base64 logic. + * Bitwise operations store and extract the base64 encoding or decoding from + * this variable. + */ + private int x; + + /** + * Default constructor: lineLength is 76, and the lineSeparator is CRLF + * when encoding, and all forms can be decoded. + */ + public Base64() { + this(CHUNK_SIZE, CHUNK_SEPARATOR); + } + + /** + *

+ * Consumer can use this constructor to choose a different lineLength + * when encoding (lineSeparator is still CRLF). All forms of data can + * be decoded. + *

+ * Note: lineLengths that aren't multiples of 4 will still essentially + * end up being multiples of 4 in the encoded data. + *

+ * + * @param lineLength each line of encoded data will be at most this long + * (rounded up to nearest multiple of 4). + * If lineLength <= 0, then the output will not be divided into lines (chunks). + * Ignored when decoding. + */ + public Base64(int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + *

+ * Consumer can use this constructor to choose a different lineLength + * and lineSeparator when encoding. All forms of data can + * be decoded. + *

+ * Note: lineLengths that aren't multiples of 4 will still essentially + * end up being multiples of 4 in the encoded data. + *

+ * @param lineLength Each line of encoded data will be at most this long + * (rounded up to nearest multiple of 4). Ignored when decoding. + * If <= 0, then output will not be divided into lines (chunks). + * @param lineSeparator Each line of encoded data will end with this + * sequence of bytes. + * If lineLength <= 0, then the lineSeparator is not used. + * @throws IllegalArgumentException The provided lineSeparator included + * some base64 characters. That's not going to work! + */ + public Base64(int lineLength, byte[] lineSeparator) { + this.lineLength = lineLength; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); + if (lineLength > 0) { + this.encodeSize = 4 + lineSeparator.length; + } else { + this.encodeSize = 4; + } + this.decodeSize = encodeSize - 1; + if (containsBase64Byte(lineSeparator)) { + String sep; + try { + sep = new String(lineSeparator, "UTF-8"); + } catch (UnsupportedEncodingException uee) { + sep = new String(lineSeparator); + } + throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]"); + } + } + + /** + * Returns true if this Base64 object has buffered data for reading. + * + * @return true if there is Base64 object still available for reading. + */ + boolean hasData() { return buf != null; } + + /** + * Returns the amount of buffered data available for reading. + * + * @return The amount of buffered data available for reading. + */ + int avail() { return buf != null ? pos - readPos : 0; } + + /** Doubles our buffer. */ + private void resizeBuf() { + if (buf == null) { + buf = new byte[8192]; + pos = 0; + readPos = 0; + } else { + byte[] b = new byte[buf.length * 2]; + System.arraycopy(buf, 0, b, 0, buf.length); + buf = b; + } + } + + /** + * Extracts buffered data into the provided byte[] array, starting + * at position bPos, up to a maximum of bAvail bytes. Returns how + * many bytes were actually extracted. + * + * @param b byte[] array to extract the buffered data into. + * @param bPos position in byte[] array to start extraction at. + * @param bAvail amount of bytes we're allowed to extract. We may extract + * fewer (if fewer are available). + * @return The number of bytes successfully extracted into the provided + * byte[] array. + */ + int readResults(byte[] b, int bPos, int bAvail) { + if (buf != null) { + int len = Math.min(avail(), bAvail); + if (buf != b) { + System.arraycopy(buf, readPos, b, bPos, len); + readPos += len; + if (readPos >= pos) { + buf = null; + } + } else { + // Re-using the original consumer's output array is only + // allowed for one round. + buf = null; + } + return len; + } else { + return eof ? -1 : 0; + } + } + + /** + * Small optimization where we try to buffer directly to the consumer's + * output array for one round (if consumer calls this method first!) instead + * of starting our own buffer. + * + * @param out byte[] array to buffer directly to. + * @param outPos Position to start buffering into. + * @param outAvail Amount of bytes available for direct buffering. + */ + void setInitialBuffer(byte[] out, int outPos, int outAvail) { + // We can re-use consumer's original output array under + // special circumstances, saving on some System.arraycopy(). + if (out != null && out.length == outAvail) { + buf = out; + pos = outPos; + readPos = outPos; + } + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. + * Must be called at least twice: once with the data to encode, and once + * with inAvail set to "-1" to alert encoder that EOF has been reached, + * so flush last remaining bytes (if not multiple of 3). + *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, + * and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in byte[] array of binary data to base64 encode. + * @param inPos Position to start reading data from. + * @param inAvail Amount of bytes available from input for encoding. + */ + void encode(byte[] in, int inPos, int inAvail) { + if (eof) { + return; + } + + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + eof = true; + if (buf == null || buf.length - pos < encodeSize) { + resizeBuf(); + } + switch (modulus) { + case 1: + buf[pos++] = intToBase64[(x >> 2) & MASK_6BITS]; + buf[pos++] = intToBase64[(x << 4) & MASK_6BITS]; + buf[pos++] = PAD; + buf[pos++] = PAD; + break; + + case 2: + buf[pos++] = intToBase64[(x >> 10) & MASK_6BITS]; + buf[pos++] = intToBase64[(x >> 4) & MASK_6BITS]; + buf[pos++] = intToBase64[(x << 2) & MASK_6BITS]; + buf[pos++] = PAD; + break; + } + if (lineLength > 0) { + System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length); + pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + if (buf == null || buf.length - pos < encodeSize) { + resizeBuf(); + } + modulus = (++modulus) % 3; + int b = in[inPos++]; + if (b < 0) { b += 256; } + x = (x << 8) + b; + if (0 == modulus) { + buf[pos++] = intToBase64[(x >> 18) & MASK_6BITS]; + buf[pos++] = intToBase64[(x >> 12) & MASK_6BITS]; + buf[pos++] = intToBase64[(x >> 6) & MASK_6BITS]; + buf[pos++] = intToBase64[x & MASK_6BITS]; + currentLinePos += 4; + if (lineLength > 0 && lineLength <= currentLinePos) { + System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length); + pos += lineSeparator.length; + currentLinePos = 0; + } + } + } + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. + * Should be called at least twice: once with the data to decode, and once + * with inAvail set to "-1" to alert decoder that EOF has been reached. + * The "-1" call is not necessary when decoding, but it doesn't hurt, either. + *

+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) + * data is handled, since CR and LF are silently ignored, but has implications + * for other bytes, too. This method subscribes to the garbage-in, garbage-out + * philosophy: it will not check the provided data for validity. + *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, + * and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ + * @param in byte[] array of ascii data to base64 decode. + * @param inPos Position to start reading data from. + * @param inAvail Amount of bytes available from input for encoding. + */ + void decode(byte[] in, int inPos, int inAvail) { + if (eof) { + return; + } + if (inAvail < 0) { + eof = true; + } + for (int i = 0; i < inAvail; i++) { + if (buf == null || buf.length - pos < decodeSize) { + resizeBuf(); + } + byte b = in[inPos++]; + if (b == PAD) { + x = x << 6; + switch (modulus) { + case 2: + x = x << 6; + buf[pos++] = (byte) ((x >> 16) & MASK_8BITS); + break; + case 3: + buf[pos++] = (byte) ((x >> 16) & MASK_8BITS); + buf[pos++] = (byte) ((x >> 8) & MASK_8BITS); + break; + } + // WE'RE DONE!!!! + eof = true; + return; + } else { + if (b >= 0 && b < base64ToInt.length) { + int result = base64ToInt[b]; + if (result >= 0) { + modulus = (++modulus) % 4; + x = (x << 6) + result; + if (modulus == 0) { + buf[pos++] = (byte) ((x >> 16) & MASK_8BITS); + buf[pos++] = (byte) ((x >> 8) & MASK_8BITS); + buf[pos++] = (byte) (x & MASK_8BITS); + } + } + } + } + } + } + + /** + * Returns whether or not the octet is in the base 64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the base 64 alphabet, false otherwise. + */ + public static boolean isBase64(byte octet) { + return octet == PAD || (octet >= 0 && octet < base64ToInt.length && base64ToInt[octet] != -1); + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. + * Currently the method treats whitespace as valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is + * empty; false, otherwise + */ + public static boolean isArrayByteBase64(byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) { + return false; + } + } + return true; + } + + /* + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. + * + * @param arrayOctet + * byte array to test + * @return true if any byte is a valid character in the Base64 alphabet; false herwise + */ + private static boolean containsBase64Byte(byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (isBase64(arrayOctet[i])) { + return true; + } + } + return false; + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * @param binaryData + * binary data to encode + * @return Base64 characters + */ + public static byte[] encodeBase64(byte[] binaryData) { + return encodeBase64(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks + * + * @param binaryData + * binary data to encode + * @return Base64 characters chunked in 76 character blocks + */ + public static byte[] encodeBase64Chunked(byte[] binaryData) { + return encodeBase64(binaryData, true); + } + + /** + * Decodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the + * Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[]. + * + * @param pObject + * Object to decode + * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] supplied. + * @throws DecoderException + * if the parameter supplied is not of type byte[] + */ + public Object decode(Object pObject) throws DecoderException { + if (!(pObject instanceof byte[])) { + throw new DecoderException("Parameter supplied to Base64 decode is not a byte[]"); + } + return decode((byte[]) pObject); + } + + /** + * Decodes a byte[] containing containing characters in the Base64 alphabet. + * + * @param pArray + * A byte array containing Base64 character data + * @return a byte array containing binary data + */ + public byte[] decode(byte[] pArray) { + return decodeBase64(pArray); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + */ + public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + Base64 b64 = isChunked ? new Base64() : new Base64(0); + + long len = (binaryData.length * 4) / 3; + long mod = len % 4; + if (mod != 0) { + len += 4 - mod; + } + if (isChunked) { + len += (1 + (len / CHUNK_SIZE)) * CHUNK_SEPARATOR.length; + } + + if (len > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Input array too big, output array would be bigger than Integer.MAX_VALUE=" + Integer.MAX_VALUE); + } + byte[] buf = new byte[(int) len]; + b64.setInitialBuffer(buf, 0, buf.length); + b64.encode(binaryData, 0, binaryData.length); + b64.encode(binaryData, 0, -1); // Notify encoder of EOF. + + // Encoder might have resized, even though it was unnecessary. + if (b64.buf != buf) { + b64.readResults(buf, 0, buf.length); + } + return buf; + } + + /** + * Decodes Base64 data into octets + * + * @param base64Data Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(byte[] base64Data) { + if (base64Data == null || base64Data.length == 0) { + return base64Data; + } + Base64 b64 = new Base64(); + + long len = (base64Data.length * 3) / 4; + byte[] buf = new byte[(int) len]; + b64.setInitialBuffer(buf, 0, buf.length); + b64.decode(base64Data, 0, base64Data.length); + b64.decode(base64Data, 0, -1); // Notify decoder of EOF. + + // We have no idea what the line-length was, so we + // cannot know how much of our array wasn't used. + byte[] result = new byte[b64.pos]; + b64.readResults(result, 0, result.length); + return result; + } + + /** + * Discards any whitespace from a base-64 encoded block. + * + * @param data + * The base-64 encoded data to discard the whitespace from. + * @return The data, less whitespace (see RFC 2045). + * @deprecated This method is no longer needed + */ + static byte[] discardWhitespace(byte[] data) { + byte groomedData[] = new byte[data.length]; + int bytesCopied = 0; + + for (int i = 0; i < data.length; i++) { + switch (data[i]) { + case ' ' : + case '\n' : + case '\r' : + case '\t' : + break; + default : + groomedData[bytesCopied++] = data[i]; + } + } + + byte packedData[] = new byte[bytesCopied]; + + System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); + + return packedData; + } + + + /** + * Check if a byte value is whitespace or not. + * + * @param byteToCheck the byte to check + * @return true if byte is whitespace, false otherwise + */ + private static boolean isWhiteSpace(byte byteToCheck){ + switch (byteToCheck) { + case ' ' : + case '\n' : + case '\r' : + case '\t' : + return true; + default : + return false; + } + } + + /** + * Discards any characters outside of the base64 alphabet, per the requirements on page 25 of RFC 2045 - "Any + * characters outside of the base64 alphabet are to be ignored in base64 encoded data." + * + * @param data + * The base-64 encoded data to groom + * @return The data, less non-base64 characters (see RFC 2045). + */ + static byte[] discardNonBase64(byte[] data) { + byte groomedData[] = new byte[data.length]; + int bytesCopied = 0; + + for (int i = 0; i < data.length; i++) { + if (isBase64(data[i])) { + groomedData[bytesCopied++] = data[i]; + } + } + + byte packedData[] = new byte[bytesCopied]; + + System.arraycopy(groomedData, 0, packedData, 0, bytesCopied); + + return packedData; + } + + // Implementation of the Encoder Interface + + /** + * Encodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the + * Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[]. + * + * @param pObject + * Object to encode + * @return An object (of type byte[]) containing the base64 encoded data which corresponds to the byte[] supplied. + * @throws EncoderException + * if the parameter supplied is not of type byte[] + */ + public Object encode(Object pObject) throws EncoderException { + if (!(pObject instanceof byte[])) { + throw new EncoderException("Parameter supplied to Base64 encode is not a byte[]"); + } + return encode((byte[]) pObject); + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing characters in the Base64 alphabet. + * + * @param pArray + * a byte array containing binary data + * @return A byte array containing only Base64 character data + */ + public byte[] encode(byte[] pArray) { + return encodeBase64(pArray, false); + } + + // Implementation of integer encoding used for crypto + /** + * Decode a byte64-encoded integer according to crypto + * standards such as W3C's XML-Signature + * + * @param pArray a byte array containing base64 character data + * @return A BigInteger + */ + public static BigInteger decodeInteger(byte[] pArray) { + return new BigInteger(1, decodeBase64(pArray)); + } + + /** + * Encode to a byte64-encoded integer according to crypto + * standards such as W3C's XML-Signature + * + * @param bigInt a BigInteger + * @return A byte array containing base64 character data + * @throws NullPointerException if null is passed in + */ + public static byte[] encodeInteger(BigInteger bigInt) { + if(bigInt == null) { + throw new NullPointerException("encodeInteger called with null parameter"); + } + + return encodeBase64(toIntegerBytes(bigInt), false); + } + + /** + * Returns a byte-array representation of a BigInteger + * without sign bit. + * + * @param bigInt BigInteger to be converted + * @return a byte array representation of the BigInteger parameter + */ + static byte[] toIntegerBytes(BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + byte[] bigBytes = bigInt.toByteArray(); + + if(((bigInt.bitLength() % 8) != 0) && + (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + + int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + byte[] resizedBytes = new byte[bitlen / 8]; + + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + + return resizedBytes; + } +} diff --git a/src/com/fsck/k9/codec/binary/Base64OutputStream.java b/src/com/fsck/k9/codec/binary/Base64OutputStream.java new file mode 100644 index 000000000..f812cd368 --- /dev/null +++ b/src/com/fsck/k9/codec/binary/Base64OutputStream.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fsck.k9.codec.binary; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). + * When encoding the default lineLength is 76 characters and the default + * lineEnding is CRLF, but these can be overridden by using the appropriate + * constructor. + *

+ * The default behaviour of the Base64OutputStream is to ENCODE, whereas the + * default behaviour of the Base64InputStream is to DECODE. But this behaviour + * can be overridden by using a different constructor. + *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ * + * @author Apache Software Foundation + * @version $Id $ + * @see RFC 2045 + * @since 1.0-dev + */ +public class Base64OutputStream extends FilterOutputStream { + private final boolean doEncode; + private final Base64 base64; + private final byte[] singleByte = new byte[1]; + + /** + * Creates a Base64OutputStream such that all data written is Base64-encoded + * to the original provided OutputStream. + * + * @param out OutputStream to wrap. + */ + public Base64OutputStream(OutputStream out) { + this(out, true); + } + + /** + * Creates a Base64OutputStream such that all data written is either + * Base64-encoded or Base64-decoded to the original provided OutputStream. + * + * @param out OutputStream to wrap. + * @param doEncode true if we should encode all data written to us, + * false if we should decode. + */ + public Base64OutputStream(OutputStream out, boolean doEncode) { + super(out); + this.doEncode = doEncode; + this.base64 = new Base64(); + } + + /** + * Creates a Base64OutputStream such that all data written is either + * Base64-encoded or Base64-decoded to the original provided OutputStream. + * + * @param out OutputStream to wrap. + * @param doEncode true if we should encode all data written to us, + * false if we should decode. + * @param lineLength If doEncode is true, each line of encoded + * data will contain lineLength characters. + * If lineLength <=0, the encoded data is not divided into lines. + * If doEncode is false, lineLength is ignored. + * @param lineSeparator If doEncode is true, each line of encoded + * data will be terminated with this byte sequence (e.g. \r\n). + * If lineLength <= 0, the lineSeparator is not used. + * If doEncode is false lineSeparator is ignored. + */ + public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) { + super(out); + this.doEncode = doEncode; + this.base64 = new Base64(lineLength, lineSeparator); + } + + /** + * Writes the specified byte to this output stream. + */ + public void write(int i) throws IOException { + singleByte[0] = (byte) i; + write(singleByte, 0, 1); + } + + /** + * Writes len bytes from the specified + * b array starting at offset to + * this output stream. + * + * @param b source byte array + * @param offset where to start reading the bytes + * @param len maximum number of bytes to write + * + * @throws IOException if an I/O error occurs. + * @throws NullPointerException if the byte array parameter is null + * @throws IndexOutOfBoundsException if offset, len or buffer size are invalid + */ + public void write(byte b[], int offset, int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (offset < 0 || len < 0 || offset + len < 0) { + throw new IndexOutOfBoundsException(); + } else if (offset > b.length || offset + len > b.length) { + throw new IndexOutOfBoundsException(); + } else if (len > 0) { + if (doEncode) { + base64.encode(b, offset, len); + } else { + base64.decode(b, offset, len); + } + flush(false); + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be written out to the stream. If propogate is true, the wrapped + * stream will also be flushed. + * + * @param propogate boolean flag to indicate whether the wrapped + * OutputStream should also be flushed. + * @throws IOException if an I/O error occurs. + */ + private void flush(boolean propogate) throws IOException { + int avail = base64.avail(); + if (avail > 0) { + byte[] buf = new byte[avail]; + int c = base64.readResults(buf, 0, avail); + if (c > 0) { + out.write(buf, 0, c); + } + } + if (propogate) { + out.flush(); + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be written out to the stream. + * + * @throws IOException if an I/O error occurs. + */ + public void flush() throws IOException { + flush(true); + } + + /** + * Closes this output stream, flushing any remaining bytes that must be encoded. The + * underlying stream is flushed but not closed. + */ + public void close() throws IOException { + // Notify encoder of EOF (-1). + if (doEncode) { + base64.encode(singleByte, 0, -1); + } else { + base64.decode(singleByte, 0, -1); + } + flush(); + } + +} diff --git a/src/com/fsck/k9/k9.java b/src/com/fsck/k9/k9.java new file mode 100644 index 000000000..d53e42490 --- /dev/null +++ b/src/com/fsck/k9/k9.java @@ -0,0 +1,169 @@ + +package com.fsck.k9; + +import java.io.File; + +import android.app.Application; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.activity.MessageCompose; +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.service.BootReceiver; +import com.fsck.k9.service.MailService; + +public class k9 extends Application { + public static final String LOG_TAG = "k9"; + + public static File tempDirectory; + + /** + * If this is enabled there will be additional logging information sent to + * Log.d, including protocol dumps. + */ + public static boolean DEBUG = false; + + /** + * If this is enabled than logging that normally hides sensitive information + * like passwords will show that information. + */ + public static boolean DEBUG_SENSITIVE = false; + + + /** + * The MIME type(s) of attachments we're willing to send. At the moment it is not possible + * to open a chooser with a list of filter types, so the chooser is only opened with the first + * item in the list. The entire list will be used to filter down attachments that are added + * with Intent.ACTION_SEND. + */ + public static final String[] ACCEPTABLE_ATTACHMENT_SEND_TYPES = new String[] { + "image/*", + }; + + /** + * The MIME type(s) of attachments we're willing to view. + */ + public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { + "image/*", + "audio/*", + "text/*", + }; + + /** + * The MIME type(s) of attachments we're not willing to view. + */ + public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { + "image/gif", + }; + + /** + * The MIME type(s) of attachments we're willing to download to SD. + */ + public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] { + "image/*", + }; + + /** + * The MIME type(s) of attachments we're not willing to download to SD. + */ + public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] { + "image/gif", + }; + + /** + * The special name "INBOX" is used throughout the application to mean "Whatever folder + * the server refers to as the user's Inbox. Placed here to ease use. + */ + public static final String INBOX = "INBOX"; + + /** + * Specifies how many messages will be shown in a folder by default. This number is set + * on each new folder and can be incremented with "Load more messages..." by the + * VISIBLE_LIMIT_INCREMENT + */ + public static final int DEFAULT_VISIBLE_LIMIT = 25; + + /** + * Number of additional messages to load when a user selectes "Load more messages..." + */ + public static final int VISIBLE_LIMIT_INCREMENT = 25; + + /** + * The maximum size of an attachment we're willing to download (either View or Save) + * Attachments that are base64 encoded (most) will be about 1.375x their actual size + * so we should probably factor that in. A 5MB attachment will generally be around + * 6.8MB downloaded but only 5MB saved. + */ + public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024); + + /** + * Called throughout the application when the number of accounts has changed. This method + * enables or disables the Compose activity, the boot receiver and the service based on + * whether any accounts are configured. + */ + public static void setServicesEnabled(Context context) { + setServicesEnabled(context, Preferences.getPreferences(context).getAccounts().length > 0); + } + + public static void setServicesEnabled(Context context, boolean enabled) { + PackageManager pm = context.getPackageManager(); + if (!enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) == + PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + /* + * If no accounts now exist but the service is still enabled we're about to disable it + * so we'll reschedule to kill off any existing alarms. + */ + MailService.actionReschedule(context); + } + pm.setComponentEnabledSetting( + new ComponentName(context, MessageCompose.class), + enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + pm.setComponentEnabledSetting( + new ComponentName(context, BootReceiver.class), + enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + pm.setComponentEnabledSetting( + new ComponentName(context, MailService.class), + enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + if (enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) == + PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + /* + * And now if accounts do exist then we've just enabled the service and we want to + * schedule alarms for the new accounts. + */ + MailService.actionReschedule(context); + } + } + + @Override + public void onCreate() { + super.onCreate(); + Preferences prefs = Preferences.getPreferences(this); + DEBUG = prefs.geteEnableDebugLogging(); + DEBUG_SENSITIVE = prefs.getEnableSensitiveLogging(); + MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts()); + + /* + * We have to give MimeMessage a temp directory because File.createTempFile(String, String) + * doesn't work in Android and MimeMessage does not have access to a Context. + */ + BinaryTempFileBody.setTempDirectory(getCacheDir()); + } +} + + + + + + + + diff --git a/src/com/fsck/k9/mail/Address.java b/src/com/fsck/k9/mail/Address.java new file mode 100644 index 000000000..9b64631d8 --- /dev/null +++ b/src/com/fsck/k9/mail/Address.java @@ -0,0 +1,215 @@ + +package com.fsck.k9.mail; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.Mailbox; +import org.apache.james.mime4j.field.address.MailboxList; +import org.apache.james.mime4j.field.address.NamedMailbox; +import org.apache.james.mime4j.field.address.parser.ParseException; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.internet.MimeUtility; + +public class Address { + String mAddress; + + String mPersonal; + + public Address(String address, String personal) { + this.mAddress = address; + this.mPersonal = personal; + } + + public Address(String address) { + this.mAddress = address; + } + + public String getAddress() { + return mAddress; + } + + public void setAddress(String address) { + this.mAddress = address; + } + + public String getPersonal() { + return mPersonal; + } + + public void setPersonal(String personal) { + this.mPersonal = personal; + } + + /** + * Parse a comma separated list of addresses in RFC-822 format and return an + * array of Address objects. + * + * @param addressList + * @return An array of 0 or more Addresses. + */ + public static Address[] parse(String addressList) { + ArrayList
addresses = new ArrayList
(); + if (addressList == null) { + return new Address[] {}; + } + try { + MailboxList parsedList = AddressList.parse(addressList).flatten(); + for (int i = 0, count = parsedList.size(); i < count; i++) { + org.apache.james.mime4j.field.address.Address address = parsedList.get(i); + if (address instanceof NamedMailbox) { + NamedMailbox namedMailbox = (NamedMailbox)address; + addresses.add(new Address(namedMailbox.getLocalPart() + "@" + + namedMailbox.getDomain(), namedMailbox.getName())); + } else if (address instanceof Mailbox) { + Mailbox mailbox = (Mailbox)address; + addresses.add(new Address(mailbox.getLocalPart() + "@" + mailbox.getDomain())); + } else { + Log.e(k9.LOG_TAG, "Unknown address type from Mime4J: " + + address.getClass().toString()); + } + + } + } catch (ParseException pe) { + } + return addresses.toArray(new Address[] {}); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Address) { + return getAddress().equals(((Address) o).getAddress()); + } + return super.equals(o); + } + + public String toString() { + if (mPersonal != null) { + if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { + return Utility.quoteString(mPersonal) + " <" + mAddress + ">"; + } else { + return mPersonal + " <" + mAddress + ">"; + } + } else { + return mAddress; + } + } + + public static String toString(Address[] addresses) { + if (addresses == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < addresses.length; i++) { + sb.append(addresses[i].toString()); + if (i < addresses.length - 1) { + sb.append(','); + } + } + return sb.toString(); + } + + /** + * Returns either the personal portion of the Address or the address portion if the personal + * is not available. + * @return + */ + public String toFriendly() { + if (mPersonal != null && mPersonal.length() > 0) { + return mPersonal; + } + else { + return mAddress; + } + } + + public static String toFriendly(Address[] addresses) { + if (addresses == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < addresses.length; i++) { + sb.append(addresses[i].toFriendly()); + if (i < addresses.length - 1) { + sb.append(','); + } + } + return sb.toString(); + } + + /** + * Unpacks an address list previously packed with packAddressList() + * @param list + * @return + */ + public static Address[] unpack(String addressList) { + if (addressList == null) { + return new Address[] { }; + } + ArrayList
addresses = new ArrayList
(); + int length = addressList.length(); + int pairStartIndex = 0; + int pairEndIndex = 0; + int addressEndIndex = 0; + while (pairStartIndex < length) { + pairEndIndex = addressList.indexOf(',', pairStartIndex); + if (pairEndIndex == -1) { + pairEndIndex = length; + } + addressEndIndex = addressList.indexOf(';', pairStartIndex); + String address = null; + String personal = null; + if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) { + address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex)); + } + else { + address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex)); + personal = Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex)); + } + addresses.add(new Address(address, personal)); + pairStartIndex = pairEndIndex + 1; + } + return addresses.toArray(new Address[] { }); + } + + /** + * Packs an address list into a String that is very quick to read + * and parse. Packed lists can be unpacked with unpackAddressList() + * The packed list is a comma seperated list of: + * URLENCODE(address)[;URLENCODE(personal)] + * @param list + * @return + */ + public static String pack(Address[] addresses) { + if (addresses == null) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0, count = addresses.length; i < count; i++) { + Address address = addresses[i]; + try { + sb.append(URLEncoder.encode(address.getAddress(), "UTF-8")); + if (address.getPersonal() != null) { + sb.append(';'); + sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8")); + } + if (i < count - 1) { + sb.append(','); + } + } + catch (UnsupportedEncodingException uee) { + return null; + } + } + return sb.toString(); + } +} diff --git a/src/com/fsck/k9/mail/AuthenticationFailedException.java b/src/com/fsck/k9/mail/AuthenticationFailedException.java new file mode 100644 index 000000000..006063a14 --- /dev/null +++ b/src/com/fsck/k9/mail/AuthenticationFailedException.java @@ -0,0 +1,14 @@ + +package com.fsck.k9.mail; + +public class AuthenticationFailedException extends MessagingException { + public static final long serialVersionUID = -1; + + public AuthenticationFailedException(String message) { + super(message); + } + + public AuthenticationFailedException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/src/com/fsck/k9/mail/Body.java b/src/com/fsck/k9/mail/Body.java new file mode 100644 index 000000000..7deb2a1eb --- /dev/null +++ b/src/com/fsck/k9/mail/Body.java @@ -0,0 +1,11 @@ + +package com.fsck.k9.mail; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface Body { + public InputStream getInputStream() throws MessagingException; + public void writeTo(OutputStream out) throws IOException, MessagingException; +} diff --git a/src/com/fsck/k9/mail/BodyPart.java b/src/com/fsck/k9/mail/BodyPart.java new file mode 100644 index 000000000..e5f9375e6 --- /dev/null +++ b/src/com/fsck/k9/mail/BodyPart.java @@ -0,0 +1,10 @@ + +package com.fsck.k9.mail; + +public abstract class BodyPart implements Part { + protected Multipart mParent; + + public Multipart getParent() { + return mParent; + } +} diff --git a/src/com/fsck/k9/mail/CertificateValidationException.java b/src/com/fsck/k9/mail/CertificateValidationException.java new file mode 100644 index 000000000..ed35cf8ac --- /dev/null +++ b/src/com/fsck/k9/mail/CertificateValidationException.java @@ -0,0 +1,14 @@ + +package com.fsck.k9.mail; + +public class CertificateValidationException extends MessagingException { + public static final long serialVersionUID = -1; + + public CertificateValidationException(String message) { + super(message); + } + + public CertificateValidationException(String message, Throwable throwable) { + super(message, throwable); + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/mail/FetchProfile.java b/src/com/fsck/k9/mail/FetchProfile.java new file mode 100644 index 000000000..95bc44712 --- /dev/null +++ b/src/com/fsck/k9/mail/FetchProfile.java @@ -0,0 +1,57 @@ + +package com.fsck.k9.mail; + +import java.util.ArrayList; + +/** + *
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ *      FetchProfile.Item:      Described below.
+ *      Message:                Indicates that the body of the entire message should be fetched.
+ *                              Synonymous with FetchProfile.Item.BODY.
+ *      Part:                   Indicates that the given Part should be fetched. The provider
+ *                              is expected have previously created the given BodyPart and stored
+ *                              any information it needs to download the content.
+ * 
+ */ +public class FetchProfile extends ArrayList { + /** + * Default items available for pre-fetching. It should be expected that any + * item fetched by using these items could potentially include all of the + * previous items. + */ + public enum Item { + /** + * Download the flags of the message. + */ + FLAGS, + + /** + * Download the envelope of the message. This should include at minimum + * the size and the following headers: date, subject, from, content-type, to, cc + */ + ENVELOPE, + + /** + * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE + * and may map to other providers. + * The provider should, if possible, fill in a properly formatted MIME structure in + * the message without actually downloading any message data. If the provider is not + * capable of this operation it should specifically set the body of the message to null + * so that upper levels can detect that a full body download is needed. + */ + STRUCTURE, + + /** + * A sane portion of the entire message, cut off at a provider determined limit. + * This should generaly be around 50kB. + */ + BODY_SANE, + + /** + * The entire message. + */ + BODY, + } +} diff --git a/src/com/fsck/k9/mail/Flag.java b/src/com/fsck/k9/mail/Flag.java new file mode 100644 index 000000000..ec898a700 --- /dev/null +++ b/src/com/fsck/k9/mail/Flag.java @@ -0,0 +1,48 @@ + +package com.fsck.k9.mail; + +/** + * Flags that can be applied to Messages. + */ +public enum Flag { + DELETED, + SEEN, + ANSWERED, + FLAGGED, + DRAFT, + RECENT, + + /* + * The following flags are for internal library use only. + * TODO Eventually we should creates a Flags class that extends ArrayList that allows + * these flags and Strings to represent user defined flags. At that point the below + * flags should become user defined flags. + */ + /** + * Delete and remove from the LocalStore immediately. + */ + X_DESTROYED, + + /** + * Sending of an unsent message failed. It will be retried. Used to show status. + */ + X_SEND_FAILED, + + /** + * Sending of an unsent message is in progress. + */ + X_SEND_IN_PROGRESS, + + /** + * Indicates that a message is fully downloaded from the server and can be viewed normally. + * This does not include attachments, which are never downloaded fully. + */ + X_DOWNLOADED_FULL, + + /** + * Indicates that a message is partially downloaded from the server and can be viewed but + * more content is available on the server. + * This does not include attachments, which are never downloaded fully. + */ + X_DOWNLOADED_PARTIAL, +} diff --git a/src/com/fsck/k9/mail/Folder.java b/src/com/fsck/k9/mail/Folder.java new file mode 100644 index 000000000..2f951e336 --- /dev/null +++ b/src/com/fsck/k9/mail/Folder.java @@ -0,0 +1,95 @@ +package com.fsck.k9.mail; + + +public abstract class Folder { + public enum OpenMode { + READ_WRITE, READ_ONLY, + } + + public enum FolderType { + HOLDS_FOLDERS, HOLDS_MESSAGES, + } + + /** + * Forces an open of the MailProvider. If the provider is already open this + * function returns without doing anything. + * + * @param mode READ_ONLY or READ_WRITE + */ + public abstract void open(OpenMode mode) throws MessagingException; + + /** + * Forces a close of the MailProvider. Any further access will attempt to + * reopen the MailProvider. + * + * @param expunge If true all deleted messages will be expunged. + */ + public abstract void close(boolean expunge) throws MessagingException; + + /** + * @return True if further commands are not expected to have to open the + * connection. + */ + public abstract boolean isOpen(); + + /** + * Get the mode the folder was opened with. This may be different than the mode the open + * was requested with. + * @return + */ + public abstract OpenMode getMode() throws MessagingException; + + public abstract boolean create(FolderType type) throws MessagingException; + + public abstract boolean exists() throws MessagingException; + + /** + * @return A count of the messages in the selected folder. + */ + public abstract int getMessageCount() throws MessagingException; + + public abstract int getUnreadMessageCount() throws MessagingException; + + public abstract Message getMessage(String uid) throws MessagingException; + + public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener) + throws MessagingException; + + /** + * Fetches the given list of messages. The specified listener is notified as + * each fetch completes. Messages are downloaded as (as) lightweight (as + * possible) objects to be filled in with later requests. In most cases this + * means that only the UID is downloaded. + * + * @param uids + * @param listener + */ + public abstract Message[] getMessages(MessageRetrievalListener listener) + throws MessagingException; + + public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) + throws MessagingException; + + public abstract void appendMessages(Message[] messages) throws MessagingException; + + public abstract void copyMessages(Message[] msgs, Folder folder) throws MessagingException; + + public abstract void setFlags(Message[] messages, Flag[] flags, boolean value) + throws MessagingException; + + public abstract Message[] expunge() throws MessagingException; + + public abstract void fetch(Message[] messages, FetchProfile fp, + MessageRetrievalListener listener) throws MessagingException; + + public abstract void delete(boolean recurse) throws MessagingException; + + public abstract String getName(); + + public abstract Flag[] getPermanentFlags() throws MessagingException; + + @Override + public String toString() { + return getName(); + } +} diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java new file mode 100644 index 000000000..58f036304 --- /dev/null +++ b/src/com/fsck/k9/mail/Message.java @@ -0,0 +1,118 @@ + +package com.fsck.k9.mail; + +import java.util.Date; +import java.util.HashSet; + +public abstract class Message implements Part, Body { + public enum RecipientType { + TO, CC, BCC, + } + + protected String mUid; + + protected HashSet mFlags = new HashSet(); + + protected Date mInternalDate; + + protected Folder mFolder; + + public String getUid() { + return mUid; + } + + public void setUid(String uid) { + this.mUid = uid; + } + + public Folder getFolder() { + return mFolder; + } + + public abstract String getSubject() throws MessagingException; + + public abstract void setSubject(String subject) throws MessagingException; + + public Date getInternalDate() { + return mInternalDate; + } + + public void setInternalDate(Date internalDate) { + this.mInternalDate = internalDate; + } + + public abstract Date getReceivedDate() throws MessagingException; + + public abstract Date getSentDate() throws MessagingException; + + public abstract void setSentDate(Date sentDate) throws MessagingException; + + public abstract Address[] getRecipients(RecipientType type) throws MessagingException; + + public abstract void setRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + public void setRecipient(RecipientType type, Address address) throws MessagingException { + setRecipients(type, new Address[] { + address + }); + } + + public abstract Address[] getFrom() throws MessagingException; + + public abstract void setFrom(Address from) throws MessagingException; + + public abstract Address[] getReplyTo() throws MessagingException; + + public abstract void setReplyTo(Address[] from) throws MessagingException; + + public abstract Body getBody() throws MessagingException; + + public abstract String getContentType() throws MessagingException; + + public abstract void addHeader(String name, String value) throws MessagingException; + + public abstract void setHeader(String name, String value) throws MessagingException; + + public abstract String[] getHeader(String name) throws MessagingException; + + public abstract void removeHeader(String name) throws MessagingException; + + public abstract void setBody(Body body) throws MessagingException; + + public boolean isMimeType(String mimeType) throws MessagingException { + return getContentType().startsWith(mimeType); + } + + /* + * TODO Refactor Flags at some point to be able to store user defined flags. + */ + public Flag[] getFlags() { + return mFlags.toArray(new Flag[] {}); + } + + public void setFlag(Flag flag, boolean set) throws MessagingException { + if (set) { + mFlags.add(flag); + } else { + mFlags.remove(flag); + } + } + + /** + * This method calls setFlag(Flag, boolean) + * @param flags + * @param set + */ + public void setFlags(Flag[] flags, boolean set) throws MessagingException { + for (Flag flag : flags) { + setFlag(flag, set); + } + } + + public boolean isSet(Flag flag) { + return mFlags.contains(flag); + } + + public abstract void saveChanges() throws MessagingException; +} diff --git a/src/com/fsck/k9/mail/MessageDateComparator.java b/src/com/fsck/k9/mail/MessageDateComparator.java new file mode 100644 index 000000000..2bde7af0b --- /dev/null +++ b/src/com/fsck/k9/mail/MessageDateComparator.java @@ -0,0 +1,19 @@ + +package com.fsck.k9.mail; + +import java.util.Comparator; + +public class MessageDateComparator implements Comparator { + public int compare(Message o1, Message o2) { + try { + if (o1.getSentDate() == null) { + return 1; + } else if (o2.getSentDate() == null) { + return -1; + } else + return o2.getSentDate().compareTo(o1.getSentDate()); + } catch (Exception e) { + return 0; + } + } +} diff --git a/src/com/fsck/k9/mail/MessageRetrievalListener.java b/src/com/fsck/k9/mail/MessageRetrievalListener.java new file mode 100644 index 000000000..2a1446211 --- /dev/null +++ b/src/com/fsck/k9/mail/MessageRetrievalListener.java @@ -0,0 +1,8 @@ + +package com.fsck.k9.mail; + +public interface MessageRetrievalListener { + public void messageStarted(String uid, int number, int ofTotal); + + public void messageFinished(Message message, int number, int ofTotal); +} diff --git a/src/com/fsck/k9/mail/MessagingException.java b/src/com/fsck/k9/mail/MessagingException.java new file mode 100644 index 000000000..7e0a0d263 --- /dev/null +++ b/src/com/fsck/k9/mail/MessagingException.java @@ -0,0 +1,14 @@ + +package com.fsck.k9.mail; + +public class MessagingException extends Exception { + public static final long serialVersionUID = -1; + + public MessagingException(String message) { + super(message); + } + + public MessagingException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/src/com/fsck/k9/mail/Multipart.java b/src/com/fsck/k9/mail/Multipart.java new file mode 100644 index 000000000..787f5b382 --- /dev/null +++ b/src/com/fsck/k9/mail/Multipart.java @@ -0,0 +1,48 @@ + +package com.fsck.k9.mail; + +import java.util.ArrayList; + +public abstract class Multipart implements Body { + protected Part mParent; + + protected ArrayList mParts = new ArrayList(); + + protected String mContentType; + + public void addBodyPart(BodyPart part) throws MessagingException { + mParts.add(part); + } + + public void addBodyPart(BodyPart part, int index) throws MessagingException { + mParts.add(index, part); + } + + public BodyPart getBodyPart(int index) throws MessagingException { + return mParts.get(index); + } + + public String getContentType() throws MessagingException { + return mContentType; + } + + public int getCount() throws MessagingException { + return mParts.size(); + } + + public boolean removeBodyPart(BodyPart part) throws MessagingException { + return mParts.remove(part); + } + + public void removeBodyPart(int index) throws MessagingException { + mParts.remove(index); + } + + public Part getParent() throws MessagingException { + return mParent; + } + + public void setParent(Part parent) throws MessagingException { + this.mParent = parent; + } +} diff --git a/src/com/fsck/k9/mail/NoSuchProviderException.java b/src/com/fsck/k9/mail/NoSuchProviderException.java new file mode 100644 index 000000000..5eb9029d0 --- /dev/null +++ b/src/com/fsck/k9/mail/NoSuchProviderException.java @@ -0,0 +1,14 @@ + +package com.fsck.k9.mail; + +public class NoSuchProviderException extends MessagingException { + public static final long serialVersionUID = -1; + + public NoSuchProviderException(String message) { + super(message); + } + + public NoSuchProviderException(String message, Throwable throwable) { + super(message, throwable); + } +} diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java new file mode 100644 index 000000000..2d06744ed --- /dev/null +++ b/src/com/fsck/k9/mail/Part.java @@ -0,0 +1,31 @@ + +package com.fsck.k9.mail; + +import java.io.IOException; +import java.io.OutputStream; + +public interface Part { + public void addHeader(String name, String value) throws MessagingException; + + public void removeHeader(String name) throws MessagingException; + + public void setHeader(String name, String value) throws MessagingException; + + public Body getBody() throws MessagingException; + + public String getContentType() throws MessagingException; + + public String getDisposition() throws MessagingException; + + public String[] getHeader(String name) throws MessagingException; + + public int getSize() throws MessagingException; + + public boolean isMimeType(String mimeType) throws MessagingException; + + public String getMimeType() throws MessagingException; + + public void setBody(Body body) throws MessagingException; + + public void writeTo(OutputStream out) throws IOException, MessagingException; +} diff --git a/src/com/fsck/k9/mail/Store.java b/src/com/fsck/k9/mail/Store.java new file mode 100644 index 000000000..9f7474ac8 --- /dev/null +++ b/src/com/fsck/k9/mail/Store.java @@ -0,0 +1,76 @@ + +package com.fsck.k9.mail; + +import java.util.HashMap; + +import android.app.Application; + +import com.fsck.k9.mail.store.ImapStore; +import com.fsck.k9.mail.store.LocalStore; +import com.fsck.k9.mail.store.Pop3Store; + +/** + * Store is the access point for an email message store. It's location can be + * local or remote and no specific protocol is defined. Store is intended to + * loosely model in combination the JavaMail classes javax.mail.Store and + * javax.mail.Folder along with some additional functionality to improve + * performance on mobile devices. Implementations of this class should focus on + * making as few network connections as possible. + */ +public abstract class Store { + /** + * A global suggestion to Store implementors on how much of the body + * should be returned on FetchProfile.Item.BODY_SANE requests. + */ + public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (50 * 1024); + + protected static final int SOCKET_CONNECT_TIMEOUT = 10000; + protected static final int SOCKET_READ_TIMEOUT = 60000; + + private static HashMap mStores = new HashMap(); + + /** + * Get an instance of a mail store. The URI is parsed as a standard URI and + * the scheme is used to determine which protocol will be used. The + * following schemes are currently recognized: imap - IMAP with no + * connection security. Ex: imap://username:password@host/ imap+tls - IMAP + * with TLS connection security, if the server supports it. Ex: + * imap+tls://username:password@host imap+tls+ - IMAP with required TLS + * connection security. Connection fails if TLS is not available. Ex: + * imap+tls+://username:password@host imap+ssl+ - IMAP with required SSL + * connection security. Connection fails if SSL is not available. Ex: + * imap+ssl+://username:password@host + * + * @param uri The URI of the store. + * @return + * @throws MessagingException + */ + public synchronized static Store getInstance(String uri, Application application) throws MessagingException { + Store store = mStores.get(uri); + if (store == null) { + if (uri.startsWith("imap")) { + store = new ImapStore(uri); + } else if (uri.startsWith("pop3")) { + store = new Pop3Store(uri); + } else if (uri.startsWith("local")) { + store = new LocalStore(uri, application); + } + + if (store != null) { + mStores.put(uri, store); + } + } + + if (store == null) { + throw new MessagingException("Unable to locate an applicable Store for " + uri); + } + + return store; + } + + public abstract Folder getFolder(String name) throws MessagingException; + + public abstract Folder[] getPersonalNamespaces() throws MessagingException; + + public abstract void checkSettings() throws MessagingException; +} diff --git a/src/com/fsck/k9/mail/Transport.java b/src/com/fsck/k9/mail/Transport.java new file mode 100644 index 000000000..eba3f8969 --- /dev/null +++ b/src/com/fsck/k9/mail/Transport.java @@ -0,0 +1,22 @@ + +package com.fsck.k9.mail; + +import com.fsck.k9.mail.transport.SmtpTransport; + +public abstract class Transport { + protected static final int SOCKET_CONNECT_TIMEOUT = 10000; + + public synchronized static Transport getInstance(String uri) throws MessagingException { + if (uri.startsWith("smtp")) { + return new SmtpTransport(uri); + } else { + throw new MessagingException("Unable to locate an applicable Transport for " + uri); + } + } + + public abstract void open() throws MessagingException; + + public abstract void sendMessage(Message message) throws MessagingException; + + public abstract void close() throws MessagingException; +} diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java new file mode 100644 index 000000000..771c17c5e --- /dev/null +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -0,0 +1,77 @@ +package com.fsck.k9.mail.internet; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.codec.binary.Base64OutputStream; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.MessagingException; + +/** + * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows + * the user to write to the temp file. After the write the body is available via getInputStream + * and writeTo one time. After writeTo is called, or the InputStream returned from + * getInputStream is closed the file is deleted and the Body should be considered disposed of. + */ +public class BinaryTempFileBody implements Body { + private static File mTempDirectory; + + private File mFile; + + public static void setTempDirectory(File tempDirectory) { + mTempDirectory = tempDirectory; + } + + public BinaryTempFileBody() throws IOException { + if (mTempDirectory == null) { + throw new + RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!"); + } + } + + public OutputStream getOutputStream() throws IOException { + mFile = File.createTempFile("body", null, mTempDirectory); + mFile.deleteOnExit(); + return new FileOutputStream(mFile); + } + + public InputStream getInputStream() throws MessagingException { + try { + return new BinaryTempFileBodyInputStream(new FileInputStream(mFile)); + } + catch (IOException ioe) { + throw new MessagingException("Unable to open body", ioe); + } + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + Base64OutputStream base64Out = new Base64OutputStream(out); + IOUtils.copy(in, base64Out); + base64Out.close(); + mFile.delete(); + } + + class BinaryTempFileBodyInputStream extends FilterInputStream { + public BinaryTempFileBodyInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + super.close(); + mFile.delete(); + } + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java new file mode 100644 index 000000000..4504a99a8 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -0,0 +1,121 @@ + +package com.fsck.k9.mail.internet; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.MessagingException; + +/** + * TODO this is a close approximation of Message, need to update along with + * Message. + */ +public class MimeBodyPart extends BodyPart { + protected MimeHeader mHeader = new MimeHeader(); + protected Body mBody; + protected int mSize; + + public MimeBodyPart() throws MessagingException { + this(null); + } + + public MimeBodyPart(Body body) throws MessagingException { + this(body, null); + } + + public MimeBodyPart(Body body, String mimeType) throws MessagingException { + if (mimeType != null) { + setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); + } + setBody(body); + } + + protected String getFirstHeader(String name) throws MessagingException { + return mHeader.getFirstHeader(name); + } + + public void addHeader(String name, String value) throws MessagingException { + mHeader.addHeader(name, value); + } + + public void setHeader(String name, String value) throws MessagingException { + mHeader.setHeader(name, value); + } + + public String[] getHeader(String name) throws MessagingException { + return mHeader.getHeader(name); + } + + public void removeHeader(String name) throws MessagingException { + mHeader.removeHeader(name); + } + + public Body getBody() throws MessagingException { + return mBody; + } + + public void setBody(Body body) throws MessagingException { + this.mBody = body; + if (body instanceof com.fsck.k9.mail.Multipart) { + com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body); + multipart.setParent(this); + setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); + } + else if (body instanceof TextBody) { + String contentType = String.format("%s;\n charset=utf-8", getMimeType()); + String name = MimeUtility.getHeaderParameter(getContentType(), "name"); + if (name != null) { + contentType += String.format(";\n name=\"%s\"", name); + } + setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + } + } + + public String getContentType() throws MessagingException { + String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + if (contentType == null) { + return "text/plain"; + } else { + return contentType; + } + } + + public String getDisposition() throws MessagingException { + String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); + if (contentDisposition == null) { + return null; + } else { + return contentDisposition; + } + } + + public String getMimeType() throws MessagingException { + return MimeUtility.getHeaderParameter(getContentType(), null); + } + + public boolean isMimeType(String mimeType) throws MessagingException { + return getMimeType().equals(mimeType); + } + + public int getSize() throws MessagingException { + return mSize; + } + + /** + * Write the MimeMessage out in MIME format. + */ + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + mHeader.writeTo(out); + writer.write("\r\n"); + writer.flush(); + if (mBody != null) { + mBody.writeTo(out); + } + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeHeader.java b/src/com/fsck/k9/mail/internet/MimeHeader.java new file mode 100644 index 000000000..35b50c357 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeHeader.java @@ -0,0 +1,105 @@ + +package com.fsck.k9.mail.internet; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; + +import com.fsck.k9.Utility; +import com.fsck.k9.mail.MessagingException; + +public class MimeHeader { + /** + * Application specific header that contains Store specific information about an attachment. + * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later + * retrieve the attachment at will from the server. + * The info is recorded from this header on LocalStore.appendMessages and is put back + * into the MIME data by LocalStore.fetch. + */ + public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData"; + + public static final String HEADER_CONTENT_TYPE = "Content-Type"; + public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding"; + public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; + + /** + * Fields that should be omitted when writing the header using writeTo() + */ + private static final String[] writeOmitFields = { +// HEADER_ANDROID_ATTACHMENT_DOWNLOADED, +// HEADER_ANDROID_ATTACHMENT_ID, + HEADER_ANDROID_ATTACHMENT_STORE_DATA + }; + + protected ArrayList mFields = new ArrayList(); + + public void clear() { + mFields.clear(); + } + + public String getFirstHeader(String name) throws MessagingException { + String[] header = getHeader(name); + if (header == null) { + return null; + } + return header[0]; + } + + public void addHeader(String name, String value) throws MessagingException { + mFields.add(new Field(name, MimeUtility.foldAndEncode(value))); + } + + public void setHeader(String name, String value) throws MessagingException { + if (name == null || value == null) { + return; + } + removeHeader(name); + addHeader(name, value); + } + + public String[] getHeader(String name) throws MessagingException { + ArrayList values = new ArrayList(); + for (Field field : mFields) { + if (field.name.equalsIgnoreCase(name)) { + values.add(field.value); + } + } + if (values.size() == 0) { + return null; + } + return values.toArray(new String[] {}); + } + + public void removeHeader(String name) throws MessagingException { + ArrayList removeFields = new ArrayList(); + for (Field field : mFields) { + if (field.name.equalsIgnoreCase(name)) { + removeFields.add(field); + } + } + mFields.removeAll(removeFields); + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + for (Field field : mFields) { + if (!Utility.arrayContains(writeOmitFields, field.name)) { + writer.write(field.name + ": " + field.value + "\r\n"); + } + } + writer.flush(); + } + + class Field { + String name; + + String value; + + public Field(String name, String value) { + this.name = name; + this.value = value; + } + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java new file mode 100644 index 000000000..41d32fd5f --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -0,0 +1,424 @@ + +package com.fsck.k9.mail.internet; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Stack; + +import org.apache.james.mime4j.BodyDescriptor; +import org.apache.james.mime4j.ContentHandler; +import org.apache.james.mime4j.EOLConvertingInputStream; +import org.apache.james.mime4j.MimeStreamParser; +import org.apache.james.mime4j.field.DateTimeField; +import org.apache.james.mime4j.field.Field; + +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; + +/** + * An implementation of Message that stores all of it's metadata in RFC 822 and + * RFC 2045 style headers. + */ +public class MimeMessage extends Message { + protected MimeHeader mHeader = new MimeHeader(); + protected Address[] mFrom; + protected Address[] mTo; + protected Address[] mCc; + protected Address[] mBcc; + protected Address[] mReplyTo; + protected Date mSentDate; + protected SimpleDateFormat mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z"); + protected Body mBody; + protected int mSize; + + public MimeMessage() { + /* + * Every new messages gets a Message-ID + */ + try { + setHeader("Message-ID", generateMessageId()); + } + catch (MessagingException me) { + throw new RuntimeException("Unable to create MimeMessage", me); + } + } + + private String generateMessageId() { + StringBuffer sb = new StringBuffer(); + sb.append("<"); + for (int i = 0; i < 24; i++) { + sb.append(Integer.toString((int)(Math.random() * 35), 36)); + } + sb.append("."); + sb.append(Long.toString(System.currentTimeMillis())); + sb.append("@email.android.com>"); + return sb.toString(); + } + + /** + * Parse the given InputStream using Apache Mime4J to build a MimeMessage. + * + * @param in + * @throws IOException + * @throws MessagingException + */ + public MimeMessage(InputStream in) throws IOException, MessagingException { + parse(in); + } + + protected void parse(InputStream in) throws IOException, MessagingException { + mHeader.clear(); + mBody = null; + mBcc = null; + mTo = null; + mFrom = null; + mSentDate = null; + + MimeStreamParser parser = new MimeStreamParser(); + parser.setContentHandler(new MimeMessageBuilder()); + parser.parse(new EOLConvertingInputStream(in)); + } + + public Date getReceivedDate() throws MessagingException { + return null; + } + + public Date getSentDate() throws MessagingException { + if (mSentDate == null) { + try { + DateTimeField field = (DateTimeField)Field.parse("Date: " + + MimeUtility.unfoldAndDecode(getFirstHeader("Date"))); + mSentDate = field.getDate(); + } catch (Exception e) { + + } + } + return mSentDate; + } + + public void setSentDate(Date sentDate) throws MessagingException { + setHeader("Date", mDateFormat.format(sentDate)); + this.mSentDate = sentDate; + } + + public String getContentType() throws MessagingException { + String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + if (contentType == null) { + return "text/plain"; + } else { + return contentType; + } + } + + public String getDisposition() throws MessagingException { + String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); + if (contentDisposition == null) { + return null; + } else { + return contentDisposition; + } + } + + public String getMimeType() throws MessagingException { + return MimeUtility.getHeaderParameter(getContentType(), null); + } + + public int getSize() throws MessagingException { + return mSize; + } + + /** + * Returns a list of the given recipient type from this message. If no addresses are + * found the method returns an empty array. + */ + public Address[] getRecipients(RecipientType type) throws MessagingException { + if (type == RecipientType.TO) { + if (mTo == null) { + mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To"))); + } + return mTo; + } else if (type == RecipientType.CC) { + if (mCc == null) { + mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC"))); + } + return mCc; + } else if (type == RecipientType.BCC) { + if (mBcc == null) { + mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC"))); + } + return mBcc; + } else { + throw new MessagingException("Unrecognized recipient type."); + } + } + + public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException { + if (type == RecipientType.TO) { + if (addresses == null || addresses.length == 0) { + removeHeader("To"); + this.mTo = null; + } else { + setHeader("To", Address.toString(addresses)); + this.mTo = addresses; + } + } else if (type == RecipientType.CC) { + if (addresses == null || addresses.length == 0) { + removeHeader("CC"); + this.mCc = null; + } else { + setHeader("CC", Address.toString(addresses)); + this.mCc = addresses; + } + } else if (type == RecipientType.BCC) { + if (addresses == null || addresses.length == 0) { + removeHeader("BCC"); + this.mBcc = null; + } else { + setHeader("BCC", Address.toString(addresses)); + this.mBcc = addresses; + } + } else { + throw new MessagingException("Unrecognized recipient type."); + } + } + + /** + * Returns the unfolded, decoded value of the Subject header. + */ + public String getSubject() throws MessagingException { + return MimeUtility.unfoldAndDecode(getFirstHeader("Subject")); + } + + public void setSubject(String subject) throws MessagingException { + setHeader("Subject", subject); + } + + public Address[] getFrom() throws MessagingException { + if (mFrom == null) { + String list = MimeUtility.unfold(getFirstHeader("From")); + if (list == null || list.length() == 0) { + list = MimeUtility.unfold(getFirstHeader("Sender")); + } + mFrom = Address.parse(list); + } + return mFrom; + } + + public void setFrom(Address from) throws MessagingException { + if (from != null) { + setHeader("From", from.toString()); + this.mFrom = new Address[] { + from + }; + } else { + this.mFrom = null; + } + } + + public Address[] getReplyTo() throws MessagingException { + if (mReplyTo == null) { + mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to"))); + } + return mReplyTo; + } + + public void setReplyTo(Address[] replyTo) throws MessagingException { + if (replyTo == null || replyTo.length == 0) { + removeHeader("Reply-to"); + mReplyTo = null; + } else { + setHeader("Reply-to", Address.toString(replyTo)); + mReplyTo = replyTo; + } + } + + public void saveChanges() throws MessagingException { + throw new MessagingException("saveChanges not yet implemented"); + } + + public Body getBody() throws MessagingException { + return mBody; + } + + public void setBody(Body body) throws MessagingException { + this.mBody = body; + if (body instanceof com.fsck.k9.mail.Multipart) { + com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body); + multipart.setParent(this); + setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType()); + setHeader("MIME-Version", "1.0"); + } + else if (body instanceof TextBody) { + setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8", + getMimeType())); + setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + } + } + + protected String getFirstHeader(String name) throws MessagingException { + return mHeader.getFirstHeader(name); + } + + public void addHeader(String name, String value) throws MessagingException { + mHeader.addHeader(name, value); + } + + public void setHeader(String name, String value) throws MessagingException { + mHeader.setHeader(name, value); + } + + public String[] getHeader(String name) throws MessagingException { + return mHeader.getHeader(name); + } + + public void removeHeader(String name) throws MessagingException { + mHeader.removeHeader(name); + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + mHeader.writeTo(out); + writer.write("\r\n"); + writer.flush(); + if (mBody != null) { + mBody.writeTo(out); + } + } + + public InputStream getInputStream() throws MessagingException { + return null; + } + + class MimeMessageBuilder implements ContentHandler { + private Stack stack = new Stack(); + + public MimeMessageBuilder() { + } + + private void expect(Class c) { + if (!c.isInstance(stack.peek())) { + throw new IllegalStateException("Internal stack error: " + "Expected '" + + c.getName() + "' found '" + stack.peek().getClass().getName() + "'"); + } + } + + public void startMessage() { + if (stack.isEmpty()) { + stack.push(MimeMessage.this); + } else { + expect(Part.class); + try { + MimeMessage m = new MimeMessage(); + ((Part)stack.peek()).setBody(m); + stack.push(m); + } catch (MessagingException me) { + throw new Error(me); + } + } + } + + public void endMessage() { + expect(MimeMessage.class); + stack.pop(); + } + + public void startHeader() { + expect(Part.class); + } + + public void field(String fieldData) { + expect(Part.class); + try { + String[] tokens = fieldData.split(":", 2); + ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim()); + } catch (MessagingException me) { + throw new Error(me); + } + } + + public void endHeader() { + expect(Part.class); + } + + public void startMultipart(BodyDescriptor bd) { + expect(Part.class); + + Part e = (Part)stack.peek(); + try { + MimeMultipart multiPart = new MimeMultipart(e.getContentType()); + e.setBody(multiPart); + stack.push(multiPart); + } catch (MessagingException me) { + throw new Error(me); + } + } + + public void body(BodyDescriptor bd, InputStream in) throws IOException { + expect(Part.class); + Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding()); + try { + ((Part)stack.peek()).setBody(body); + } catch (MessagingException me) { + throw new Error(me); + } + } + + public void endMultipart() { + stack.pop(); + } + + public void startBodyPart() { + expect(MimeMultipart.class); + + try { + MimeBodyPart bodyPart = new MimeBodyPart(); + ((MimeMultipart)stack.peek()).addBodyPart(bodyPart); + stack.push(bodyPart); + } catch (MessagingException me) { + throw new Error(me); + } + } + + public void endBodyPart() { + expect(BodyPart.class); + stack.pop(); + } + + public void epilogue(InputStream is) throws IOException { + expect(MimeMultipart.class); + StringBuffer sb = new StringBuffer(); + int b; + while ((b = is.read()) != -1) { + sb.append((char)b); + } + // ((Multipart) stack.peek()).setEpilogue(sb.toString()); + } + + public void preamble(InputStream is) throws IOException { + expect(MimeMultipart.class); + StringBuffer sb = new StringBuffer(); + int b; + while ((b = is.read()) != -1) { + sb.append((char)b); + } + try { + ((MimeMultipart)stack.peek()).setPreamble(sb.toString()); + } catch (MessagingException me) { + throw new Error(me); + } + } + + public void raw(InputStream is) throws IOException { + throw new UnsupportedOperationException("Not supported"); + } + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeMultipart.java b/src/com/fsck/k9/mail/internet/MimeMultipart.java new file mode 100644 index 000000000..6db58a5d8 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeMultipart.java @@ -0,0 +1,95 @@ + +package com.fsck.k9.mail.internet; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; + +public class MimeMultipart extends Multipart { + protected String mPreamble; + + protected String mContentType; + + protected String mBoundary; + + protected String mSubType; + + public MimeMultipart() throws MessagingException { + mBoundary = generateBoundary(); + setSubType("mixed"); + } + + public MimeMultipart(String contentType) throws MessagingException { + this.mContentType = contentType; + try { + mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1]; + mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary"); + if (mBoundary == null) { + throw new MessagingException("MultiPart does not contain boundary: " + contentType); + } + } catch (Exception e) { + throw new MessagingException( + "Invalid MultiPart Content-Type; must contain subtype and boundary. (" + + contentType + ")", e); + } + } + + public String generateBoundary() { + StringBuffer sb = new StringBuffer(); + sb.append("----"); + for (int i = 0; i < 30; i++) { + sb.append(Integer.toString((int)(Math.random() * 35), 36)); + } + return sb.toString().toUpperCase(); + } + + public String getPreamble() throws MessagingException { + return mPreamble; + } + + public void setPreamble(String preamble) throws MessagingException { + this.mPreamble = preamble; + } + + public String getContentType() throws MessagingException { + return mContentType; + } + + public void setSubType(String subType) throws MessagingException { + this.mSubType = subType; + mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary); + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + + if (mPreamble != null) { + writer.write(mPreamble + "\r\n"); + } + + if(mParts.size() == 0){ + writer.write("--" + mBoundary + "\r\n"); + } + + for (int i = 0, count = mParts.size(); i < count; i++) { + BodyPart bodyPart = (BodyPart)mParts.get(i); + writer.write("--" + mBoundary + "\r\n"); + writer.flush(); + bodyPart.writeTo(out); + writer.write("\r\n"); + } + + writer.write("--" + mBoundary + "--\r\n"); + writer.flush(); + } + + public InputStream getInputStream() throws MessagingException { + return null; + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java new file mode 100644 index 000000000..2066eb8d6 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -0,0 +1,304 @@ + +package com.fsck.k9.mail.internet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.decoder.Base64InputStream; +import org.apache.james.mime4j.decoder.DecoderUtil; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; +import org.apache.james.mime4j.util.CharsetUtil; + +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; + +public class MimeUtility { + public static String unfold(String s) { + if (s == null) { + return null; + } + return s.replaceAll("\r|\n", ""); + } + + public static String decode(String s) { + if (s == null) { + return null; + } + return DecoderUtil.decodeEncodedWords(s); + } + + public static String unfoldAndDecode(String s) { + return decode(unfold(s)); + } + + // TODO implement proper foldAndEncode + public static String foldAndEncode(String s) { + return s; + } + + /** + * Returns the named parameter of a header field. If name is null the first + * parameter is returned, or if there are no additional parameters in the + * field the entire field is returned. Otherwise the named parameter is + * searched for in a case insensitive fashion and returned. If the parameter + * cannot be found the method returns null. + * + * @param header + * @param name + * @return + */ + public static String getHeaderParameter(String header, String name) { + if (header == null) { + return null; + } + header = header.replaceAll("\r|\n", ""); + String[] parts = header.split(";"); + if (name == null) { + return parts[0]; + } + for (String part : parts) { + if (part.trim().toLowerCase().startsWith(name.toLowerCase())) { + String parameter = part.split("=", 2)[1].trim(); + if (parameter.startsWith("\"") && parameter.endsWith("\"")) { + return parameter.substring(1, parameter.length() - 1); + } + else { + return parameter; + } + } + } + return null; + } + + public static Part findFirstPartByMimeType(Part part, String mimeType) + throws MessagingException { + if (part.getBody() instanceof Multipart) { + Multipart multipart = (Multipart)part.getBody(); + for (int i = 0, count = multipart.getCount(); i < count; i++) { + BodyPart bodyPart = multipart.getBodyPart(i); + Part ret = findFirstPartByMimeType(bodyPart, mimeType); + if (ret != null) { + return ret; + } + } + } + else if (part.getMimeType().equalsIgnoreCase(mimeType)) { + return part; + } + return null; + } + + public static Part findPartByContentId(Part part, String contentId) throws Exception { + if (part.getBody() instanceof Multipart) { + Multipart multipart = (Multipart)part.getBody(); + for (int i = 0, count = multipart.getCount(); i < count; i++) { + BodyPart bodyPart = multipart.getBodyPart(i); + Part ret = findPartByContentId(bodyPart, contentId); + if (ret != null) { + return ret; + } + } + } + String[] header = part.getHeader("Content-ID"); + if (header != null) { + for (String s : header) { + if (s.equals(contentId)) { + return part; + } + } + } + return null; + } + + /** + * Reads the Part's body and returns a String based on any charset conversion that needed + * to be done. + * @param part + * @return + * @throws IOException + */ + public static String getTextFromPart(Part part) { + try { + if (part != null && part.getBody() != null) { + InputStream in = part.getBody().getInputStream(); + String mimeType = part.getMimeType(); + if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) { + /* + * Now we read the part into a buffer for further processing. Because + * the stream is now wrapped we'll remove any transfer encoding at this point. + */ + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.copy(in, out); + + byte[] bytes = out.toByteArray(); + in.close(); + out.close(); + + String charset = getHeaderParameter(part.getContentType(), "charset"); + /* + * We've got a text part, so let's see if it needs to be processed further. + */ + if (charset != null) { + /* + * See if there is conversion from the MIME charset to the Java one. + */ + charset = CharsetUtil.toJavaCharset(charset); + } + if (charset != null) { + /* + * We've got a charset encoding, so decode using it. + */ + return new String(bytes, 0, bytes.length, charset); + } + else { + /* + * No encoding, so use us-ascii, which is the standard. + */ + return new String(bytes, 0, bytes.length, "ASCII"); + } + } + } + + } + catch (Exception e) { + /* + * If we are not able to process the body there's nothing we can do about it. Return + * null and let the upper layers handle the missing content. + */ + Log.e(k9.LOG_TAG, "Unable to getTextFromPart", e); + } + return null; + } + + /** + * Returns true if the given mimeType matches the matchAgainst specification. + * @param mimeType A MIME type to check. + * @param matchAgainst A MIME type to check against. May include wildcards such as image/* or + * * /*. + * @return + */ + public static boolean mimeTypeMatches(String mimeType, String matchAgainst) { + return mimeType.matches(matchAgainst.replaceAll("\\*", "\\.\\*")); + } + + /** + * Returns true if the given mimeType matches any of the matchAgainst specifications. + * @param mimeType A MIME type to check. + * @param matchAgainst An array of MIME types to check against. May include wildcards such + * as image/* or * /*. + * @return + */ + public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) { + for (String matchType : matchAgainst) { + if (mimeType.matches(matchType.replaceAll("\\*", "\\.\\*"))) { + return true; + } + } + return false; + } + + /** + * Removes any content transfer encoding from the stream and returns a Body. + */ + public static Body decodeBody(InputStream in, String contentTransferEncoding) + throws IOException { + /* + * We'll remove any transfer encoding by wrapping the stream. + */ + if (contentTransferEncoding != null) { + contentTransferEncoding = + MimeUtility.getHeaderParameter(contentTransferEncoding, null); + if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) { + in = new QuotedPrintableInputStream(in); + } + else if ("base64".equalsIgnoreCase(contentTransferEncoding)) { + in = new Base64InputStream(in); + } + } + + BinaryTempFileBody tempBody = new BinaryTempFileBody(); + OutputStream out = tempBody.getOutputStream(); + IOUtils.copy(in, out); + out.close(); + return tempBody; + } + + /** + * An unfortunately named method that makes decisions about a Part (usually a Message) + * as to which of it's children will be "viewable" and which will be attachments. + * The method recursively sorts the viewables and attachments into seperate + * lists for further processing. + * @param part + * @param viewables + * @param attachments + * @throws MessagingException + */ + public static void collectParts(Part part, ArrayList viewables, + ArrayList attachments) throws MessagingException { + String disposition = part.getDisposition(); + String dispositionType = null; + String dispositionFilename = null; + if (disposition != null) { + dispositionType = MimeUtility.getHeaderParameter(disposition, null); + dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename"); + } + + /* + * A best guess that this part is intended to be an attachment and not inline. + */ + boolean attachment = ("attachment".equalsIgnoreCase(dispositionType)) + || (dispositionFilename != null) + && (!"inline".equalsIgnoreCase(dispositionType)); + + /* + * If the part is Multipart but not alternative it's either mixed or + * something we don't know about, which means we treat it as mixed + * per the spec. We just process it's pieces recursively. + */ + if (part.getBody() instanceof Multipart) { + Multipart mp = (Multipart)part.getBody(); + for (int i = 0; i < mp.getCount(); i++) { + collectParts(mp.getBodyPart(i), viewables, attachments); + } + } + /* + * If the part is an embedded message we just continue to process + * it, pulling any viewables or attachments into the running list. + */ + else if (part.getBody() instanceof Message) { + Message message = (Message)part.getBody(); + collectParts(message, viewables, attachments); + } + /* + * If the part is HTML and it got this far it's part of a mixed (et + * al) and should be rendered inline. + */ + else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) { + viewables.add(part); + } + /* + * If the part is plain text and it got this far it's part of a + * mixed (et al) and should be rendered inline. + */ + else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) { + viewables.add(part); + } + /* + * Finally, if it's nothing else we will include it as an attachment. + */ + else { + attachments.add(part); + } + } +} diff --git a/src/com/fsck/k9/mail/internet/TextBody.java b/src/com/fsck/k9/mail/internet/TextBody.java new file mode 100644 index 000000000..0b85ba115 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/TextBody.java @@ -0,0 +1,47 @@ + +package com.fsck.k9.mail.internet; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + + +import com.fsck.k9.codec.binary.Base64; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.MessagingException; + +public class TextBody implements Body { + String mBody; + + public TextBody(String body) { + this.mBody = body; + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + byte[] bytes = mBody.getBytes("UTF-8"); + out.write(Base64.encodeBase64Chunked(bytes)); + } + + /** + * Get the text of the body in it's unencoded format. + * @return + */ + public String getText() { + return mBody; + } + + /** + * Returns an InputStream that reads this body's text in UTF-8 format. + */ + public InputStream getInputStream() throws MessagingException { + try { + byte[] b = mBody.getBytes("UTF-8"); + return new ByteArrayInputStream(b); + } + catch (UnsupportedEncodingException usee) { + return null; + } + } +} diff --git a/src/com/fsck/k9/mail/store/ImapResponseParser.java b/src/com/fsck/k9/mail/store/ImapResponseParser.java new file mode 100644 index 000000000..6a759e53c --- /dev/null +++ b/src/com/fsck/k9/mail/store/ImapResponseParser.java @@ -0,0 +1,356 @@ +/** + * + */ + +package com.fsck.k9.mail.store; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.FixedLengthInputStream; +import com.fsck.k9.PeekableInputStream; +import com.fsck.k9.mail.MessagingException; + +public class ImapResponseParser { + SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z"); + PeekableInputStream mIn; + InputStream mActiveLiteral; + + public ImapResponseParser(PeekableInputStream in) { + this.mIn = in; + } + + /** + * Reads the next response available on the stream and returns an + * ImapResponse object that represents it. + * + * @return + * @throws IOException + */ + public ImapResponse readResponse() throws IOException { + ImapResponse response = new ImapResponse(); + if (mActiveLiteral != null) { + while (mActiveLiteral.read() != -1) + ; + mActiveLiteral = null; + } + int ch = mIn.peek(); + if (ch == '*') { + parseUntaggedResponse(); + readTokens(response); + } else if (ch == '+') { + response.mCommandContinuationRequested = + parseCommandContinuationRequest(); + readTokens(response); + } else { + response.mTag = parseTaggedResponse(); + readTokens(response); + } + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, "<<< " + response.toString()); + } + } + return response; + } + + private void readTokens(ImapResponse response) throws IOException { + response.clear(); + Object token; + while ((token = readToken()) != null) { + if (response != null) { + response.add(token); + } + if (mActiveLiteral != null) { + break; + } + } + response.mCompleted = token == null; + } + + /** + * Reads the next token of the response. The token can be one of: String - + * for NIL, QUOTED, NUMBER, ATOM. InputStream - for LITERAL. + * InputStream.available() returns the total length of the stream. + * ImapResponseList - for PARENTHESIZED LIST. Can contain any of the above + * elements including List. + * + * @return The next token in the response or null if there are no more + * tokens. + * @throws IOException + */ + public Object readToken() throws IOException { + while (true) { + Object token = parseToken(); + if (token == null || !token.equals(")")) { + return token; + } + } + } + + private Object parseToken() throws IOException { + if (mActiveLiteral != null) { + while (mActiveLiteral.read() != -1) + ; + mActiveLiteral = null; + } + while (true) { + int ch = mIn.peek(); + if (ch == '(') { + return parseList(); + } else if (ch == ')') { + expect(')'); + return ")"; + } else if (ch == '"') { + return parseQuoted(); + } else if (ch == '{') { + mActiveLiteral = parseLiteral(); + return mActiveLiteral; + } else if (ch == ' ') { + expect(' '); + } else if (ch == '\r') { + expect('\r'); + expect('\n'); + return null; + } else if (ch == '\n') { + expect('\n'); + return null; + } else if (ch == '\t') { + expect('\t'); + } else { + return parseAtom(); + } + } + } + + private boolean parseCommandContinuationRequest() throws IOException { + expect('+'); + expect(' '); + return true; + } + + // * OK [UIDNEXT 175] Predicted next UID + private void parseUntaggedResponse() throws IOException { + expect('*'); + expect(' '); + } + + // 3 OK [READ-WRITE] Select completed. + private String parseTaggedResponse() throws IOException { + String tag = readStringUntil(' '); + return tag; + } + + private ImapList parseList() throws IOException { + expect('('); + ImapList list = new ImapList(); + Object token; + while (true) { + token = parseToken(); + if (token == null) { + break; + } else if (token instanceof InputStream) { + list.add(token); + break; + } else if (token.equals(")")) { + break; + } else { + list.add(token); + } + } + return list; + } + + private String parseAtom() throws IOException { + StringBuffer sb = new StringBuffer(); + int ch; + while (true) { + ch = mIn.peek(); + if (ch == -1) { + throw new IOException("parseAtom(): end of stream reached"); + } else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || + // docs claim that flags are \ atom but atom isn't supposed to + // contain + // * and some falgs contain * + // ch == '%' || ch == '*' || + ch == '%' || + // TODO probably should not allow \ and should recognize + // it as a flag instead + // ch == '"' || ch == '\' || + ch == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) { + if (sb.length() == 0) { + throw new IOException(String.format("parseAtom(): (%04x %c)", (int)ch, ch)); + } + return sb.toString(); + } else { + sb.append((char)mIn.read()); + } + } + } + + /** + * A { has been read, read the rest of the size string, the space and then + * notify the listener with an InputStream. + * + * @param mListener + * @throws IOException + */ + private InputStream parseLiteral() throws IOException { + expect('{'); + int size = Integer.parseInt(readStringUntil('}')); + expect('\r'); + expect('\n'); + FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size); + return fixed; + } + + /** + * A " has been read, read to the end of the quoted string and notify the + * listener. + * + * @param mListener + * @throws IOException + */ + private String parseQuoted() throws IOException { + expect('"'); + return readStringUntil('"'); + } + + private String readStringUntil(char end) throws IOException { + StringBuffer sb = new StringBuffer(); + int ch; + while ((ch = mIn.read()) != -1) { + if (ch == end) { + return sb.toString(); + } else { + sb.append((char)ch); + } + } + throw new IOException("readQuotedString(): end of stream reached"); + } + + private int expect(char ch) throws IOException { + int d; + if ((d = mIn.read()) != ch) { + throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch, + ch, d, (char)d)); + } + return d; + } + + /** + * Represents an IMAP LIST response and is also the base class for the + * ImapResponse. + */ + public class ImapList extends ArrayList { + public ImapList getList(int index) { + return (ImapList)get(index); + } + + public String getString(int index) { + return (String)get(index); + } + + public InputStream getLiteral(int index) { + return (InputStream)get(index); + } + + public int getNumber(int index) { + return Integer.parseInt(getString(index)); + } + + public Date getDate(int index) throws MessagingException { + try { + return mDateTimeFormat.parse(getString(index)); + } catch (ParseException pe) { + throw new MessagingException("Unable to parse IMAP datetime", pe); + } + } + + public Object getKeyedValue(Object key) { + for (int i = 0, count = size(); i < count; i++) { + if (get(i).equals(key)) { + return get(i + 1); + } + } + return null; + } + + public ImapList getKeyedList(Object key) { + return (ImapList)getKeyedValue(key); + } + + public String getKeyedString(Object key) { + return (String)getKeyedValue(key); + } + + public InputStream getKeyedLiteral(Object key) { + return (InputStream)getKeyedValue(key); + } + + public int getKeyedNumber(Object key) { + return Integer.parseInt(getKeyedString(key)); + } + + public Date getKeyedDate(Object key) throws MessagingException { + try { + String value = getKeyedString(key); + if (value == null) { + return null; + } + return mDateTimeFormat.parse(value); + } catch (ParseException pe) { + throw new MessagingException("Unable to parse IMAP datetime", pe); + } + } + } + + /** + * Represents a single response from the IMAP server. Tagged responses will + * have a non-null tag. Untagged responses will have a null tag. The object + * will contain all of the available tokens at the time the response is + * received. In general, it will either contain all of the tokens of the + * response or all of the tokens up until the first LITERAL. If the object + * does not contain the entire response the caller must call more() to + * continue reading the response until more returns false. + */ + public class ImapResponse extends ImapList { + private boolean mCompleted; + + boolean mCommandContinuationRequested; + String mTag; + + public boolean more() throws IOException { + if (mCompleted) { + return false; + } + readTokens(this); + return true; + } + + public String getAlertText() { + if (size() > 1 && "[ALERT]".equals(getString(1))) { + StringBuffer sb = new StringBuffer(); + for (int i = 2, count = size(); i < count; i++) { + sb.append(get(i).toString()); + sb.append(' '); + } + return sb.toString(); + } else { + return null; + } + } + + public String toString() { + return "#" + mTag + "# " + super.toString(); + } + } +} diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java new file mode 100644 index 000000000..43266e52d --- /dev/null +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -0,0 +1,1283 @@ + +package com.fsck.k9.mail.store; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.SSLException; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.PeekableInputStream; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.store.ImapResponseParser.ImapList; +import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; +import com.fsck.k9.mail.transport.CountingOutputStream; +import com.fsck.k9.mail.transport.EOLConvertingOutputStream; +import com.beetstra.jutf7.CharsetProvider; + +/** + *
+ * TODO Need to start keeping track of UIDVALIDITY
+ * TODO Need a default response handler for things like folder updates
+ * TODO In fetch(), if we need a ImapMessage and were given
+ * something else we can try to do a pre-fetch first.
+ *
+ * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
+ * certain information in a FETCH command, the server may return the requested
+ * information in any order, not necessarily in the order that it was requested.
+ * Further, the server may return the information in separate FETCH responses
+ * and may also return information that was not explicitly requested (to reflect
+ * to the client changes in the state of the subject message).
+ * 
+ */ +public class ImapStore extends Store { + public static final int CONNECTION_SECURITY_NONE = 0; + public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; + public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; + public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; + public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + + private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN }; + + private String mHost; + private int mPort; + private String mUsername; + private String mPassword; + private int mConnectionSecurity; + private String mPathPrefix; + + private LinkedList mConnections = + new LinkedList(); + + /** + * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. + */ + private Charset mModifiedUtf7Charset; + + /** + * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server + * and as long as their associated connection remains open they are reusable between + * requests. This cache lets us make sure we always reuse, if possible, for a given + * folder name. + */ + private HashMap mFolderCache = new HashMap(); + + /** + * imap://user:password@server:port CONNECTION_SECURITY_NONE + * imap+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL + * imap+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED + * imap+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED + * imap+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL + * + * @param _uri + */ + public ImapStore(String _uri) throws MessagingException { + URI uri; + try { + uri = new URI(_uri); + } catch (URISyntaxException use) { + throw new MessagingException("Invalid ImapStore URI", use); + } + + String scheme = uri.getScheme(); + if (scheme.equals("imap")) { + mConnectionSecurity = CONNECTION_SECURITY_NONE; + mPort = 143; + } else if (scheme.equals("imap+tls")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; + mPort = 143; + } else if (scheme.equals("imap+tls+")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; + mPort = 143; + } else if (scheme.equals("imap+ssl+")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + mPort = 993; + } else if (scheme.equals("imap+ssl")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + mPort = 993; + } else { + throw new MessagingException("Unsupported protocol"); + } + + mHost = uri.getHost(); + + if (uri.getPort() != -1) { + mPort = uri.getPort(); + } + + if (uri.getUserInfo() != null) { + String[] userInfoParts = uri.getUserInfo().split(":", 2); + mUsername = userInfoParts[0]; + if (userInfoParts.length > 1) { + mPassword = userInfoParts[1]; + } + } + + if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { + mPathPrefix = uri.getPath().substring(1); + } + + mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); + } + + @Override + public Folder getFolder(String name) throws MessagingException { + ImapFolder folder; + synchronized (mFolderCache) { + folder = mFolderCache.get(name); + if (folder == null) { + folder = new ImapFolder(name); + mFolderCache.put(name, folder); + } + } + return folder; + } + + + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + ImapConnection connection = getConnection(); + try { + ArrayList folders = new ArrayList(); + List responses = + connection.executeSimpleCommand(String.format("LIST \"\" \"%s*\"", + mPathPrefix == null ? "" : mPathPrefix)); + for (ImapResponse response : responses) { + if (response.get(0).equals("LIST")) { + boolean includeFolder = true; + String folder = decodeFolderName(response.getString(3)); + if (folder.equalsIgnoreCase("INBOX")) { + continue; + } + ImapList attributes = response.getList(1); + for (int i = 0, count = attributes.size(); i < count; i++) { + String attribute = attributes.getString(i); + if (attribute.equalsIgnoreCase("\\NoSelect")) { + includeFolder = false; + } + } + if (includeFolder) { + folders.add(getFolder(folder)); + } + } + } + folders.add(getFolder("INBOX")); + return folders.toArray(new Folder[] {}); + } catch (IOException ioe) { + connection.close(); + throw new MessagingException("Unable to get folder list.", ioe); + } finally { + releaseConnection(connection); + } + } + + @Override + public void checkSettings() throws MessagingException { + try { + ImapConnection connection = new ImapConnection(); + connection.open(); + connection.close(); + } + catch (IOException ioe) { + throw new MessagingException("Unable to connect.", ioe); + } + } + + /** + * Gets a connection if one is available for reuse, or creates a new one if not. + * @return + */ + private ImapConnection getConnection() throws MessagingException { + synchronized (mConnections) { + ImapConnection connection = null; + while ((connection = mConnections.poll()) != null) { + try { + connection.executeSimpleCommand("NOOP"); + break; + } + catch (IOException ioe) { + connection.close(); + } + } + if (connection == null) { + connection = new ImapConnection(); + } + return connection; + } + } + + private void releaseConnection(ImapConnection connection) { + mConnections.offer(connection); + } + + private String encodeFolderName(String name) { + try { + ByteBuffer bb = mModifiedUtf7Charset.encode(name); + byte[] b = new byte[bb.limit()]; + bb.get(b); + return new String(b, "US-ASCII"); + } + catch (UnsupportedEncodingException uee) { + /* + * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't + * exist we're totally screwed. + */ + throw new RuntimeException("Unabel to encode folder name: " + name, uee); + } + } + + private String decodeFolderName(String name) { + /* + * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7 + * decoder and return the Unicode String. + */ + try { + byte[] encoded = name.getBytes("US-ASCII"); + CharBuffer cb = mModifiedUtf7Charset.decode(ByteBuffer.wrap(encoded)); + return cb.toString(); + } + catch (UnsupportedEncodingException uee) { + /* + * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't + * exist we're totally screwed. + */ + throw new RuntimeException("Unable to decode folder name: " + name, uee); + } + } + + class ImapFolder extends Folder { + private String mName; + private int mMessageCount = -1; + private ImapConnection mConnection; + private OpenMode mMode; + private boolean mExists; + + public ImapFolder(String name) { + this.mName = name; + } + + public void open(OpenMode mode) throws MessagingException { + if (isOpen() && mMode == mode) { + // Make sure the connection is valid. If it's not we'll close it down and continue + // on to get a new one. + try { + mConnection.executeSimpleCommand("NOOP"); + return; + } + catch (IOException ioe) { + ioExceptionHandler(mConnection, ioe); + } + } + synchronized (this) { + mConnection = getConnection(); + } + // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk + // $MDNSent) + // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft + // NonJunk $MDNSent \*)] Flags permitted. + // * 23 EXISTS + // * 0 RECENT + // * OK [UIDVALIDITY 1125022061] UIDs valid + // * OK [UIDNEXT 57576] Predicted next UID + // 2 OK [READ-WRITE] Select completed. + try { + List responses = mConnection.executeSimpleCommand( + String.format("SELECT \"%s\"", + encodeFolderName(mName))); + /* + * If the command succeeds we expect the folder has been opened read-write + * unless we are notified otherwise in the responses. + */ + mMode = OpenMode.READ_WRITE; + + for (ImapResponse response : responses) { + if (response.mTag == null && response.get(1).equals("EXISTS")) { + mMessageCount = response.getNumber(0); + } + else if (response.mTag != null && response.size() >= 2) { + if ("[READ-ONLY]".equalsIgnoreCase(response.getString(1))) { + mMode = OpenMode.READ_ONLY; + } + else if ("[READ-WRITE]".equalsIgnoreCase(response.getString(1))) { + mMode = OpenMode.READ_WRITE; + } + } + } + + if (mMessageCount == -1) { + throw new MessagingException( + "Did not find message count during select"); + } + mExists = true; + + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + public boolean isOpen() { + return mConnection != null; + } + + @Override + public OpenMode getMode() throws MessagingException { + return mMode; + } + + public void close(boolean expunge) { + if (!isOpen()) { + return; + } + // TODO implement expunge + mMessageCount = -1; + synchronized (this) { + releaseConnection(mConnection); + mConnection = null; + } + } + + public String getName() { + return mName; + } + + public boolean exists() throws MessagingException { + if (mExists) { + return true; + } + /* + * This method needs to operate in the unselected mode as well as the selected mode + * so we must get the connection ourselves if it's not there. We are specifically + * not calling checkOpen() since we don't care if the folder is open. + */ + ImapConnection connection = null; + synchronized(this) { + if (mConnection == null) { + connection = getConnection(); + } + else { + connection = mConnection; + } + } + try { + connection.executeSimpleCommand(String.format("STATUS \"%s\" (UIDVALIDITY)", + encodeFolderName(mName))); + mExists = true; + return true; + } + catch (MessagingException me) { + return false; + } + catch (IOException ioe) { + throw ioExceptionHandler(connection, ioe); + } + finally { + if (mConnection == null) { + releaseConnection(connection); + } + } + } + + public boolean create(FolderType type) throws MessagingException { + /* + * This method needs to operate in the unselected mode as well as the selected mode + * so we must get the connection ourselves if it's not there. We are specifically + * not calling checkOpen() since we don't care if the folder is open. + */ + ImapConnection connection = null; + synchronized(this) { + if (mConnection == null) { + connection = getConnection(); + } + else { + connection = mConnection; + } + } + try { + connection.executeSimpleCommand(String.format("CREATE \"%s\"", + encodeFolderName(mName))); + return true; + } + catch (MessagingException me) { + return false; + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + finally { + if (mConnection == null) { + releaseConnection(connection); + } + } + } + + @Override + public void copyMessages(Message[] messages, Folder folder) throws MessagingException { + checkOpen(); + String[] uids = new String[messages.length]; + for (int i = 0, count = messages.length; i < count; i++) { + uids[i] = messages[i].getUid(); + } + try { + mConnection.executeSimpleCommand(String.format("UID COPY %s \"%s\"", + Utility.combine(uids, ','), + encodeFolderName(folder.getName()))); + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + @Override + public int getMessageCount() { + return mMessageCount; + } + + @Override + public int getUnreadMessageCount() throws MessagingException { + checkOpen(); + try { + int unreadMessageCount = 0; + List responses = mConnection.executeSimpleCommand( + String.format("STATUS \"%s\" (UNSEEN)", + encodeFolderName(mName))); + for (ImapResponse response : responses) { + if (response.mTag == null && response.get(0).equals("STATUS")) { + ImapList status = response.getList(2); + unreadMessageCount = status.getKeyedNumber("UNSEEN"); + } + } + return unreadMessageCount; + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + @Override + public void delete(boolean recurse) throws MessagingException { + throw new Error("ImapStore.delete() not yet implemented"); + } + + @Override + public Message getMessage(String uid) throws MessagingException { + checkOpen(); + + try { + try { + List responses = + mConnection.executeSimpleCommand(String.format("UID SEARCH UID %S", uid)); + for (ImapResponse response : responses) { + if (response.mTag == null && response.get(0).equals("SEARCH")) { + for (int i = 1, count = response.size(); i < count; i++) { + if (uid.equals(response.get(i))) { + return new ImapMessage(uid, this); + } + } + } + } + } + catch (MessagingException me) { + return null; + } + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + return null; + } + + @Override + public Message[] getMessages(int start, int end, MessageRetrievalListener listener) + throws MessagingException { + if (start < 1 || end < 1 || end < start) { + throw new MessagingException( + String.format("Invalid message set %d %d", + start, end)); + } + checkOpen(); + ArrayList messages = new ArrayList(); + try { + ArrayList uids = new ArrayList(); + List responses = mConnection + .executeSimpleCommand(String.format("UID SEARCH %d:%d NOT DELETED", start, end)); + for (ImapResponse response : responses) { + if (response.get(0).equals("SEARCH")) { + for (int i = 1, count = response.size(); i < count; i++) { + uids.add(response.getString(i)); + } + } + } + for (int i = 0, count = uids.size(); i < count; i++) { + if (listener != null) { + listener.messageStarted(uids.get(i), i, count); + } + ImapMessage message = new ImapMessage(uids.get(i), this); + messages.add(message); + if (listener != null) { + listener.messageFinished(message, i, count); + } + } + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + return messages.toArray(new Message[] {}); + } + + public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + return getMessages(null, listener); + } + + public Message[] getMessages(String[] uids, MessageRetrievalListener listener) + throws MessagingException { + checkOpen(); + ArrayList messages = new ArrayList(); + try { + if (uids == null) { + List responses = mConnection + .executeSimpleCommand("UID SEARCH 1:* NOT DELETED"); + ArrayList tempUids = new ArrayList(); + for (ImapResponse response : responses) { + if (response.get(0).equals("SEARCH")) { + for (int i = 1, count = response.size(); i < count; i++) { + tempUids.add(response.getString(i)); + } + } + } + uids = tempUids.toArray(new String[] {}); + } + for (int i = 0, count = uids.length; i < count; i++) { + if (listener != null) { + listener.messageStarted(uids[i], i, count); + } + ImapMessage message = new ImapMessage(uids[i], this); + messages.add(message); + if (listener != null) { + listener.messageFinished(message, i, count); + } + } + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + return messages.toArray(new Message[] {}); + } + + public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + throws MessagingException { + if (messages == null || messages.length == 0) { + return; + } + checkOpen(); + String[] uids = new String[messages.length]; + HashMap messageMap = new HashMap(); + for (int i = 0, count = messages.length; i < count; i++) { + uids[i] = messages[i].getUid(); + messageMap.put(uids[i], messages[i]); + } + + /* + * Figure out what command we are going to run: + * Flags - UID FETCH (FLAGS) + * Envelope - UID FETCH ([FLAGS] INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]) + * + */ + LinkedHashSet fetchFields = new LinkedHashSet(); + fetchFields.add("UID"); + if (fp.contains(FetchProfile.Item.FLAGS)) { + fetchFields.add("FLAGS"); + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + fetchFields.add("INTERNALDATE"); + fetchFields.add("RFC822.SIZE"); + fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]"); + } + if (fp.contains(FetchProfile.Item.STRUCTURE)) { + fetchFields.add("BODYSTRUCTURE"); + } + if (fp.contains(FetchProfile.Item.BODY_SANE)) { + fetchFields.add(String.format("BODY.PEEK[]<0.%d>", FETCH_BODY_SANE_SUGGESTED_SIZE)); + } + if (fp.contains(FetchProfile.Item.BODY)) { + fetchFields.add("BODY.PEEK[]"); + } + for (Object o : fp) { + if (o instanceof Part) { + Part part = (Part) o; + String partId = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0]; + fetchFields.add("BODY.PEEK[" + partId + "]"); + } + } + + try { + String tag = mConnection.sendCommand(String.format("UID FETCH %s (%s)", + Utility.combine(uids, ','), + Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') + ), false); + ImapResponse response; + int messageNumber = 0; + do { + response = mConnection.readResponse(); + + if (response.mTag == null && response.get(1).equals("FETCH")) { + ImapList fetchList = (ImapList)response.getKeyedValue("FETCH"); + String uid = fetchList.getKeyedString("UID"); + + Message message = messageMap.get(uid); + + if (listener != null) { + listener.messageStarted(uid, messageNumber++, messageMap.size()); + } + + if (fp.contains(FetchProfile.Item.FLAGS)) { + ImapList flags = fetchList.getKeyedList("FLAGS"); + ImapMessage imapMessage = (ImapMessage) message; + if (flags != null) { + for (int i = 0, count = flags.size(); i < count; i++) { + String flag = flags.getString(i); + if (flag.equals("\\Deleted")) { + imapMessage.setFlagInternal(Flag.DELETED, true); + } + else if (flag.equals("\\Answered")) { + imapMessage.setFlagInternal(Flag.ANSWERED, true); + } + else if (flag.equals("\\Seen")) { + imapMessage.setFlagInternal(Flag.SEEN, true); + } + else if (flag.equals("\\Flagged")) { + imapMessage.setFlagInternal(Flag.FLAGGED, true); + } + } + } + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + Date internalDate = fetchList.getKeyedDate("INTERNALDATE"); + int size = fetchList.getKeyedNumber("RFC822.SIZE"); + InputStream headerStream = fetchList.getLiteral(fetchList.size() - 1); + + ImapMessage imapMessage = (ImapMessage) message; + + message.setInternalDate(internalDate); + imapMessage.setSize(size); + imapMessage.parse(headerStream); + } + if (fp.contains(FetchProfile.Item.STRUCTURE)) { + ImapList bs = fetchList.getKeyedList("BODYSTRUCTURE"); + if (bs != null) { + try { + parseBodyStructure(bs, message, "TEXT"); + } + catch (MessagingException e) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "Error handling message", e); + } + message.setBody(null); + } + } + } + if (fp.contains(FetchProfile.Item.BODY)) { + InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); + ImapMessage imapMessage = (ImapMessage) message; + imapMessage.parse(bodyStream); + } + if (fp.contains(FetchProfile.Item.BODY_SANE)) { + InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); + ImapMessage imapMessage = (ImapMessage) message; + imapMessage.parse(bodyStream); + } + for (Object o : fp) { + if (o instanceof Part) { + Part part = (Part) o; + InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); + String contentType = part.getContentType(); + String contentTransferEncoding = part.getHeader( + MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; + part.setBody(MimeUtility.decodeBody( + bodyStream, + contentTransferEncoding)); + } + } + + if (listener != null) { + listener.messageFinished(message, messageNumber, messageMap.size()); + } + } + + while (response.more()); + + } while (response.mTag == null); + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + @Override + public Flag[] getPermanentFlags() throws MessagingException { + return PERMANENT_FLAGS; + } + + /** + * Handle any untagged responses that the caller doesn't care to handle themselves. + * @param responses + */ + private void handleUntaggedResponses(List responses) { + for (ImapResponse response : responses) { + handleUntaggedResponse(response); + } + } + + /** + * Handle an untagged response that the caller doesn't care to handle themselves. + * @param response + */ + private void handleUntaggedResponse(ImapResponse response) { + if (response.mTag == null && response.get(1).equals("EXISTS")) { + mMessageCount = response.getNumber(0); + } + } + + private void parseBodyStructure(ImapList bs, Part part, String id) + throws MessagingException { + if (bs.get(0) instanceof ImapList) { + /* + * This is a multipart/* + */ + MimeMultipart mp = new MimeMultipart(); + for (int i = 0, count = bs.size(); i < count; i++) { + if (bs.get(i) instanceof ImapList) { + /* + * For each part in the message we're going to add a new BodyPart and parse + * into it. + */ + ImapBodyPart bp = new ImapBodyPart(); + if (id.equals("TEXT")) { + parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1)); + } + else { + parseBodyStructure(bs.getList(i), bp, id + "." + (i + 1)); + } + mp.addBodyPart(bp); + } + else { + /* + * We've got to the end of the children of the part, so now we can find out + * what type it is and bail out. + */ + String subType = bs.getString(i); + mp.setSubType(subType.toLowerCase()); + break; + } + } + part.setBody(mp); + } + else{ + /* + * This is a body. We need to add as much information as we can find out about + * it to the Part. + */ + + /* + body type + body subtype + body parameter parenthesized list + body id + body description + body encoding + body size + */ + + + String type = bs.getString(0); + String subType = bs.getString(1); + String mimeType = (type + "/" + subType).toLowerCase(); + + ImapList bodyParams = null; + if (bs.get(2) instanceof ImapList) { + bodyParams = bs.getList(2); + } + String encoding = bs.getString(5); + int size = bs.getNumber(6); + + if (MimeUtility.mimeTypeMatches(mimeType, "message/rfc822")) { +// A body type of type MESSAGE and subtype RFC822 +// contains, immediately after the basic fields, the +// envelope structure, body structure, and size in +// text lines of the encapsulated message. +// [MESSAGE, RFC822, [NAME, Fwd: [#HTR-517941]: update plans at 1am Friday - Memory allocation - displayware.eml], NIL, NIL, 7BIT, 5974, NIL, [INLINE, [FILENAME*0, Fwd: [#HTR-517941]: update plans at 1am Friday - Memory all, FILENAME*1, ocation - displayware.eml]], NIL] + /* + * This will be caught by fetch and handled appropriately. + */ + throw new MessagingException("BODYSTRUCTURE message/rfc822 not yet supported."); + } + + /* + * Set the content type with as much information as we know right now. + */ + String contentType = String.format("%s", mimeType); + + if (bodyParams != null) { + /* + * If there are body params we might be able to get some more information out + * of them. + */ + for (int i = 0, count = bodyParams.size(); i < count; i += 2) { + contentType += String.format(";\n %s=\"%s\"", + bodyParams.getString(i), + bodyParams.getString(i + 1)); + } + } + + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + + // Extension items + ImapList bodyDisposition = null; + if (("text".equalsIgnoreCase(type)) + && (bs.size() > 8) + && (bs.get(9) instanceof ImapList)) { + bodyDisposition = bs.getList(9); + } + else if (!("text".equalsIgnoreCase(type)) + && (bs.size() > 7) + && (bs.get(8) instanceof ImapList)) { + bodyDisposition = bs.getList(8); + } + + String contentDisposition = ""; + + if (bodyDisposition != null && bodyDisposition.size() > 0) { + if (!"NIL".equalsIgnoreCase(bodyDisposition.getString(0))) { + contentDisposition = bodyDisposition.getString(0).toLowerCase(); + } + + if ((bodyDisposition.size() > 1) + && (bodyDisposition.get(1) instanceof ImapList)) { + ImapList bodyDispositionParams = bodyDisposition.getList(1); + /* + * If there is body disposition information we can pull some more information + * about the attachment out. + */ + for (int i = 0, count = bodyDispositionParams.size(); i < count; i += 2) { + contentDisposition += String.format(";\n %s=\"%s\"", + bodyDispositionParams.getString(i).toLowerCase(), + bodyDispositionParams.getString(i + 1)); + } + } + } + + if (MimeUtility.getHeaderParameter(contentDisposition, "size") == null) { + contentDisposition += String.format(";\n size=%d", size); + } + + /* + * Set the content disposition containing at least the size. Attachment + * handling code will use this down the road. + */ + part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition); + + + /* + * Set the Content-Transfer-Encoding header. Attachment code will use this + * to parse the body. + */ + part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + + if (part instanceof ImapMessage) { + ((ImapMessage) part).setSize(size); + } + else if (part instanceof ImapBodyPart) { + ((ImapBodyPart) part).setSize(size); + } + else { + throw new MessagingException("Unknown part type " + part.toString()); + } + part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); + } + + } + + /** + * Appends the given messages to the selected folder. This implementation also determines + * the new UID of the given message on the IMAP server and sets the Message's UID to the + * new server UID. + */ + public void appendMessages(Message[] messages) throws MessagingException { + checkOpen(); + try { + for (Message message : messages) { + CountingOutputStream out = new CountingOutputStream(); + EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); + message.writeTo(eolOut); + eolOut.flush(); + mConnection.sendCommand( + String.format("APPEND \"%s\" {%d}", + encodeFolderName(mName), + out.getCount()), false); + ImapResponse response; + do { + response = mConnection.readResponse(); + if (response.mCommandContinuationRequested) { + eolOut = new EOLConvertingOutputStream(mConnection.mOut); + message.writeTo(eolOut); + eolOut.write('\r'); + eolOut.write('\n'); + eolOut.flush(); + } + else if (response.mTag == null) { + handleUntaggedResponse(response); + } + while (response.more()); + } while(response.mTag == null); + + /* + * Try to find the UID of the message we just appended using the + * Message-ID header. + */ + String[] messageIdHeader = message.getHeader("Message-ID"); + if (messageIdHeader == null || messageIdHeader.length == 0) { + continue; + } + String messageId = messageIdHeader[0]; + List responses = + mConnection.executeSimpleCommand( + String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId)); + for (ImapResponse response1 : responses) { + if (response1.mTag == null && response1.get(0).equals("SEARCH") + && response1.size() > 1) { + message.setUid(response1.getString(1)); + } + } + + } + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + public Message[] expunge() throws MessagingException { + checkOpen(); + try { + handleUntaggedResponses(mConnection.executeSimpleCommand("EXPUNGE")); + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + return null; + } + + public void setFlags(Message[] messages, Flag[] flags, boolean value) + throws MessagingException { + checkOpen(); + String[] uids = new String[messages.length]; + for (int i = 0, count = messages.length; i < count; i++) { + uids[i] = messages[i].getUid(); + } + ArrayList flagNames = new ArrayList(); + for (int i = 0, count = flags.length; i < count; i++) { + Flag flag = flags[i]; + if (flag == Flag.SEEN) { + flagNames.add("\\Seen"); + } + else if (flag == Flag.DELETED) { + flagNames.add("\\Deleted"); + } + } + try { + mConnection.executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", + Utility.combine(uids, ','), + value ? "+" : "-", + Utility.combine(flagNames.toArray(new String[flagNames.size()]), ' '))); + } + catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } + } + + private void checkOpen() throws MessagingException { + if (!isOpen()) { + throw new MessagingException("Folder " + mName + " is not open."); + } + } + + private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) + throws MessagingException { + connection.close(); + close(false); + return new MessagingException("IO Error", ioe); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ImapFolder) { + return ((ImapFolder)o).mName.equals(mName); + } + return super.equals(o); + } + } + + /** + * A cacheable class that stores the details for a single IMAP connection. + */ + class ImapConnection { + private Socket mSocket; + private PeekableInputStream mIn; + private OutputStream mOut; + private ImapResponseParser mParser; + private int mNextCommandTag; + + public void open() throws IOException, MessagingException { + if (isOpen()) { + return; + } + + mNextCommandTag = 1; + + try { + SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); + if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || + mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + } else { + mSocket = new Socket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + } + + mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + + mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), + 1024)); + mParser = new ImapResponseParser(mIn); + mOut = mSocket.getOutputStream(); + + // BANNER + mParser.readResponse(); + + if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL + || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + // CAPABILITY + List responses = executeSimpleCommand("CAPABILITY"); + if (responses.size() != 2) { + throw new MessagingException("Invalid CAPABILITY response received"); + } + if (responses.get(0).contains("STARTTLS")) { + // STARTTLS + executeSimpleCommand("STARTTLS"); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, + true); + mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + mIn = new PeekableInputStream(new BufferedInputStream(mSocket + .getInputStream(), 1024)); + mParser = new ImapResponseParser(mIn); + mOut = mSocket.getOutputStream(); + } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + throw new MessagingException("TLS not supported but required"); + } + } + + mOut = new BufferedOutputStream(mOut); + + try { + // TODO eventually we need to add additional authentication + // options such as SASL + executeSimpleCommand("LOGIN " + mUsername + " " + mPassword, true); + } catch (ImapException ie) { + throw new AuthenticationFailedException(ie.getAlertText(), ie); + + } catch (MessagingException me) { + throw new AuthenticationFailedException(null, me); + } + } catch (SSLException e) { + throw new CertificateValidationException(e.getMessage(), e); + } catch (GeneralSecurityException gse) { + throw new MessagingException( + "Unable to open connection to IMAP server due to security error.", gse); + } + } + + public boolean isOpen() { + return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket + .isClosed()); + } + + public void close() { +// if (isOpen()) { +// try { +// executeSimpleCommand("LOGOUT"); +// } catch (Exception e) { +// +// } +// } + try { + mIn.close(); + } catch (Exception e) { + + } + try { + mOut.close(); + } catch (Exception e) { + + } + try { + mSocket.close(); + } catch (Exception e) { + + } + mIn = null; + mOut = null; + mSocket = null; + } + + public ImapResponse readResponse() throws IOException, MessagingException { + return mParser.readResponse(); + } + + public String sendCommand(String command, boolean sensitive) + throws MessagingException, IOException { + open(); + String tag = Integer.toString(mNextCommandTag++); + String commandToSend = tag + " " + command; + mOut.write(commandToSend.getBytes()); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + if (Config.LOGD) { + if (k9.DEBUG) { + if (sensitive && !k9.DEBUG_SENSITIVE) { + Log.d(k9.LOG_TAG, ">>> " + + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); + } else { + Log.d(k9.LOG_TAG, ">>> " + commandToSend); + } + } + } + return tag; + } + + public List executeSimpleCommand(String command) throws IOException, + ImapException, MessagingException { + return executeSimpleCommand(command, false); + } + + public List executeSimpleCommand(String command, boolean sensitive) + throws IOException, ImapException, MessagingException { + String tag = sendCommand(command, sensitive); + ArrayList responses = new ArrayList(); + ImapResponse response; + do { + response = mParser.readResponse(); + responses.add(response); + } while (response.mTag == null); + if (response.size() < 1 || !response.get(0).equals("OK")) { + throw new ImapException(response.toString(), response.getAlertText()); + } + return responses; + } + } + + class ImapMessage extends MimeMessage { + ImapMessage(String uid, Folder folder) throws MessagingException { + this.mUid = uid; + this.mFolder = folder; + } + + public void setSize(int size) { + this.mSize = size; + } + + public void parse(InputStream in) throws IOException, MessagingException { + super.parse(in); + } + + public void setFlagInternal(Flag flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + } + + @Override + public void setFlag(Flag flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); + } + } + + class ImapBodyPart extends MimeBodyPart { + public ImapBodyPart() throws MessagingException { + super(); + } + + public void setSize(int size) { + this.mSize = size; + } + } + + class ImapException extends MessagingException { + String mAlertText; + + public ImapException(String message, String alertText, Throwable throwable) { + super(message, throwable); + this.mAlertText = alertText; + } + + public ImapException(String message, String alertText) { + super(message); + this.mAlertText = alertText; + } + + public String getAlertText() { + return mAlertText; + } + + public void setAlertText(String alertText) { + mAlertText = alertText; + } + } +} diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java new file mode 100644 index 000000000..d97f0b293 --- /dev/null +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -0,0 +1,1187 @@ + +package com.fsck.k9.mail.store; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Date; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; + +import android.app.Application; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.Utility; +import com.fsck.k9.codec.binary.Base64OutputStream; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.MimeUtility; +import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.provider.AttachmentProvider; + +/** + *
+ * Implements a SQLite database backed local store for Messages.
+ * 
+ */ +public class LocalStore extends Store { + private static final int DB_VERSION = 18; + private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN }; + + private String mPath; + private SQLiteDatabase mDb; + private File mAttachmentsDir; + private Application mApplication; + + /** + * @param uri local://localhost/path/to/database/uuid.db + */ + public LocalStore(String _uri, Application application) throws MessagingException { + mApplication = application; + URI uri = null; + try { + uri = new URI(_uri); + } catch (Exception e) { + throw new MessagingException("Invalid uri for LocalStore"); + } + if (!uri.getScheme().equals("local")) { + throw new MessagingException("Invalid scheme"); + } + mPath = uri.getPath(); + + File parentDir = new File(mPath).getParentFile(); + if (!parentDir.exists()) { + parentDir.mkdirs(); + } + mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null); + if (mDb.getVersion() != DB_VERSION) { + doDbUpgrade(mDb); + } + + + mAttachmentsDir = new File(mPath + "_att"); + if (!mAttachmentsDir.exists()) { + mAttachmentsDir.mkdirs(); + } + } + + + private void doDbUpgrade ( SQLiteDatabase mDb) { + + if (mDb.getVersion() < 18) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, String.format("Upgrading database from %d to %d", mDb + .getVersion(), 18)); + } + mDb.execSQL("DROP TABLE IF EXISTS folders"); + mDb.execSQL("CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, " + + "last_updated INTEGER, unread_count INTEGER, visible_limit INTEGER)"); + + mDb.execSQL("DROP TABLE IF EXISTS messages"); + mDb.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, folder_id INTEGER, uid TEXT, subject TEXT, " + + "date INTEGER, flags TEXT, sender_list TEXT, to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, " + + "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER)"); + + mDb.execSQL("DROP TABLE IF EXISTS attachments"); + mDb.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," + + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," + + "mime_type TEXT)"); + + mDb.execSQL("DROP TABLE IF EXISTS pending_commands"); + mDb.execSQL("CREATE TABLE pending_commands " + + "(id INTEGER PRIMARY KEY, command TEXT, arguments TEXT)"); + + mDb.execSQL("DROP TRIGGER IF EXISTS delete_folder"); + mDb.execSQL("CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN DELETE FROM messages WHERE old.id = folder_id; END;"); + + mDb.execSQL("DROP TRIGGER IF EXISTS delete_message"); + mDb.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; END;"); + mDb.setVersion(18); + } + if (mDb.getVersion() != DB_VERSION) { + throw new Error("Database upgrade failed!"); + } + } + + @Override + public Folder getFolder(String name) throws MessagingException { + return new LocalFolder(name); + } + + // TODO this takes about 260-300ms, seems slow. + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + ArrayList folders = new ArrayList(); + Cursor cursor = null; + try { + cursor = mDb.rawQuery("SELECT name FROM folders", null); + while (cursor.moveToNext()) { + folders.add(new LocalFolder(cursor.getString(0))); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + return folders.toArray(new Folder[] {}); + } + + @Override + public void checkSettings() throws MessagingException { + } + + /** + * Delete the entire Store and it's backing database. + */ + public void delete() { + try { + mDb.close(); + } catch (Exception e) { + + } + try{ + File[] attachments = mAttachmentsDir.listFiles(); + for (File attachment : attachments) { + if (attachment.exists()) { + attachment.delete(); + } + } + if (mAttachmentsDir.exists()) { + mAttachmentsDir.delete(); + } + } + catch (Exception e) { + } + try { + new File(mPath).delete(); + } + catch (Exception e) { + + } + } + + /** + * Deletes all cached attachments for the entire store. + */ + public void pruneCachedAttachments() throws MessagingException { + File[] files = mAttachmentsDir.listFiles(); + for (File file : files) { + if (file.exists()) { + try { + Cursor cursor = null; + try { + cursor = mDb.query( + "attachments", + new String[] { "store_data" }, + "id = ?", + new String[] { file.getName() }, + null, + null, + null); + if (cursor.moveToNext()) { + if (cursor.getString(0) == null) { + /* + * If the attachment has no store data it is not recoverable, so + * we won't delete it. + */ + continue; + } + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + ContentValues cv = new ContentValues(); + cv.putNull("content_uri"); + mDb.update("attachments", cv, "id = ?", new String[] { file.getName() }); + } + catch (Exception e) { + /* + * If the row has gone away before we got to mark it not-downloaded that's + * okay. + */ + } + if (!file.delete()) { + file.deleteOnExit(); + } + } + } + } + + public void resetVisibleLimits() { + ContentValues cv = new ContentValues(); + cv.put("visible_limit", Integer.toString(k9.DEFAULT_VISIBLE_LIMIT)); + mDb.update("folders", cv, null, null); + } + + public ArrayList getPendingCommands() { + Cursor cursor = null; + try { + cursor = mDb.query("pending_commands", + new String[] { "id", "command", "arguments" }, + null, + null, + null, + null, + "id ASC"); + ArrayList commands = new ArrayList(); + while (cursor.moveToNext()) { + PendingCommand command = new PendingCommand(); + command.mId = cursor.getLong(0); + command.command = cursor.getString(1); + String arguments = cursor.getString(2); + command.arguments = arguments.split(","); + for (int i = 0; i < command.arguments.length; i++) { + command.arguments[i] = Utility.fastUrlDecode(command.arguments[i]); + } + commands.add(command); + } + return commands; + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public void addPendingCommand(PendingCommand command) { + try { + for (int i = 0; i < command.arguments.length; i++) { + command.arguments[i] = URLEncoder.encode(command.arguments[i], "UTF-8"); + } + ContentValues cv = new ContentValues(); + cv.put("command", command.command); + cv.put("arguments", Utility.combine(command.arguments, ',')); + mDb.insert("pending_commands", "command", cv); + } + catch (UnsupportedEncodingException usee) { + throw new Error("Aparently UTF-8 has been lost to the annals of history."); + } + } + + public void removePendingCommand(PendingCommand command) { + mDb.delete("pending_commands", "id = ?", new String[] { Long.toString(command.mId) }); + } + + public static class PendingCommand { + private long mId; + public String command; + public String[] arguments; + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(command); + sb.append("\n"); + for (String argument : arguments) { + sb.append(" "); + sb.append(argument); + sb.append("\n"); + } + return sb.toString(); + } + } + + public class LocalFolder extends Folder { + private String mName; + private long mFolderId = -1; + private int mUnreadMessageCount = -1; + private int mVisibleLimit = -1; + + public LocalFolder(String name) { + this.mName = name; + } + + public long getId() { + return mFolderId; + } + + @Override + public void open(OpenMode mode) throws MessagingException { + if (isOpen()) { + return; + } + if (!exists()) { + create(FolderType.HOLDS_MESSAGES); + } + Cursor cursor = null; + try { + cursor = mDb.rawQuery("SELECT id, unread_count, visible_limit FROM folders " + + "where folders.name = ?", + new String[] { + mName + }); + cursor.moveToFirst(); + mFolderId = cursor.getInt(0); + mUnreadMessageCount = cursor.getInt(1); + mVisibleLimit = cursor.getInt(2); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public boolean isOpen() { + return mFolderId != -1; + } + + @Override + public OpenMode getMode() throws MessagingException { + return OpenMode.READ_WRITE; + } + + @Override + public String getName() { + return mName; + } + + @Override + public boolean exists() throws MessagingException { + return Utility.arrayContains(getPersonalNamespaces(), this); + } + + @Override + public boolean create(FolderType type) throws MessagingException { + if (exists()) { + throw new MessagingException("Folder " + mName + " already exists."); + } + mDb.execSQL("INSERT INTO folders (name, visible_limit) VALUES (?, ?)", new Object[] { + mName, + k9.DEFAULT_VISIBLE_LIMIT + }); + return true; + } + + @Override + public void close(boolean expunge) throws MessagingException { + if (expunge) { + expunge(); + } + mFolderId = -1; + } + + @Override + public int getMessageCount() throws MessagingException { + open(OpenMode.READ_WRITE); + Cursor cursor = null; + try { + cursor = mDb.rawQuery("SELECT COUNT(*) FROM messages WHERE messages.folder_id = ?", + new String[] { + Long.toString(mFolderId) + }); + cursor.moveToFirst(); + int messageCount = cursor.getInt(0); + return messageCount; + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public int getUnreadMessageCount() throws MessagingException { + open(OpenMode.READ_WRITE); + return mUnreadMessageCount; + } + + + public void setUnreadMessageCount(int unreadMessageCount) throws MessagingException { + open(OpenMode.READ_WRITE); + mUnreadMessageCount = Math.max(0, unreadMessageCount); + mDb.execSQL("UPDATE folders SET unread_count = ? WHERE id = ?", + new Object[] { mUnreadMessageCount, mFolderId }); + } + + public int getVisibleLimit() throws MessagingException { + open(OpenMode.READ_WRITE); + return mVisibleLimit; + } + + + public void setVisibleLimit(int visibleLimit) throws MessagingException { + open(OpenMode.READ_WRITE); + mVisibleLimit = visibleLimit; + mDb.execSQL("UPDATE folders SET visible_limit = ? WHERE id = ?", + new Object[] { mVisibleLimit, mFolderId }); + } + + + @Override + public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + throws MessagingException { + open(OpenMode.READ_WRITE); + if (fp.contains(FetchProfile.Item.BODY)) { + for (Message message : messages) { + LocalMessage localMessage = (LocalMessage)message; + Cursor cursor = null; + localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); + MimeMultipart mp = new MimeMultipart(); + mp.setSubType("mixed"); + localMessage.setBody(mp); + try { + cursor = mDb.rawQuery("SELECT html_content, text_content FROM messages " + + "WHERE id = ?", + new String[] { Long.toString(localMessage.mId) }); + cursor.moveToNext(); + String htmlContent = cursor.getString(0); + String textContent = cursor.getString(1); + + if (htmlContent != null) { + TextBody body = new TextBody(htmlContent); + MimeBodyPart bp = new MimeBodyPart(body, "text/html"); + mp.addBodyPart(bp); + } + + if (textContent != null) { + TextBody body = new TextBody(textContent); + MimeBodyPart bp = new MimeBodyPart(body, "text/plain"); + mp.addBodyPart(bp); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + + try { + cursor = mDb.query( + "attachments", + new String[] { + "id", + "size", + "name", + "mime_type", + "store_data", + "content_uri" }, + "message_id = ?", + new String[] { Long.toString(localMessage.mId) }, + null, + null, + null); + + while (cursor.moveToNext()) { + long id = cursor.getLong(0); + int size = cursor.getInt(1); + String name = cursor.getString(2); + String type = cursor.getString(3); + String storeData = cursor.getString(4); + String contentUri = cursor.getString(5); + Body body = null; + if (contentUri != null) { + body = new LocalAttachmentBody(Uri.parse(contentUri), mApplication); + } + MimeBodyPart bp = new LocalAttachmentBodyPart(body, id); + bp.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + String.format("%s;\n name=\"%s\"", + type, + name)); + bp.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + bp.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + String.format("attachment;\n filename=\"%s\";\n size=%d", + name, + size)); + + /* + * HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that + * we can later pull the attachment from the remote store if neccesary. + */ + bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData); + + mp.addBodyPart(bp); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + } + } + + private void populateMessageFromGetMessageCursor(LocalMessage message, Cursor cursor) + throws MessagingException{ + message.setSubject(cursor.getString(0) == null ? "" : cursor.getString(0)); + Address[] from = Address.unpack(cursor.getString(1)); + if (from.length > 0) { + message.setFrom(from[0]); + } + message.setSentDate(new Date(cursor.getLong(2))); + message.setUid(cursor.getString(3)); + String flagList = cursor.getString(4); + if (flagList != null && flagList.length() > 0) { + String[] flags = flagList.split(","); + try { + for (String flag : flags) { + message.setFlagInternal(Flag.valueOf(flag.toUpperCase()), true); + } + } catch (Exception e) { + } + } + message.mId = cursor.getLong(5); + message.setRecipients(RecipientType.TO, Address.unpack(cursor.getString(6))); + message.setRecipients(RecipientType.CC, Address.unpack(cursor.getString(7))); + message.setRecipients(RecipientType.BCC, Address.unpack(cursor.getString(8))); + message.setReplyTo(Address.unpack(cursor.getString(9))); + message.mAttachmentCount = cursor.getInt(10); + message.setInternalDate(new Date(cursor.getLong(11))); + } + + @Override + public Message[] getMessages(int start, int end, MessageRetrievalListener listener) + throws MessagingException { + open(OpenMode.READ_WRITE); + throw new MessagingException( + "LocalStore.getMessages(int, int, MessageRetrievalListener) not yet implemented"); + } + + @Override + public Message getMessage(String uid) throws MessagingException { + open(OpenMode.READ_WRITE); + LocalMessage message = new LocalMessage(uid, this); + Cursor cursor = null; + try { + cursor = mDb.rawQuery( + "SELECT subject, sender_list, date, uid, flags, id, to_list, cc_list, " + + "bcc_list, reply_to_list, attachment_count, internal_date " + + "FROM messages " + "WHERE uid = ? " + "AND folder_id = ?", + new String[] { + message.getUid(), Long.toString(mFolderId) + }); + if (!cursor.moveToNext()) { + return null; + } + populateMessageFromGetMessageCursor(message, cursor); + } + finally { + if (cursor != null) { + cursor.close(); + } + } + return message; + } + + @Override + public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + open(OpenMode.READ_WRITE); + ArrayList messages = new ArrayList(); + Cursor cursor = null; + try { + cursor = mDb.rawQuery( + "SELECT subject, sender_list, date, uid, flags, id, to_list, cc_list, " + + "bcc_list, reply_to_list, attachment_count, internal_date " + + "FROM messages " + "WHERE folder_id = ?", new String[] { + Long.toString(mFolderId) + }); + + while (cursor.moveToNext()) { + LocalMessage message = new LocalMessage(null, this); + populateMessageFromGetMessageCursor(message, cursor); + messages.add(message); + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + + return messages.toArray(new Message[] {}); + } + + @Override + public Message[] getMessages(String[] uids, MessageRetrievalListener listener) + throws MessagingException { + open(OpenMode.READ_WRITE); + if (uids == null) { + return getMessages(listener); + } + ArrayList messages = new ArrayList(); + for (String uid : uids) { + messages.add(getMessage(uid)); + } + return messages.toArray(new Message[] {}); + } + + @Override + public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { + if (!(folder instanceof LocalFolder)) { + throw new MessagingException("copyMessages called with incorrect Folder"); + } + ((LocalFolder) folder).appendMessages(msgs, true); + } + + /** + * The method differs slightly from the contract; If an incoming message already has a uid + * assigned and it matches the uid of an existing message then this message will replace the + * old message. It is implemented as a delete/insert. This functionality is used in saving + * of drafts and re-synchronization of updated server messages. + */ + @Override + public void appendMessages(Message[] messages) throws MessagingException { + appendMessages(messages, false); + } + + /** + * The method differs slightly from the contract; If an incoming message already has a uid + * assigned and it matches the uid of an existing message then this message will replace the + * old message. It is implemented as a delete/insert. This functionality is used in saving + * of drafts and re-synchronization of updated server messages. + */ + public void appendMessages(Message[] messages, boolean copy) throws MessagingException { + open(OpenMode.READ_WRITE); + for (Message message : messages) { + if (!(message instanceof MimeMessage)) { + throw new Error("LocalStore can only store Messages that extend MimeMessage"); + } + + String uid = message.getUid(); + if (uid == null) { + message.setUid("Local" + UUID.randomUUID().toString()); + } + else { + /* + * The message may already exist in this Folder, so delete it first. + */ + deleteAttachments(message.getUid()); + mDb.execSQL("DELETE FROM messages WHERE folder_id = ? AND uid = ?", + new Object[] { mFolderId, message.getUid() }); + } + + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + StringBuffer sbHtml = new StringBuffer(); + StringBuffer sbText = new StringBuffer(); + for (Part viewable : viewables) { + try { + String text = MimeUtility.getTextFromPart(viewable); + /* + * Anything with MIME type text/html will be stored as such. Anything + * else will be stored as text/plain. + */ + if (viewable.getMimeType().equalsIgnoreCase("text/html")) { + sbHtml.append(text); + } + else { + sbText.append(text); + } + } catch (Exception e) { + throw new MessagingException("Unable to get text for message part", e); + } + } + + try { + ContentValues cv = new ContentValues(); + cv.put("uid", message.getUid()); + cv.put("subject", message.getSubject()); + cv.put("sender_list", Address.pack(message.getFrom())); + cv.put("date", message.getSentDate() == null + ? System.currentTimeMillis() : message.getSentDate().getTime()); + cv.put("flags", Utility.combine(message.getFlags(), ',').toUpperCase()); + cv.put("folder_id", mFolderId); + cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); + cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); + cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); + cv.put("html_content", sbHtml.length() > 0 ? sbHtml.toString() : null); + cv.put("text_content", sbText.length() > 0 ? sbText.toString() : null); + cv.put("reply_to_list", Address.pack(message.getReplyTo())); + cv.put("attachment_count", attachments.size()); + cv.put("internal_date", message.getInternalDate() == null + ? System.currentTimeMillis() : message.getInternalDate().getTime()); + long messageId = mDb.insert("messages", "uid", cv); + for (Part attachment : attachments) { + saveAttachment(messageId, attachment, copy); + } + } catch (Exception e) { + throw new MessagingException("Error appending message", e); + } + } + } + + /** + * Update the given message in the LocalStore without first deleting the existing + * message (contrast with appendMessages). This method is used to store changes + * to the given message while updating attachments and not removing existing + * attachment data. + * TODO In the future this method should be combined with appendMessages since the Message + * contains enough data to decide what to do. + * @param message + * @throws MessagingException + */ + public void updateMessage(LocalMessage message) throws MessagingException { + open(OpenMode.READ_WRITE); + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + StringBuffer sbHtml = new StringBuffer(); + StringBuffer sbText = new StringBuffer(); + for (int i = 0, count = viewables.size(); i < count; i++) { + Part viewable = viewables.get(i); + try { + String text = MimeUtility.getTextFromPart(viewable); + /* + * Anything with MIME type text/html will be stored as such. Anything + * else will be stored as text/plain. + */ + if (viewable.getMimeType().equalsIgnoreCase("text/html")) { + sbHtml.append(text); + } + else { + sbText.append(text); + } + } catch (Exception e) { + throw new MessagingException("Unable to get text for message part", e); + } + } + + try { + mDb.execSQL("UPDATE messages SET " + + "uid = ?, subject = ?, sender_list = ?, date = ?, flags = ?, " + + "folder_id = ?, to_list = ?, cc_list = ?, bcc_list = ?, " + + "html_content = ?, text_content = ?, reply_to_list = ?, " + + "attachment_count = ? WHERE id = ?", + new Object[] { + message.getUid(), + message.getSubject(), + Address.pack(message.getFrom()), + message.getSentDate() == null ? System + .currentTimeMillis() : message.getSentDate() + .getTime(), + Utility.combine(message.getFlags(), ',').toUpperCase(), + mFolderId, + Address.pack(message + .getRecipients(RecipientType.TO)), + Address.pack(message + .getRecipients(RecipientType.CC)), + Address.pack(message + .getRecipients(RecipientType.BCC)), + sbHtml.length() > 0 ? sbHtml.toString() : null, + sbText.length() > 0 ? sbText.toString() : null, + Address.pack(message.getReplyTo()), + attachments.size(), + message.mId + }); + + for (int i = 0, count = attachments.size(); i < count; i++) { + Part attachment = attachments.get(i); + saveAttachment(message.mId, attachment, false); + } + } catch (Exception e) { + throw new MessagingException("Error appending message", e); + } + } + + /** + * @param messageId + * @param attachment + * @param attachmentId -1 to create a new attachment or >= 0 to update an existing + * @throws IOException + * @throws MessagingException + */ + private void saveAttachment(long messageId, Part attachment, boolean saveAsNew) + throws IOException, MessagingException { + long attachmentId = -1; + Uri contentUri = null; + int size = -1; + File tempAttachmentFile = null; + + if ((!saveAsNew) && (attachment instanceof LocalAttachmentBodyPart)) { + attachmentId = ((LocalAttachmentBodyPart) attachment).getAttachmentId(); + } + + if (attachment.getBody() != null) { + Body body = attachment.getBody(); + if (body instanceof LocalAttachmentBody) { + contentUri = ((LocalAttachmentBody) body).getContentUri(); + } + else { + /* + * If the attachment has a body we're expected to save it into the local store + * so we copy the data into a cached attachment file. + */ + InputStream in = attachment.getBody().getInputStream(); + tempAttachmentFile = File.createTempFile("att", null, mAttachmentsDir); + FileOutputStream out = new FileOutputStream(tempAttachmentFile); + size = IOUtils.copy(in, out); + in.close(); + out.close(); + } + } + + if (size == -1) { + /* + * If the attachment is not yet downloaded see if we can pull a size + * off the Content-Disposition. + */ + String disposition = attachment.getDisposition(); + if (disposition != null) { + String s = MimeUtility.getHeaderParameter(disposition, "size"); + if (s != null) { + size = Integer.parseInt(s); + } + } + } + if (size == -1) { + size = 0; + } + + String storeData = + Utility.combine(attachment.getHeader( + MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ','); + + String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name"); + + if (attachmentId == -1) { + ContentValues cv = new ContentValues(); + cv.put("message_id", messageId); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + cv.put("store_data", storeData); + cv.put("size", size); + cv.put("name", name); + cv.put("mime_type", attachment.getMimeType()); + + attachmentId = mDb.insert("attachments", "message_id", cv); + } + else { + ContentValues cv = new ContentValues(); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + cv.put("size", size); + mDb.update( + "attachments", + cv, + "id = ?", + new String[] { Long.toString(attachmentId) }); + } + + if (tempAttachmentFile != null) { + File attachmentFile = new File(mAttachmentsDir, Long.toString(attachmentId)); + tempAttachmentFile.renameTo(attachmentFile); + contentUri = AttachmentProvider.getAttachmentUri( + new File(mPath).getName(), + attachmentId); + attachment.setBody(new LocalAttachmentBody(contentUri, mApplication)); + ContentValues cv = new ContentValues(); + cv.put("content_uri", contentUri != null ? contentUri.toString() : null); + mDb.update( + "attachments", + cv, + "id = ?", + new String[] { Long.toString(attachmentId) }); + } + + if (attachment instanceof LocalAttachmentBodyPart) { + ((LocalAttachmentBodyPart) attachment).setAttachmentId(attachmentId); + } + } + + /** + * Changes the stored uid of the given message (using it's internal id as a key) to + * the uid in the message. + * @param message + */ + public void changeUid(LocalMessage message) throws MessagingException { + open(OpenMode.READ_WRITE); + ContentValues cv = new ContentValues(); + cv.put("uid", message.getUid()); + mDb.update("messages", cv, "id = ?", new String[] { Long.toString(message.mId) }); + } + + @Override + public void setFlags(Message[] messages, Flag[] flags, boolean value) + throws MessagingException { + open(OpenMode.READ_WRITE); + for (Message message : messages) { + message.setFlags(flags, value); + } + } + + @Override + public Message[] expunge() throws MessagingException { + open(OpenMode.READ_WRITE); + ArrayList expungedMessages = new ArrayList(); + /* + * epunge() doesn't do anything because deleted messages are saved for their uids + * and really, really deleted messages are "Destroyed" and removed immediately. + */ + return expungedMessages.toArray(new Message[] {}); + } + + @Override + public void delete(boolean recurse) throws MessagingException { + // We need to open the folder first to make sure we've got it's id + open(OpenMode.READ_ONLY); + Message[] messages = getMessages(null); + for (Message message : messages) { + deleteAttachments(message.getUid()); + } + mDb.execSQL("DELETE FROM folders WHERE id = ?", new Object[] { + Long.toString(mFolderId), + }); + } + + @Override + public boolean equals(Object o) { + if (o instanceof LocalFolder) { + return ((LocalFolder)o).mName.equals(mName); + } + return super.equals(o); + } + + @Override + public Flag[] getPermanentFlags() throws MessagingException { + return PERMANENT_FLAGS; + } + + private void deleteAttachments(String uid) throws MessagingException { + open(OpenMode.READ_WRITE); + Cursor messagesCursor = null; + try { + messagesCursor = mDb.query( + "messages", + new String[] { "id" }, + "folder_id = ? AND uid = ?", + new String[] { Long.toString(mFolderId), uid }, + null, + null, + null); + while (messagesCursor.moveToNext()) { + long messageId = messagesCursor.getLong(0); + Cursor attachmentsCursor = null; + try { + attachmentsCursor = mDb.query( + "attachments", + new String[] { "id" }, + "message_id = ?", + new String[] { Long.toString(messageId) }, + null, + null, + null); + while (attachmentsCursor.moveToNext()) { + long attachmentId = attachmentsCursor.getLong(0); + try{ + File file = new File(mAttachmentsDir, Long.toString(attachmentId)); + if (file.exists()) { + file.delete(); + } + } + catch (Exception e) { + + } + } + } + finally { + if (attachmentsCursor != null) { + attachmentsCursor.close(); + } + } + } + } + finally { + if (messagesCursor != null) { + messagesCursor.close(); + } + } + } + } + + public class LocalMessage extends MimeMessage { + private long mId; + private int mAttachmentCount; + + LocalMessage(String uid, Folder folder) throws MessagingException { + this.mUid = uid; + this.mFolder = folder; + } + + public int getAttachmentCount() { + return mAttachmentCount; + } + + public void parse(InputStream in) throws IOException, MessagingException { + super.parse(in); + } + + public void setFlagInternal(Flag flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + } + + public long getId() { + return mId; + } + + public void setFlag(Flag flag, boolean set) throws MessagingException { + if (flag == Flag.DELETED && set) { + /* + * If a message is being marked as deleted we want to clear out it's content + * and attachments as well. Delete will not actually remove the row since we need + * to retain the uid for synchronization purposes. + */ + + /* + * Delete all of the messages' content to save space. + */ + mDb.execSQL( + "UPDATE messages SET " + + "subject = NULL, " + + "sender_list = NULL, " + + "date = NULL, " + + "to_list = NULL, " + + "cc_list = NULL, " + + "bcc_list = NULL, " + + "html_content = NULL, " + + "text_content = NULL, " + + "reply_to_list = NULL " + + "WHERE id = ?", + new Object[] { + mId + }); + + ((LocalFolder) mFolder).deleteAttachments(getUid()); + + /* + * Delete all of the messages' attachments to save space. + */ + mDb.execSQL("DELETE FROM attachments WHERE id = ?", + new Object[] { + mId + }); + } + else if (flag == Flag.X_DESTROYED && set) { + ((LocalFolder) mFolder).deleteAttachments(getUid()); + mDb.execSQL("DELETE FROM messages WHERE id = ?", + new Object[] { mId }); + } + + /* + * Update the unread count on the folder. + */ + try { + if (flag == Flag.DELETED || flag == Flag.X_DESTROYED || flag == Flag.SEEN) { + LocalFolder folder = (LocalFolder)mFolder; + if (set && !isSet(Flag.SEEN)) { + folder.setUnreadMessageCount(folder.getUnreadMessageCount() - 1); + } + else if (!set && isSet(Flag.SEEN)) { + folder.setUnreadMessageCount(folder.getUnreadMessageCount() + 1); + } + } + } + catch (MessagingException me) { + Log.e(k9.LOG_TAG, "Unable to update LocalStore unread message count", + me); + throw new RuntimeException(me); + } + + super.setFlag(flag, set); + /* + * Set the flags on the message. + */ + mDb.execSQL("UPDATE messages " + "SET flags = ? " + "WHERE id = ?", new Object[] { + Utility.combine(getFlags(), ',').toUpperCase(), mId + }); + } + } + + public class LocalAttachmentBodyPart extends MimeBodyPart { + private long mAttachmentId = -1; + + public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException { + super(body); + mAttachmentId = attachmentId; + } + + /** + * Returns the local attachment id of this body, or -1 if it is not stored. + * @return + */ + public long getAttachmentId() { + return mAttachmentId; + } + + public void setAttachmentId(long attachmentId) { + mAttachmentId = attachmentId; + } + + public String toString() { + return "" + mAttachmentId; + } + } + + public static class LocalAttachmentBody implements Body { + private Application mApplication; + private Uri mUri; + + public LocalAttachmentBody(Uri uri, Application application) { + mApplication = application; + mUri = uri; + } + + public InputStream getInputStream() throws MessagingException { + try { + return mApplication.getContentResolver().openInputStream(mUri); + } + catch (FileNotFoundException fnfe) { + /* + * Since it's completely normal for us to try to serve up attachments that + * have been blown away, we just return an empty stream. + */ + return new ByteArrayInputStream(new byte[0]); + } + catch (IOException ioe) { + throw new MessagingException("Invalid attachment.", ioe); + } + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + Base64OutputStream base64Out = new Base64OutputStream(out); + IOUtils.copy(in, base64Out); + base64Out.close(); + } + + public Uri getContentUri() { + return mUri; + } + } +} diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java new file mode 100644 index 000000000..c3782efed --- /dev/null +++ b/src/com/fsck/k9/mail/store/Pop3Store.java @@ -0,0 +1,880 @@ + +package com.fsck.k9.mail.store; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.SSLException; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Folder; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Store; +import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.Folder.OpenMode; +import com.fsck.k9.mail.internet.MimeMessage; + +public class Pop3Store extends Store { + public static final int CONNECTION_SECURITY_NONE = 0; + public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; + public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; + public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; + public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + + private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; + + private String mHost; + private int mPort; + private String mUsername; + private String mPassword; + private int mConnectionSecurity; + private HashMap mFolders = new HashMap(); + private Pop3Capabilities mCapabilities; + +// /** +// * Detected latency, used for usage scaling. +// * Usage scaling occurs when it is neccesary to get information about +// * messages that could result in large data loads. This value allows +// * the code that loads this data to decide between using large downloads +// * (high latency) or multiple round trips (low latency) to accomplish +// * the same thing. +// * Default is Integer.MAX_VALUE implying massive latency so that the large +// * download method is used by default until latency data is collected. +// */ +// private int mLatencyMs = Integer.MAX_VALUE; +// +// /** +// * Detected throughput, used for usage scaling. +// * Usage scaling occurs when it is neccesary to get information about +// * messages that could result in large data loads. This value allows +// * the code that loads this data to decide between using large downloads +// * (high latency) or multiple round trips (low latency) to accomplish +// * the same thing. +// * Default is Integer.MAX_VALUE implying massive bandwidth so that the +// * large download method is used by default until latency data is +// * collected. +// */ +// private int mThroughputKbS = Integer.MAX_VALUE; + + /** + * pop3://user:password@server:port CONNECTION_SECURITY_NONE + * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL + * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED + * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED + * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL + * + * @param _uri + */ + public Pop3Store(String _uri) throws MessagingException { + URI uri; + try { + uri = new URI(_uri); + } catch (URISyntaxException use) { + throw new MessagingException("Invalid Pop3Store URI", use); + } + + String scheme = uri.getScheme(); + if (scheme.equals("pop3")) { + mConnectionSecurity = CONNECTION_SECURITY_NONE; + mPort = 110; + } else if (scheme.equals("pop3+tls")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; + mPort = 110; + } else if (scheme.equals("pop3+tls+")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; + mPort = 110; + } else if (scheme.equals("pop3+ssl+")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + mPort = 995; + } else if (scheme.equals("pop3+ssl")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + mPort = 995; + } else { + throw new MessagingException("Unsupported protocol"); + } + + mHost = uri.getHost(); + + if (uri.getPort() != -1) { + mPort = uri.getPort(); + } + + if (uri.getUserInfo() != null) { + String[] userInfoParts = uri.getUserInfo().split(":", 2); + mUsername = userInfoParts[0]; + if (userInfoParts.length > 1) { + mPassword = userInfoParts[1]; + } + } + } + + @Override + public Folder getFolder(String name) throws MessagingException { + Folder folder = mFolders.get(name); + if (folder == null) { + folder = new Pop3Folder(name); + mFolders.put(folder.getName(), folder); + } + return folder; + } + + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + return new Folder[] { + getFolder("INBOX"), + }; + } + + @Override + public void checkSettings() throws MessagingException { + Pop3Folder folder = new Pop3Folder("INBOX"); + folder.open(OpenMode.READ_WRITE); + if (!mCapabilities.uidl) { + /* + * Run an additional test to see if UIDL is supported on the server. If it's not we + * can't service this account. + */ + try{ + /* + * If the server doesn't support UIDL it will return a - response, which causes + * executeSimpleCommand to throw a MessagingException, exiting this method. + */ + folder.executeSimpleCommand("UIDL"); + } + catch (IOException ioe) { + throw new MessagingException(null, ioe); + } + } + folder.close(false); + } + + class Pop3Folder extends Folder { + private Socket mSocket; + private InputStream mIn; + private OutputStream mOut; + private HashMap mUidToMsgMap = new HashMap(); + private HashMap mMsgNumToMsgMap = new HashMap(); + private HashMap mUidToMsgNumMap = new HashMap(); + private String mName; + private int mMessageCount; + + public Pop3Folder(String name) { + this.mName = name; + if (mName.equalsIgnoreCase("INBOX")) { + mName = "INBOX"; + } + } + + @Override + public synchronized void open(OpenMode mode) throws MessagingException { + if (isOpen()) { + return; + } + + if (!mName.equalsIgnoreCase("INBOX")) { + throw new MessagingException("Folder does not exist"); + } + + try { + SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); + if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || + mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); + } else { + mSocket = new Socket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); + } + + mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + + + // Eat the banner + executeSimpleCommand(null); + + mCapabilities = getCapabilities(); + + if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL + || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + if (mCapabilities.stls) { + writeLine("STLS"); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, + true); + mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); + mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); + } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + throw new MessagingException("TLS not supported but required"); + } + } + + try { + executeSimpleCommand("USER " + mUsername); + executeSimpleCommand("PASS " + mPassword); + } catch (MessagingException me) { + throw new AuthenticationFailedException(null, me); + } + } catch (SSLException e) { + throw new CertificateValidationException(e.getMessage(), e); + } catch (GeneralSecurityException gse) { + throw new MessagingException( + "Unable to open connection to POP server due to security error.", gse); + } catch (IOException ioe) { + throw new MessagingException("Unable to open connection to POP server.", ioe); + } + + try { + String response = executeSimpleCommand("STAT"); + String[] parts = response.split(" "); + mMessageCount = Integer.parseInt(parts[1]); + } + catch (IOException ioe) { + throw new MessagingException("Unable to STAT folder.", ioe); + } + mUidToMsgMap.clear(); + mMsgNumToMsgMap.clear(); + mUidToMsgNumMap.clear(); + } + + public boolean isOpen() { + return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket + .isClosed()); + } + + @Override + public OpenMode getMode() throws MessagingException { + return OpenMode.READ_ONLY; + } + + @Override + public void close(boolean expunge) { + try { + executeSimpleCommand("QUIT"); + } + catch (Exception e) { + /* + * QUIT may fail if the connection is already closed. We don't care. It's just + * being friendly. + */ + } + try { + mIn.close(); + } catch (Exception e) { + /* + * May fail if the connection is already closed. + */ + } + try { + mOut.close(); + } catch (Exception e) { + /* + * May fail if the connection is already closed. + */ + } + try { + mSocket.close(); + } catch (Exception e) { + /* + * May fail if the connection is already closed. + */ + } + mIn = null; + mOut = null; + mSocket = null; + } + + @Override + public String getName() { + return mName; + } + + @Override + public boolean create(FolderType type) throws MessagingException { + return false; + } + + @Override + public boolean exists() throws MessagingException { + return mName.equalsIgnoreCase("INBOX"); + } + + @Override + public int getMessageCount() { + return mMessageCount; + } + + @Override + public int getUnreadMessageCount() throws MessagingException { + return -1; + } + + @Override + public Message getMessage(String uid) throws MessagingException { + Pop3Message message = mUidToMsgMap.get(uid); + if (message == null) { + message = new Pop3Message(uid, this); + } + return message; + } + + @Override + public Message[] getMessages(int start, int end, MessageRetrievalListener listener) + throws MessagingException { + if (start < 1 || end < 1 || end < start) { + throw new MessagingException(String.format("Invalid message set %d %d", + start, end)); + } + try { + indexMsgNums(start, end); + } catch (IOException ioe) { + throw new MessagingException("getMessages", ioe); + } + ArrayList messages = new ArrayList(); + int i = 0; + for (int msgNum = start; msgNum <= end; msgNum++) { + Pop3Message message = mMsgNumToMsgMap.get(msgNum); + if (listener != null) { + listener.messageStarted(message.getUid(), i++, (end - start) + 1); + } + messages.add(message); + if (listener != null) { + listener.messageFinished(message, i++, (end - start) + 1); + } + } + return messages.toArray(new Message[messages.size()]); + } + + /** + * Ensures that the given message set (from start to end inclusive) + * has been queried so that uids are available in the local cache. + * @param start + * @param end + * @throws MessagingException + * @throws IOException + */ + private void indexMsgNums(int start, int end) + throws MessagingException, IOException { + int unindexedMessageCount = 0; + for (int msgNum = start; msgNum <= end; msgNum++) { + if (mMsgNumToMsgMap.get(msgNum) == null) { + unindexedMessageCount++; + } + } + if (unindexedMessageCount == 0) { + return; + } + if (unindexedMessageCount < 50 && mMessageCount > 5000) { + /* + * In extreme cases we'll do a UIDL command per message instead of a bulk + * download. + */ + for (int msgNum = start; msgNum <= end; msgNum++) { + Pop3Message message = mMsgNumToMsgMap.get(msgNum); + if (message == null) { + String response = executeSimpleCommand("UIDL " + msgNum); + int uidIndex = response.lastIndexOf(' '); + String msgUid = response.substring(uidIndex + 1); + message = new Pop3Message(msgUid, this); + indexMessage(msgNum, message); + } + } + } + else { + String response = executeSimpleCommand("UIDL"); + while ((response = readLine()) != null) { + if (response.equals(".")) { + break; + } + String[] uidParts = response.split(" "); + Integer msgNum = Integer.valueOf(uidParts[0]); + String msgUid = uidParts[1]; + if (msgNum >= start && msgNum <= end) { + Pop3Message message = mMsgNumToMsgMap.get(msgNum); + if (message == null) { + message = new Pop3Message(msgUid, this); + indexMessage(msgNum, message); + } + } + } + } + } + + private void indexUids(ArrayList uids) + throws MessagingException, IOException { + HashSet unindexedUids = new HashSet(); + for (String uid : uids) { + if (mUidToMsgMap.get(uid) == null) { + unindexedUids.add(uid); + } + } + if (unindexedUids.size() == 0) { + return; + } + /* + * If we are missing uids in the cache the only sure way to + * get them is to do a full UIDL list. A possible optimization + * would be trying UIDL for the latest X messages and praying. + */ + String response = executeSimpleCommand("UIDL"); + while ((response = readLine()) != null) { + if (response.equals(".")) { + break; + } + String[] uidParts = response.split(" "); + Integer msgNum = Integer.valueOf(uidParts[0]); + String msgUid = uidParts[1]; + if (unindexedUids.contains(msgUid)) { + if (Config.LOGD) { + Pop3Message message = mUidToMsgMap.get(msgUid); + if (message == null) { + message = new Pop3Message(msgUid, this); + } + indexMessage(msgNum, message); + } + } + } + } + + private void indexMessage(int msgNum, Pop3Message message) { + mMsgNumToMsgMap.put(msgNum, message); + mUidToMsgMap.put(message.getUid(), message); + mUidToMsgNumMap.put(message.getUid(), msgNum); + } + + @Override + public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { + throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)"); + } + + @Override + public Message[] getMessages(String[] uids, MessageRetrievalListener listener) + throws MessagingException { + throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)"); + } + + /** + * Fetch the items contained in the FetchProfile into the given set of + * Messages in as efficient a manner as possible. + * @param messages + * @param fp + * @throws MessagingException + */ + public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) + throws MessagingException { + if (messages == null || messages.length == 0) { + return; + } + ArrayList uids = new ArrayList(); + for (Message message : messages) { + uids.add(message.getUid()); + } + try { + indexUids(uids); + } + catch (IOException ioe) { + throw new MessagingException("fetch", ioe); + } + try { + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + /* + * We pass the listener only if there are other things to do in the + * FetchProfile. Since fetchEnvelop works in bulk and eveything else + * works one at a time if we let fetchEnvelope send events the + * event would get sent twice. + */ + fetchEnvelope(messages, fp.size() == 1 ? listener : null); + } + } + catch (IOException ioe) { + throw new MessagingException("fetch", ioe); + } + for (int i = 0, count = messages.length; i < count; i++) { + Message message = messages[i]; + if (!(message instanceof Pop3Message)) { + throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); + } + Pop3Message pop3Message = (Pop3Message)message; + try { + if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) { + listener.messageStarted(pop3Message.getUid(), i, count); + } + if (fp.contains(FetchProfile.Item.BODY)) { + fetchBody(pop3Message, -1); + } + else if (fp.contains(FetchProfile.Item.BODY_SANE)) { + /* + * To convert the suggested download size we take the size + * divided by the maximum line size (76). + */ + fetchBody(pop3Message, + FETCH_BODY_SANE_SUGGESTED_SIZE / 76); + } + else if (fp.contains(FetchProfile.Item.STRUCTURE)) { + /* + * If the user is requesting STRUCTURE we are required to set the body + * to null since we do not support the function. + */ + pop3Message.setBody(null); + } + if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) { + listener.messageFinished(message, i, count); + } + } catch (IOException ioe) { + throw new MessagingException("Unable to fetch message", ioe); + } + } + } + + private void fetchEnvelope(Message[] messages, + MessageRetrievalListener listener) throws IOException, MessagingException { + int unsizedMessages = 0; + for (Message message : messages) { + if (message.getSize() == -1) { + unsizedMessages++; + } + } + if (unsizedMessages == 0) { + return; + } + if (unsizedMessages < 50 && mMessageCount > 5000) { + /* + * In extreme cases we'll do a command per message instead of a bulk request + * to hopefully save some time and bandwidth. + */ + for (int i = 0, count = messages.length; i < count; i++) { + Message message = messages[i]; + if (!(message instanceof Pop3Message)) { + throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); + } + Pop3Message pop3Message = (Pop3Message)message; + if (listener != null) { + listener.messageStarted(pop3Message.getUid(), i, count); + } + String response = executeSimpleCommand(String.format("LIST %d", + mUidToMsgNumMap.get(pop3Message.getUid()))); + String[] listParts = response.split(" "); + int msgNum = Integer.parseInt(listParts[1]); + int msgSize = Integer.parseInt(listParts[2]); + pop3Message.setSize(msgSize); + if (listener != null) { + listener.messageFinished(pop3Message, i, count); + } + } + } + else { + HashSet msgUidIndex = new HashSet(); + for (Message message : messages) { + msgUidIndex.add(message.getUid()); + } + int i = 0, count = messages.length; + String response = executeSimpleCommand("LIST"); + while ((response = readLine()) != null) { + if (response.equals(".")) { + break; + } + String[] listParts = response.split(" "); + int msgNum = Integer.parseInt(listParts[0]); + int msgSize = Integer.parseInt(listParts[1]); + Pop3Message pop3Message = mMsgNumToMsgMap.get(msgNum); + if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) { + if (listener != null) { + listener.messageStarted(pop3Message.getUid(), i, count); + } + pop3Message.setSize(msgSize); + if (listener != null) { + listener.messageFinished(pop3Message, i, count); + } + i++; + } + } + } + } + + /** + * Fetches the body of the given message, limiting the stored data + * to the specified number of lines. If lines is -1 the entire message + * is fetched. This is implemented with RETR for lines = -1 or TOP + * for any other value. If the server does not support TOP it is + * emulated with RETR and extra lines are thrown away. + * @param message + * @param lines + */ + private void fetchBody(Pop3Message message, int lines) + throws IOException, MessagingException { + String response = null; + if (lines == -1 || !mCapabilities.top) { + response = executeSimpleCommand(String.format("RETR %d", + mUidToMsgNumMap.get(message.getUid()))); + } + else { + response = executeSimpleCommand(String.format("TOP %d %d", + mUidToMsgNumMap.get(message.getUid()), + lines)); + } + if (response != null) { + try { + message.parse(new Pop3ResponseInputStream(mIn)); + } + catch (MessagingException me) { + /* + * If we're only downloading headers it's possible + * we'll get a broken MIME message which we're not + * real worried about. If we've downloaded the body + * and can't parse it we need to let the user know. + */ + if (lines == -1) { + throw me; + } + } + } + } + + @Override + public Flag[] getPermanentFlags() throws MessagingException { + return PERMANENT_FLAGS; + } + + public void appendMessages(Message[] messages) throws MessagingException { + } + + public void delete(boolean recurse) throws MessagingException { + } + + public Message[] expunge() throws MessagingException { + return null; + } + + public void setFlags(Message[] messages, Flag[] flags, boolean value) + throws MessagingException { + if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { + /* + * The only flagging we support is setting the Deleted flag. + */ + return; + } + try { + for (Message message : messages) { + executeSimpleCommand(String.format("DELE %s", + mUidToMsgNumMap.get(message.getUid()))); + } + } + catch (IOException ioe) { + throw new MessagingException("setFlags()", ioe); + } + } + + @Override + public void copyMessages(Message[] msgs, Folder folder) throws MessagingException { + throw new UnsupportedOperationException("copyMessages is not supported in POP3"); + } + +// private boolean isRoundTripModeSuggested() { +// long roundTripMethodMs = +// (uncachedMessageCount * 2 * mLatencyMs); +// long bulkMethodMs = +// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000; +// } + + private String readLine() throws IOException { + StringBuffer sb = new StringBuffer(); + int d = mIn.read(); + if (d == -1) { + throw new IOException("End of stream reached while trying to read line."); + } + do { + if (((char)d) == '\r') { + continue; + } else if (((char)d) == '\n') { + break; + } else { + sb.append((char)d); + } + } while ((d = mIn.read()) != -1); + String ret = sb.toString(); + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, "<<< " + ret); + } + } + return ret; + } + + private void writeLine(String s) throws IOException { + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, ">>> " + s); + } + } + mOut.write(s.getBytes()); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + } + + private Pop3Capabilities getCapabilities() throws IOException, MessagingException { + Pop3Capabilities capabilities = new Pop3Capabilities(); + try { + String response = executeSimpleCommand("CAPA"); + while ((response = readLine()) != null) { + if (response.equals(".")) { + break; + } + if (response.equalsIgnoreCase("STLS")){ + capabilities.stls = true; + } + else if (response.equalsIgnoreCase("UIDL")) { + capabilities.uidl = true; + } + else if (response.equalsIgnoreCase("PIPELINING")) { + capabilities.pipelining = true; + } + else if (response.equalsIgnoreCase("USER")) { + capabilities.user = true; + } + else if (response.equalsIgnoreCase("TOP")) { + capabilities.top = true; + } + } + } + catch (MessagingException me) { + /* + * The server may not support the CAPA command, so we just eat this Exception + * and allow the empty capabilities object to be returned. + */ + } + return capabilities; + } + + private String executeSimpleCommand(String command) throws IOException, MessagingException { + open(OpenMode.READ_WRITE); + + if (command != null) { + writeLine(command); + } + + String response = readLine(); + + if (response.length() > 1 && response.charAt(0) == '-') { + throw new MessagingException(response); + } + + return response; + } + + @Override + public boolean equals(Object o) { + if (o instanceof Pop3Folder) { + return ((Pop3Folder) o).mName.equals(mName); + } + return super.equals(o); + } + } + + class Pop3Message extends MimeMessage { + public Pop3Message(String uid, Pop3Folder folder) throws MessagingException { + mUid = uid; + mFolder = folder; + mSize = -1; + } + + public void setSize(int size) { + mSize = size; + } + + protected void parse(InputStream in) throws IOException, MessagingException { + super.parse(in); + } + + @Override + public void setFlag(Flag flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); + } + } + + class Pop3Capabilities { + public boolean stls; + public boolean top; + public boolean user; + public boolean uidl; + public boolean pipelining; + + public String toString() { + return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b", + stls, + top, + user, + uidl, + pipelining); + } + } + + class Pop3ResponseInputStream extends InputStream { + InputStream mIn; + boolean mStartOfLine = true; + boolean mFinished; + + public Pop3ResponseInputStream(InputStream in) { + mIn = in; + } + + @Override + public int read() throws IOException { + if (mFinished) { + return -1; + } + int d = mIn.read(); + if (mStartOfLine && d == '.') { + d = mIn.read(); + if (d == '\r') { + mFinished = true; + mIn.read(); + return -1; + } + } + + mStartOfLine = (d == '\n'); + + return d; + } + } +} diff --git a/src/com/fsck/k9/mail/store/TrustManagerFactory.java b/src/com/fsck/k9/mail/store/TrustManagerFactory.java new file mode 100644 index 000000000..dcfa2070c --- /dev/null +++ b/src/com/fsck/k9/mail/store/TrustManagerFactory.java @@ -0,0 +1,95 @@ + +package com.fsck.k9.mail.store; + +import android.util.Log; +import android.net.http.DomainNameChecker; + +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.KeyStoreException; +import java.security.cert.X509Certificate; +import java.security.cert.CertificateException; + +import javax.net.ssl.X509TrustManager; +import javax.net.ssl.TrustManager; + +public final class TrustManagerFactory { + private static final String LOG_TAG = "TrustManagerFactory"; + + private static X509TrustManager sSecureTrustManager; + private static X509TrustManager sUnsecureTrustManager; + + private static class SimpleX509TrustManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + private static class SecureX509TrustManager implements X509TrustManager { + private X509TrustManager mTrustManager; + private String mHost; + + SecureX509TrustManager(X509TrustManager trustManager, String host) { + mTrustManager = trustManager; + mHost = host; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + mTrustManager.checkClientTrusted(chain, authType); + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + + mTrustManager.checkServerTrusted(chain, authType); + + if (!DomainNameChecker.match(chain[0], mHost)) { + throw new CertificateException("Certificate domain name does not match " + + mHost); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return mTrustManager.getAcceptedIssuers(); + } + } + + static { + try { + javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore) null); + TrustManager[] tms = tmf.getTrustManagers(); + if (tms != null) { + for (TrustManager tm : tms) { + if (tm instanceof X509TrustManager) { + sSecureTrustManager = (X509TrustManager) tm; + break; + } + } + } + } catch (NoSuchAlgorithmException e) { + Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e); + } catch (KeyStoreException e) { + Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e); + } + + sUnsecureTrustManager = new SimpleX509TrustManager(); + } + + private TrustManagerFactory() { + } + + public static X509TrustManager get(String host, boolean secure) { + return secure ? new SecureX509TrustManager(sSecureTrustManager, host) : + sUnsecureTrustManager; + } +} diff --git a/src/com/fsck/k9/mail/transport/CountingOutputStream.java b/src/com/fsck/k9/mail/transport/CountingOutputStream.java new file mode 100644 index 000000000..51a786c8c --- /dev/null +++ b/src/com/fsck/k9/mail/transport/CountingOutputStream.java @@ -0,0 +1,24 @@ +package com.fsck.k9.mail.transport; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple OutputStream that does nothing but count how many bytes are written to it and + * makes that count available to callers. + */ +public class CountingOutputStream extends OutputStream { + private long mCount; + + public CountingOutputStream() { + } + + public long getCount() { + return mCount; + } + + @Override + public void write(int oneByte) throws IOException { + mCount++; + } +} diff --git a/src/com/fsck/k9/mail/transport/EOLConvertingOutputStream.java b/src/com/fsck/k9/mail/transport/EOLConvertingOutputStream.java new file mode 100644 index 000000000..02d465615 --- /dev/null +++ b/src/com/fsck/k9/mail/transport/EOLConvertingOutputStream.java @@ -0,0 +1,33 @@ +package com.fsck.k9.mail.transport; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class EOLConvertingOutputStream extends FilterOutputStream { + int lastChar; + + public EOLConvertingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) throws IOException { + if (oneByte == '\n') { + if (lastChar != '\r') { + super.write('\r'); + } + } + super.write(oneByte); + lastChar = oneByte; + } + + @Override + public void flush() throws IOException { + if (lastChar == '\r') { + super.write('\n'); + lastChar = '\n'; + } + super.flush(); + } +} diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java new file mode 100644 index 000000000..d02b27364 --- /dev/null +++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java @@ -0,0 +1,376 @@ + +package com.fsck.k9.mail.transport; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.SSLException; + +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.k9; +import com.fsck.k9.PeekableInputStream; +import com.fsck.k9.codec.binary.Base64; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Transport; +import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.store.TrustManagerFactory; + +public class SmtpTransport extends Transport { + public static final int CONNECTION_SECURITY_NONE = 0; + + public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; + + public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; + + public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; + + public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; + + String mHost; + + int mPort; + + String mUsername; + + String mPassword; + + int mConnectionSecurity; + + boolean mSecure; + + Socket mSocket; + + PeekableInputStream mIn; + + OutputStream mOut; + + /** + * smtp://user:password@server:port CONNECTION_SECURITY_NONE + * smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL + * smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED + * smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED + * smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL + * + * @param _uri + */ + public SmtpTransport(String _uri) throws MessagingException { + URI uri; + try { + uri = new URI(_uri); + } catch (URISyntaxException use) { + throw new MessagingException("Invalid SmtpTransport URI", use); + } + + String scheme = uri.getScheme(); + if (scheme.equals("smtp")) { + mConnectionSecurity = CONNECTION_SECURITY_NONE; + mPort = 25; + } else if (scheme.equals("smtp+tls")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; + mPort = 25; + } else if (scheme.equals("smtp+tls+")) { + mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; + mPort = 25; + } else if (scheme.equals("smtp+ssl+")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; + mPort = 465; + } else if (scheme.equals("smtp+ssl")) { + mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; + mPort = 465; + } else { + throw new MessagingException("Unsupported protocol"); + } + + mHost = uri.getHost(); + + if (uri.getPort() != -1) { + mPort = uri.getPort(); + } + + if (uri.getUserInfo() != null) { + String[] userInfoParts = uri.getUserInfo().split(":", 2); + mUsername = userInfoParts[0]; + if (userInfoParts.length > 1) { + mPassword = userInfoParts[1]; + } + } + } + + public void open() throws MessagingException { + try { + SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); + if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || + mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) { + SSLContext sslContext = SSLContext.getInstance("TLS"); + boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + mSecure = true; + } else { + mSocket = new Socket(); + mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); + } + + mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); + mOut = mSocket.getOutputStream(); + + // Eat the banner + executeSimpleCommand(null); + + String localHost = "localhost.localdomain"; + try { + InetAddress localAddress = InetAddress.getLocalHost(); + if (! localAddress.isLoopbackAddress()) { + // The loopback address will resolve to 'localhost' + // some mail servers only accept qualified hostnames, so make sure + // never to override "localhost.localdomain" with "localhost" + // TODO - this is a hack. but a better hack than what was there before + localHost = localAddress.getHostName(); + } + } catch (Exception e) { + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, "Unable to look up localhost"); + } + } + } + + String result = executeSimpleCommand("EHLO " + localHost); + + /* + * TODO may need to add code to fall back to HELO I switched it from + * using HELO on non STARTTLS connections because of AOL's mail + * server. It won't let you use AUTH without EHLO. + * We should really be paying more attention to the capabilities + * and only attempting auth if it's available, and warning the user + * if not. + */ + if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL + || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + if (result.contains("-STARTTLS")) { + executeSimpleCommand("STARTTLS"); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; + sslContext.init(null, new TrustManager[] { + TrustManagerFactory.get(mHost, secure) + }, new SecureRandom()); + mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, + true); + mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), + 1024)); + mOut = mSocket.getOutputStream(); + mSecure = true; + /* + * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, + * Exim. + */ + result = executeSimpleCommand("EHLO " + localHost); + } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) { + throw new MessagingException("TLS not supported but required"); + } + } + + /* + * result contains the results of the EHLO in concatenated form + */ + boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$"); + boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$"); + + if (mUsername != null && mUsername.length() > 0 && mPassword != null + && mPassword.length() > 0) { + if (authPlainSupported) { + saslAuthPlain(mUsername, mPassword); + } + else if (authLoginSupported) { + saslAuthLogin(mUsername, mPassword); + } + else { + throw new MessagingException("No valid authentication mechanism found."); + } + } + } catch (SSLException e) { + throw new CertificateValidationException(e.getMessage(), e); + } catch (GeneralSecurityException gse) { + throw new MessagingException( + "Unable to open connection to SMTP server due to security error.", gse); + } catch (IOException ioe) { + throw new MessagingException("Unable to open connection to SMTP server.", ioe); + } + } + + public void sendMessage(Message message) throws MessagingException { + close(); + open(); + Address[] from = message.getFrom(); + + try { + executeSimpleCommand("MAIL FROM: " + "<" + from[0].getAddress() + ">"); + for (Address address : message.getRecipients(RecipientType.TO)) { + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + } + for (Address address : message.getRecipients(RecipientType.CC)) { + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + } + for (Address address : message.getRecipients(RecipientType.BCC)) { + executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">"); + } + message.setRecipients(RecipientType.BCC, null); + executeSimpleCommand("DATA"); + // TODO byte stuffing + message.writeTo( + new EOLConvertingOutputStream( + new BufferedOutputStream(mOut, 1024))); + executeSimpleCommand("\r\n."); + } catch (IOException ioe) { + throw new MessagingException("Unable to send message", ioe); + } + } + + public void close() { + try { + mIn.close(); + } catch (Exception e) { + + } + try { + mOut.close(); + } catch (Exception e) { + + } + try { + mSocket.close(); + } catch (Exception e) { + + } + mIn = null; + mOut = null; + mSocket = null; + } + + private String readLine() throws IOException { + StringBuffer sb = new StringBuffer(); + int d; + while ((d = mIn.read()) != -1) { + if (((char)d) == '\r') { + continue; + } else if (((char)d) == '\n') { + break; + } else { + sb.append((char)d); + } + } + String ret = sb.toString(); + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, "<<< " + ret); + } + } + return ret; + } + + private void writeLine(String s) throws IOException { + if (Config.LOGD) { + if (k9.DEBUG) { + Log.d(k9.LOG_TAG, ">>> " + s); + } + } + mOut.write(s.getBytes()); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + } + + private String executeSimpleCommand(String command) throws IOException, MessagingException { + if (command != null) { + writeLine(command); + } + + String line = readLine(); + + String result = line; + + while (line.length() >= 4 && line.charAt(3) == '-') { + line = readLine(); + result += line.substring(3); + } + + char c = result.charAt(0); + if ((c == '4') || (c == '5')) { + throw new MessagingException(result); + } + + return result; + } + + +// C: AUTH LOGIN +// S: 334 VXNlcm5hbWU6 +// C: d2VsZG9u +// S: 334 UGFzc3dvcmQ6 +// C: dzNsZDBu +// S: 235 2.0.0 OK Authenticated +// +// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads: +// +// +// C: AUTH LOGIN +// S: 334 Username: +// C: weldon +// S: 334 Password: +// C: w3ld0n +// S: 235 2.0.0 OK Authenticated + + private void saslAuthLogin(String username, String password) throws MessagingException, + AuthenticationFailedException, IOException { + try { + executeSimpleCommand("AUTH LOGIN"); + executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes()))); + executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes()))); + } + catch (MessagingException me) { + if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { + throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage() + + ")"); + } + throw me; + } + } + + private void saslAuthPlain(String username, String password) throws MessagingException, + AuthenticationFailedException, IOException { + byte[] data = ("\000" + username + "\000" + password).getBytes(); + data = new Base64().encode(data); + try { + executeSimpleCommand("AUTH PLAIN " + new String(data)); + } + catch (MessagingException me) { + if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') { + throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage() + + ")"); + } + throw me; + } + } +} diff --git a/src/com/fsck/k9/mail/transport/StatusOutputStream.java b/src/com/fsck/k9/mail/transport/StatusOutputStream.java new file mode 100644 index 000000000..c97e08e2e --- /dev/null +++ b/src/com/fsck/k9/mail/transport/StatusOutputStream.java @@ -0,0 +1,29 @@ +package com.fsck.k9.mail.transport; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import com.fsck.k9.k9; + +import android.util.Config; +import android.util.Log; + +public class StatusOutputStream extends FilterOutputStream { + private long mCount = 0; + + public StatusOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) throws IOException { + super.write(oneByte); + mCount++; + if (Config.LOGV) { + if (mCount % 1024 == 0) { + Log.v(k9.LOG_TAG, "# " + mCount); + } + } + } +} diff --git a/src/com/fsck/k9/provider/AttachmentProvider.java b/src/com/fsck/k9/provider/AttachmentProvider.java new file mode 100644 index 000000000..70e0dbb2c --- /dev/null +++ b/src/com/fsck/k9/provider/AttachmentProvider.java @@ -0,0 +1,272 @@ +package com.fsck.k9.provider; + +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.util.List; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; +import android.util.Config; +import android.util.Log; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.Utility; +import com.fsck.k9.mail.internet.MimeUtility; + +/* + * A simple ContentProvider that allows file access to Email's attachments. + */ +public class AttachmentProvider extends ContentProvider { + public static final Uri CONTENT_URI = Uri.parse( "content://com.fsck.k9.attachmentprovider"); + + private static final String FORMAT_RAW = "RAW"; + private static final String FORMAT_THUMBNAIL = "THUMBNAIL"; + + public static class AttachmentProviderColumns { + public static final String _ID = "_id"; + public static final String DATA = "_data"; + public static final String DISPLAY_NAME = "_display_name"; + public static final String SIZE = "_size"; + } + + public static Uri getAttachmentUri(Account account, long id) { + return CONTENT_URI.buildUpon() + .appendPath(account.getUuid() + ".db") + .appendPath(Long.toString(id)) + .appendPath(FORMAT_RAW) + .build(); + } + + public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) { + return CONTENT_URI.buildUpon() + .appendPath(account.getUuid() + ".db") + .appendPath(Long.toString(id)) + .appendPath(FORMAT_THUMBNAIL) + .appendPath(Integer.toString(width)) + .appendPath(Integer.toString(height)) + .build(); + } + + public static Uri getAttachmentUri(String db, long id) { + return CONTENT_URI.buildUpon() + .appendPath(db) + .appendPath(Long.toString(id)) + .appendPath(FORMAT_RAW) + .build(); + } + + @Override + public boolean onCreate() { + /* + * We use the cache dir as a temporary directory (since Android doesn't give us one) so + * on startup we'll clean up any .tmp files from the last run. + */ + File[] files = getContext().getCacheDir().listFiles(); + for (File file : files) { + if (file.getName().endsWith(".tmp")) { + file.delete(); + } + } + return true; + } + + @Override + public String getType(Uri uri) { + List segments = uri.getPathSegments(); + String dbName = segments.get(0); + String id = segments.get(1); + String format = segments.get(2); + if (FORMAT_THUMBNAIL.equals(format)) { + return "image/png"; + } + else { + String path = getContext().getDatabasePath(dbName).getAbsolutePath(); + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = SQLiteDatabase.openDatabase(path, null, 0); + cursor = db.query( + "attachments", + new String[] { "mime_type" }, + "id = ?", + new String[] { id }, + null, + null, + null); + cursor.moveToFirst(); + String type = cursor.getString(0); + cursor.close(); + db.close(); + return type; + + } + finally { + if (cursor != null) { + cursor.close(); + } + if (db != null) { + db.close(); + } + + } + } + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + List segments = uri.getPathSegments(); + String dbName = segments.get(0); + String id = segments.get(1); + String format = segments.get(2); + if (FORMAT_THUMBNAIL.equals(format)) { + int width = Integer.parseInt(segments.get(3)); + int height = Integer.parseInt(segments.get(4)); + String filename = "thmb_" + dbName + "_" + id; + File dir = getContext().getCacheDir(); + File file = new File(dir, filename); + if (!file.exists()) { + Uri attachmentUri = getAttachmentUri(dbName, Long.parseLong(id)); + String type = getType(attachmentUri); + try { + FileInputStream in = new FileInputStream( + new File(getContext().getDatabasePath(dbName + "_att"), id)); + Bitmap thumbnail = createThumbnail(type, in); + thumbnail = thumbnail.createScaledBitmap(thumbnail, width, height, true); + FileOutputStream out = new FileOutputStream(file); + thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out); + out.close(); + in.close(); + } + catch (IOException ioe) { + return null; + } + } + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + } + else { + return ParcelFileDescriptor.open( + new File(getContext().getDatabasePath(dbName + "_att"), id), + ParcelFileDescriptor.MODE_READ_ONLY); + } + } + + @Override + public int delete(Uri uri, String arg1, String[] arg2) { + return 0; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + if (projection == null) { + projection = + new String[] { + AttachmentProviderColumns._ID, + AttachmentProviderColumns.DATA, + }; + } + + List segments = uri.getPathSegments(); + String dbName = segments.get(0); + String id = segments.get(1); + String format = segments.get(2); + String path = getContext().getDatabasePath(dbName).getAbsolutePath(); + String name = null; + int size = -1; + SQLiteDatabase db = null; + Cursor cursor = null; + try { + db = SQLiteDatabase.openDatabase(path, null, 0); + cursor = db.query( + "attachments", + new String[] { "name", "size" }, + "id = ?", + new String[] { id }, + null, + null, + null); + if (!cursor.moveToFirst()) { + return null; + } + name = cursor.getString(0); + size = cursor.getInt(1); + } + finally { + if (cursor != null) { + cursor.close(); + } + if (db != null) { + db.close(); + } + } + + MatrixCursor ret = new MatrixCursor(projection); + Object[] values = new Object[projection.length]; + for (int i = 0, count = projection.length; i < count; i++) { + String column = projection[i]; + if (AttachmentProviderColumns._ID.equals(column)) { + values[i] = id; + } + else if (AttachmentProviderColumns.DATA.equals(column)) { + values[i] = uri.toString(); + } + else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) { + values[i] = name; + } + else if (AttachmentProviderColumns.SIZE.equals(column)) { + values[i] = size; + } + } + ret.addRow(values); + return ret; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + private Bitmap createThumbnail(String type, InputStream data) { + if(MimeUtility.mimeTypeMatches(type, "image/*")) { + return createImageThumbnail(data); + } + return null; + } + + private Bitmap createImageThumbnail(InputStream data) { + try { + Bitmap bitmap = BitmapFactory.decodeStream(data); + return bitmap; + } + catch (OutOfMemoryError oome) { + /* + * Improperly downloaded images, corrupt bitmaps and the like can commonly + * cause OOME due to invalid allocation sizes. We're happy with a null bitmap in + * that case. If the system is really out of memory we'll know about it soon + * enough. + */ + return null; + } + catch (Exception e) { + return null; + } + } +} diff --git a/src/com/fsck/k9/service/BootReceiver.java b/src/com/fsck/k9/service/BootReceiver.java new file mode 100644 index 000000000..19714e34e --- /dev/null +++ b/src/com/fsck/k9/service/BootReceiver.java @@ -0,0 +1,22 @@ + +package com.fsck.k9.service; + +import com.fsck.k9.MessagingController; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class BootReceiver extends BroadcastReceiver { + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + MailService.actionReschedule(context); + } + else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) { + MailService.actionCancel(context); + } + else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) { + MailService.actionReschedule(context); + } + } +} diff --git a/src/com/fsck/k9/service/MailService.java b/src/com/fsck/k9/service/MailService.java new file mode 100644 index 000000000..b0a76db84 --- /dev/null +++ b/src/com/fsck/k9/service/MailService.java @@ -0,0 +1,193 @@ + +package com.fsck.k9.service; + +import java.util.HashMap; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.SystemClock; +import android.util.Config; +import android.util.Log; +import android.text.TextUtils; +import android.net.Uri; + +import com.fsck.k9.Account; +import com.fsck.k9.k9; +import com.fsck.k9.MessagingController; +import com.fsck.k9.MessagingListener; +import com.fsck.k9.Preferences; +import com.fsck.k9.R; +import com.fsck.k9.activity.Accounts; +import com.fsck.k9.activity.FolderMessageList; + +/** + */ +public class MailService extends Service { + private static final String ACTION_CHECK_MAIL = "com.fsck.k9.intent.action.MAIL_SERVICE_WAKEUP"; + private static final String ACTION_RESCHEDULE = "com.fsck.k9.intent.action.MAIL_SERVICE_RESCHEDULE"; + private static final String ACTION_CANCEL = "com.fsck.k9.intent.action.MAIL_SERVICE_CANCEL"; + + private Listener mListener = new Listener(); + + private int mStartId; + + public static void actionReschedule(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_RESCHEDULE); + context.startService(i); + } + + public static void actionCancel(Context context) { + Intent i = new Intent(); + i.setClass(context, MailService.class); + i.setAction(MailService.ACTION_CANCEL); + context.startService(i); + } + + @Override + public void onStart(Intent intent, int startId) { + super.onStart(intent, startId); + this.mStartId = startId; + + MessagingController.getInstance(getApplication()).addListener(mListener); + if (ACTION_CHECK_MAIL.equals(intent.getAction())) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "***** MailService *****: checking mail"); + } + MessagingController.getInstance(getApplication()).checkMail(this, null, mListener); + } + else if (ACTION_CANCEL.equals(intent.getAction())) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "***** MailService *****: cancel"); + } + cancel(); + stopSelf(startId); + } + else if (ACTION_RESCHEDULE.equals(intent.getAction())) { + if (Config.LOGV) { + Log.v(k9.LOG_TAG, "***** MailService *****: reschedule"); + } + reschedule(); + stopSelf(startId); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + MessagingController.getInstance(getApplication()).removeListener(mListener); + } + + private void cancel() { + AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(); + i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService"); + i.setAction(ACTION_CHECK_MAIL); + PendingIntent pi = PendingIntent.getService(this, 0, i, 0); + alarmMgr.cancel(pi); + } + + private void reschedule() { + AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(); + i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService"); + i.setAction(ACTION_CHECK_MAIL); + PendingIntent pi = PendingIntent.getService(this, 0, i, 0); + + int shortestInterval = -1; + for (Account account : Preferences.getPreferences(this).getAccounts()) { + if (account.getAutomaticCheckIntervalMinutes() != -1 + && (account.getAutomaticCheckIntervalMinutes() < shortestInterval || shortestInterval == -1)) { + shortestInterval = account.getAutomaticCheckIntervalMinutes(); + } + } + + if (shortestInterval == -1) { + alarmMgr.cancel(pi); + } + else { + alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + + (shortestInterval * (60 * 1000)), pi); + } + } + + public IBinder onBind(Intent intent) { + return null; + } + + class Listener extends MessagingListener { + HashMap accountsWithNewMail = new HashMap(); + + @Override + public void checkMailStarted(Context context, Account account) { + accountsWithNewMail.clear(); + } + + @Override + public void checkMailFailed(Context context, Account account, String reason) { + reschedule(); + stopSelf(mStartId); + } + + @Override + public void synchronizeMailboxFinished( + Account account, + String folder, + int totalMessagesInMailbox, + int numNewMessages) { + if (account.isNotifyNewMail() && numNewMessages > 0) { + accountsWithNewMail.put(account, numNewMessages); + } + } + + @Override + public void checkMailFinished(Context context, Account account) { + NotificationManager notifMgr = (NotificationManager)context + .getSystemService(Context.NOTIFICATION_SERVICE); + + if (accountsWithNewMail.size() > 0) { + Notification notif = new Notification(R.drawable.stat_notify_email_generic, + getString(R.string.notification_new_title), System.currentTimeMillis()); + boolean vibrate = false; + String ringtone = null; + if (accountsWithNewMail.size() > 1) { + for (Account account1 : accountsWithNewMail.keySet()) { + if (account1.isVibrate()) vibrate = true; + ringtone = account1.getRingtone(); + } + Intent i = new Intent(context, Accounts.class); + PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); + notif.setLatestEventInfo(context, getString(R.string.notification_new_title), + getString(R.string.notification_new_multi_account_fmt, + accountsWithNewMail.size()), pi); + } else { + Account account1 = accountsWithNewMail.keySet().iterator().next(); + int totalNewMails = accountsWithNewMail.get(account1); + Intent i = FolderMessageList.actionHandleAccountIntent(context, account1, k9.INBOX); + PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0); + notif.setLatestEventInfo(context, getString(R.string.notification_new_title), + getString(R.string.notification_new_one_account_fmt, totalNewMails, + account1.getDescription()), pi); + vibrate = account1.isVibrate(); + ringtone = account1.getRingtone(); + } + notif.defaults = Notification.DEFAULT_LIGHTS; + notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone); + if (vibrate) { + notif.defaults |= Notification.DEFAULT_VIBRATE; + } + notifMgr.notify(1, notif); + } + + reschedule(); + stopSelf(mStartId); + } + } +} diff --git a/src/org/apache/commons/io/CopyUtils.java b/src/org/apache/commons/io/CopyUtils.java new file mode 100644 index 000000000..eab8307e7 --- /dev/null +++ b/src/org/apache/commons/io/CopyUtils.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; + +/** + * This class provides static utility methods for buffered + * copying between sources (InputStream, Reader, + * String and byte[]) and destinations + * (OutputStream, Writer, String and + * byte[]). + *

+ * Unless otherwise noted, these copy methods do not + * flush or close the streams. Often doing so would require making non-portable + * assumptions about the streams' origin and further use. This means that both + * streams' close() methods must be called after copying. if one + * omits this step, then the stream resources (sockets, file descriptors) are + * released when the associated Stream is garbage-collected. It is not a good + * idea to rely on this mechanism. For a good overview of the distinction + * between "memory management" and "resource management", see + * this + * UnixReview article. + *

+ * For byte-to-char methods, a copy variant allows the encoding + * to be selected (otherwise the platform default is used). We would like to + * encourage you to always specify the encoding because relying on the platform + * default can lead to unexpected results. + *

copy methods that + * let you specify the buffer size because in modern VMs the impact on speed + * seems to be minimal. We're using a default buffer size of 4 KB. + *

+ * The copy methods use an internal buffer when copying. It is + * therefore advisable not to deliberately wrap the stream arguments + * to the copy methods in Buffered* streams. For + * example, don't do the following: + *

+ *  copy( new BufferedInputStream( in ), new BufferedOutputStream( out ) );
+ *  
+ * The rationale is as follows: + *

+ * Imagine that an InputStream's read() is a very expensive operation, which + * would usually suggest wrapping in a BufferedInputStream. The + * BufferedInputStream works by issuing infrequent + * {@link java.io.InputStream#read(byte[] b, int off, int len)} requests on the + * underlying InputStream, to fill an internal buffer, from which further + * read requests can inexpensively get their data (until the buffer + * runs out). + *

+ * However, the copy methods do the same thing, keeping an + * internal buffer, populated by + * {@link InputStream#read(byte[] b, int off, int len)} requests. Having two + * buffers (or three if the destination stream is also buffered) is pointless, + * and the unnecessary buffer management hurts performance slightly (about 3%, + * according to some simple experiments). + *

+ * Behold, intrepid explorers; a map of this class: + *

+ *       Method      Input               Output          Dependency
+ *       ------      -----               ------          -------
+ * 1     copy        InputStream         OutputStream    (primitive)
+ * 2     copy        Reader              Writer          (primitive)
+ *
+ * 3     copy        InputStream         Writer          2
+ *
+ * 4     copy        Reader              OutputStream    2
+ *
+ * 5     copy        String              OutputStream    2
+ * 6     copy        String              Writer          (trivial)
+ *
+ * 7     copy        byte[]              Writer          3
+ * 8     copy        byte[]              OutputStream    (trivial)
+ * 
+ *

+ * Note that only the first two methods shuffle bytes; the rest use these + * two, or (if possible) copy using native Java copy methods. As there are + * method variants to specify the encoding, each row may + * correspond to up to 2 methods. + *

+ * Origin of code: Excalibur. + * + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @version $Id: CopyUtils.java 437680 2006-08-28 11:57:00Z scolebourne $ + * @deprecated Use IOUtils. Will be removed in 2.0. + * Methods renamed to IOUtils.write() or IOUtils.copy(). + * Null handling behaviour changed in IOUtils (null data does not + * throw NullPointerException). + */ +public class CopyUtils { + + /** + * The default size of the buffer. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Instances should NOT be constructed in standard programming. + */ + public CopyUtils() { } + + // ---------------------------------------------------------------- + // byte[] -> OutputStream + // ---------------------------------------------------------------- + + /** + * Copy bytes from a byte[] to an OutputStream. + * @param input the byte array to read from + * @param output the OutputStream to write to + * @throws IOException In case of an I/O problem + */ + public static void copy(byte[] input, OutputStream output) + throws IOException { + output.write(input); + } + + // ---------------------------------------------------------------- + // byte[] -> Writer + // ---------------------------------------------------------------- + + /** + * Copy and convert bytes from a byte[] to chars on a + * Writer. + * The platform's default encoding is used for the byte-to-char conversion. + * @param input the byte array to read from + * @param output the Writer to write to + * @throws IOException In case of an I/O problem + */ + public static void copy(byte[] input, Writer output) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(input); + copy(in, output); + } + + + /** + * Copy and convert bytes from a byte[] to chars on a + * Writer, using the specified encoding. + * @param input the byte array to read from + * @param output the Writer to write to + * @param encoding The name of a supported character encoding. See the + * IANA + * Charset Registry for a list of valid encoding types. + * @throws IOException In case of an I/O problem + */ + public static void copy( + byte[] input, + Writer output, + String encoding) + throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(input); + copy(in, output, encoding); + } + + + // ---------------------------------------------------------------- + // Core copy methods + // ---------------------------------------------------------------- + + /** + * Copy bytes from an InputStream to an + * OutputStream. + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws IOException In case of an I/O problem + */ + public static int copy( + InputStream input, + OutputStream output) + throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + // ---------------------------------------------------------------- + // Reader -> Writer + // ---------------------------------------------------------------- + + /** + * Copy chars from a Reader to a Writer. + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws IOException In case of an I/O problem + */ + public static int copy( + Reader input, + Writer output) + throws IOException { + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + int count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + // ---------------------------------------------------------------- + // InputStream -> Writer + // ---------------------------------------------------------------- + + /** + * Copy and convert bytes from an InputStream to chars on a + * Writer. + * The platform's default encoding is used for the byte-to-char conversion. + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws IOException In case of an I/O problem + */ + public static void copy( + InputStream input, + Writer output) + throws IOException { + InputStreamReader in = new InputStreamReader(input); + copy(in, output); + } + + /** + * Copy and convert bytes from an InputStream to chars on a + * Writer, using the specified encoding. + * @param input the InputStream to read from + * @param output the Writer to write to + * @param encoding The name of a supported character encoding. See the + * IANA + * Charset Registry for a list of valid encoding types. + * @throws IOException In case of an I/O problem + */ + public static void copy( + InputStream input, + Writer output, + String encoding) + throws IOException { + InputStreamReader in = new InputStreamReader(input, encoding); + copy(in, output); + } + + + // ---------------------------------------------------------------- + // Reader -> OutputStream + // ---------------------------------------------------------------- + + /** + * Serialize chars from a Reader to bytes on an + * OutputStream, and flush the OutputStream. + * @param input the Reader to read from + * @param output the OutputStream to write to + * @throws IOException In case of an I/O problem + */ + public static void copy( + Reader input, + OutputStream output) + throws IOException { + OutputStreamWriter out = new OutputStreamWriter(output); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, we + // have to flush here. + out.flush(); + } + + // ---------------------------------------------------------------- + // String -> OutputStream + // ---------------------------------------------------------------- + + /** + * Serialize chars from a String to bytes on an + * OutputStream, and + * flush the OutputStream. + * @param input the String to read from + * @param output the OutputStream to write to + * @throws IOException In case of an I/O problem + */ + public static void copy( + String input, + OutputStream output) + throws IOException { + StringReader in = new StringReader(input); + OutputStreamWriter out = new OutputStreamWriter(output); + copy(in, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, we + // have to flush here. + out.flush(); + } + + // ---------------------------------------------------------------- + // String -> Writer + // ---------------------------------------------------------------- + + /** + * Copy chars from a String to a Writer. + * @param input the String to read from + * @param output the Writer to write to + * @throws IOException In case of an I/O problem + */ + public static void copy(String input, Writer output) + throws IOException { + output.write(input); + } + +} diff --git a/src/org/apache/commons/io/DirectoryWalker.java b/src/org/apache/commons/io/DirectoryWalker.java new file mode 100644 index 000000000..9e564ae86 --- /dev/null +++ b/src/org/apache/commons/io/DirectoryWalker.java @@ -0,0 +1,620 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.util.Collection; + +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.TrueFileFilter; + +/** + * Abstract class that walks through a directory hierarchy and provides + * subclasses with convenient hooks to add specific behaviour. + *

+ * This class operates with a {@link FileFilter} and maximum depth to + * limit the files and direcories visited. + * Commons IO supplies many common filter implementations in the + * filefilter package. + *

+ * The following sections describe: + *

+ * + * + *

1. Example Implementation

+ * + * There are many possible extensions, for example, to delete all + * files and '.svn' directories, and return a list of deleted files: + *
+ *  public class FileCleaner extends DirectoryWalker {
+ *
+ *    public FileCleaner() {
+ *      super();
+ *    }
+ *
+ *    public List clean(File startDirectory) {
+ *      List results = new ArrayList();
+ *      walk(startDirectory, results);
+ *      return results;
+ *    }
+ *
+ *    protected boolean handleDirectory(File directory, int depth, Collection results) {
+ *      // delete svn directories and then skip
+ *      if (".svn".equals(directory.getName())) {
+ *        directory.delete();
+ *        return false;
+ *      } else {
+ *        return true;
+ *      }
+ *
+ *    }
+ *
+ *    protected void handleFile(File file, int depth, Collection results) {
+ *      // delete file and add to list of deleted
+ *      file.delete();
+ *      results.add(file);
+ *    }
+ *  }
+ * 
+ * + * + *

2. Filter Example

+ * + * Choosing which directories and files to process can be a key aspect + * of using this class. This information can be setup in three ways, + * via three different constructors. + *

+ * The first option is to visit all directories and files. + * This is achieved via the no-args constructor. + *

+ * The second constructor option is to supply a single {@link FileFilter} + * that describes the files and directories to visit. Care must be taken + * with this option as the same filter is used for both directories + * and files. + *

+ * For example, if you wanted all directories which are not hidden + * and files which end in ".txt": + *

+ *  public class FooDirectoryWalker extends DirectoryWalker {
+ *    public FooDirectoryWalker(FileFilter filter) {
+ *      super(filter, -1);
+ *    }
+ *  }
+ *  
+ *  // Build up the filters and create the walker
+ *    // Create a filter for Non-hidden directories
+ *    IOFileFilter fooDirFilter = 
+ *        FileFilterUtils.andFileFilter(FileFilterUtils.directoryFileFilter,
+ *                                      HiddenFileFilter.VISIBLE);
+ *
+ *    // Create a filter for Files ending in ".txt"
+ *    IOFileFilter fooFileFilter = 
+ *        FileFilterUtils.andFileFilter(FileFilterUtils.fileFileFilter,
+ *                                      FileFilterUtils.suffixFileFilter(".txt"));
+ *
+ *    // Combine the directory and file filters using an OR condition
+ *    java.io.FileFilter fooFilter = 
+ *        FileFilterUtils.orFileFilter(fooDirFilter, fooFileFilter);
+ *
+ *    // Use the filter to construct a DirectoryWalker implementation
+ *    FooDirectoryWalker walker = new FooDirectoryWalker(fooFilter);
+ * 
+ *

+ * The third constructor option is to specify separate filters, one for + * directories and one for files. These are combined internally to form + * the correct FileFilter, something which is very easy to + * get wrong when attempted manually, particularly when trying to + * express constructs like 'any file in directories named docs'. + *

+ * For example, if you wanted all directories which are not hidden + * and files which end in ".txt": + *

+ *  public class FooDirectoryWalker extends DirectoryWalker {
+ *    public FooDirectoryWalker(IOFileFilter dirFilter, IOFileFilter fileFilter) {
+ *      super(dirFilter, fileFilter, -1);
+ *    }
+ *  }
+ *  
+ *  // Use the filters to construct the walker
+ *  FooDirectoryWalker walker = new FooDirectoryWalker(
+ *    HiddenFileFilter.VISIBLE,
+ *    FileFilterUtils.suffixFileFilter(".txt"),
+ *  );
+ * 
+ * This is much simpler than the previous example, and is why it is the preferred + * option for filtering. + * + * + *

3. Cancellation

+ * + * The DirectoryWalker contains some of the logic required for cancel processing. + * Subclasses must complete the implementation. + *

+ * What DirectoryWalker does provide for cancellation is: + *

    + *
  • {@link CancelException} which can be thrown in any of the + * lifecycle methods to stop processing.
  • + *
  • The walk() method traps thrown {@link CancelException} + * and calls the handleCancelled() method, providing + * a place for custom cancel processing.
  • + *
+ *

+ * Implementations need to provide: + *

    + *
  • The decision logic on whether to cancel processing or not.
  • + *
  • Constructing and throwing a {@link CancelException}.
  • + *
  • Custom cancel processing in the handleCancelled() method. + *
+ *

+ * Two possible scenarios are envisaged for cancellation: + *

    + *
  • 3.1 External / Mult-threaded - cancellation being + * decided/initiated by an external process.
  • + *
  • 3.2 Internal - cancellation being decided/initiated + * from within a DirectoryWalker implementation.
  • + *
+ *

+ * The following sections provide example implementations for these two different + * scenarios. + * + * + *

3.1 External / Multi-threaded

+ * + * This example provides a public cancel() method that can be + * called by another thread to stop the processing. A typical example use-case + * would be a cancel button on a GUI. Calling this method sets a + * + * volatile flag to ensure it will work properly in a multi-threaded environment. + * The flag is returned by the handleIsCancelled() method, which + * will cause the walk to stop immediately. The handleCancelled() + * method will be the next, and last, callback method received once cancellation + * has occurred. + * + *
+ *  public class FooDirectoryWalker extends DirectoryWalker {
+ *
+ *    private volatile boolean cancelled = false;
+ *
+ *    public void cancel() {
+ *        cancelled = true;
+ *    }
+ *
+ *    private void handleIsCancelled(File file, int depth, Collection results) {
+ *        return cancelled;
+ *    }
+ *
+ *    protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) {
+ *        // implement processing required when a cancellation occurs
+ *    }
+ *  }
+ * 
+ * + * + *

3.2 Internal

+ * + * This shows an example of how internal cancellation processing could be implemented. + * Note the decision logic and throwing a {@link CancelException} could be implemented + * in any of the lifecycle methods. + * + *
+ *  public class BarDirectoryWalker extends DirectoryWalker {
+ *
+ *    protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException {
+ *        // cancel if hidden directory
+ *        if (directory.isHidden()) {
+ *            throw new CancelException(file, depth);
+ *        }
+ *        return true;
+ *    }
+ *
+ *    protected void handleFile(File file, int depth, Collection results) throws IOException {
+ *        // cancel if read-only file
+ *        if (!file.canWrite()) {
+ *            throw new CancelException(file, depth);
+ *        }
+ *        results.add(file);
+ *    }
+ *
+ *    protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) {
+ *        // implement processing required when a cancellation occurs
+ *    }
+ *  }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 424748 $ + */ +public abstract class DirectoryWalker { + + /** + * The file filter to use to filter files and directories. + */ + private final FileFilter filter; + /** + * The limit on the directory depth to walk. + */ + private final int depthLimit; + + /** + * Construct an instance with no filtering and unlimited depth. + */ + protected DirectoryWalker() { + this(null, -1); + } + + /** + * Construct an instance with a filter and limit the depth navigated to. + *

+ * The filter controls which files and directories will be navigated to as + * part of the walk. The {@link FileFilterUtils} class is useful for combining + * various filters together. A null filter means that no + * filtering should occur and all files and directories will be visited. + * + * @param filter the filter to apply, null means visit all files + * @param depthLimit controls how deep the hierarchy is + * navigated to (less than 0 means unlimited) + */ + protected DirectoryWalker(FileFilter filter, int depthLimit) { + this.filter = filter; + this.depthLimit = depthLimit; + } + + /** + * Construct an instance with a directory and a file filter and an optional + * limit on the depth navigated to. + *

+ * The filters control which files and directories will be navigated to as part + * of the walk. This constructor uses {@link FileFilterUtils#makeDirectoryOnly(IOFileFilter)} + * and {@link FileFilterUtils#makeFileOnly(IOFileFilter)} internally to combine the filters. + * A null filter means that no filtering should occur. + * + * @param directoryFilter the filter to apply to directories, null means visit all directories + * @param fileFilter the filter to apply to files, null means visit all files + * @param depthLimit controls how deep the hierarchy is + * navigated to (less than 0 means unlimited) + */ + protected DirectoryWalker(IOFileFilter directoryFilter, IOFileFilter fileFilter, int depthLimit) { + if (directoryFilter == null && fileFilter == null) { + this.filter = null; + } else { + directoryFilter = (directoryFilter != null ? directoryFilter : TrueFileFilter.TRUE); + fileFilter = (fileFilter != null ? fileFilter : TrueFileFilter.TRUE); + directoryFilter = FileFilterUtils.makeDirectoryOnly(directoryFilter); + fileFilter = FileFilterUtils.makeFileOnly(fileFilter); + this.filter = FileFilterUtils.orFileFilter(directoryFilter, fileFilter); + } + this.depthLimit = depthLimit; + } + + //----------------------------------------------------------------------- + /** + * Internal method that walks the directory hierarchy in a depth-first manner. + *

+ * Users of this class do not need to call this method. This method will + * be called automatically by another (public) method on the specific subclass. + *

+ * Writers of subclasses should call this method to start the directory walk. + * Once called, this method will emit events as it walks the hierarchy. + * The event methods have the prefix handle. + * + * @param startDirectory the directory to start from, not null + * @param results the collection of result objects, may be updated + * @throws NullPointerException if the start directory is null + * @throws IOException if an I/O Error occurs + */ + protected final void walk(File startDirectory, Collection results) throws IOException { + if (startDirectory == null) { + throw new NullPointerException("Start Directory is null"); + } + try { + handleStart(startDirectory, results); + walk(startDirectory, 0, results); + handleEnd(results); + } catch(CancelException cancel) { + handleCancelled(startDirectory, results, cancel); + } + } + + /** + * Main recursive method to examine the directory hierarchy. + * + * @param directory the directory to examine, not null + * @param depth the directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + private void walk(File directory, int depth, Collection results) throws IOException { + checkIfCancelled(directory, depth, results); + if (handleDirectory(directory, depth, results)) { + handleDirectoryStart(directory, depth, results); + int childDepth = depth + 1; + if (depthLimit < 0 || childDepth <= depthLimit) { + checkIfCancelled(directory, depth, results); + File[] childFiles = (filter == null ? directory.listFiles() : directory.listFiles(filter)); + if (childFiles == null) { + handleRestricted(directory, childDepth, results); + } else { + for (int i = 0; i < childFiles.length; i++) { + File childFile = childFiles[i]; + if (childFile.isDirectory()) { + walk(childFile, childDepth, results); + } else { + checkIfCancelled(childFile, childDepth, results); + handleFile(childFile, childDepth, results); + checkIfCancelled(childFile, childDepth, results); + } + } + } + } + handleDirectoryEnd(directory, depth, results); + } + checkIfCancelled(directory, depth, results); + } + + //----------------------------------------------------------------------- + /** + * Checks whether the walk has been cancelled by calling {@link #handleIsCancelled}, + * throwing a CancelException if it has. + *

+ * Writers of subclasses should not normally call this method as it is called + * automatically by the walk of the tree. However, sometimes a single method, + * typically {@link #handleFile}, may take a long time to run. In that case, + * you may wish to check for cancellation by calling this method. + * + * @param file the current file being processed + * @param depth the current file level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected final void checkIfCancelled(File file, int depth, Collection results) throws IOException { + if (handleIsCancelled(file, depth, results)) { + throw new CancelException(file, depth); + } + } + + /** + * Overridable callback method invoked to determine if the entire walk + * operation should be immediately cancelled. + *

+ * This method should be implemented by those subclasses that want to + * provide a public cancel() method available from another + * thread. The design pattern for the subclass should be as follows: + *

+     *  public class FooDirectoryWalker extends DirectoryWalker {
+     *    private volatile boolean cancelled = false;
+     *
+     *    public void cancel() {
+     *        cancelled = true;
+     *    }
+     *    private void handleIsCancelled(File file, int depth, Collection results) {
+     *        return cancelled;
+     *    }
+     *    protected void handleCancelled(File startDirectory,
+     *              Collection results, CancelException cancel) {
+     *        // implement processing required when a cancellation occurs
+     *    }
+     *  }
+     * 
+ *

+ * If this method returns true, then the directory walk is immediately + * cancelled. The next callback method will be {@link #handleCancelled}. + *

+ * This implementation returns false. + * + * @param file the file or directory being processed + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @return true if the walk has been cancelled + * @throws IOException if an I/O Error occurs + */ + protected boolean handleIsCancelled( + File file, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + return false; // not cancelled + } + + /** + * Overridable callback method invoked when the operation is cancelled. + * The file being processed when the cancellation occurred can be + * obtained from the exception. + *

+ * This implementation just re-throws the {@link CancelException}. + * + * @param startDirectory the directory that the walk started from + * @param results the collection of result objects, may be updated + * @param cancel the exception throw to cancel further processing + * containing details at the point of cancellation. + * @throws IOException if an I/O Error occurs + */ + protected void handleCancelled(File startDirectory, Collection results, + CancelException cancel) throws IOException { + // re-throw exception - overridable by subclass + throw cancel; + } + + //----------------------------------------------------------------------- + /** + * Overridable callback method invoked at the start of processing. + *

+ * This implementation does nothing. + * + * @param startDirectory the directory to start from + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleStart(File startDirectory, Collection results) throws IOException { + // do nothing - overridable by subclass + } + + /** + * Overridable callback method invoked to determine if a directory should be processed. + *

+ * This method returns a boolean to indicate if the directory should be examined or not. + * If you return false, the entire directory and any subdirectories will be skipped. + * Note that this functionality is in addition to the filtering by file filter. + *

+ * This implementation does nothing and returns true. + * + * @param directory the current directory being processed + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @return true to process this directory, false to skip this directory + * @throws IOException if an I/O Error occurs + */ + protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + return true; // process directory + } + + /** + * Overridable callback method invoked at the start of processing each directory. + *

+ * This implementation does nothing. + * + * @param directory the current directory being processed + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleDirectoryStart(File directory, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + } + + /** + * Overridable callback method invoked for each (non-directory) file. + *

+ * This implementation does nothing. + * + * @param file the current file being processed + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleFile(File file, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + } + + /** + * Overridable callback method invoked for each restricted directory. + *

+ * This implementation does nothing. + * + * @param directory the restricted directory + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleRestricted(File directory, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + } + + /** + * Overridable callback method invoked at the end of processing each directory. + *

+ * This implementation does nothing. + * + * @param directory the directory being processed + * @param depth the current directory level (starting directory = 0) + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleDirectoryEnd(File directory, int depth, Collection results) throws IOException { + // do nothing - overridable by subclass + } + + /** + * Overridable callback method invoked at the end of processing. + *

+ * This implementation does nothing. + * + * @param results the collection of result objects, may be updated + * @throws IOException if an I/O Error occurs + */ + protected void handleEnd(Collection results) throws IOException { + // do nothing - overridable by subclass + } + + //----------------------------------------------------------------------- + /** + * CancelException is thrown in DirectoryWalker to cancel the current + * processing. + */ + public static class CancelException extends IOException { + + /** Serialization id. */ + private static final long serialVersionUID = 1347339620135041008L; + + /** The file being processed when the exception was thrown. */ + private File file; + /** The file depth when the exception was thrown. */ + private int depth = -1; + + /** + * Constructs a CancelException with + * the file and depth when cancellation occurred. + * + * @param file the file when the operation was cancelled, may be null + * @param depth the depth when the operation was cancelled, may be null + */ + public CancelException(File file, int depth) { + this("Operation Cancelled", file, depth); + } + + /** + * Constructs a CancelException with + * an appropriate message and the file and depth when + * cancellation occurred. + * + * @param message the detail message + * @param file the file when the operation was cancelled + * @param depth the depth when the operation was cancelled + */ + public CancelException(String message, File file, int depth) { + super(message); + this.file = file; + this.depth = depth; + } + + /** + * Return the file when the operation was cancelled. + * + * @return the file when the operation was cancelled + */ + public File getFile() { + return file; + } + + /** + * Return the depth when the operation was cancelled. + * + * @return the depth when the operation was cancelled + */ + public int getDepth() { + return depth; + } + } +} diff --git a/src/org/apache/commons/io/EndianUtils.java b/src/org/apache/commons/io/EndianUtils.java new file mode 100644 index 000000000..810feac04 --- /dev/null +++ b/src/org/apache/commons/io/EndianUtils.java @@ -0,0 +1,489 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Utility code for dealing with different endian systems. + *

+ * Different computer architectures adopt different conventions for + * byte ordering. In so-called "Little Endian" architectures (eg Intel), + * the low-order byte is stored in memory at the lowest address, and + * subsequent bytes at higher addresses. For "Big Endian" architectures + * (eg Motorola), the situation is reversed. + * This class helps you solve this incompatability. + *

+ * Origin of code: Excalibur + * + * @author Peter Donald + * @version $Id: EndianUtils.java 539632 2007-05-18 23:37:59Z bayard $ + * @see org.apache.commons.io.input.SwappedDataInputStream + */ +public class EndianUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public EndianUtils() { + super(); + } + + // ========================================== Swapping routines + + /** + * Converts a "short" value between endian systems. + * @param value value to convert + * @return the converted value + */ + public static short swapShort(short value) { + return (short) ( ( ( ( value >> 0 ) & 0xff ) << 8 ) + + ( ( ( value >> 8 ) & 0xff ) << 0 ) ); + } + + /** + * Converts a "int" value between endian systems. + * @param value value to convert + * @return the converted value + */ + public static int swapInteger(int value) { + return + ( ( ( value >> 0 ) & 0xff ) << 24 ) + + ( ( ( value >> 8 ) & 0xff ) << 16 ) + + ( ( ( value >> 16 ) & 0xff ) << 8 ) + + ( ( ( value >> 24 ) & 0xff ) << 0 ); + } + + /** + * Converts a "long" value between endian systems. + * @param value value to convert + * @return the converted value + */ + public static long swapLong(long value) { + return + ( ( ( value >> 0 ) & 0xff ) << 56 ) + + ( ( ( value >> 8 ) & 0xff ) << 48 ) + + ( ( ( value >> 16 ) & 0xff ) << 40 ) + + ( ( ( value >> 24 ) & 0xff ) << 32 ) + + ( ( ( value >> 32 ) & 0xff ) << 24 ) + + ( ( ( value >> 40 ) & 0xff ) << 16 ) + + ( ( ( value >> 48 ) & 0xff ) << 8 ) + + ( ( ( value >> 56 ) & 0xff ) << 0 ); + } + + /** + * Converts a "float" value between endian systems. + * @param value value to convert + * @return the converted value + */ + public static float swapFloat(float value) { + return Float.intBitsToFloat( swapInteger( Float.floatToIntBits( value ) ) ); + } + + /** + * Converts a "double" value between endian systems. + * @param value value to convert + * @return the converted value + */ + public static double swapDouble(double value) { + return Double.longBitsToDouble( swapLong( Double.doubleToLongBits( value ) ) ); + } + + // ========================================== Swapping read/write routines + + /** + * Writes a "short" value to a byte array at a given offset. The value is + * converted to the opposed endian system while writing. + * @param data target byte array + * @param offset starting offset in the byte array + * @param value value to write + */ + public static void writeSwappedShort(byte[] data, int offset, short value) { + data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff ); + data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff ); + } + + /** + * Reads a "short" value from a byte array at a given offset. The value is + * converted to the opposed endian system while reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static short readSwappedShort(byte[] data, int offset) { + return (short)( ( ( data[ offset + 0 ] & 0xff ) << 0 ) + + ( ( data[ offset + 1 ] & 0xff ) << 8 ) ); + } + + /** + * Reads an unsigned short (16-bit) value from a byte array at a given + * offset. The value is converted to the opposed endian system while + * reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static int readSwappedUnsignedShort(byte[] data, int offset) { + return ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) + + ( ( data[ offset + 1 ] & 0xff ) << 8 ) ); + } + + /** + * Writes a "int" value to a byte array at a given offset. The value is + * converted to the opposed endian system while writing. + * @param data target byte array + * @param offset starting offset in the byte array + * @param value value to write + */ + public static void writeSwappedInteger(byte[] data, int offset, int value) { + data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff ); + data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff ); + data[ offset + 2 ] = (byte)( ( value >> 16 ) & 0xff ); + data[ offset + 3 ] = (byte)( ( value >> 24 ) & 0xff ); + } + + /** + * Reads a "int" value from a byte array at a given offset. The value is + * converted to the opposed endian system while reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static int readSwappedInteger(byte[] data, int offset) { + return ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) + + ( ( data[ offset + 1 ] & 0xff ) << 8 ) + + ( ( data[ offset + 2 ] & 0xff ) << 16 ) + + ( ( data[ offset + 3 ] & 0xff ) << 24 ) ); + } + + /** + * Reads an unsigned integer (32-bit) value from a byte array at a given + * offset. The value is converted to the opposed endian system while + * reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static long readSwappedUnsignedInteger(byte[] data, int offset) { + long low = ( ( ( data[ offset + 0 ] & 0xff ) << 0 ) + + ( ( data[ offset + 1 ] & 0xff ) << 8 ) + + ( ( data[ offset + 2 ] & 0xff ) << 16 ) ); + + long high = data[ offset + 3 ] & 0xff; + + return (high << 24) + (0xffffffffL & low); + } + + /** + * Writes a "long" value to a byte array at a given offset. The value is + * converted to the opposed endian system while writing. + * @param data target byte array + * @param offset starting offset in the byte array + * @param value value to write + */ + public static void writeSwappedLong(byte[] data, int offset, long value) { + data[ offset + 0 ] = (byte)( ( value >> 0 ) & 0xff ); + data[ offset + 1 ] = (byte)( ( value >> 8 ) & 0xff ); + data[ offset + 2 ] = (byte)( ( value >> 16 ) & 0xff ); + data[ offset + 3 ] = (byte)( ( value >> 24 ) & 0xff ); + data[ offset + 4 ] = (byte)( ( value >> 32 ) & 0xff ); + data[ offset + 5 ] = (byte)( ( value >> 40 ) & 0xff ); + data[ offset + 6 ] = (byte)( ( value >> 48 ) & 0xff ); + data[ offset + 7 ] = (byte)( ( value >> 56 ) & 0xff ); + } + + /** + * Reads a "long" value from a byte array at a given offset. The value is + * converted to the opposed endian system while reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static long readSwappedLong(byte[] data, int offset) { + long low = + ( ( data[ offset + 0 ] & 0xff ) << 0 ) + + ( ( data[ offset + 1 ] & 0xff ) << 8 ) + + ( ( data[ offset + 2 ] & 0xff ) << 16 ) + + ( ( data[ offset + 3 ] & 0xff ) << 24 ); + long high = + ( ( data[ offset + 4 ] & 0xff ) << 0 ) + + ( ( data[ offset + 5 ] & 0xff ) << 8 ) + + ( ( data[ offset + 6 ] & 0xff ) << 16 ) + + ( ( data[ offset + 7 ] & 0xff ) << 24 ); + return (high << 32) + (0xffffffffL & low); + } + + /** + * Writes a "float" value to a byte array at a given offset. The value is + * converted to the opposed endian system while writing. + * @param data target byte array + * @param offset starting offset in the byte array + * @param value value to write + */ + public static void writeSwappedFloat(byte[] data, int offset, float value) { + writeSwappedInteger( data, offset, Float.floatToIntBits( value ) ); + } + + /** + * Reads a "float" value from a byte array at a given offset. The value is + * converted to the opposed endian system while reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static float readSwappedFloat(byte[] data, int offset) { + return Float.intBitsToFloat( readSwappedInteger( data, offset ) ); + } + + /** + * Writes a "double" value to a byte array at a given offset. The value is + * converted to the opposed endian system while writing. + * @param data target byte array + * @param offset starting offset in the byte array + * @param value value to write + */ + public static void writeSwappedDouble(byte[] data, int offset, double value) { + writeSwappedLong( data, offset, Double.doubleToLongBits( value ) ); + } + + /** + * Reads a "double" value from a byte array at a given offset. The value is + * converted to the opposed endian system while reading. + * @param data source byte array + * @param offset starting offset in the byte array + * @return the value read + */ + public static double readSwappedDouble(byte[] data, int offset) { + return Double.longBitsToDouble( readSwappedLong( data, offset ) ); + } + + /** + * Writes a "short" value to an OutputStream. The value is + * converted to the opposed endian system while writing. + * @param output target OutputStream + * @param value value to write + * @throws IOException in case of an I/O problem + */ + public static void writeSwappedShort(OutputStream output, short value) + throws IOException + { + output.write( (byte)( ( value >> 0 ) & 0xff ) ); + output.write( (byte)( ( value >> 8 ) & 0xff ) ); + } + + /** + * Reads a "short" value from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static short readSwappedShort(InputStream input) + throws IOException + { + return (short)( ( ( read( input ) & 0xff ) << 0 ) + + ( ( read( input ) & 0xff ) << 8 ) ); + } + + /** + * Reads a unsigned short (16-bit) from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static int readSwappedUnsignedShort(InputStream input) + throws IOException + { + int value1 = read( input ); + int value2 = read( input ); + + return ( ( ( value1 & 0xff ) << 0 ) + + ( ( value2 & 0xff ) << 8 ) ); + } + + /** + * Writes a "int" value to an OutputStream. The value is + * converted to the opposed endian system while writing. + * @param output target OutputStream + * @param value value to write + * @throws IOException in case of an I/O problem + */ + public static void writeSwappedInteger(OutputStream output, int value) + throws IOException + { + output.write( (byte)( ( value >> 0 ) & 0xff ) ); + output.write( (byte)( ( value >> 8 ) & 0xff ) ); + output.write( (byte)( ( value >> 16 ) & 0xff ) ); + output.write( (byte)( ( value >> 24 ) & 0xff ) ); + } + + /** + * Reads a "int" value from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static int readSwappedInteger(InputStream input) + throws IOException + { + int value1 = read( input ); + int value2 = read( input ); + int value3 = read( input ); + int value4 = read( input ); + + return ( ( value1 & 0xff ) << 0 ) + + ( ( value2 & 0xff ) << 8 ) + + ( ( value3 & 0xff ) << 16 ) + + ( ( value4 & 0xff ) << 24 ); + } + + /** + * Reads a unsigned integer (32-bit) from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static long readSwappedUnsignedInteger(InputStream input) + throws IOException + { + int value1 = read( input ); + int value2 = read( input ); + int value3 = read( input ); + int value4 = read( input ); + + long low = ( ( ( value1 & 0xff ) << 0 ) + + ( ( value2 & 0xff ) << 8 ) + + ( ( value3 & 0xff ) << 16 ) ); + + long high = value4 & 0xff; + + return (high << 24) + (0xffffffffL & low); + } + + /** + * Writes a "long" value to an OutputStream. The value is + * converted to the opposed endian system while writing. + * @param output target OutputStream + * @param value value to write + * @throws IOException in case of an I/O problem + */ + public static void writeSwappedLong(OutputStream output, long value) + throws IOException + { + output.write( (byte)( ( value >> 0 ) & 0xff ) ); + output.write( (byte)( ( value >> 8 ) & 0xff ) ); + output.write( (byte)( ( value >> 16 ) & 0xff ) ); + output.write( (byte)( ( value >> 24 ) & 0xff ) ); + output.write( (byte)( ( value >> 32 ) & 0xff ) ); + output.write( (byte)( ( value >> 40 ) & 0xff ) ); + output.write( (byte)( ( value >> 48 ) & 0xff ) ); + output.write( (byte)( ( value >> 56 ) & 0xff ) ); + } + + /** + * Reads a "long" value from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static long readSwappedLong(InputStream input) + throws IOException + { + byte[] bytes = new byte[8]; + for ( int i=0; i<8; i++ ) { + bytes[i] = (byte) read( input ); + } + return readSwappedLong( bytes, 0 ); + } + + /** + * Writes a "float" value to an OutputStream. The value is + * converted to the opposed endian system while writing. + * @param output target OutputStream + * @param value value to write + * @throws IOException in case of an I/O problem + */ + public static void writeSwappedFloat(OutputStream output, float value) + throws IOException + { + writeSwappedInteger( output, Float.floatToIntBits( value ) ); + } + + /** + * Reads a "float" value from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static float readSwappedFloat(InputStream input) + throws IOException + { + return Float.intBitsToFloat( readSwappedInteger( input ) ); + } + + /** + * Writes a "double" value to an OutputStream. The value is + * converted to the opposed endian system while writing. + * @param output target OutputStream + * @param value value to write + * @throws IOException in case of an I/O problem + */ + public static void writeSwappedDouble(OutputStream output, double value) + throws IOException + { + writeSwappedLong( output, Double.doubleToLongBits( value ) ); + } + + /** + * Reads a "double" value from an InputStream. The value is + * converted to the opposed endian system while reading. + * @param input source InputStream + * @return the value just read + * @throws IOException in case of an I/O problem + */ + public static double readSwappedDouble(InputStream input) + throws IOException + { + return Double.longBitsToDouble( readSwappedLong( input ) ); + } + + /** + * Reads the next byte from the input stream. + * @param input the stream + * @return the byte + * @throws IOException if the end of file is reached + */ + private static int read(InputStream input) + throws IOException + { + int value = input.read(); + + if( -1 == value ) { + throw new EOFException( "Unexpected EOF reached" ); + } + + return value; + } +} diff --git a/src/org/apache/commons/io/FileCleaner.java b/src/org/apache/commons/io/FileCleaner.java new file mode 100644 index 000000000..59c2f4109 --- /dev/null +++ b/src/org/apache/commons/io/FileCleaner.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; + +/** + * Keeps track of files awaiting deletion, and deletes them when an associated + * marker object is reclaimed by the garbage collector. + *

+ * This utility creates a background thread to handle file deletion. + * Each file to be deleted is registered with a handler object. + * When the handler object is garbage collected, the file is deleted. + *

+ * In an environment with multiple class loaders (a servlet container, for + * example), you should consider stopping the background thread if it is no + * longer needed. This is done by invoking the method + * {@link #exitWhenFinished}, typically in + * {@link javax.servlet.ServletContextListener#contextDestroyed} or similar. + * + * @author Noel Bergman + * @author Martin Cooper + * @version $Id: FileCleaner.java 553012 2007-07-03 23:01:07Z ggregory $ + * @deprecated Use {@link FileCleaningTracker} + */ +public class FileCleaner { + /** + * The instance to use for the deprecated, static methods. + */ + static final FileCleaningTracker theInstance = new FileCleaningTracker(); + + //----------------------------------------------------------------------- + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the file is null + * @deprecated Use {@link FileCleaningTracker#track(File, Object)}. + */ + public static void track(File file, Object marker) { + theInstance.track(file, marker); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the file is null + * @deprecated Use {@link FileCleaningTracker#track(File, Object, FileDeleteStrategy)}. + */ + public static void track(File file, Object marker, FileDeleteStrategy deleteStrategy) { + theInstance.track(file, marker, deleteStrategy); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the path is null + * @deprecated Use {@link FileCleaningTracker#track(String, Object)}. + */ + public static void track(String path, Object marker) { + theInstance.track(path, marker); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the path is null + * @deprecated Use {@link FileCleaningTracker#track(String, Object, FileDeleteStrategy)}. + */ + public static void track(String path, Object marker, FileDeleteStrategy deleteStrategy) { + theInstance.track(path, marker, deleteStrategy); + } + + //----------------------------------------------------------------------- + /** + * Retrieve the number of files currently being tracked, and therefore + * awaiting deletion. + * + * @return the number of files being tracked + * @deprecated Use {@link FileCleaningTracker#getTrackCount()}. + */ + public static int getTrackCount() { + return theInstance.getTrackCount(); + } + + /** + * Call this method to cause the file cleaner thread to terminate when + * there are no more objects being tracked for deletion. + *

+ * In a simple environment, you don't need this method as the file cleaner + * thread will simply exit when the JVM exits. In a more complex environment, + * with multiple class loaders (such as an application server), you should be + * aware that the file cleaner thread will continue running even if the class + * loader it was started from terminates. This can consitute a memory leak. + *

+ * For example, suppose that you have developed a web application, which + * contains the commons-io jar file in your WEB-INF/lib directory. In other + * words, the FileCleaner class is loaded through the class loader of your + * web application. If the web application is terminated, but the servlet + * container is still running, then the file cleaner thread will still exist, + * posing a memory leak. + *

+ * This method allows the thread to be terminated. Simply call this method + * in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}. + * One called, no new objects can be tracked by the file cleaner. + * @deprecated Use {@link FileCleaningTracker#exitWhenFinished()}. + */ + public static synchronized void exitWhenFinished() { + theInstance.exitWhenFinished(); + } + + /** + * Returns the singleton instance, which is used by the deprecated, static methods. + * This is mainly useful for code, which wants to support the new + * {@link FileCleaningTracker} class while maintain compatibility with the + * deprecated {@link FileCleaner}. + * + * @return the singleton instance + */ + public static FileCleaningTracker getInstance() { + return theInstance; + } +} diff --git a/src/org/apache/commons/io/FileCleaningTracker.java b/src/org/apache/commons/io/FileCleaningTracker.java new file mode 100644 index 000000000..ea976d60a --- /dev/null +++ b/src/org/apache/commons/io/FileCleaningTracker.java @@ -0,0 +1,258 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.lang.ref.PhantomReference; +import java.lang.ref.ReferenceQueue; +import java.util.Collection; +import java.util.Vector; + +/** + * Keeps track of files awaiting deletion, and deletes them when an associated + * marker object is reclaimed by the garbage collector. + *

+ * This utility creates a background thread to handle file deletion. + * Each file to be deleted is registered with a handler object. + * When the handler object is garbage collected, the file is deleted. + *

+ * In an environment with multiple class loaders (a servlet container, for + * example), you should consider stopping the background thread if it is no + * longer needed. This is done by invoking the method + * {@link #exitWhenFinished}, typically in + * {@link javax.servlet.ServletContextListener#contextDestroyed} or similar. + * + * @author Noel Bergman + * @author Martin Cooper + * @version $Id: FileCleaner.java 490987 2006-12-29 12:11:48Z scolebourne $ + */ +public class FileCleaningTracker { + /** + * Queue of Tracker instances being watched. + */ + ReferenceQueue /* Tracker */ q = new ReferenceQueue(); + /** + * Collection of Tracker instances in existence. + */ + final Collection /* Tracker */ trackers = new Vector(); // synchronized + /** + * Whether to terminate the thread when the tracking is complete. + */ + volatile boolean exitWhenFinished = false; + /** + * The thread that will clean up registered files. + */ + Thread reaper; + + //----------------------------------------------------------------------- + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the file is null + */ + public void track(File file, Object marker) { + track(file, marker, (FileDeleteStrategy) null); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param file the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the file is null + */ + public void track(File file, Object marker, FileDeleteStrategy deleteStrategy) { + if (file == null) { + throw new NullPointerException("The file must not be null"); + } + addTracker(file.getPath(), marker, deleteStrategy); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @throws NullPointerException if the path is null + */ + public void track(String path, Object marker) { + track(path, marker, (FileDeleteStrategy) null); + } + + /** + * Track the specified file, using the provided marker, deleting the file + * when the marker instance is garbage collected. + * The speified deletion strategy is used. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @throws NullPointerException if the path is null + */ + public void track(String path, Object marker, FileDeleteStrategy deleteStrategy) { + if (path == null) { + throw new NullPointerException("The path must not be null"); + } + addTracker(path, marker, deleteStrategy); + } + + /** + * Adds a tracker to the list of trackers. + * + * @param path the full path to the file to be tracked, not null + * @param marker the marker object used to track the file, not null + * @param deleteStrategy the strategy to delete the file, null means normal + */ + private synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) { + // synchronized block protects reaper + if (exitWhenFinished) { + throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called"); + } + if (reaper == null) { + reaper = new Reaper(); + reaper.start(); + } + trackers.add(new Tracker(path, deleteStrategy, marker, q)); + } + + //----------------------------------------------------------------------- + /** + * Retrieve the number of files currently being tracked, and therefore + * awaiting deletion. + * + * @return the number of files being tracked + */ + public int getTrackCount() { + return trackers.size(); + } + + /** + * Call this method to cause the file cleaner thread to terminate when + * there are no more objects being tracked for deletion. + *

+ * In a simple environment, you don't need this method as the file cleaner + * thread will simply exit when the JVM exits. In a more complex environment, + * with multiple class loaders (such as an application server), you should be + * aware that the file cleaner thread will continue running even if the class + * loader it was started from terminates. This can consitute a memory leak. + *

+ * For example, suppose that you have developed a web application, which + * contains the commons-io jar file in your WEB-INF/lib directory. In other + * words, the FileCleaner class is loaded through the class loader of your + * web application. If the web application is terminated, but the servlet + * container is still running, then the file cleaner thread will still exist, + * posing a memory leak. + *

+ * This method allows the thread to be terminated. Simply call this method + * in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}. + * One called, no new objects can be tracked by the file cleaner. + */ + public synchronized void exitWhenFinished() { + // synchronized block protects reaper + exitWhenFinished = true; + if (reaper != null) { + synchronized (reaper) { + reaper.interrupt(); + } + } + } + + //----------------------------------------------------------------------- + /** + * The reaper thread. + */ + private final class Reaper extends Thread { + /** Construct a new Reaper */ + Reaper() { + super("File Reaper"); + setPriority(Thread.MAX_PRIORITY); + setDaemon(true); + } + + /** + * Run the reaper thread that will delete files as their associated + * marker objects are reclaimed by the garbage collector. + */ + public void run() { + // thread exits when exitWhenFinished is true and there are no more tracked objects + while (exitWhenFinished == false || trackers.size() > 0) { + Tracker tracker = null; + try { + // Wait for a tracker to remove. + tracker = (Tracker) q.remove(); + } catch (Exception e) { + continue; + } + if (tracker != null) { + tracker.delete(); + tracker.clear(); + trackers.remove(tracker); + } + } + } + } + + //----------------------------------------------------------------------- + /** + * Inner class which acts as the reference for a file pending deletion. + */ + private static final class Tracker extends PhantomReference { + + /** + * The full path to the file being tracked. + */ + private final String path; + /** + * The strategy for deleting files. + */ + private final FileDeleteStrategy deleteStrategy; + + /** + * Constructs an instance of this class from the supplied parameters. + * + * @param path the full path to the file to be tracked, not null + * @param deleteStrategy the strategy to delete the file, null means normal + * @param marker the marker object used to track the file, not null + * @param queue the queue on to which the tracker will be pushed, not null + */ + Tracker(String path, FileDeleteStrategy deleteStrategy, Object marker, ReferenceQueue queue) { + super(marker, queue); + this.path = path; + this.deleteStrategy = (deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy); + } + + /** + * Deletes the file associated with this tracker instance. + * + * @return true if the file was deleted successfully; + * false otherwise. + */ + public boolean delete() { + return deleteStrategy.deleteQuietly(new File(path)); + } + } + +} diff --git a/src/org/apache/commons/io/FileDeleteStrategy.java b/src/org/apache/commons/io/FileDeleteStrategy.java new file mode 100644 index 000000000..8b6b4b9aa --- /dev/null +++ b/src/org/apache/commons/io/FileDeleteStrategy.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.io.IOException; + +/** + * Strategy for deleting files. + *

+ * There is more than one way to delete a file. + * You may want to limit access to certain directories, to only delete + * directories if they are empty, or maybe to force deletion. + *

+ * This class captures the strategy to use and is designed for user subclassing. + * + * @author Stephen Colebourne + * @version $Id: FileDeleteStrategy.java 453903 2006-10-07 13:47:06Z scolebourne $ + * @since Commons IO 1.3 + */ +public class FileDeleteStrategy { + + /** + * The singleton instance for normal file deletion, which does not permit + * the deletion of directories that are not empty. + */ + public static final FileDeleteStrategy NORMAL = new FileDeleteStrategy("Normal"); + /** + * The singleton instance for forced file deletion, which always deletes, + * even if the file represents a non-empty directory. + */ + public static final FileDeleteStrategy FORCE = new ForceFileDeleteStrategy(); + + /** The name of the strategy. */ + private final String name; + + //----------------------------------------------------------------------- + /** + * Restricted constructor. + * + * @param name the name by which the strategy is known + */ + protected FileDeleteStrategy(String name) { + this.name = name; + } + + //----------------------------------------------------------------------- + /** + * Deletes the file object, which may be a file or a directory. + * All IOExceptions are caught and false returned instead. + * If the file does not exist or is null, true is returned. + *

+ * Subclass writers should override {@link #doDelete(File)}, not this method. + * + * @param fileToDelete the file to delete, null returns true + * @return true if the file was deleted, or there was no such file + */ + public boolean deleteQuietly(File fileToDelete) { + if (fileToDelete == null || fileToDelete.exists() == false) { + return true; + } + try { + return doDelete(fileToDelete); + } catch (IOException ex) { + return false; + } + } + + /** + * Deletes the file object, which may be a file or a directory. + * If the file does not exist, the method just returns. + *

+ * Subclass writers should override {@link #doDelete(File)}, not this method. + * + * @param fileToDelete the file to delete, not null + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + public void delete(File fileToDelete) throws IOException { + if (fileToDelete.exists() && doDelete(fileToDelete) == false) { + throw new IOException("Deletion failed: " + fileToDelete); + } + } + + /** + * Actually deletes the file object, which may be a file or a directory. + *

+ * This method is designed for subclasses to override. + * The implementation may return either false or an IOException + * when deletion fails. The {@link #delete(File)} and {@link #deleteQuietly(File)} + * methods will handle either response appropriately. + * A check has been made to ensure that the file will exist. + *

+ * This implementation uses {@link File#delete()}. + * + * @param fileToDelete the file to delete, exists, not null + * @return true if the file was deleteds + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + protected boolean doDelete(File fileToDelete) throws IOException { + return fileToDelete.delete(); + } + + //----------------------------------------------------------------------- + /** + * Gets a string describing the delete strategy. + * + * @return a string describing the delete strategy + */ + public String toString() { + return "FileDeleteStrategy[" + name + "]"; + } + + //----------------------------------------------------------------------- + /** + * Force file deletion strategy. + */ + static class ForceFileDeleteStrategy extends FileDeleteStrategy { + /** Default Constructor */ + ForceFileDeleteStrategy() { + super("Force"); + } + + /** + * Deletes the file object. + *

+ * This implementation uses FileUtils.forceDelete() + * if the file exists. + * + * @param fileToDelete the file to delete, not null + * @return Always returns true + * @throws NullPointerException if the file is null + * @throws IOException if an error occurs during file deletion + */ + protected boolean doDelete(File fileToDelete) throws IOException { + FileUtils.forceDelete(fileToDelete); + return true; + } + } + +} diff --git a/src/org/apache/commons/io/FileSystemUtils.java b/src/org/apache/commons/io/FileSystemUtils.java new file mode 100644 index 000000000..a29dba439 --- /dev/null +++ b/src/org/apache/commons/io/FileSystemUtils.java @@ -0,0 +1,457 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.StringTokenizer; + +/** + * General File System utilities. + *

+ * This class provides static utility methods for general file system + * functions not provided via the JDK {@link java.io.File File} class. + *

+ * The current functions provided are: + *

    + *
  • Get the free space on a drive + *
+ * + * @author Frank W. Zammetti + * @author Stephen Colebourne + * @author Thomas Ledoux + * @author James Urie + * @author Magnus Grimsell + * @author Thomas Ledoux + * @version $Id: FileSystemUtils.java 453889 2006-10-07 11:56:25Z scolebourne $ + * @since Commons IO 1.1 + */ +public class FileSystemUtils { + + /** Singleton instance, used mainly for testing. */ + private static final FileSystemUtils INSTANCE = new FileSystemUtils(); + + /** Operating system state flag for error. */ + private static final int INIT_PROBLEM = -1; + /** Operating system state flag for neither Unix nor Windows. */ + private static final int OTHER = 0; + /** Operating system state flag for Windows. */ + private static final int WINDOWS = 1; + /** Operating system state flag for Unix. */ + private static final int UNIX = 2; + /** Operating system state flag for Posix flavour Unix. */ + private static final int POSIX_UNIX = 3; + + /** The operating system flag. */ + private static final int OS; + static { + int os = OTHER; + try { + String osName = System.getProperty("os.name"); + if (osName == null) { + throw new IOException("os.name not found"); + } + osName = osName.toLowerCase(); + // match + if (osName.indexOf("windows") != -1) { + os = WINDOWS; + } else if (osName.indexOf("linux") != -1 || + osName.indexOf("sun os") != -1 || + osName.indexOf("sunos") != -1 || + osName.indexOf("solaris") != -1 || + osName.indexOf("mpe/ix") != -1 || + osName.indexOf("freebsd") != -1 || + osName.indexOf("irix") != -1 || + osName.indexOf("digital unix") != -1 || + osName.indexOf("unix") != -1 || + osName.indexOf("mac os x") != -1) { + os = UNIX; + } else if (osName.indexOf("hp-ux") != -1 || + osName.indexOf("aix") != -1) { + os = POSIX_UNIX; + } else { + os = OTHER; + } + + } catch (Exception ex) { + os = INIT_PROBLEM; + } + OS = os; + } + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileSystemUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Returns the free space on a drive or volume by invoking + * the command line. + * This method does not normalize the result, and typically returns + * bytes on Windows, 512 byte units on OS X and kilobytes on Unix. + * As this is not very useful, this method is deprecated in favour + * of {@link #freeSpaceKb(String)} which returns a result in kilobytes. + *

+ * Note that some OS's are NOT currently supported, including OS/390, + * OpenVMS and and SunOS 5. (SunOS is supported by freeSpaceKb.) + *

+     * FileSystemUtils.freeSpace("C:");       // Windows
+     * FileSystemUtils.freeSpace("/volume");  // *nix
+     * 
+ * The free space is calculated via the command line. + * It uses 'dir /-c' on Windows and 'df' on *nix. + * + * @param path the path to get free space for, not null, not empty on Unix + * @return the amount of free drive space on the drive or volume + * @throws IllegalArgumentException if the path is invalid + * @throws IllegalStateException if an error occurred in initialisation + * @throws IOException if an error occurs when finding the free space + * @since Commons IO 1.1, enhanced OS support in 1.2 and 1.3 + * @deprecated Use freeSpaceKb(String) + * Deprecated from 1.3, may be removed in 2.0 + */ + public static long freeSpace(String path) throws IOException { + return INSTANCE.freeSpaceOS(path, OS, false); + } + + //----------------------------------------------------------------------- + /** + * Returns the free space on a drive or volume in kilobytes by invoking + * the command line. + *
+     * FileSystemUtils.freeSpaceKb("C:");       // Windows
+     * FileSystemUtils.freeSpaceKb("/volume");  // *nix
+     * 
+ * The free space is calculated via the command line. + * It uses 'dir /-c' on Windows, 'df -kP' on AIX/HP-UX and 'df -k' on other Unix. + *

+ * In order to work, you must be running Windows, or have a implementation of + * Unix df that supports GNU format when passed -k (or -kP). If you are going + * to rely on this code, please check that it works on your OS by running + * some simple tests to compare the command line with the output from this class. + * If your operating system isn't supported, please raise a JIRA call detailing + * the exact result from df -k and as much other detail as possible, thanks. + * + * @param path the path to get free space for, not null, not empty on Unix + * @return the amount of free drive space on the drive or volume in kilobytes + * @throws IllegalArgumentException if the path is invalid + * @throws IllegalStateException if an error occurred in initialisation + * @throws IOException if an error occurs when finding the free space + * @since Commons IO 1.2, enhanced OS support in 1.3 + */ + public static long freeSpaceKb(String path) throws IOException { + return INSTANCE.freeSpaceOS(path, OS, true); + } + + //----------------------------------------------------------------------- + /** + * Returns the free space on a drive or volume in a cross-platform manner. + * Note that some OS's are NOT currently supported, including OS/390. + *

+     * FileSystemUtils.freeSpace("C:");  // Windows
+     * FileSystemUtils.freeSpace("/volume");  // *nix
+     * 
+ * The free space is calculated via the command line. + * It uses 'dir /-c' on Windows and 'df' on *nix. + * + * @param path the path to get free space for, not null, not empty on Unix + * @param os the operating system code + * @param kb whether to normalize to kilobytes + * @return the amount of free drive space on the drive or volume + * @throws IllegalArgumentException if the path is invalid + * @throws IllegalStateException if an error occurred in initialisation + * @throws IOException if an error occurs when finding the free space + */ + long freeSpaceOS(String path, int os, boolean kb) throws IOException { + if (path == null) { + throw new IllegalArgumentException("Path must not be empty"); + } + switch (os) { + case WINDOWS: + return (kb ? freeSpaceWindows(path) / 1024 : freeSpaceWindows(path)); + case UNIX: + return freeSpaceUnix(path, kb, false); + case POSIX_UNIX: + return freeSpaceUnix(path, kb, true); + case OTHER: + throw new IllegalStateException("Unsupported operating system"); + default: + throw new IllegalStateException( + "Exception caught when determining operating system"); + } + } + + //----------------------------------------------------------------------- + /** + * Find free space on the Windows platform using the 'dir' command. + * + * @param path the path to get free space for, including the colon + * @return the amount of free drive space on the drive + * @throws IOException if an error occurs + */ + long freeSpaceWindows(String path) throws IOException { + path = FilenameUtils.normalize(path); + if (path.length() > 2 && path.charAt(1) == ':') { + path = path.substring(0, 2); // seems to make it work + } + + // build and run the 'dir' command + String[] cmdAttribs = new String[] {"cmd.exe", "/C", "dir /-c " + path}; + + // read in the output of the command to an ArrayList + List lines = performCommand(cmdAttribs, Integer.MAX_VALUE); + + // now iterate over the lines we just read and find the LAST + // non-empty line (the free space bytes should be in the last element + // of the ArrayList anyway, but this will ensure it works even if it's + // not, still assuming it is on the last non-blank line) + for (int i = lines.size() - 1; i >= 0; i--) { + String line = (String) lines.get(i); + if (line.length() > 0) { + return parseDir(line, path); + } + } + // all lines are blank + throw new IOException( + "Command line 'dir /-c' did not return any info " + + "for path '" + path + "'"); + } + + /** + * Parses the Windows dir response last line + * + * @param line the line to parse + * @param path the path that was sent + * @return the number of bytes + * @throws IOException if an error occurs + */ + long parseDir(String line, String path) throws IOException { + // read from the end of the line to find the last numeric + // character on the line, then continue until we find the first + // non-numeric character, and everything between that and the last + // numeric character inclusive is our free space bytes count + int bytesStart = 0; + int bytesEnd = 0; + int j = line.length() - 1; + innerLoop1: while (j >= 0) { + char c = line.charAt(j); + if (Character.isDigit(c)) { + // found the last numeric character, this is the end of + // the free space bytes count + bytesEnd = j + 1; + break innerLoop1; + } + j--; + } + innerLoop2: while (j >= 0) { + char c = line.charAt(j); + if (!Character.isDigit(c) && c != ',' && c != '.') { + // found the next non-numeric character, this is the + // beginning of the free space bytes count + bytesStart = j + 1; + break innerLoop2; + } + j--; + } + if (j < 0) { + throw new IOException( + "Command line 'dir /-c' did not return valid info " + + "for path '" + path + "'"); + } + + // remove commas and dots in the bytes count + StringBuffer buf = new StringBuffer(line.substring(bytesStart, bytesEnd)); + for (int k = 0; k < buf.length(); k++) { + if (buf.charAt(k) == ',' || buf.charAt(k) == '.') { + buf.deleteCharAt(k--); + } + } + return parseBytes(buf.toString(), path); + } + + //----------------------------------------------------------------------- + /** + * Find free space on the *nix platform using the 'df' command. + * + * @param path the path to get free space for + * @param kb whether to normalize to kilobytes + * @param posix whether to use the posix standard format flag + * @return the amount of free drive space on the volume + * @throws IOException if an error occurs + */ + long freeSpaceUnix(String path, boolean kb, boolean posix) throws IOException { + if (path.length() == 0) { + throw new IllegalArgumentException("Path must not be empty"); + } + path = FilenameUtils.normalize(path); + + // build and run the 'dir' command + String flags = "-"; + if (kb) { + flags += "k"; + } + if (posix) { + flags += "P"; + } + String[] cmdAttribs = + (flags.length() > 1 ? new String[] {"df", flags, path} : new String[] {"df", path}); + + // perform the command, asking for up to 3 lines (header, interesting, overflow) + List lines = performCommand(cmdAttribs, 3); + if (lines.size() < 2) { + // unknown problem, throw exception + throw new IOException( + "Command line 'df' did not return info as expected " + + "for path '" + path + "'- response was " + lines); + } + String line2 = (String) lines.get(1); // the line we're interested in + + // Now, we tokenize the string. The fourth element is what we want. + StringTokenizer tok = new StringTokenizer(line2, " "); + if (tok.countTokens() < 4) { + // could be long Filesystem, thus data on third line + if (tok.countTokens() == 1 && lines.size() >= 3) { + String line3 = (String) lines.get(2); // the line may be interested in + tok = new StringTokenizer(line3, " "); + } else { + throw new IOException( + "Command line 'df' did not return data as expected " + + "for path '" + path + "'- check path is valid"); + } + } else { + tok.nextToken(); // Ignore Filesystem + } + tok.nextToken(); // Ignore 1K-blocks + tok.nextToken(); // Ignore Used + String freeSpace = tok.nextToken(); + return parseBytes(freeSpace, path); + } + + //----------------------------------------------------------------------- + /** + * Parses the bytes from a string. + * + * @param freeSpace the free space string + * @param path the path + * @return the number of bytes + * @throws IOException if an error occurs + */ + long parseBytes(String freeSpace, String path) throws IOException { + try { + long bytes = Long.parseLong(freeSpace); + if (bytes < 0) { + throw new IOException( + "Command line 'df' did not find free space in response " + + "for path '" + path + "'- check path is valid"); + } + return bytes; + + } catch (NumberFormatException ex) { + throw new IOException( + "Command line 'df' did not return numeric data as expected " + + "for path '" + path + "'- check path is valid"); + } + } + + //----------------------------------------------------------------------- + /** + * Performs the os command. + * + * @param cmdAttribs the command line parameters + * @param max The maximum limit for the lines returned + * @return the parsed data + * @throws IOException if an error occurs + */ + List performCommand(String[] cmdAttribs, int max) throws IOException { + // this method does what it can to avoid the 'Too many open files' error + // based on trial and error and these links: + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4784692 + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4801027 + // http://forum.java.sun.com/thread.jspa?threadID=533029&messageID=2572018 + // however, its still not perfect as the JDK support is so poor + // (see commond-exec or ant for a better multi-threaded multi-os solution) + + List lines = new ArrayList(20); + Process proc = null; + InputStream in = null; + OutputStream out = null; + InputStream err = null; + BufferedReader inr = null; + try { + proc = openProcess(cmdAttribs); + in = proc.getInputStream(); + out = proc.getOutputStream(); + err = proc.getErrorStream(); + inr = new BufferedReader(new InputStreamReader(in)); + String line = inr.readLine(); + while (line != null && lines.size() < max) { + line = line.toLowerCase().trim(); + lines.add(line); + line = inr.readLine(); + } + + proc.waitFor(); + if (proc.exitValue() != 0) { + // os command problem, throw exception + throw new IOException( + "Command line returned OS error code '" + proc.exitValue() + + "' for command " + Arrays.asList(cmdAttribs)); + } + if (lines.size() == 0) { + // unknown problem, throw exception + throw new IOException( + "Command line did not return any info " + + "for command " + Arrays.asList(cmdAttribs)); + } + return lines; + + } catch (InterruptedException ex) { + throw new IOException( + "Command line threw an InterruptedException '" + ex.getMessage() + + "' for command " + Arrays.asList(cmdAttribs)); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(err); + IOUtils.closeQuietly(inr); + if (proc != null) { + proc.destroy(); + } + } + } + + /** + * Opens the process to the operating system. + * + * @param cmdAttribs the command line parameters + * @return the process + * @throws IOException if an error occurs + */ + Process openProcess(String[] cmdAttribs) throws IOException { + return Runtime.getRuntime().exec(cmdAttribs); + } + +} diff --git a/src/org/apache/commons/io/FileUtils.java b/src/org/apache/commons/io/FileUtils.java new file mode 100644 index 000000000..254800cd1 --- /dev/null +++ b/src/org/apache/commons/io/FileUtils.java @@ -0,0 +1,1890 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.io.FileFilter; +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.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; +import java.util.zip.Checksum; + +import org.apache.commons.io.filefilter.DirectoryFileFilter; +import org.apache.commons.io.filefilter.FalseFileFilter; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.SuffixFileFilter; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.apache.commons.io.output.NullOutputStream; + +/** + * General file manipulation utilities. + *

+ * Facilities are provided in the following areas: + *

    + *
  • writing to a file + *
  • reading from a file + *
  • make a directory including parent directories + *
  • copying files and directories + *
  • deleting files and directories + *
  • converting to and from a URL + *
  • listing files and directories by filter and extension + *
  • comparing file content + *
  • file last changed date + *
  • calculating a checksum + *
+ *

+ * Origin of code: Excalibur, Alexandria, Commons-Utils + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Jeremias Maerki + * @author Stephen Colebourne + * @author Ian Springer + * @author Chris Eldredge + * @author Jim Harrington + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: FileUtils.java 610810 2008-01-10 15:04:49Z niallp $ + */ +public class FileUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileUtils() { + super(); + } + + /** + * The number of bytes in a kilobyte. + */ + public static final long ONE_KB = 1024; + + /** + * The number of bytes in a megabyte. + */ + public static final long ONE_MB = ONE_KB * ONE_KB; + + /** + * The number of bytes in a gigabyte. + */ + public static final long ONE_GB = ONE_KB * ONE_MB; + + /** + * An empty array of type File. + */ + public static final File[] EMPTY_FILE_ARRAY = new File[0]; + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileInputStream} for the specified file, providing better + * error messages than simply calling new FileInputStream(file). + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * An exception is thrown if the file does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be read. + * + * @param file the file to open for input, must not be null + * @return a new {@link FileInputStream} for the specified file + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be read + * @since Commons IO 1.3 + */ + public static FileInputStream openInputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canRead() == false) { + throw new IOException("File '" + file + "' cannot be read"); + } + } else { + throw new FileNotFoundException("File '" + file + "' does not exist"); + } + return new FileInputStream(file); + } + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be null + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since Commons IO 1.3 + */ + public static FileOutputStream openOutputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canWrite() == false) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null && parent.exists() == false) { + if (parent.mkdirs() == false) { + throw new IOException("File '" + file + "' could not be created"); + } + } + } + return new FileOutputStream(file); + } + + //----------------------------------------------------------------------- + /** + * Returns a human-readable version of the file size, where the input + * represents a specific number of bytes. + * + * @param size the number of bytes + * @return a human-readable display value (includes units) + */ + public static String byteCountToDisplaySize(long size) { + String displaySize; + + if (size / ONE_GB > 0) { + displaySize = String.valueOf(size / ONE_GB) + " GB"; + } else if (size / ONE_MB > 0) { + displaySize = String.valueOf(size / ONE_MB) + " MB"; + } else if (size / ONE_KB > 0) { + displaySize = String.valueOf(size / ONE_KB) + " KB"; + } else { + displaySize = String.valueOf(size) + " bytes"; + } + return displaySize; + } + + //----------------------------------------------------------------------- + /** + * Implements the same behaviour as the "touch" utility on Unix. It creates + * a new file with size 0 or, if the file exists already, it is opened and + * closed without modifying it, but updating the file date and time. + *

+ * NOTE: As from v1.3, this method throws an IOException if the last + * modified date of the file cannot be set. Also, as from v1.3 this method + * creates parent directories if they do not exist. + * + * @param file the File to touch + * @throws IOException If an I/O problem occurs + */ + public static void touch(File file) throws IOException { + if (!file.exists()) { + OutputStream out = openOutputStream(file); + IOUtils.closeQuietly(out); + } + boolean success = file.setLastModified(System.currentTimeMillis()); + if (!success) { + throw new IOException("Unable to set the last modification time for " + file); + } + } + + //----------------------------------------------------------------------- + /** + * Converts a Collection containing java.io.File instanced into array + * representation. This is to account for the difference between + * File.listFiles() and FileUtils.listFiles(). + * + * @param files a Collection containing java.io.File instances + * @return an array of java.io.File + */ + public static File[] convertFileCollectionToFileArray(Collection files) { + return (File[]) files.toArray(new File[files.size()]); + } + + //----------------------------------------------------------------------- + /** + * Finds files within a given directory (and optionally its + * subdirectories). All files found are filtered by an IOFileFilter. + * + * @param files the collection of files found. + * @param directory the directory to search in. + * @param filter the filter to apply to files and directories. + */ + private static void innerListFiles(Collection files, File directory, + IOFileFilter filter) { + File[] found = directory.listFiles((FileFilter) filter); + if (found != null) { + for (int i = 0; i < found.length; i++) { + if (found[i].isDirectory()) { + innerListFiles(files, found[i], filter); + } else { + files.add(found[i]); + } + } + } + } + + /** + * Finds files within a given directory (and optionally its + * subdirectories). All files found are filtered by an IOFileFilter. + *

+ * If your search should recurse into subdirectories you can pass in + * an IOFileFilter for directories. You don't need to bind a + * DirectoryFileFilter (via logical AND) to this filter. This method does + * that for you. + *

+ * An example: If you want to search through all directories called + * "temp" you pass in FileFilterUtils.NameFileFilter("temp") + *

+ * Another common usage of this method is find files in a directory + * tree but ignoring the directories generated CVS. You can simply pass + * in FileFilterUtils.makeCVSAware(null). + * + * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is null, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an collection of java.io.File with the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + */ + public static Collection listFiles( + File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + if (!directory.isDirectory()) { + throw new IllegalArgumentException( + "Parameter 'directory' is not a directory"); + } + if (fileFilter == null) { + throw new NullPointerException("Parameter 'fileFilter' is null"); + } + + //Setup effective file filter + IOFileFilter effFileFilter = FileFilterUtils.andFileFilter(fileFilter, + FileFilterUtils.notFileFilter(DirectoryFileFilter.INSTANCE)); + + //Setup effective directory filter + IOFileFilter effDirFilter; + if (dirFilter == null) { + effDirFilter = FalseFileFilter.INSTANCE; + } else { + effDirFilter = FileFilterUtils.andFileFilter(dirFilter, + DirectoryFileFilter.INSTANCE); + } + + //Find files + Collection files = new java.util.LinkedList(); + innerListFiles(files, directory, + FileFilterUtils.orFileFilter(effFileFilter, effDirFilter)); + return files; + } + + /** + * Allows iteration over the files in given directory (and optionally + * its subdirectories). + *

+ * All files found are filtered by an IOFileFilter. This method is + * based on {@link #listFiles(File, IOFileFilter, IOFileFilter)}. + * + * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is null, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an iterator of java.io.File for the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + * @since Commons IO 1.2 + */ + public static Iterator iterateFiles( + File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + return listFiles(directory, fileFilter, dirFilter).iterator(); + } + + //----------------------------------------------------------------------- + /** + * Converts an array of file extensions to suffixes for use + * with IOFileFilters. + * + * @param extensions an array of extensions. Format: {"java", "xml"} + * @return an array of suffixes. Format: {".java", ".xml"} + */ + private static String[] toSuffixes(String[] extensions) { + String[] suffixes = new String[extensions.length]; + for (int i = 0; i < extensions.length; i++) { + suffixes[i] = "." + extensions[i]; + } + return suffixes; + } + + + /** + * Finds files within a given directory (and optionally its subdirectories) + * which match an array of extensions. + * + * @param directory the directory to search in + * @param extensions an array of extensions, ex. {"java","xml"}. If this + * parameter is null, all files are returned. + * @param recursive if true all subdirectories are searched as well + * @return an collection of java.io.File with the matching files + */ + public static Collection listFiles( + File directory, String[] extensions, boolean recursive) { + IOFileFilter filter; + if (extensions == null) { + filter = TrueFileFilter.INSTANCE; + } else { + String[] suffixes = toSuffixes(extensions); + filter = new SuffixFileFilter(suffixes); + } + return listFiles(directory, filter, + (recursive ? TrueFileFilter.INSTANCE : FalseFileFilter.INSTANCE)); + } + + /** + * Allows iteration over the files in a given directory (and optionally + * its subdirectories) which match an array of extensions. This method + * is based on {@link #listFiles(File, String[], boolean)}. + * + * @param directory the directory to search in + * @param extensions an array of extensions, ex. {"java","xml"}. If this + * parameter is null, all files are returned. + * @param recursive if true all subdirectories are searched as well + * @return an iterator of java.io.File with the matching files + * @since Commons IO 1.2 + */ + public static Iterator iterateFiles( + File directory, String[] extensions, boolean recursive) { + return listFiles(directory, extensions, recursive).iterator(); + } + + //----------------------------------------------------------------------- + /** + * Compares the contents of two files to determine if they are equal or not. + *

+ * This method checks to see if the two files are different lengths + * or if they point to the same file, before resorting to byte-by-byte + * comparison of the contents. + *

+ * Code origin: Avalon + * + * @param file1 the first file + * @param file2 the second file + * @return true if the content of the files are equal or they both don't + * exist, false otherwise + * @throws IOException in case of an I/O error + */ + public static boolean contentEquals(File file1, File file2) throws IOException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // two not existing files are equal + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // don't want to compare directory contents + throw new IOException("Can't compare directories, only files"); + } + + if (file1.length() != file2.length()) { + // lengths differ, cannot be equal + return false; + } + + if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) { + // same file + return true; + } + + InputStream input1 = null; + InputStream input2 = null; + try { + input1 = new FileInputStream(file1); + input2 = new FileInputStream(file2); + return IOUtils.contentEquals(input1, input2); + + } finally { + IOUtils.closeQuietly(input1); + IOUtils.closeQuietly(input2); + } + } + + //----------------------------------------------------------------------- + /** + * Convert from a URL to a File. + *

+ * From version 1.1 this method will decode the URL. + * Syntax such as file:///my%20docs/file.txt will be + * correctly decoded to /my docs/file.txt. + * + * @param url the file URL to convert, null returns null + * @return the equivalent File object, or null + * if the URL's protocol is not file + * @throws IllegalArgumentException if the file is incorrectly encoded + */ + public static File toFile(URL url) { + if (url == null || !url.getProtocol().equals("file")) { + return null; + } else { + String filename = url.getFile().replace('/', File.separatorChar); + int pos =0; + while ((pos = filename.indexOf('%', pos)) >= 0) { + if (pos + 2 < filename.length()) { + String hexStr = filename.substring(pos + 1, pos + 3); + char ch = (char) Integer.parseInt(hexStr, 16); + filename = filename.substring(0, pos) + ch + filename.substring(pos + 3); + } + } + return new File(filename); + } + } + + /** + * Converts each of an array of URL to a File. + *

+ * Returns an array of the same size as the input. + * If the input is null, an empty array is returned. + * If the input contains null, the output array contains null at the same + * index. + *

+ * This method will decode the URL. + * Syntax such as file:///my%20docs/file.txt will be + * correctly decoded to /my docs/file.txt. + * + * @param urls the file URLs to convert, null returns empty array + * @return a non-null array of Files matching the input, with a null item + * if there was a null at that index in the input array + * @throws IllegalArgumentException if any file is not a URL file + * @throws IllegalArgumentException if any file is incorrectly encoded + * @since Commons IO 1.1 + */ + public static File[] toFiles(URL[] urls) { + if (urls == null || urls.length == 0) { + return EMPTY_FILE_ARRAY; + } + File[] files = new File[urls.length]; + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + if (url != null) { + if (url.getProtocol().equals("file") == false) { + throw new IllegalArgumentException( + "URL could not be converted to a File: " + url); + } + files[i] = toFile(url); + } + } + return files; + } + + /** + * Converts each of an array of File to a URL. + *

+ * Returns an array of the same size as the input. + * + * @param files the files to convert + * @return an array of URLs matching the input + * @throws IOException if a file cannot be converted + */ + public static URL[] toURLs(File[] files) throws IOException { + URL[] urls = new URL[files.length]; + + for (int i = 0; i < urls.length; i++) { + urls[i] = files[i].toURL(); + } + + return urls; + } + + //----------------------------------------------------------------------- + /** + * Copies a file to a directory preserving the file date. + *

+ * This method copies the contents of the specified source file + * to a file of the same name in the specified destination directory. + * The destination directory is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destDir the directory to place the copy in, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + */ + public static void copyFileToDirectory(File srcFile, File destDir) throws IOException { + copyFileToDirectory(srcFile, destDir, true); + } + + /** + * Copies a file to a directory optionally preserving the file date. + *

+ * This method copies the contents of the specified source file + * to a file of the same name in the specified destination directory. + * The destination directory is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destDir the directory to place the copy in, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + * @since Commons IO 1.3 + */ + public static void copyFileToDirectory(File srcFile, File destDir, boolean preserveFileDate) throws IOException { + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (destDir.exists() && destDir.isDirectory() == false) { + throw new IllegalArgumentException("Destination '" + destDir + "' is not a directory"); + } + copyFile(srcFile, new File(destDir, srcFile.getName()), preserveFileDate); + } + + /** + * Copies a file to a new location preserving the file date. + *

+ * This method copies the contents of the specified source file to the + * specified destination file. The directory holding the destination file is + * created if it does not exist. If the destination file exists, then this + * method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destFile the new file, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File) + */ + public static void copyFile(File srcFile, File destFile) throws IOException { + copyFile(srcFile, destFile, true); + } + + /** + * Copies a file to a new location. + *

+ * This method copies the contents of the specified source file + * to the specified destination file. + * The directory holding the destination file is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destFile the new file, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File, boolean) + */ + public static void copyFile(File srcFile, File destFile, + boolean preserveFileDate) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcFile.exists() == false) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' exists but is a directory"); + } + if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { + throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same"); + } + if (destFile.getParentFile() != null && destFile.getParentFile().exists() == false) { + if (destFile.getParentFile().mkdirs() == false) { + throw new IOException("Destination '" + destFile + "' directory cannot be created"); + } + } + if (destFile.exists() && destFile.canWrite() == false) { + throw new IOException("Destination '" + destFile + "' exists but is read-only"); + } + doCopyFile(srcFile, destFile, preserveFileDate); + } + + /** + * Internal copy file method. + * + * @param srcFile the validated source file, must not be null + * @param destFile the validated destination file, must not be null + * @param preserveFileDate whether to preserve the file date + * @throws IOException if an error occurs + */ + private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException { + if (destFile.exists() && destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' exists but is a directory"); + } + + FileInputStream input = new FileInputStream(srcFile); + try { + FileOutputStream output = new FileOutputStream(destFile); + try { + IOUtils.copy(input, output); + } finally { + IOUtils.closeQuietly(output); + } + } finally { + IOUtils.closeQuietly(input); + } + + if (srcFile.length() != destFile.length()) { + throw new IOException("Failed to copy full contents from '" + + srcFile + "' to '" + destFile + "'"); + } + if (preserveFileDate) { + destFile.setLastModified(srcFile.lastModified()); + } + } + + //----------------------------------------------------------------------- + /** + * Copies a directory to within another directory preserving the file dates. + *

+ * This method copies the source directory and all its contents to a + * directory of the same name in the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the directory to place the copy in, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.2 + */ + public static void copyDirectoryToDirectory(File srcDir, File destDir) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (srcDir.exists() && srcDir.isDirectory() == false) { + throw new IllegalArgumentException("Source '" + destDir + "' is not a directory"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (destDir.exists() && destDir.isDirectory() == false) { + throw new IllegalArgumentException("Destination '" + destDir + "' is not a directory"); + } + copyDirectory(srcDir, new File(destDir, srcDir.getName()), true); + } + + /** + * Copies a whole directory to a new location preserving the file dates. + *

+ * This method copies the specified directory and all its child + * directories and files to the specified destination. + * The destination is the new location and name of the directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.1 + */ + public static void copyDirectory(File srcDir, File destDir) throws IOException { + copyDirectory(srcDir, destDir, true); + } + + /** + * Copies a whole directory to a new location. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.1 + */ + public static void copyDirectory(File srcDir, File destDir, + boolean preserveFileDate) throws IOException { + copyDirectory(srcDir, destDir, null, preserveFileDate); + } + + /** + * Copies a filtered directory to a new location preserving the file dates. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + * + *

Example: Copy directories only

+ *
+     *  // only copy the directory structure
+     *  FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY);
+     *  
+ * + *

Example: Copy directories and txt files

+ *
+     *  // Create a filter for ".txt" files
+     *  IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
+     *  IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
+     *
+     *  // Create a filter for either directories or ".txt" files
+     *  FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
+     *
+     *  // Copy using the filter
+     *  FileUtils.copyDirectory(srcDir, destDir, filter);
+     *  
+ * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * @param filter the filter to apply, null means copy all directories and files + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.4 + */ + public static void copyDirectory(File srcDir, File destDir, + FileFilter filter) throws IOException { + copyDirectory(srcDir, destDir, filter, true); + } + + /** + * Copies a filtered directory to a new location. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + * + *

Example: Copy directories only

+ *
+     *  // only copy the directory structure
+     *  FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY, false);
+     *  
+ * + *

Example: Copy directories and txt files

+ *
+     *  // Create a filter for ".txt" files
+     *  IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
+     *  IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
+     *
+     *  // Create a filter for either directories or ".txt" files
+     *  FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
+     *
+     *  // Copy using the filter
+     *  FileUtils.copyDirectory(srcDir, destDir, filter, false);
+     *  
+ * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.4 + */ + public static void copyDirectory(File srcDir, File destDir, + FileFilter filter, boolean preserveFileDate) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcDir.exists() == false) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (srcDir.isDirectory() == false) { + throw new IOException("Source '" + srcDir + "' exists but is not a directory"); + } + if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath())) { + throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same"); + } + + // Cater for destination being directory within the source directory (see IO-141) + List exclusionList = null; + if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) { + File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (srcFiles != null && srcFiles.length > 0) { + exclusionList = new ArrayList(srcFiles.length); + for (int i = 0; i < srcFiles.length; i++) { + File copiedFile = new File(destDir, srcFiles[i].getName()); + exclusionList.add(copiedFile.getCanonicalPath()); + } + } + } + doCopyDirectory(srcDir, destDir, filter, preserveFileDate, exclusionList); + } + + /** + * Internal copy directory method. + * + * @param srcDir the validated source directory, must not be null + * @param destDir the validated destination directory, must not be null + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate whether to preserve the file date + * @param exclusionList List of files and directories to exclude from the copy, may be null + * @throws IOException if an error occurs + * @since Commons IO 1.1 + */ + private static void doCopyDirectory(File srcDir, File destDir, FileFilter filter, + boolean preserveFileDate, List exclusionList) throws IOException { + if (destDir.exists()) { + if (destDir.isDirectory() == false) { + throw new IOException("Destination '" + destDir + "' exists but is not a directory"); + } + } else { + if (destDir.mkdirs() == false) { + throw new IOException("Destination '" + destDir + "' directory cannot be created"); + } + if (preserveFileDate) { + destDir.setLastModified(srcDir.lastModified()); + } + } + if (destDir.canWrite() == false) { + throw new IOException("Destination '" + destDir + "' cannot be written to"); + } + // recurse + File[] files = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + srcDir); + } + for (int i = 0; i < files.length; i++) { + File copiedFile = new File(destDir, files[i].getName()); + if (exclusionList == null || !exclusionList.contains(files[i].getCanonicalPath())) { + if (files[i].isDirectory()) { + doCopyDirectory(files[i], copiedFile, filter, preserveFileDate, exclusionList); + } else { + doCopyFile(files[i], copiedFile, preserveFileDate); + } + } + } + } + + //----------------------------------------------------------------------- + /** + * Copies bytes from the URL source to a file + * destination. The directories up to destination + * will be created if they don't already exist. destination + * will be overwritten if it already exists. + * + * @param source the URL to copy bytes from, must not be null + * @param destination the non-directory File to write bytes to + * (possibly overwriting), must not be null + * @throws IOException if source URL cannot be opened + * @throws IOException if destination is a directory + * @throws IOException if destination cannot be written + * @throws IOException if destination needs creating but can't be + * @throws IOException if an IO error occurs during copying + */ + public static void copyURLToFile(URL source, File destination) throws IOException { + InputStream input = source.openStream(); + try { + FileOutputStream output = openOutputStream(destination); + try { + IOUtils.copy(input, output); + } finally { + IOUtils.closeQuietly(output); + } + } finally { + IOUtils.closeQuietly(input); + } + } + + //----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectory(directory); + if (!directory.delete()) { + String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • No exceptions are thrown when a file or directory cannot be deleted.
  • + *
+ * + * @param file file or directory to delete, can be null + * @return true if the file or directory was deleted, otherwise + * false + * + * @since Commons IO 1.4 + */ + public static boolean deleteQuietly(File file) { + if (file == null) { + return false; + } + try { + if (file.isDirectory()) { + cleanDirectory(file); + } + } catch (Exception e) { + } + + try { + return file.delete(); + } catch (Exception e) { + return false; + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + */ + public static void cleanDirectory(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDelete(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + //----------------------------------------------------------------------- + /** + * Waits for NFS to propagate a file creation, imposing a timeout. + *

+ * This method repeatedly tests {@link File#exists()} until it returns + * true up to the maximum time specified in seconds. + * + * @param file the file to check, must not be null + * @param seconds the maximum time in seconds to wait + * @return true if file exists + * @throws NullPointerException if the file is null + */ + public static boolean waitFor(File file, int seconds) { + int timeout = 0; + int tick = 0; + while (!file.exists()) { + if (tick++ >= 10) { + tick = 0; + if (timeout++ > seconds) { + return false; + } + } + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + // ignore exception + } catch (Exception ex) { + break; + } + } + return true; + } + + //----------------------------------------------------------------------- + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be null + * @param encoding the encoding to use, null means platform default + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + */ + public static String readFileToString(File file, String encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.toString(in, encoding); + } finally { + IOUtils.closeQuietly(in); + } + } + + + /** + * Reads the contents of a file into a String using the default encoding for the VM. + * The file is always closed. + * + * @param file the file to read, must not be null + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @since Commons IO 1.3.1 + */ + public static String readFileToString(File file) throws IOException { + return readFileToString(file, null); + } + + /** + * Reads the contents of a file into a byte array. + * The file is always closed. + * + * @param file the file to read, must not be null + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @since Commons IO 1.1 + */ + public static byte[] readFileToByteArray(File file) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.toByteArray(in); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file line by line to a List of Strings. + * The file is always closed. + * + * @param file the file to read, must not be null + * @param encoding the encoding to use, null means platform default + * @return the list of Strings representing each line in the file, never null + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since Commons IO 1.1 + */ + public static List readLines(File file, String encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.readLines(in, encoding); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file line by line to a List of Strings using the default encoding for the VM. + * The file is always closed. + * + * @param file the file to read, must not be null + * @return the list of Strings representing each line in the file, never null + * @throws IOException in case of an I/O error + * @since Commons IO 1.3 + */ + public static List readLines(File file) throws IOException { + return readLines(file, null); + } + + /** + * Returns an Iterator for the lines in a File. + *

+ * This method opens an InputStream for the file. + * When you have finished with the iterator you should close the stream + * to free internal resources. This can be done by calling the + * {@link LineIterator#close()} or + * {@link LineIterator#closeQuietly(LineIterator)} method. + *

+ * The recommended usage pattern is: + *

+     * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
+     * try {
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   LineIterator.closeQuietly(iterator);
+     * }
+     * 
+ *

+ * If an exception occurs during the creation of the iterator, the + * underlying stream is closed. + * + * @param file the file to open for input, must not be null + * @param encoding the encoding to use, null means platform default + * @return an Iterator of the lines in the file, never null + * @throws IOException in case of an I/O error (file closed) + * @since Commons IO 1.2 + */ + public static LineIterator lineIterator(File file, String encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.lineIterator(in, encoding); + } catch (IOException ex) { + IOUtils.closeQuietly(in); + throw ex; + } catch (RuntimeException ex) { + IOUtils.closeQuietly(in); + throw ex; + } + } + + /** + * Returns an Iterator for the lines in a File using the default encoding for the VM. + * + * @param file the file to open for input, must not be null + * @return an Iterator of the lines in the file, never null + * @throws IOException in case of an I/O error (file closed) + * @since Commons IO 1.3 + * @see #lineIterator(File, String) + */ + public static LineIterator lineIterator(File file) throws IOException { + return lineIterator(file, null); + } + + //----------------------------------------------------------------------- + /** + * Writes a String to a file creating the file if it does not exist. + * + * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, null means platform default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + */ + public static void writeStringToFile(File file, String data, String encoding) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file); + IOUtils.write(data, out, encoding); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes a String to a file creating the file if it does not exist using the default encoding for the VM. + * + * @param file the file to write + * @param data the content to write to the file + * @throws IOException in case of an I/O error + */ + public static void writeStringToFile(File file, String data) throws IOException { + writeStringToFile(file, data, null); + } + + /** + * Writes a byte array to a file creating the file if it does not exist. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @since Commons IO 1.1 + */ + public static void writeByteArrayToFile(File file, byte[] data) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file); + out.write(data); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The specified character encoding and the default line ending will be used. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param encoding the encoding to use, null means platform default + * @param lines the lines to write, null entries produce blank lines + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since Commons IO 1.1 + */ + public static void writeLines(File file, String encoding, Collection lines) throws IOException { + writeLines(file, encoding, lines, null); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the default line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, null entries produce blank lines + * @throws IOException in case of an I/O error + * @since Commons IO 1.3 + */ + public static void writeLines(File file, Collection lines) throws IOException { + writeLines(file, null, lines, null); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The specified character encoding and the line ending will be used. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param encoding the encoding to use, null means platform default + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since Commons IO 1.1 + */ + public static void writeLines(File file, String encoding, Collection lines, String lineEnding) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file); + IOUtils.writeLines(lines, lineEnding, out, encoding); + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the specified line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @throws IOException in case of an I/O error + * @since Commons IO 1.3 + */ + public static void writeLines(File file, Collection lines, String lineEnding) throws IOException { + writeLines(file, null, lines, lineEnding); + } + + //----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • You get exceptions when a file or directory cannot be deleted. + * (java.io.File methods returns a boolean)
  • + *
+ * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent){ + throw new FileNotFoundException("File does not exist: " + file); + } + String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Schedules a file to be deleted when JVM exits. + * If file is directory delete it and all sub-directories. + * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the file is null + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDeleteOnExit(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectoryOnExit(file); + } else { + file.deleteOnExit(); + } + } + + /** + * Schedules a directory recursively for deletion on JVM exit. + * + * @param directory directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case deletion is unsuccessful + */ + private static void deleteDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectoryOnExit(directory); + directory.deleteOnExit(); + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case cleaning is unsuccessful + */ + private static void cleanDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDeleteOnExit(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + /** + * Makes a directory, including any necessary but nonexistent parent + * directories. If there already exists a file with specified name or + * the directory cannot be created then an exception is thrown. + * + * @param directory directory to create, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException if the directory cannot be created + */ + public static void forceMkdir(File directory) throws IOException { + if (directory.exists()) { + if (directory.isFile()) { + String message = + "File " + + directory + + " exists and is " + + "not a directory. Unable to create directory."; + throw new IOException(message); + } + } else { + if (!directory.mkdirs()) { + String message = + "Unable to create directory " + directory; + throw new IOException(message); + } + } + } + + //----------------------------------------------------------------------- + /** + * Counts the size of a directory recursively (sum of the length of all files). + * + * @param directory directory to inspect, must not be null + * @return size of directory in bytes, 0 if directory is security restricted + * @throws NullPointerException if the directory is null + */ + public static long sizeOfDirectory(File directory) { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + long size = 0; + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + return 0L; + } + for (int i = 0; i < files.length; i++) { + File file = files[i]; + + if (file.isDirectory()) { + size += sizeOfDirectory(file); + } else { + size += file.length(); + } + } + + return size; + } + + //----------------------------------------------------------------------- + /** + * Tests if the specified File is newer than the reference + * File. + * + * @param file the File of which the modification date must + * be compared, must not be null + * @param reference the File of which the modification date + * is used, must not be null + * @return true if the File exists and has been modified more + * recently than the reference File + * @throws IllegalArgumentException if the file is null + * @throws IllegalArgumentException if the reference file is null or doesn't exist + */ + public static boolean isFileNewer(File file, File reference) { + if (reference == null) { + throw new IllegalArgumentException("No specified reference file"); + } + if (!reference.exists()) { + throw new IllegalArgumentException("The reference file '" + + file + "' doesn't exist"); + } + return isFileNewer(file, reference.lastModified()); + } + + /** + * Tests if the specified File is newer than the specified + * Date. + * + * @param file the File of which the modification date + * must be compared, must not be null + * @param date the date reference, must not be null + * @return true if the File exists and has been modified + * after the given Date. + * @throws IllegalArgumentException if the file is null + * @throws IllegalArgumentException if the date is null + */ + public static boolean isFileNewer(File file, Date date) { + if (date == null) { + throw new IllegalArgumentException("No specified date"); + } + return isFileNewer(file, date.getTime()); + } + + /** + * Tests if the specified File is newer than the specified + * time reference. + * + * @param file the File of which the modification date must + * be compared, must not be null + * @param timeMillis the time reference measured in milliseconds since the + * epoch (00:00:00 GMT, January 1, 1970) + * @return true if the File exists and has been modified after + * the given time reference. + * @throws IllegalArgumentException if the file is null + */ + public static boolean isFileNewer(File file, long timeMillis) { + if (file == null) { + throw new IllegalArgumentException("No specified file"); + } + if (!file.exists()) { + return false; + } + return file.lastModified() > timeMillis; + } + + + //----------------------------------------------------------------------- + /** + * Tests if the specified File is older than the reference + * File. + * + * @param file the File of which the modification date must + * be compared, must not be null + * @param reference the File of which the modification date + * is used, must not be null + * @return true if the File exists and has been modified before + * the reference File + * @throws IllegalArgumentException if the file is null + * @throws IllegalArgumentException if the reference file is null or doesn't exist + */ + public static boolean isFileOlder(File file, File reference) { + if (reference == null) { + throw new IllegalArgumentException("No specified reference file"); + } + if (!reference.exists()) { + throw new IllegalArgumentException("The reference file '" + + file + "' doesn't exist"); + } + return isFileOlder(file, reference.lastModified()); + } + + /** + * Tests if the specified File is older than the specified + * Date. + * + * @param file the File of which the modification date + * must be compared, must not be null + * @param date the date reference, must not be null + * @return true if the File exists and has been modified + * before the given Date. + * @throws IllegalArgumentException if the file is null + * @throws IllegalArgumentException if the date is null + */ + public static boolean isFileOlder(File file, Date date) { + if (date == null) { + throw new IllegalArgumentException("No specified date"); + } + return isFileOlder(file, date.getTime()); + } + + /** + * Tests if the specified File is older than the specified + * time reference. + * + * @param file the File of which the modification date must + * be compared, must not be null + * @param timeMillis the time reference measured in milliseconds since the + * epoch (00:00:00 GMT, January 1, 1970) + * @return true if the File exists and has been modified before + * the given time reference. + * @throws IllegalArgumentException if the file is null + */ + public static boolean isFileOlder(File file, long timeMillis) { + if (file == null) { + throw new IllegalArgumentException("No specified file"); + } + if (!file.exists()) { + return false; + } + return file.lastModified() < timeMillis; + } + + //----------------------------------------------------------------------- + /** + * Computes the checksum of a file using the CRC32 checksum routine. + * The value of the checksum is returned. + * + * @param file the file to checksum, must not be null + * @return the checksum value + * @throws NullPointerException if the file or checksum is null + * @throws IllegalArgumentException if the file is a directory + * @throws IOException if an IO error occurs reading the file + * @since Commons IO 1.3 + */ + public static long checksumCRC32(File file) throws IOException { + CRC32 crc = new CRC32(); + checksum(file, crc); + return crc.getValue(); + } + + /** + * Computes the checksum of a file using the specified checksum object. + * Multiple files may be checked using one Checksum instance + * if desired simply by reusing the same checksum object. + * For example: + *
+     *   long csum = FileUtils.checksum(file, new CRC32()).getValue();
+     * 
+ * + * @param file the file to checksum, must not be null + * @param checksum the checksum object to be used, must not be null + * @return the checksum specified, updated with the content of the file + * @throws NullPointerException if the file or checksum is null + * @throws IllegalArgumentException if the file is a directory + * @throws IOException if an IO error occurs reading the file + * @since Commons IO 1.3 + */ + public static Checksum checksum(File file, Checksum checksum) throws IOException { + if (file.isDirectory()) { + throw new IllegalArgumentException("Checksums can't be computed on directories"); + } + InputStream in = null; + try { + in = new CheckedInputStream(new FileInputStream(file), checksum); + IOUtils.copy(in, new NullOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + return checksum; + } + + /** + * Moves a directory. + *

+ * When the destination directory is on another file system, do a "copy and delete". + * + * @param srcDir the directory to be moved + * @param destDir the destination directory + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveDirectory(File srcDir, File destDir) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcDir.exists()) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (!srcDir.isDirectory()) { + throw new IOException("Source '" + srcDir + "' is not a directory"); + } + if (destDir.exists()) { + throw new IOException("Destination '" + destDir + "' already exists"); + } + boolean rename = srcDir.renameTo(destDir); + if (!rename) { + copyDirectory( srcDir, destDir ); + deleteDirectory( srcDir ); + if (srcDir.exists()) { + throw new IOException("Failed to delete original directory '" + srcDir + + "' after copy to '" + destDir + "'"); + } + } + } + + /** + * Moves a directory to another directory. + * + * @param src the file to be moved + * @param destDir the destination file + * @param createDestDir If true create the destination directory, + * otherwise if false throw an IOException + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveDirectoryToDirectory(File src, File destDir, boolean createDestDir) throws IOException { + if (src == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination directory must not be null"); + } + if (!destDir.exists() && createDestDir) { + destDir.mkdirs(); + } + if (!destDir.exists()) { + throw new FileNotFoundException("Destination directory '" + destDir + + "' does not exist [createDestDir=" + createDestDir +"]"); + } + if (!destDir.isDirectory()) { + throw new IOException("Destination '" + destDir + "' is not a directory"); + } + moveDirectory(src, new File(destDir, src.getName())); + + } + + /** + * Moves a file. + *

+ * When the destination file is on another file system, do a "copy and delete". + * + * @param srcFile the file to be moved + * @param destFile the destination file + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveFile(File srcFile, File destFile) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' is a directory"); + } + if (destFile.exists()) { + throw new IOException("Destination '" + destFile + "' already exists"); + } + if (destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' is a directory"); + } + boolean rename = srcFile.renameTo(destFile); + if (!rename) { + copyFile( srcFile, destFile ); + if (!srcFile.delete()) { + FileUtils.deleteQuietly(destFile); + throw new IOException("Failed to delete original file '" + srcFile + + "' after copy to '" + destFile + "'"); + } + } + } + + /** + * Moves a file to a directory. + * + * @param srcFile the file to be moved + * @param destDir the destination file + * @param createDestDir If true create the destination directory, + * otherwise if false throw an IOException + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveFileToDirectory(File srcFile, File destDir, boolean createDestDir) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination directory must not be null"); + } + if (!destDir.exists() && createDestDir) { + destDir.mkdirs(); + } + if (!destDir.exists()) { + throw new FileNotFoundException("Destination directory '" + destDir + + "' does not exist [createDestDir=" + createDestDir +"]"); + } + if (!destDir.isDirectory()) { + throw new IOException("Destination '" + destDir + "' is not a directory"); + } + moveFile(srcFile, new File(destDir, srcFile.getName())); + } + + /** + * Moves a file or directory to the destination directory. + *

+ * When the destination is on another file system, do a "copy and delete". + * + * @param src the file or directory to be moved + * @param destDir the destination directory + * @param createDestDir If true create the destination directory, + * otherwise if false throw an IOException + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveToDirectory(File src, File destDir, boolean createDestDir) throws IOException { + if (src == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!src.exists()) { + throw new FileNotFoundException("Source '" + src + "' does not exist"); + } + if (src.isDirectory()) { + moveDirectoryToDirectory(src, destDir, createDestDir); + } else { + moveFileToDirectory(src, destDir, createDestDir); + } + } + +} diff --git a/src/org/apache/commons/io/FilenameUtils.java b/src/org/apache/commons/io/FilenameUtils.java new file mode 100644 index 000000000..8e170b147 --- /dev/null +++ b/src/org/apache/commons/io/FilenameUtils.java @@ -0,0 +1,1260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Stack; + +/** + * General filename and filepath manipulation utilities. + *

+ * When dealing with filenames you can hit problems when moving from a Windows + * based development machine to a Unix based production machine. + * This class aims to help avoid those problems. + *

+ * NOTE: You may be able to avoid using this class entirely simply by + * using JDK {@link java.io.File File} objects and the two argument constructor + * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}. + *

+ * Most methods on this class are designed to work the same on both Unix and Windows. + * Those that don't include 'System', 'Unix' or 'Windows' in their name. + *

+ * Most methods recognise both separators (forward and back), and both + * sets of prefixes. See the javadoc of each method for details. + *

+ * This class defines six components within a filename + * (example C:\dev\project\file.txt): + *

    + *
  • the prefix - C:\
  • + *
  • the path - dev\project\
  • + *
  • the full path - C:\dev\project\
  • + *
  • the name - file.txt
  • + *
  • the base name - file
  • + *
  • the extension - txt
  • + *
+ * Note that this class works best if directory filenames end with a separator. + * If you omit the last separator, it is impossible to determine if the filename + * corresponds to a file or a directory. As a result, we have chosen to say + * it corresponds to a file. + *

+ * This class only supports Unix and Windows style names. + * Prefixes are matched as follows: + *

+ * Windows:
+ * a\b\c.txt           --> ""          --> relative
+ * \a\b\c.txt          --> "\"         --> current drive absolute
+ * C:a\b\c.txt         --> "C:"        --> drive relative
+ * C:\a\b\c.txt        --> "C:\"       --> absolute
+ * \\server\a\b\c.txt  --> "\\server\" --> UNC
+ *
+ * Unix:
+ * a/b/c.txt           --> ""          --> relative
+ * /a/b/c.txt          --> "/"         --> absolute
+ * ~/a/b/c.txt         --> "~/"        --> current user
+ * ~                   --> "~/"        --> current user (slash added)
+ * ~user/a/b/c.txt     --> "~user/"    --> named user
+ * ~user               --> "~user/"    --> named user (slash added)
+ * 
+ * Both prefix styles are matched always, irrespective of the machine that you are + * currently running on. + *

+ * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils. + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Martin Cooper + * @author Jeremias Maerki + * @author Stephen Colebourne + * @version $Id: FilenameUtils.java 609870 2008-01-08 04:46:26Z niallp $ + * @since Commons IO 1.1 + */ +public class FilenameUtils { + + /** + * The extension separator character. + * @since Commons IO 1.4 + */ + public static final char EXTENSION_SEPARATOR = '.'; + + /** + * The extension separator String. + * @since Commons IO 1.4 + */ + public static final String EXTENSION_SEPARATOR_STR = (new Character(EXTENSION_SEPARATOR)).toString(); + + /** + * The Unix separator character. + */ + private static final char UNIX_SEPARATOR = '/'; + + /** + * The Windows separator character. + */ + private static final char WINDOWS_SEPARATOR = '\\'; + + /** + * The system separator character. + */ + private static final char SYSTEM_SEPARATOR = File.separatorChar; + + /** + * The separator character that is the opposite of the system separator. + */ + private static final char OTHER_SEPARATOR; + static { + if (isSystemWindows()) { + OTHER_SEPARATOR = UNIX_SEPARATOR; + } else { + OTHER_SEPARATOR = WINDOWS_SEPARATOR; + } + } + + /** + * Instances should NOT be constructed in standard programming. + */ + public FilenameUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Determines if Windows file system is in use. + * + * @return true if the system is Windows + */ + static boolean isSystemWindows() { + return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR; + } + + //----------------------------------------------------------------------- + /** + * Checks if the character is a separator. + * + * @param ch the character to check + * @return true if it is a separator character + */ + private static boolean isSeparator(char ch) { + return (ch == UNIX_SEPARATOR) || (ch == WINDOWS_SEPARATOR); + } + + //----------------------------------------------------------------------- + /** + * Normalizes a path, removing double and single dot path steps. + *

+ * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + *

+ * A trailing slash will be retained. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, null + * is returned. + *

+ * The output will be the same on both Unix and Windows except + * for the separator character. + *

+     * /foo//               -->   /foo/
+     * /foo/./              -->   /foo/
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar/
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo/
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar/
+     * ~/../bar             -->   null
+     * 
+ * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename the filename to normalize, null returns null + * @return the normalized filename, or null if invalid + */ + public static String normalize(String filename) { + return doNormalize(filename, true); + } + + //----------------------------------------------------------------------- + /** + * Normalizes a path, removing double and single dot path steps, + * and removing any final directory separator. + *

+ * This method normalizes a path to a standard format. + * The input may contain separators in either Unix or Windows format. + * The output will contain separators in the format of the system. + *

+ * A trailing slash will be removed. + * A double slash will be merged to a single slash (but UNC names are handled). + * A single dot path segment will be removed. + * A double dot will cause that path segment and the one before to be removed. + * If the double dot has no parent path segment to work with, null + * is returned. + *

+ * The output will be the same on both Unix and Windows except + * for the separator character. + *

+     * /foo//               -->   /foo
+     * /foo/./              -->   /foo
+     * /foo/../bar          -->   /bar
+     * /foo/../bar/         -->   /bar
+     * /foo/../bar/../baz   -->   /baz
+     * //foo//./bar         -->   /foo/bar
+     * /../                 -->   null
+     * ../foo               -->   null
+     * foo/bar/..           -->   foo
+     * foo/../../bar        -->   null
+     * foo/../bar           -->   bar
+     * //server/foo/../bar  -->   //server/bar
+     * //server/../bar      -->   null
+     * C:\foo\..\bar        -->   C:\bar
+     * C:\..\bar            -->   null
+     * ~/foo/../bar/        -->   ~/bar
+     * ~/../bar             -->   null
+     * 
+ * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename the filename to normalize, null returns null + * @return the normalized filename, or null if invalid + */ + public static String normalizeNoEndSeparator(String filename) { + return doNormalize(filename, false); + } + + /** + * Internal method to perform the normalization. + * + * @param filename the filename + * @param keepSeparator true to keep the final separator + * @return the normalized filename + */ + private static String doNormalize(String filename, boolean keepSeparator) { + if (filename == null) { + return null; + } + int size = filename.length(); + if (size == 0) { + return filename; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + + char[] array = new char[size + 2]; // +1 for possible extra slash, +2 for arraycopy + filename.getChars(0, filename.length(), array, 0); + + // fix separators throughout + for (int i = 0; i < array.length; i++) { + if (array[i] == OTHER_SEPARATOR) { + array[i] = SYSTEM_SEPARATOR; + } + } + + // add extra separator on the end to simplify code below + boolean lastIsDirectory = true; + if (array[size - 1] != SYSTEM_SEPARATOR) { + array[size++] = SYSTEM_SEPARATOR; + lastIsDirectory = false; + } + + // adjoining slashes + for (int i = prefix + 1; i < size; i++) { + if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == SYSTEM_SEPARATOR) { + System.arraycopy(array, i, array, i - 1, size - i); + size--; + i--; + } + } + + // dot slash + for (int i = prefix + 1; i < size; i++) { + if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == '.' && + (i == prefix + 1 || array[i - 2] == SYSTEM_SEPARATOR)) { + if (i == size - 1) { + lastIsDirectory = true; + } + System.arraycopy(array, i + 1, array, i - 1, size - i); + size -=2; + i--; + } + } + + // double dot slash + outer: + for (int i = prefix + 2; i < size; i++) { + if (array[i] == SYSTEM_SEPARATOR && array[i - 1] == '.' && array[i - 2] == '.' && + (i == prefix + 2 || array[i - 3] == SYSTEM_SEPARATOR)) { + if (i == prefix + 2) { + return null; + } + if (i == size - 1) { + lastIsDirectory = true; + } + int j; + for (j = i - 4 ; j >= prefix; j--) { + if (array[j] == SYSTEM_SEPARATOR) { + // remove b/../ from a/b/../c + System.arraycopy(array, i + 1, array, j + 1, size - i); + size -= (i - j); + i = j + 1; + continue outer; + } + } + // remove a/../ from a/../c + System.arraycopy(array, i + 1, array, prefix, size - i); + size -= (i + 1 - prefix); + i = prefix + 1; + } + } + + if (size <= 0) { // should never be less than 0 + return ""; + } + if (size <= prefix) { // should never be less than prefix + return new String(array, 0, size); + } + if (lastIsDirectory && keepSeparator) { + return new String(array, 0, size); // keep trailing separator + } + return new String(array, 0, size - 1); // lose trailing separator + } + + //----------------------------------------------------------------------- + /** + * Concatenates a filename to a base path using normal command line style rules. + *

+ * The effect is equivalent to resultant directory after changing + * directory to the first argument, followed by changing directory to + * the second argument. + *

+ * The first argument is the base path, the second is the path to concatenate. + * The returned path is always normalized via {@link #normalize(String)}, + * thus .. is handled. + *

+ * If pathToAdd is absolute (has an absolute prefix), then + * it will be normalized and returned. + * Otherwise, the paths will be joined, normalized and returned. + *

+ * The output will be the same on both Unix and Windows except + * for the separator character. + *

+     * /foo/ + bar          -->   /foo/bar
+     * /foo + bar           -->   /foo/bar
+     * /foo + /bar          -->   /bar
+     * /foo + C:/bar        -->   C:/bar
+     * /foo + C:bar         -->   C:bar (*)
+     * /foo/a/ + ../bar     -->   foo/bar
+     * /foo/ + ../../bar    -->   null
+     * /foo/ + /bar         -->   /bar
+     * /foo/.. + /bar       -->   /bar
+     * /foo + bar/c.txt     -->   /foo/bar/c.txt
+     * /foo/c.txt + bar     -->   /foo/c.txt/bar (!)
+     * 
+ * (*) Note that the Windows relative drive prefix is unreliable when + * used with this method. + * (!) Note that the first parameter must be a path. If it ends with a name, then + * the name will be built into the concatenated path. If this might be a problem, + * use {@link #getFullPath(String)} on the base path argument. + * + * @param basePath the base path to attach to, always treated as a path + * @param fullFilenameToAdd the filename (or path) to attach to the base + * @return the concatenated path, or null if invalid + */ + public static String concat(String basePath, String fullFilenameToAdd) { + int prefix = getPrefixLength(fullFilenameToAdd); + if (prefix < 0) { + return null; + } + if (prefix > 0) { + return normalize(fullFilenameToAdd); + } + if (basePath == null) { + return null; + } + int len = basePath.length(); + if (len == 0) { + return normalize(fullFilenameToAdd); + } + char ch = basePath.charAt(len - 1); + if (isSeparator(ch)) { + return normalize(basePath + fullFilenameToAdd); + } else { + return normalize(basePath + '/' + fullFilenameToAdd); + } + } + + //----------------------------------------------------------------------- + /** + * Converts all separators to the Unix separator of forward slash. + * + * @param path the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToUnix(String path) { + if (path == null || path.indexOf(WINDOWS_SEPARATOR) == -1) { + return path; + } + return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR); + } + + /** + * Converts all separators to the Windows separator of backslash. + * + * @param path the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToWindows(String path) { + if (path == null || path.indexOf(UNIX_SEPARATOR) == -1) { + return path; + } + return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR); + } + + /** + * Converts all separators to the system separator. + * + * @param path the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToSystem(String path) { + if (path == null) { + return null; + } + if (isSystemWindows()) { + return separatorsToWindows(path); + } else { + return separatorsToUnix(path); + } + } + + //----------------------------------------------------------------------- + /** + * Returns the length of the filename prefix, such as C:/ or ~/. + *

+ * This method will handle a file in either Unix or Windows format. + *

+ * The prefix length includes the first slash in the full filename + * if applicable. Thus, it is possible that the length returned is greater + * than the length of the input string. + *

+     * Windows:
+     * a\b\c.txt           --> ""          --> relative
+     * \a\b\c.txt          --> "\"         --> current drive absolute
+     * C:a\b\c.txt         --> "C:"        --> drive relative
+     * C:\a\b\c.txt        --> "C:\"       --> absolute
+     * \\server\a\b\c.txt  --> "\\server\" --> UNC
+     *
+     * Unix:
+     * a/b/c.txt           --> ""          --> relative
+     * /a/b/c.txt          --> "/"         --> absolute
+     * ~/a/b/c.txt         --> "~/"        --> current user
+     * ~                   --> "~/"        --> current user (slash added)
+     * ~user/a/b/c.txt     --> "~user/"    --> named user
+     * ~user               --> "~user/"    --> named user (slash added)
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename the filename to find the prefix in, null returns -1 + * @return the length of the prefix, -1 if invalid or null + */ + public static int getPrefixLength(String filename) { + if (filename == null) { + return -1; + } + int len = filename.length(); + if (len == 0) { + return 0; + } + char ch0 = filename.charAt(0); + if (ch0 == ':') { + return -1; + } + if (len == 1) { + if (ch0 == '~') { + return 2; // return a length greater than the input + } + return (isSeparator(ch0) ? 1 : 0); + } else { + if (ch0 == '~') { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); + if (posUnix == -1 && posWin == -1) { + return len + 1; // return a length greater than the input + } + posUnix = (posUnix == -1 ? posWin : posUnix); + posWin = (posWin == -1 ? posUnix : posWin); + return Math.min(posUnix, posWin) + 1; + } + char ch1 = filename.charAt(1); + if (ch1 == ':') { + ch0 = Character.toUpperCase(ch0); + if (ch0 >= 'A' && ch0 <= 'Z') { + if (len == 2 || isSeparator(filename.charAt(2)) == false) { + return 2; + } + return 3; + } + return -1; + + } else if (isSeparator(ch0) && isSeparator(ch1)) { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); + if ((posUnix == -1 && posWin == -1) || posUnix == 2 || posWin == 2) { + return -1; + } + posUnix = (posUnix == -1 ? posWin : posUnix); + posWin = (posWin == -1 ? posUnix : posWin); + return Math.min(posUnix, posWin) + 1; + } else { + return (isSeparator(ch0) ? 1 : 0); + } + } + } + + /** + * Returns the index of the last directory separator character. + *

+ * This method will handle a file in either Unix or Windows format. + * The position of the last forward or backslash is returned. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there + * is no such character + */ + public static int indexOfLastSeparator(String filename) { + if (filename == null) { + return -1; + } + int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR); + int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR); + return Math.max(lastUnixPos, lastWindowsPos); + } + + /** + * Returns the index of the last extension separator character, which is a dot. + *

+ * This method also checks that there is no directory separator after the last dot. + * To do this it uses {@link #indexOfLastSeparator(String)} which will + * handle a file in either Unix or Windows format. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there + * is no such character + */ + public static int indexOfExtension(String filename) { + if (filename == null) { + return -1; + } + int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR); + int lastSeparator = indexOfLastSeparator(filename); + return (lastSeparator > extensionPos ? -1 : extensionPos); + } + + //----------------------------------------------------------------------- + /** + * Gets the prefix from a full filename, such as C:/ + * or ~/. + *

+ * This method will handle a file in either Unix or Windows format. + * The prefix includes the first slash in the full filename where applicable. + *

+     * Windows:
+     * a\b\c.txt           --> ""          --> relative
+     * \a\b\c.txt          --> "\"         --> current drive absolute
+     * C:a\b\c.txt         --> "C:"        --> drive relative
+     * C:\a\b\c.txt        --> "C:\"       --> absolute
+     * \\server\a\b\c.txt  --> "\\server\" --> UNC
+     *
+     * Unix:
+     * a/b/c.txt           --> ""          --> relative
+     * /a/b/c.txt          --> "/"         --> absolute
+     * ~/a/b/c.txt         --> "~/"        --> current user
+     * ~                   --> "~/"        --> current user (slash added)
+     * ~user/a/b/c.txt     --> "~user/"    --> named user
+     * ~user               --> "~user/"    --> named user (slash added)
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename the filename to query, null returns null + * @return the prefix of the file, null if invalid + */ + public static String getPrefix(String filename) { + if (filename == null) { + return null; + } + int len = getPrefixLength(filename); + if (len < 0) { + return null; + } + if (len > filename.length()) { + return filename + UNIX_SEPARATOR; // we know this only happens for unix + } + return filename.substring(0, len); + } + + /** + * Gets the path from a full filename, which excludes the prefix. + *

+ * This method will handle a file in either Unix or Windows format. + * The method is entirely text based, and returns the text before and + * including the last forward or backslash. + *

+     * C:\a\b\c.txt --> a\b\
+     * ~/a/b/c.txt  --> a/b/
+     * a.txt        --> ""
+     * a/b/c        --> a/b/
+     * a/b/c/       --> a/b/c/
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + *

+ * This method drops the prefix from the result. + * See {@link #getFullPath(String)} for the method that retains the prefix. + * + * @param filename the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getPath(String filename) { + return doGetPath(filename, 1); + } + + /** + * Gets the path from a full filename, which excludes the prefix, and + * also excluding the final directory separator. + *

+ * This method will handle a file in either Unix or Windows format. + * The method is entirely text based, and returns the text before the + * last forward or backslash. + *

+     * C:\a\b\c.txt --> a\b
+     * ~/a/b/c.txt  --> a/b
+     * a.txt        --> ""
+     * a/b/c        --> a/b
+     * a/b/c/       --> a/b/c
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + *

+ * This method drops the prefix from the result. + * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix. + * + * @param filename the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getPathNoEndSeparator(String filename) { + return doGetPath(filename, 0); + } + + /** + * Does the work of getting the path. + * + * @param filename the filename + * @param separatorAdd 0 to omit the end separator, 1 to return it + * @return the path + */ + private static String doGetPath(String filename, int separatorAdd) { + if (filename == null) { + return null; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + int index = indexOfLastSeparator(filename); + if (prefix >= filename.length() || index < 0) { + return ""; + } + return filename.substring(prefix, index + separatorAdd); + } + + /** + * Gets the full path from a full filename, which is the prefix + path. + *

+ * This method will handle a file in either Unix or Windows format. + * The method is entirely text based, and returns the text before and + * including the last forward or backslash. + *

+     * C:\a\b\c.txt --> C:\a\b\
+     * ~/a/b/c.txt  --> ~/a/b/
+     * a.txt        --> ""
+     * a/b/c        --> a/b/
+     * a/b/c/       --> a/b/c/
+     * C:           --> C:
+     * C:\          --> C:\
+     * ~            --> ~/
+     * ~/           --> ~/
+     * ~user        --> ~user/
+     * ~user/       --> ~user/
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getFullPath(String filename) { + return doGetFullPath(filename, true); + } + + /** + * Gets the full path from a full filename, which is the prefix + path, + * and also excluding the final directory separator. + *

+ * This method will handle a file in either Unix or Windows format. + * The method is entirely text based, and returns the text before the + * last forward or backslash. + *

+     * C:\a\b\c.txt --> C:\a\b
+     * ~/a/b/c.txt  --> ~/a/b
+     * a.txt        --> ""
+     * a/b/c        --> a/b
+     * a/b/c/       --> a/b/c
+     * C:           --> C:
+     * C:\          --> C:\
+     * ~            --> ~
+     * ~/           --> ~
+     * ~user        --> ~user
+     * ~user/       --> ~user
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getFullPathNoEndSeparator(String filename) { + return doGetFullPath(filename, false); + } + + /** + * Does the work of getting the path. + * + * @param filename the filename + * @param includeSeparator true to include the end separator + * @return the path + */ + private static String doGetFullPath(String filename, boolean includeSeparator) { + if (filename == null) { + return null; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + if (prefix >= filename.length()) { + if (includeSeparator) { + return getPrefix(filename); // add end slash if necessary + } else { + return filename; + } + } + int index = indexOfLastSeparator(filename); + if (index < 0) { + return filename.substring(0, prefix); + } + int end = index + (includeSeparator ? 1 : 0); + return filename.substring(0, end); + } + + /** + * Gets the name minus the path from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. + * The text after the last forward or backslash is returned. + *

+     * a/b/c.txt --> c.txt
+     * a.txt     --> a.txt
+     * a/b/c     --> c
+     * a/b/c/    --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + public static String getName(String filename) { + if (filename == null) { + return null; + } + int index = indexOfLastSeparator(filename); + return filename.substring(index + 1); + } + + /** + * Gets the base name, minus the full path and extension, from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. + * The text after the last forward or backslash and before the last dot is returned. + *

+     * a/b/c.txt --> c
+     * a.txt     --> a
+     * a/b/c     --> c
+     * a/b/c/    --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + public static String getBaseName(String filename) { + return removeExtension(getName(filename)); + } + + /** + * Gets the extension of a filename. + *

+ * This method returns the textual part of the filename after the last dot. + * There must be no directory separator after the dot. + *

+     * foo.txt      --> "txt"
+     * a/b/c.jpg    --> "jpg"
+     * a/b.txt/c    --> ""
+     * a/b/c        --> ""
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to retrieve the extension of. + * @return the extension of the file or an empty string if none exists. + */ + public static String getExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return ""; + } else { + return filename.substring(index + 1); + } + } + + //----------------------------------------------------------------------- + /** + * Removes the extension from a filename. + *

+ * This method returns the textual part of the filename before the last dot. + * There must be no directory separator after the dot. + *

+     * foo.txt    --> foo
+     * a\b\c.jpg  --> a\b\c
+     * a\b\c      --> a\b\c
+     * a.b\c      --> a.b\c
+     * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename the filename to query, null returns null + * @return the filename minus the extension + */ + public static String removeExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return filename; + } else { + return filename.substring(0, index); + } + } + + //----------------------------------------------------------------------- + /** + * Checks whether two filenames are equal exactly. + *

+ * No processing is performed on the filenames other than comparison, + * thus this is merely a null-safe case-sensitive equals. + * + * @param filename1 the first filename to query, may be null + * @param filename2 the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SENSITIVE + */ + public static boolean equals(String filename1, String filename2) { + return equals(filename1, filename2, false, IOCase.SENSITIVE); + } + + /** + * Checks whether two filenames are equal using the case rules of the system. + *

+ * No processing is performed on the filenames other than comparison. + * The check is case-sensitive on Unix and case-insensitive on Windows. + * + * @param filename1 the first filename to query, may be null + * @param filename2 the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SYSTEM + */ + public static boolean equalsOnSystem(String filename1, String filename2) { + return equals(filename1, filename2, false, IOCase.SYSTEM); + } + + //----------------------------------------------------------------------- + /** + * Checks whether two filenames are equal after both have been normalized. + *

+ * Both filenames are first passed to {@link #normalize(String)}. + * The check is then performed in a case-sensitive manner. + * + * @param filename1 the first filename to query, may be null + * @param filename2 the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SENSITIVE + */ + public static boolean equalsNormalized(String filename1, String filename2) { + return equals(filename1, filename2, true, IOCase.SENSITIVE); + } + + /** + * Checks whether two filenames are equal after both have been normalized + * and using the case rules of the system. + *

+ * Both filenames are first passed to {@link #normalize(String)}. + * The check is then performed case-sensitive on Unix and + * case-insensitive on Windows. + * + * @param filename1 the first filename to query, may be null + * @param filename2 the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SYSTEM + */ + public static boolean equalsNormalizedOnSystem(String filename1, String filename2) { + return equals(filename1, filename2, true, IOCase.SYSTEM); + } + + /** + * Checks whether two filenames are equal, optionally normalizing and providing + * control over the case-sensitivity. + * + * @param filename1 the first filename to query, may be null + * @param filename2 the second filename to query, may be null + * @param normalized whether to normalize the filenames + * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive + * @return true if the filenames are equal, null equals null + * @since Commons IO 1.3 + */ + public static boolean equals( + String filename1, String filename2, + boolean normalized, IOCase caseSensitivity) { + + if (filename1 == null || filename2 == null) { + return filename1 == filename2; + } + if (normalized) { + filename1 = normalize(filename1); + filename2 = normalize(filename2); + if (filename1 == null || filename2 == null) { + throw new NullPointerException( + "Error normalizing one or both of the file names"); + } + } + if (caseSensitivity == null) { + caseSensitivity = IOCase.SENSITIVE; + } + return caseSensitivity.checkEquals(filename1, filename2); + } + + //----------------------------------------------------------------------- + /** + * Checks whether the extension of the filename is that specified. + *

+ * This method obtains the extension as the textual part of the filename + * after the last dot. There must be no directory separator after the dot. + * The extension check is case-sensitive on all platforms. + * + * @param filename the filename to query, null returns false + * @param extension the extension to check for, null or empty checks for no extension + * @return true if the filename has the specified extension + */ + public static boolean isExtension(String filename, String extension) { + if (filename == null) { + return false; + } + if (extension == null || extension.length() == 0) { + return (indexOfExtension(filename) == -1); + } + String fileExt = getExtension(filename); + return fileExt.equals(extension); + } + + /** + * Checks whether the extension of the filename is one of those specified. + *

+ * This method obtains the extension as the textual part of the filename + * after the last dot. There must be no directory separator after the dot. + * The extension check is case-sensitive on all platforms. + * + * @param filename the filename to query, null returns false + * @param extensions the extensions to check for, null checks for no extension + * @return true if the filename is one of the extensions + */ + public static boolean isExtension(String filename, String[] extensions) { + if (filename == null) { + return false; + } + if (extensions == null || extensions.length == 0) { + return (indexOfExtension(filename) == -1); + } + String fileExt = getExtension(filename); + for (int i = 0; i < extensions.length; i++) { + if (fileExt.equals(extensions[i])) { + return true; + } + } + return false; + } + + /** + * Checks whether the extension of the filename is one of those specified. + *

+ * This method obtains the extension as the textual part of the filename + * after the last dot. There must be no directory separator after the dot. + * The extension check is case-sensitive on all platforms. + * + * @param filename the filename to query, null returns false + * @param extensions the extensions to check for, null checks for no extension + * @return true if the filename is one of the extensions + */ + public static boolean isExtension(String filename, Collection extensions) { + if (filename == null) { + return false; + } + if (extensions == null || extensions.isEmpty()) { + return (indexOfExtension(filename) == -1); + } + String fileExt = getExtension(filename); + for (Iterator it = extensions.iterator(); it.hasNext();) { + if (fileExt.equals(it.next())) { + return true; + } + } + return false; + } + + //----------------------------------------------------------------------- + /** + * Checks a filename to see if it matches the specified wildcard matcher, + * always testing case-sensitive. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple wildcard characters. + * This is the same as often found on Dos/Unix command lines. + * The check is case-sensitive always. + *

+     * wildcardMatch("c.txt", "*.txt")      --> true
+     * wildcardMatch("c.txt", "*.jpg")      --> false
+     * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
+     * wildcardMatch("c.txt", "*.???")      --> true
+     * wildcardMatch("c.txt", "*.????")     --> false
+     * 
+ * + * @param filename the filename to match on + * @param wildcardMatcher the wildcard string to match against + * @return true if the filename matches the wilcard string + * @see IOCase#SENSITIVE + */ + public static boolean wildcardMatch(String filename, String wildcardMatcher) { + return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE); + } + + /** + * Checks a filename to see if it matches the specified wildcard matcher + * using the case rules of the system. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple wildcard characters. + * This is the same as often found on Dos/Unix command lines. + * The check is case-sensitive on Unix and case-insensitive on Windows. + *

+     * wildcardMatch("c.txt", "*.txt")      --> true
+     * wildcardMatch("c.txt", "*.jpg")      --> false
+     * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
+     * wildcardMatch("c.txt", "*.???")      --> true
+     * wildcardMatch("c.txt", "*.????")     --> false
+     * 
+ * + * @param filename the filename to match on + * @param wildcardMatcher the wildcard string to match against + * @return true if the filename matches the wilcard string + * @see IOCase#SYSTEM + */ + public static boolean wildcardMatchOnSystem(String filename, String wildcardMatcher) { + return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM); + } + + /** + * Checks a filename to see if it matches the specified wildcard matcher + * allowing control over case-sensitivity. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple wildcard characters. + * + * @param filename the filename to match on + * @param wildcardMatcher the wildcard string to match against + * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive + * @return true if the filename matches the wilcard string + * @since Commons IO 1.3 + */ + public static boolean wildcardMatch(String filename, String wildcardMatcher, IOCase caseSensitivity) { + if (filename == null && wildcardMatcher == null) { + return true; + } + if (filename == null || wildcardMatcher == null) { + return false; + } + if (caseSensitivity == null) { + caseSensitivity = IOCase.SENSITIVE; + } + filename = caseSensitivity.convertCase(filename); + wildcardMatcher = caseSensitivity.convertCase(wildcardMatcher); + String[] wcs = splitOnTokens(wildcardMatcher); + boolean anyChars = false; + int textIdx = 0; + int wcsIdx = 0; + Stack backtrack = new Stack(); + + // loop around a backtrack stack, to handle complex * matching + do { + if (backtrack.size() > 0) { + int[] array = (int[]) backtrack.pop(); + wcsIdx = array[0]; + textIdx = array[1]; + anyChars = true; + } + + // loop whilst tokens and text left to process + while (wcsIdx < wcs.length) { + + if (wcs[wcsIdx].equals("?")) { + // ? so move to next text char + textIdx++; + anyChars = false; + + } else if (wcs[wcsIdx].equals("*")) { + // set any chars status + anyChars = true; + if (wcsIdx == wcs.length - 1) { + textIdx = filename.length(); + } + + } else { + // matching text token + if (anyChars) { + // any chars then try to locate text token + textIdx = filename.indexOf(wcs[wcsIdx], textIdx); + if (textIdx == -1) { + // token not found + break; + } + int repeat = filename.indexOf(wcs[wcsIdx], textIdx + 1); + if (repeat >= 0) { + backtrack.push(new int[] {wcsIdx, repeat}); + } + } else { + // matching from current position + if (!filename.startsWith(wcs[wcsIdx], textIdx)) { + // couldnt match token + break; + } + } + + // matched text token, move text index to end of matched token + textIdx += wcs[wcsIdx].length(); + anyChars = false; + } + + wcsIdx++; + } + + // full match + if (wcsIdx == wcs.length && textIdx == filename.length()) { + return true; + } + + } while (backtrack.size() > 0); + + return false; + } + + /** + * Splits a string into a number of tokens. + * + * @param text the text to split + * @return the tokens, never null + */ + static String[] splitOnTokens(String text) { + // used by wildcardMatch + // package level so a unit test may run on this + + if (text.indexOf("?") == -1 && text.indexOf("*") == -1) { + return new String[] { text }; + } + + char[] array = text.toCharArray(); + ArrayList list = new ArrayList(); + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < array.length; i++) { + if (array[i] == '?' || array[i] == '*') { + if (buffer.length() != 0) { + list.add(buffer.toString()); + buffer.setLength(0); + } + if (array[i] == '?') { + list.add("?"); + } else if (list.size() == 0 || + (i > 0 && list.get(list.size() - 1).equals("*") == false)) { + list.add("*"); + } + } else { + buffer.append(array[i]); + } + } + if (buffer.length() != 0) { + list.add(buffer.toString()); + } + + return (String[]) list.toArray( new String[ list.size() ] ); + } + +} diff --git a/src/org/apache/commons/io/HexDump.java b/src/org/apache/commons/io/HexDump.java new file mode 100644 index 000000000..b0d468d18 --- /dev/null +++ b/src/org/apache/commons/io/HexDump.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Dumps data in hexadecimal format. + *

+ * Provides a single function to take an array of bytes and display it + * in hexadecimal form. + *

+ * Origin of code: POI. + * + * @author Scott Sanders + * @author Marc Johnson + * @version $Id: HexDump.java 596667 2007-11-20 13:50:14Z niallp $ + */ +public class HexDump { + + /** + * Instances should NOT be constructed in standard programming. + */ + public HexDump() { + super(); + } + + /** + * Dump an array of bytes to an OutputStream. + * + * @param data the byte array to be dumped + * @param offset its offset, whatever that might mean + * @param stream the OutputStream to which the data is to be + * written + * @param index initial index into the byte array + * + * @throws IOException is thrown if anything goes wrong writing + * the data to stream + * @throws ArrayIndexOutOfBoundsException if the index is + * outside the data array's bounds + * @throws IllegalArgumentException if the output stream is null + */ + + public static void dump(byte[] data, long offset, + OutputStream stream, int index) + throws IOException, ArrayIndexOutOfBoundsException, + IllegalArgumentException { + + if ((index < 0) || (index >= data.length)) { + throw new ArrayIndexOutOfBoundsException( + "illegal index: " + index + " into array of length " + + data.length); + } + if (stream == null) { + throw new IllegalArgumentException("cannot write to nullstream"); + } + long display_offset = offset + index; + StringBuffer buffer = new StringBuffer(74); + + for (int j = index; j < data.length; j += 16) { + int chars_read = data.length - j; + + if (chars_read > 16) { + chars_read = 16; + } + dump(buffer, display_offset).append(' '); + for (int k = 0; k < 16; k++) { + if (k < chars_read) { + dump(buffer, data[k + j]); + } else { + buffer.append(" "); + } + buffer.append(' '); + } + for (int k = 0; k < chars_read; k++) { + if ((data[k + j] >= ' ') && (data[k + j] < 127)) { + buffer.append((char) data[k + j]); + } else { + buffer.append('.'); + } + } + buffer.append(EOL); + stream.write(buffer.toString().getBytes()); + stream.flush(); + buffer.setLength(0); + display_offset += chars_read; + } + } + + /** + * The line-separator (initializes to "line.separator" system property. + */ + public static final String EOL = + System.getProperty("line.separator"); + private static final char[] _hexcodes = + { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static final int[] _shifts = + { + 28, 24, 20, 16, 12, 8, 4, 0 + }; + + /** + * Dump a long value into a StringBuffer. + * + * @param _lbuffer the StringBuffer to dump the value in + * @param value the long value to be dumped + * @return StringBuffer containing the dumped value. + */ + private static StringBuffer dump(StringBuffer _lbuffer, long value) { + for (int j = 0; j < 8; j++) { + _lbuffer + .append(_hexcodes[((int) (value >> _shifts[j])) & 15]); + } + return _lbuffer; + } + + /** + * Dump a byte value into a StringBuffer. + * + * @param _cbuffer the StringBuffer to dump the value in + * @param value the byte value to be dumped + * @return StringBuffer containing the dumped value. + */ + private static StringBuffer dump(StringBuffer _cbuffer, byte value) { + for (int j = 0; j < 2; j++) { + _cbuffer.append(_hexcodes[(value >> _shifts[j + 6]) & 15]); + } + return _cbuffer; + } + +} diff --git a/src/org/apache/commons/io/IOCase.java b/src/org/apache/commons/io/IOCase.java new file mode 100644 index 000000000..4230f450d --- /dev/null +++ b/src/org/apache/commons/io/IOCase.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.Serializable; + +/** + * Enumeration of IO case sensitivity. + *

+ * Different filing systems have different rules for case-sensitivity. + * Windows is case-insensitive, Unix is case-sensitive. + *

+ * This class captures that difference, providing an enumeration to + * control how filename comparisons should be performed. It also provides + * methods that use the enumeration to perform comparisons. + *

+ * Wherever possible, you should use the check methods in this + * class to compare filenames. + * + * @author Stephen Colebourne + * @version $Id: IOCase.java 606345 2007-12-21 23:43:01Z ggregory $ + * @since Commons IO 1.3 + */ +public final class IOCase implements Serializable { + + /** + * The constant for case sensitive regardless of operating system. + */ + public static final IOCase SENSITIVE = new IOCase("Sensitive", true); + + /** + * The constant for case insensitive regardless of operating system. + */ + public static final IOCase INSENSITIVE = new IOCase("Insensitive", false); + + /** + * The constant for case sensitivity determined by the current operating system. + * Windows is case-insensitive when comparing filenames, Unix is case-sensitive. + *

+ * If you derialize this constant of Windows, and deserialize on Unix, or vice + * versa, then the value of the case-sensitivity flag will change. + */ + public static final IOCase SYSTEM = new IOCase("System", !FilenameUtils.isSystemWindows()); + + /** Serialization version. */ + private static final long serialVersionUID = -6343169151696340687L; + + /** The enumeration name. */ + private final String name; + + /** The sensitivity flag. */ + private final transient boolean sensitive; + + //----------------------------------------------------------------------- + /** + * Factory method to create an IOCase from a name. + * + * @param name the name to find + * @return the IOCase object + * @throws IllegalArgumentException if the name is invalid + */ + public static IOCase forName(String name) { + if (IOCase.SENSITIVE.name.equals(name)){ + return IOCase.SENSITIVE; + } + if (IOCase.INSENSITIVE.name.equals(name)){ + return IOCase.INSENSITIVE; + } + if (IOCase.SYSTEM.name.equals(name)){ + return IOCase.SYSTEM; + } + throw new IllegalArgumentException("Invalid IOCase name: " + name); + } + + //----------------------------------------------------------------------- + /** + * Private constructor. + * + * @param name the name + * @param sensitive the sensitivity + */ + private IOCase(String name, boolean sensitive) { + this.name = name; + this.sensitive = sensitive; + } + + /** + * Replaces the enumeration from the stream with a real one. + * This ensures that the correct flag is set for SYSTEM. + * + * @return the resolved object + */ + private Object readResolve() { + return forName(name); + } + + //----------------------------------------------------------------------- + /** + * Gets the name of the constant. + * + * @return the name of the constant + */ + public String getName() { + return name; + } + + /** + * Does the object represent case sensitive comparison. + * + * @return true if case sensitive + */ + public boolean isCaseSensitive() { + return sensitive; + } + + //----------------------------------------------------------------------- + /** + * Compares two strings using the case-sensitivity rule. + *

+ * This method mimics {@link String#compareTo} but takes case-sensitivity + * into account. + * + * @param str1 the first string to compare, not null + * @param str2 the second string to compare, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public int checkCompareTo(String str1, String str2) { + if (str1 == null || str2 == null) { + throw new NullPointerException("The strings must not be null"); + } + return sensitive ? str1.compareTo(str2) : str1.compareToIgnoreCase(str2); + } + + /** + * Compares two strings using the case-sensitivity rule. + *

+ * This method mimics {@link String#equals} but takes case-sensitivity + * into account. + * + * @param str1 the first string to compare, not null + * @param str2 the second string to compare, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkEquals(String str1, String str2) { + if (str1 == null || str2 == null) { + throw new NullPointerException("The strings must not be null"); + } + return sensitive ? str1.equals(str2) : str1.equalsIgnoreCase(str2); + } + + /** + * Checks if one string starts with another using the case-sensitivity rule. + *

+ * This method mimics {@link String#startsWith(String)} but takes case-sensitivity + * into account. + * + * @param str the string to check, not null + * @param start the start to compare against, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkStartsWith(String str, String start) { + return str.regionMatches(!sensitive, 0, start, 0, start.length()); + } + + /** + * Checks if one string ends with another using the case-sensitivity rule. + *

+ * This method mimics {@link String#endsWith} but takes case-sensitivity + * into account. + * + * @param str the string to check, not null + * @param end the end to compare against, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkEndsWith(String str, String end) { + int endLen = end.length(); + return str.regionMatches(!sensitive, str.length() - endLen, end, 0, endLen); + } + + /** + * Checks if one string contains another at a specific index using the case-sensitivity rule. + *

+ * This method mimics parts of {@link String#regionMatches(boolean, int, String, int, int)} + * but takes case-sensitivity into account. + * + * @param str the string to check, not null + * @param strStartIndex the index to start at in str + * @param search the start to search for, not null + * @return true if equal using the case rules + * @throws NullPointerException if either string is null + */ + public boolean checkRegionMatches(String str, int strStartIndex, String search) { + return str.regionMatches(!sensitive, strStartIndex, search, 0, search.length()); + } + + /** + * Converts the case of the input String to a standard format. + * Subsequent operations can then use standard String methods. + * + * @param str the string to convert, null returns null + * @return the lower-case version if case-insensitive + */ + String convertCase(String str) { + if (str == null) { + return null; + } + return sensitive ? str : str.toLowerCase(); + } + + //----------------------------------------------------------------------- + /** + * Gets a string describing the sensitivity. + * + * @return a string describing the sensitivity + */ + public String toString() { + return name; + } + +} diff --git a/src/org/apache/commons/io/IOExceptionWithCause.java b/src/org/apache/commons/io/IOExceptionWithCause.java new file mode 100644 index 000000000..a15815a22 --- /dev/null +++ b/src/org/apache/commons/io/IOExceptionWithCause.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io; + +import java.io.IOException; + +/** + * Subclasses IOException with the {@link Throwable} constructors missing before Java 6. If you are using Java 6, + * consider this class deprecated and use {@link IOException}. + * + * @author Apache Commons IO + * @version $Id$ + * @since Commons IO 1.4 + */ +public class IOExceptionWithCause extends IOException { + + /** + * Defines the serial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new instance with the given message and cause. + *

+ * As specified in {@link Throwable}, the message in the given cause is not used in this instance's + * message. + *

+ * + * @param message + * the message (see {@link #getMessage()}) + * @param cause + * the cause (see {@link #getCause()}). A null value is allowed. + */ + public IOExceptionWithCause(String message, Throwable cause) { + super(message); + this.initCause(cause); + } + + /** + * Constructs a new instance with the given cause. + *

+ * The message is set to cause==null ? null : cause.toString(), which by default contains the class + * and message of cause. This constructor is useful for call sites that just wrap another throwable. + *

+ * + * @param cause + * the cause (see {@link #getCause()}). A null value is allowed. + */ + public IOExceptionWithCause(Throwable cause) { + super(cause == null ? null : cause.toString()); + this.initCause(cause); + } + +} diff --git a/src/org/apache/commons/io/IOUtils.java b/src/org/apache/commons/io/IOUtils.java new file mode 100644 index 000000000..1f91e2562 --- /dev/null +++ b/src/org/apache/commons/io/IOUtils.java @@ -0,0 +1,1274 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.io.output.ByteArrayOutputStream; + +/** + * General IO stream manipulation utilities. + *

+ * This class provides static utility methods for input/output operations. + *

    + *
  • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
  • toXxx/read - these methods read data from a stream + *
  • write - these methods write data to a stream + *
  • copy - these methods copy all the data from one stream to another + *
  • contentEquals - these methods compare the content of two streams + *
+ *

+ * The byte-to-char methods and char-to-byte methods involve a conversion step. + * Two methods are provided in each case, one that uses the platform default + * encoding and the other which allows you to specify an encoding. You are + * encouraged to always specify an encoding because relying on the platform + * default can lead to unexpected results, for example when moving from + * development to production. + *

+ * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a BufferedInputStream + * or BufferedReader. The default buffer size of 4K has been shown + * to be efficient in tests. + *

+ * Wherever possible, the methods in this class do not flush or close + * the stream. This is to avoid making non-portable assumptions about the + * streams' origin and further use. Thus the caller is still responsible for + * closing streams after use. + *

+ * Origin of code: Excalibur. + * + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Stephen Colebourne + * @author Gareth Davis + * @author Ian Springer + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: IOUtils.java 481854 2006-12-03 18:30:07Z scolebourne $ + */ +public class IOUtils { + // NOTE: This class is focussed on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + /** + * The Unix directory separator character. + */ + public static final char DIR_SEPARATOR_UNIX = '/'; + /** + * The Windows directory separator character. + */ + public static final char DIR_SEPARATOR_WINDOWS = '\\'; + /** + * The system directory separator character. + */ + public static final char DIR_SEPARATOR = File.separatorChar; + /** + * The Unix line separator string. + */ + public static final String LINE_SEPARATOR_UNIX = "\n"; + /** + * The Windows line separator string. + */ + public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; + /** + * The system line separator string. + */ + public static final String LINE_SEPARATOR; + static { + // avoid security issues + StringWriter buf = new StringWriter(4); + PrintWriter out = new PrintWriter(buf); + out.println(); + LINE_SEPARATOR = buf.toString(); + } + + /** + * The default buffer size to use. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Instances should NOT be constructed in standard programming. + */ + public IOUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Unconditionally close an Reader. + *

+ * Equivalent to {@link Reader#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the Reader to close, may be null or already closed + */ + public static void closeQuietly(Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close a Writer. + *

+ * Equivalent to {@link Writer#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param output the Writer to close, may be null or already closed + */ + public static void closeQuietly(Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an InputStream. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an OutputStream. + *

+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(OutputStream output) { + try { + if (output != null) { + output.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + // read toByteArray + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get the contents of a Reader as a byte[] + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(Reader input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get the contents of a Reader as a byte[] + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @param encoding the encoding to use, null means platform default + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static byte[] toByteArray(Reader input, String encoding) + throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output, encoding); + return output.toByteArray(); + } + + /** + * Get the contents of a String as a byte[] + * using the default character encoding of the platform. + *

+ * This is the same as {@link String#getBytes()}. + * + * @param input the String to convert + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#getBytes()} + */ + public static byte[] toByteArray(String input) throws IOException { + return input.getBytes(); + } + + // read char[] + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a character array + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(InputStream is) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + copy(is, output); + return output.toCharArray(); + } + + /** + * Get the contents of an InputStream as a character array + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(InputStream is, String encoding) + throws IOException { + CharArrayWriter output = new CharArrayWriter(); + copy(is, output, encoding); + return output.toCharArray(); + } + + /** + * Get the contents of a Reader as a character array. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(Reader input) throws IOException { + CharArrayWriter sw = new CharArrayWriter(); + copy(input, sw); + return sw.toCharArray(); + } + + // read toString + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a String + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input) throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw); + return sw.toString(); + } + + /** + * Get the contents of an InputStream as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input, String encoding) + throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw, encoding); + return sw.toString(); + } + + /** + * Get the contents of a Reader as a String. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(Reader input) throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw); + return sw.toString(); + } + + /** + * Get the contents of a byte[] as a String + * using the default character encoding of the platform. + * + * @param input the byte array to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#String(byte[])} + */ + public static String toString(byte[] input) throws IOException { + return new String(input); + } + + /** + * Get the contents of a byte[] as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @param input the byte array to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#String(byte[],String)} + */ + public static String toString(byte[] input, String encoding) + throws IOException { + if (encoding == null) { + return new String(input); + } else { + return new String(input, encoding); + } + } + + // readLines + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a list of Strings, + * one entry per line, using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List readLines(InputStream input) throws IOException { + InputStreamReader reader = new InputStreamReader(input); + return readLines(reader); + } + + /** + * Get the contents of an InputStream as a list of Strings, + * one entry per line, using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List readLines(InputStream input, String encoding) throws IOException { + if (encoding == null) { + return readLines(input); + } else { + InputStreamReader reader = new InputStreamReader(input, encoding); + return readLines(reader); + } + } + + /** + * Get the contents of a Reader as a list of Strings, + * one entry per line. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List readLines(Reader input) throws IOException { + BufferedReader reader = new BufferedReader(input); + List list = new ArrayList(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + return list; + } + + // lineIterator + //----------------------------------------------------------------------- + /** + * Return an Iterator for the lines in a Reader. + *

+ * LineIterator holds a reference to the open + * Reader specified here. When you have finished with the + * iterator you should close the reader to free internal resources. + * This can be done by closing the reader directly, or by calling + * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}. + *

+ * The recommended usage pattern is: + *

+     * try {
+     *   LineIterator it = IOUtils.lineIterator(reader);
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   IOUtils.closeQuietly(reader);
+     * }
+     * 
+ * + * @param reader the Reader to read from, not null + * @return an Iterator of the lines in the reader, never null + * @throws IllegalArgumentException if the reader is null + * @since Commons IO 1.2 + */ + public static LineIterator lineIterator(Reader reader) { + return new LineIterator(reader); + } + + /** + * Return an Iterator for the lines in an InputStream, using + * the character encoding specified (or default encoding if null). + *

+ * LineIterator holds a reference to the open + * InputStream specified here. When you have finished with + * the iterator you should close the stream to free internal resources. + * This can be done by closing the stream directly, or by calling + * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}. + *

+ * The recommended usage pattern is: + *

+     * try {
+     *   LineIterator it = IOUtils.lineIterator(stream, "UTF-8");
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   IOUtils.closeQuietly(stream);
+     * }
+     * 
+ * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return an Iterator of the lines in the reader, never null + * @throws IllegalArgumentException if the input is null + * @throws IOException if an I/O error occurs, such as if the encoding is invalid + * @since Commons IO 1.2 + */ + public static LineIterator lineIterator(InputStream input, String encoding) + throws IOException { + Reader reader = null; + if (encoding == null) { + reader = new InputStreamReader(input); + } else { + reader = new InputStreamReader(input, encoding); + } + return new LineIterator(reader); + } + + //----------------------------------------------------------------------- + /** + * Convert the specified string to an input stream, encoded as bytes + * using the default character encoding of the platform. + * + * @param input the string to convert + * @return an input stream + * @since Commons IO 1.1 + */ + public static InputStream toInputStream(String input) { + byte[] bytes = input.getBytes(); + return new ByteArrayInputStream(bytes); + } + + /** + * Convert the specified string to an input stream, encoded as bytes + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @param input the string to convert + * @param encoding the encoding to use, null means platform default + * @throws IOException if the encoding is invalid + * @return an input stream + * @since Commons IO 1.1 + */ + public static InputStream toInputStream(String input, String encoding) throws IOException { + byte[] bytes = encoding != null ? input.getBytes(encoding) : input.getBytes(); + return new ByteArrayInputStream(bytes); + } + + // write byte[] + //----------------------------------------------------------------------- + /** + * Writes bytes from a byte[] to an OutputStream. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes bytes from a byte[] to chars on a Writer + * using the default character encoding of the platform. + *

+ * This method uses {@link String#String(byte[])}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, Writer output) throws IOException { + if (data != null) { + output.write(new String(data)); + } + } + + /** + * Writes bytes from a byte[] to chars on a Writer + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#String(byte[], String)}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, Writer output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(new String(data, encoding)); + } + } + } + + // write char[] + //----------------------------------------------------------------------- + /** + * Writes chars from a char[] to a Writer + * using the default character encoding of the platform. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a char[] to bytes on an + * OutputStream. + *

+ * This method uses {@link String#String(char[])} and + * {@link String#getBytes()}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, OutputStream output) + throws IOException { + if (data != null) { + output.write(new String(data).getBytes()); + } + } + + /** + * Writes chars from a char[] to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#String(char[])} and + * {@link String#getBytes(String)}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, OutputStream output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(new String(data).getBytes(encoding)); + } + } + } + + // write String + //----------------------------------------------------------------------- + /** + * Writes chars from a String to a Writer. + * + * @param data the String to write, null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data.getBytes()); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, OutputStream output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(data.getBytes(encoding)); + } + } + } + + // write StringBuffer + //----------------------------------------------------------------------- + /** + * Writes chars from a StringBuffer to a Writer. + * + * @param data the StringBuffer to write, null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, Writer output) + throws IOException { + if (data != null) { + output.write(data.toString()); + } + } + + /** + * Writes chars from a StringBuffer to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data.toString().getBytes()); + } + } + + /** + * Writes chars from a StringBuffer to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, OutputStream output, + String encoding) throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(data.toString().getBytes(encoding)); + } + } + } + + // writeLines + //----------------------------------------------------------------------- + /** + * Writes the toString() value of each item in a collection to + * an OutputStream line by line, using the default character + * encoding of the platform and the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the OutputStream to write to, not null, not closed + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + OutputStream output) throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + output.write(line.toString().getBytes()); + } + output.write(lineEnding.getBytes()); + } + } + + /** + * Writes the toString() value of each item in a collection to + * an OutputStream line by line, using the specified character + * encoding and the specified line ending. + *

+ * Character encoding names can be found at + * IANA. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the OutputStream to write to, not null, not closed + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + OutputStream output, String encoding) throws IOException { + if (encoding == null) { + writeLines(lines, lineEnding, output); + } else { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + output.write(line.toString().getBytes(encoding)); + } + output.write(lineEnding.getBytes(encoding)); + } + } + } + + /** + * Writes the toString() value of each item in a collection to + * a Writer line by line, using the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param writer the Writer to write to, not null, not closed + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + Writer writer) throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + writer.write(line.toString()); + } + writer.write(lineEnding); + } + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copy bytes from an InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the byte count is too large + * @since Commons IO 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy bytes from an InputStream to chars on a + * Writer using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output) + throws IOException { + InputStreamReader in = new InputStreamReader(input); + copy(in, output); + } + + /** + * Copy bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output, String encoding) + throws IOException { + if (encoding == null) { + copy(input, output); + } else { + InputStreamReader in = new InputStreamReader(input, encoding); + copy(in, output); + } + } + + // copy from Reader + //----------------------------------------------------------------------- + /** + * Copy chars from a Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the character count is too large + * @since Commons IO 1.1 + */ + public static int copy(Reader input, Writer output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy chars from a large (over 2GB) Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(Reader input, Writer output) throws IOException { + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy chars from a Reader to bytes on an + * OutputStream using the default character encoding of the + * platform, and calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(Reader input, OutputStream output) + throws IOException { + OutputStreamWriter out = new OutputStreamWriter(output); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, we + // have to flush here. + out.flush(); + } + + /** + * Copy chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Character encoding names can be found at + * IANA. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(Reader input, OutputStream output, String encoding) + throws IOException { + if (encoding == null) { + copy(input, output); + } else { + OutputStreamWriter out = new OutputStreamWriter(output, encoding); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, + // we have to flush here. + out.flush(); + } + } + + // content equals + //----------------------------------------------------------------------- + /** + * Compare the contents of two Streams to determine if they are equal or + * not. + *

+ * This method buffers the input internally using + * BufferedInputStream if they are not already buffered. + * + * @param input1 the first stream + * @param input2 the second stream + * @return true if the content of the streams are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + */ + public static boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return (ch2 == -1); + } + + /** + * Compare the contents of two Readers to determine if they are equal or + * not. + *

+ * This method buffers the input internally using + * BufferedReader if they are not already buffered. + * + * @param input1 the first reader + * @param input2 the second reader + * @return true if the content of the readers are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static boolean contentEquals(Reader input1, Reader input2) + throws IOException { + if (!(input1 instanceof BufferedReader)) { + input1 = new BufferedReader(input1); + } + if (!(input2 instanceof BufferedReader)) { + input2 = new BufferedReader(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return (ch2 == -1); + } + +} diff --git a/src/org/apache/commons/io/LineIterator.java b/src/org/apache/commons/io/LineIterator.java new file mode 100644 index 000000000..eac47d23a --- /dev/null +++ b/src/org/apache/commons/io/LineIterator.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An Iterator over the lines in a Reader. + *

+ * LineIterator holds a reference to an open Reader. + * When you have finished with the iterator you should close the reader + * to free internal resources. This can be done by closing the reader directly, + * or by calling the {@link #close()} or {@link #closeQuietly(LineIterator)} + * method on the iterator. + *

+ * The recommended usage pattern is: + *

+ * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
+ * try {
+ *   while (it.hasNext()) {
+ *     String line = it.nextLine();
+ *     /// do something with line
+ *   }
+ * } finally {
+ *   LineIterator.closeQuietly(iterator);
+ * }
+ * 
+ * + * @author Niall Pemberton + * @author Stephen Colebourne + * @author Sandy McArthur + * @version $Id: LineIterator.java 437567 2006-08-28 06:39:07Z bayard $ + * @since Commons IO 1.2 + */ +public class LineIterator implements Iterator { + + /** The reader that is being read. */ + private final BufferedReader bufferedReader; + /** The current line. */ + private String cachedLine; + /** A flag indicating if the iterator has been fully read. */ + private boolean finished = false; + + /** + * Constructs an iterator of the lines for a Reader. + * + * @param reader the Reader to read from, not null + * @throws IllegalArgumentException if the reader is null + */ + public LineIterator(final Reader reader) throws IllegalArgumentException { + if (reader == null) { + throw new IllegalArgumentException("Reader must not be null"); + } + if (reader instanceof BufferedReader) { + bufferedReader = (BufferedReader) reader; + } else { + bufferedReader = new BufferedReader(reader); + } + } + + //----------------------------------------------------------------------- + /** + * Indicates whether the Reader has more lines. + * If there is an IOException then {@link #close()} will + * be called on this instance. + * + * @return true if the Reader has more lines + * @throws IllegalStateException if an IO exception occurs + */ + public boolean hasNext() { + if (cachedLine != null) { + return true; + } else if (finished) { + return false; + } else { + try { + while (true) { + String line = bufferedReader.readLine(); + if (line == null) { + finished = true; + return false; + } else if (isValidLine(line)) { + cachedLine = line; + return true; + } + } + } catch(IOException ioe) { + close(); + throw new IllegalStateException(ioe.toString()); + } + } + } + + /** + * Overridable method to validate each line that is returned. + * + * @param line the line that is to be validated + * @return true if valid, false to remove from the iterator + */ + protected boolean isValidLine(String line) { + return true; + } + + /** + * Returns the next line in the wrapped Reader. + * + * @return the next line from the input + * @throws NoSuchElementException if there is no line to return + */ + public Object next() { + return nextLine(); + } + + /** + * Returns the next line in the wrapped Reader. + * + * @return the next line from the input + * @throws NoSuchElementException if there is no line to return + */ + public String nextLine() { + if (!hasNext()) { + throw new NoSuchElementException("No more lines"); + } + String currentLine = cachedLine; + cachedLine = null; + return currentLine; + } + + /** + * Closes the underlying Reader quietly. + * This method is useful if you only want to process the first few + * lines of a larger file. If you do not close the iterator + * then the Reader remains open. + * This method can safely be called multiple times. + */ + public void close() { + finished = true; + IOUtils.closeQuietly(bufferedReader); + cachedLine = null; + } + + /** + * Unsupported. + * + * @throws UnsupportedOperationException always + */ + public void remove() { + throw new UnsupportedOperationException("Remove unsupported on LineIterator"); + } + + //----------------------------------------------------------------------- + /** + * Closes the iterator, handling null and ignoring exceptions. + * + * @param iterator the iterator to close + */ + public static void closeQuietly(LineIterator iterator) { + if (iterator != null) { + iterator.close(); + } + } + +} diff --git a/src/org/apache/commons/io/comparator/DefaultFileComparator.java b/src/org/apache/commons/io/comparator/DefaultFileComparator.java new file mode 100644 index 000000000..d36076288 --- /dev/null +++ b/src/org/apache/commons/io/comparator/DefaultFileComparator.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +/** + * Compare two files using the default {@link File#compareTo(File)} method. + *

+ * This comparator can be used to sort lists or arrays of files + * by using the default file comparison. + *

+ * Example of sorting a list of files using the + * {@link #DEFAULT_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, DefaultFileComparator.DEFAULT_COMPARATOR);
+ * 
+ *

+ * Example of doing a reverse sort of an array of files using the + * {@link #DEFAULT_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, DefaultFileComparator.DEFAULT_REVERSE);
+ * 
+ *

+ * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class DefaultFileComparator implements Comparator, Serializable { + + /** Singleton default comparator instance */ + public static final Comparator DEFAULT_COMPARATOR = new DefaultFileComparator(); + + /** Singleton reverse default comparator instance */ + public static final Comparator DEFAULT_REVERSE = new ReverseComparator(DEFAULT_COMPARATOR); + + /** + * Compare the two files using the {@link File#compareTo(File)} method. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return the result of calling file1's + * {@link File#compareTo(File)} with file2 as the parameter. + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + return file1.compareTo(file2); + } +} diff --git a/src/org/apache/commons/io/comparator/ExtensionFileComparator.java b/src/org/apache/commons/io/comparator/ExtensionFileComparator.java new file mode 100644 index 000000000..158480fb3 --- /dev/null +++ b/src/org/apache/commons/io/comparator/ExtensionFileComparator.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOCase; + +/** + * Compare the file name extensions for order + * (see {@link FilenameUtils#getExtension(String)}). + *

+ * This comparator can be used to sort lists or arrays of files + * by their file extension either in a case-sensitive, case-insensitive or + * system dependant case sensitive way. A number of singleton instances + * are provided for the various case sensitivity options (using {@link IOCase}) + * and the reverse of those options. + *

+ * Example of a case-sensitive file extension sort using the + * {@link #EXTENSION_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, ExtensionFileComparator.EXTENSION_COMPARATOR);
+ * 
+ *

+ * Example of a reverse case-insensitive file extension sort using the + * {@link #EXTENSION_INSENSITIVE_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, ExtensionFileComparator.EXTENSION_INSENSITIVE_REVERSE);
+ * 
+ *

+ * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class ExtensionFileComparator implements Comparator, Serializable { + + /** Case-sensitive extension comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator EXTENSION_COMPARATOR = new ExtensionFileComparator(); + + /** Reverse case-sensitive extension comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator EXTENSION_REVERSE = new ReverseComparator(EXTENSION_COMPARATOR); + + /** Case-insensitive extension comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator EXTENSION_INSENSITIVE_COMPARATOR = new ExtensionFileComparator(IOCase.INSENSITIVE); + + /** Reverse case-insensitive extension comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator EXTENSION_INSENSITIVE_REVERSE + = new ReverseComparator(EXTENSION_INSENSITIVE_COMPARATOR); + + /** System sensitive extension comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator EXTENSION_SYSTEM_COMPARATOR = new ExtensionFileComparator(IOCase.SYSTEM); + + /** Reverse system sensitive path comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator EXTENSION_SYSTEM_REVERSE = new ReverseComparator(EXTENSION_SYSTEM_COMPARATOR); + + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Construct a case sensitive file extension comparator instance. + */ + public ExtensionFileComparator() { + this.caseSensitivity = IOCase.SENSITIVE; + } + + /** + * Construct a file extension comparator instance with the specified case-sensitivity. + * + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + */ + public ExtensionFileComparator(IOCase caseSensitivity) { + this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity; + } + + /** + * Compare the extensions of two files the specified case sensitivity. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return a negative value if the first file's extension + * is less than the second, zero if the extensions are the + * same and a positive value if the first files extension + * is greater than the second file. + * + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + String suffix1 = FilenameUtils.getExtension(file1.getName()); + String suffix2 = FilenameUtils.getExtension(file2.getName()); + return caseSensitivity.checkCompareTo(suffix1, suffix2); + } +} diff --git a/src/org/apache/commons/io/comparator/LastModifiedFileComparator.java b/src/org/apache/commons/io/comparator/LastModifiedFileComparator.java new file mode 100644 index 000000000..8265023d0 --- /dev/null +++ b/src/org/apache/commons/io/comparator/LastModifiedFileComparator.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +/** + * Compare the last modified date/time of two files for order + * (see {@link File#lastModified()}). + *

+ * This comparator can be used to sort lists or arrays of files + * by their last modified date/time. + *

+ * Example of sorting a list of files using the + * {@link #LASTMODIFIED_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, LastModifiedFileComparator.LASTMODIFIED_COMPARATOR);
+ * 
+ *

+ * Example of doing a reverse sort of an array of files using the + * {@link #LASTMODIFIED_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, LastModifiedFileComparator.LASTMODIFIED_REVERSE);
+ * 
+ *

+ * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class LastModifiedFileComparator implements Comparator, Serializable { + + /** Last modified comparator instance */ + public static final Comparator LASTMODIFIED_COMPARATOR = new LastModifiedFileComparator(); + + /** Reverse last modified comparator instance */ + public static final Comparator LASTMODIFIED_REVERSE = new ReverseComparator(LASTMODIFIED_COMPARATOR); + + /** + * Compare the last the last modified date/time of two files. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return a negative value if the first file's lastmodified date/time + * is less than the second, zero if the lastmodified date/time are the + * same and a positive value if the first files lastmodified date/time + * is greater than the second file. + * + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + long result = file1.lastModified() - file2.lastModified(); + if (result < 0) { + return -1; + } else if (result > 0) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/org/apache/commons/io/comparator/NameFileComparator.java b/src/org/apache/commons/io/comparator/NameFileComparator.java new file mode 100644 index 000000000..76af21eeb --- /dev/null +++ b/src/org/apache/commons/io/comparator/NameFileComparator.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +import org.apache.commons.io.IOCase; + +/** + * Compare the names of two files for order (see {@link File#getName()}). + *

+ * This comparator can be used to sort lists or arrays of files + * by their name either in a case-sensitive, case-insensitive or + * system dependant case sensitive way. A number of singleton instances + * are provided for the various case sensitivity options (using {@link IOCase}) + * and the reverse of those options. + *

+ * Example of a case-sensitive file name sort using the + * {@link #NAME_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, NameFileComparator.NAME_COMPARATOR);
+ * 
+ *

+ * Example of a reverse case-insensitive file name sort using the + * {@link #NAME_INSENSITIVE_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, NameFileComparator.NAME_INSENSITIVE_REVERSE);
+ * 
+ *

+ * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class NameFileComparator implements Comparator, Serializable { + + /** Case-sensitive name comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator NAME_COMPARATOR = new NameFileComparator(); + + /** Reverse case-sensitive name comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator NAME_REVERSE = new ReverseComparator(NAME_COMPARATOR); + + /** Case-insensitive name comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator NAME_INSENSITIVE_COMPARATOR = new NameFileComparator(IOCase.INSENSITIVE); + + /** Reverse case-insensitive name comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator NAME_INSENSITIVE_REVERSE = new ReverseComparator(NAME_INSENSITIVE_COMPARATOR); + + /** System sensitive name comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator NAME_SYSTEM_COMPARATOR = new NameFileComparator(IOCase.SYSTEM); + + /** Reverse system sensitive name comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator NAME_SYSTEM_REVERSE = new ReverseComparator(NAME_SYSTEM_COMPARATOR); + + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Construct a case sensitive file name comparator instance. + */ + public NameFileComparator() { + this.caseSensitivity = IOCase.SENSITIVE; + } + + /** + * Construct a file name comparator instance with the specified case-sensitivity. + * + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + */ + public NameFileComparator(IOCase caseSensitivity) { + this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity; + } + + /** + * Compare the names of two files with the specified case sensitivity. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return a negative value if the first file's name + * is less than the second, zero if the names are the + * same and a positive value if the first files name + * is greater than the second file. + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + return caseSensitivity.checkCompareTo(file1.getName(), file2.getName()); + } +} diff --git a/src/org/apache/commons/io/comparator/PathFileComparator.java b/src/org/apache/commons/io/comparator/PathFileComparator.java new file mode 100644 index 000000000..0b28b6960 --- /dev/null +++ b/src/org/apache/commons/io/comparator/PathFileComparator.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +import org.apache.commons.io.IOCase; + +/** + * Compare the path of two files for order (see {@link File#getPath()}). + *

+ * This comparator can be used to sort lists or arrays of files + * by their path either in a case-sensitive, case-insensitive or + * system dependant case sensitive way. A number of singleton instances + * are provided for the various case sensitivity options (using {@link IOCase}) + * and the reverse of those options. + *

+ * Example of a case-sensitive file path sort using the + * {@link #PATH_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, PathFileComparator.PATH_COMPARATOR);
+ * 
+ *

+ * Example of a reverse case-insensitive file path sort using the + * {@link #PATH_INSENSITIVE_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, PathFileComparator.PATH_INSENSITIVE_REVERSE);
+ * 
+ *

+ * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class PathFileComparator implements Comparator, Serializable { + + /** Case-sensitive path comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator PATH_COMPARATOR = new PathFileComparator(); + + /** Reverse case-sensitive path comparator instance (see {@link IOCase#SENSITIVE}) */ + public static final Comparator PATH_REVERSE = new ReverseComparator(PATH_COMPARATOR); + + /** Case-insensitive path comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator PATH_INSENSITIVE_COMPARATOR = new PathFileComparator(IOCase.INSENSITIVE); + + /** Reverse case-insensitive path comparator instance (see {@link IOCase#INSENSITIVE}) */ + public static final Comparator PATH_INSENSITIVE_REVERSE = new ReverseComparator(PATH_INSENSITIVE_COMPARATOR); + + /** System sensitive path comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator PATH_SYSTEM_COMPARATOR = new PathFileComparator(IOCase.SYSTEM); + + /** Reverse system sensitive path comparator instance (see {@link IOCase#SYSTEM}) */ + public static final Comparator PATH_SYSTEM_REVERSE = new ReverseComparator(PATH_SYSTEM_COMPARATOR); + + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Construct a case sensitive file path comparator instance. + */ + public PathFileComparator() { + this.caseSensitivity = IOCase.SENSITIVE; + } + + /** + * Construct a file path comparator instance with the specified case-sensitivity. + * + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + */ + public PathFileComparator(IOCase caseSensitivity) { + this.caseSensitivity = caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity; + } + + /** + * Compare the paths of two files the specified case sensitivity. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return a negative value if the first file's path + * is less than the second, zero if the paths are the + * same and a positive value if the first files path + * is greater than the second file. + * + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + return caseSensitivity.checkCompareTo(file1.getPath(), file2.getPath()); + } +} diff --git a/src/org/apache/commons/io/comparator/ReverseComparator.java b/src/org/apache/commons/io/comparator/ReverseComparator.java new file mode 100644 index 000000000..af9749ee3 --- /dev/null +++ b/src/org/apache/commons/io/comparator/ReverseComparator.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * Reverses the result of comparing two objects using + * the delegate {@link Comparator}. + * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +class ReverseComparator implements Comparator, Serializable { + + private final Comparator delegate; + + /** + * Construct an instance with the sepecified delegate {@link Comparator}. + * + * @param delegate The comparator to delegate to + */ + public ReverseComparator(Comparator delegate) { + if (delegate == null) { + throw new IllegalArgumentException("Delegate comparator is missing"); + } + this.delegate = delegate; + } + + /** + * Compare using the delegate Comparator, but reversing the result. + * + * @param obj1 The first object to compare + * @param obj2 The second object to compare + * @return the result from the delegate {@link Comparator#compare(Object, Object)} + * reversing the value (i.e. positive becomes negative and vice versa) + */ + public int compare(Object obj1, Object obj2) { + return delegate.compare(obj2, obj1); // parameters switched round + } + +} diff --git a/src/org/apache/commons/io/comparator/SizeFileComparator.java b/src/org/apache/commons/io/comparator/SizeFileComparator.java new file mode 100644 index 000000000..a1621671c --- /dev/null +++ b/src/org/apache/commons/io/comparator/SizeFileComparator.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.comparator; + +import java.io.File; +import java.io.Serializable; +import java.util.Comparator; + +import org.apache.commons.io.FileUtils; + +/** + * Compare the length/size of two files for order (see + * {@link File#length()} and {@link FileUtils#sizeOfDirectory(File)}). + *

+ * This comparator can be used to sort lists or arrays of files + * by their length/size. + *

+ * Example of sorting a list of files using the + * {@link #SIZE_COMPARATOR} singleton instance: + *

+ *       List<File> list = ...
+ *       Collections.sort(list, LengthFileComparator.LENGTH_COMPARATOR);
+ * 
+ *

+ * Example of doing a reverse sort of an array of files using the + * {@link #SIZE_REVERSE} singleton instance: + *

+ *       File[] array = ...
+ *       Arrays.sort(array, LengthFileComparator.LENGTH_REVERSE);
+ * 
+ *

+ * N.B. Directories are treated as zero size unless + * sumDirectoryContents is true. + * + * @version $Revision: 609243 $ $Date: 2008-01-06 00:30:42 +0000 (Sun, 06 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class SizeFileComparator implements Comparator, Serializable { + + /** Size comparator instance - directories are treated as zero size */ + public static final Comparator SIZE_COMPARATOR = new SizeFileComparator(); + + /** Reverse size comparator instance - directories are treated as zero size */ + public static final Comparator SIZE_REVERSE = new ReverseComparator(SIZE_COMPARATOR); + + /** + * Size comparator instance which sums the size of a directory's contents + * using {@link FileUtils#sizeOfDirectory(File)} + */ + public static final Comparator SIZE_SUMDIR_COMPARATOR = new SizeFileComparator(true); + + /** + * Reverse size comparator instance which sums the size of a directory's contents + * using {@link FileUtils#sizeOfDirectory(File)} + */ + public static final Comparator SIZE_SUMDIR_REVERSE = new ReverseComparator(SIZE_SUMDIR_COMPARATOR); + + /** Whether the sum of the directory's contents should be calculated. */ + private final boolean sumDirectoryContents; + + /** + * Construct a file size comparator instance (directories treated as zero size). + */ + public SizeFileComparator() { + this.sumDirectoryContents = false; + } + + /** + * Construct a file size comparator instance specifying whether the size of + * the directory contents should be aggregated. + *

+ * If the sumDirectoryContents is true The size of + * directories is calculated using {@link FileUtils#sizeOfDirectory(File)}. + * + * @param sumDirectoryContents true if the sum of the directoryies contents + * should be calculated, otherwise false if directories should be treated + * as size zero (see {@link FileUtils#sizeOfDirectory(File)}). + */ + public SizeFileComparator(boolean sumDirectoryContents) { + this.sumDirectoryContents = sumDirectoryContents; + } + + /** + * Compare the length of two files. + * + * @param obj1 The first file to compare + * @param obj2 The second file to compare + * @return a negative value if the first file's length + * is less than the second, zero if the lengths are the + * same and a positive value if the first files length + * is greater than the second file. + * + */ + public int compare(Object obj1, Object obj2) { + File file1 = (File)obj1; + File file2 = (File)obj2; + long size1 = 0; + if (file1.isDirectory()) { + size1 = sumDirectoryContents && file1.exists() ? FileUtils.sizeOfDirectory(file1) : 0; + } else { + size1 = file1.length(); + } + long size2 = 0; + if (file2.isDirectory()) { + size2 = sumDirectoryContents && file2.exists() ? FileUtils.sizeOfDirectory(file2) : 0; + } else { + size2 = file2.length(); + } + long result = size1 - size2; + if (result < 0) { + return -1; + } else if (result > 0) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/org/apache/commons/io/comparator/package.html b/src/org/apache/commons/io/comparator/package.html new file mode 100644 index 000000000..a2f756f18 --- /dev/null +++ b/src/org/apache/commons/io/comparator/package.html @@ -0,0 +1,25 @@ + + + + +

This package provides various {@link java.util.Comparator} implementations +for {@link java.io.File}s. +

+ + + diff --git a/src/org/apache/commons/io/filefilter/AbstractFileFilter.java b/src/org/apache/commons/io/filefilter/AbstractFileFilter.java new file mode 100644 index 000000000..9e188f82e --- /dev/null +++ b/src/org/apache/commons/io/filefilter/AbstractFileFilter.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; + +/** + * An abstract class which implements the Java FileFilter and FilenameFilter + * interfaces via the IOFileFilter interface. + *

+ * Note that a subclass must override one of the accept methods, + * otherwise your class will infinitely loop. + * + * @since Commons IO 1.0 + * @version $Revision: 539231 $ $Date: 2007-05-18 04:10:33 +0100 (Fri, 18 May 2007) $ + * + * @author Stephen Colebourne + */ +public abstract class AbstractFileFilter implements IOFileFilter { + + /** + * Checks to see if the File should be accepted by this filter. + * + * @param file the File to check + * @return true if this file matches the test + */ + public boolean accept(File file) { + return accept(file.getParentFile(), file.getName()); + } + + /** + * Checks to see if the File should be accepted by this filter. + * + * @param dir the directory File to check + * @param name the filename within the directory to check + * @return true if this file matches the test + */ + public boolean accept(File dir, String name) { + return accept(new File(dir, name)); + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + String name = getClass().getName(); + int period = name.lastIndexOf('.'); + return (period > 0 ? name.substring(period + 1) : name); + } + +} diff --git a/src/org/apache/commons/io/filefilter/AgeFileFilter.java b/src/org/apache/commons/io/filefilter/AgeFileFilter.java new file mode 100644 index 000000000..ab73cb840 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/AgeFileFilter.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.Date; + +import org.apache.commons.io.FileUtils; + +/** + * Filters files based on a cutoff time, can filter either newer + * files or files equal to or older. + *

+ * For example, to print all files and directories in the + * current directory older than one day: + * + *

+ * File dir = new File(".");
+ * // We are interested in files older than one day
+ * long cutoff = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
+ * String[] files = dir.list( new AgeFileFilter(cutoff) );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @author Rahul Akolkar + * @version $Id: AgeFileFilter.java 606381 2007-12-22 02:03:16Z ggregory $ + * @since Commons IO 1.2 + */ +public class AgeFileFilter extends AbstractFileFilter implements Serializable { + + /** The cutoff time threshold. */ + private final long cutoff; + /** Whether the files accepted will be older or newer. */ + private final boolean acceptOlder; + + /** + * Constructs a new age file filter for files equal to or older than + * a certain cutoff + * + * @param cutoff the threshold age of the files + */ + public AgeFileFilter(long cutoff) { + this(cutoff, true); + } + + /** + * Constructs a new age file filter for files on any one side + * of a certain cutoff. + * + * @param cutoff the threshold age of the files + * @param acceptOlder if true, older files (at or before the cutoff) + * are accepted, else newer ones (after the cutoff). + */ + public AgeFileFilter(long cutoff, boolean acceptOlder) { + this.acceptOlder = acceptOlder; + this.cutoff = cutoff; + } + + /** + * Constructs a new age file filter for files older than (at or before) + * a certain cutoff date. + * + * @param cutoffDate the threshold age of the files + */ + public AgeFileFilter(Date cutoffDate) { + this(cutoffDate, true); + } + + /** + * Constructs a new age file filter for files on any one side + * of a certain cutoff date. + * + * @param cutoffDate the threshold age of the files + * @param acceptOlder if true, older files (at or before the cutoff) + * are accepted, else newer ones (after the cutoff). + */ + public AgeFileFilter(Date cutoffDate, boolean acceptOlder) { + this(cutoffDate.getTime(), acceptOlder); + } + + /** + * Constructs a new age file filter for files older than (at or before) + * a certain File (whose last modification time will be used as reference). + * + * @param cutoffReference the file whose last modification + * time is usesd as the threshold age of the files + */ + public AgeFileFilter(File cutoffReference) { + this(cutoffReference, true); + } + + /** + * Constructs a new age file filter for files on any one side + * of a certain File (whose last modification time will be used as + * reference). + * + * @param cutoffReference the file whose last modification + * time is usesd as the threshold age of the files + * @param acceptOlder if true, older files (at or before the cutoff) + * are accepted, else newer ones (after the cutoff). + */ + public AgeFileFilter(File cutoffReference, boolean acceptOlder) { + this(cutoffReference.lastModified(), acceptOlder); + } + + //----------------------------------------------------------------------- + /** + * Checks to see if the last modification of the file matches cutoff + * favorably. + *

+ * If last modification time equals cutoff and newer files are required, + * file IS NOT selected. + * If last modification time equals cutoff and older files are required, + * file IS selected. + * + * @param file the File to check + * @return true if the filename matches + */ + public boolean accept(File file) { + boolean newer = FileUtils.isFileNewer(file, cutoff); + return acceptOlder ? !newer : newer; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + String condition = acceptOlder ? "<=" : ">"; + return super.toString() + "(" + condition + cutoff + ")"; + } +} diff --git a/src/org/apache/commons/io/filefilter/AndFileFilter.java b/src/org/apache/commons/io/filefilter/AndFileFilter.java new file mode 100644 index 000000000..deda11f93 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/AndFileFilter.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A {@link java.io.FileFilter} providing conditional AND logic across a list of + * file filters. This filter returns true if all filters in the + * list return true. Otherwise, it returns false. + * Checking of the file filter list stops when the first filter returns + * false. + * + * @since Commons IO 1.0 + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * + * @author Steven Caswell + */ +public class AndFileFilter + extends AbstractFileFilter + implements ConditionalFileFilter, Serializable { + + /** The list of file filters. */ + private List fileFilters; + + /** + * Constructs a new instance of AndFileFilter. + * + * @since Commons IO 1.1 + */ + public AndFileFilter() { + this.fileFilters = new ArrayList(); + } + + /** + * Constructs a new instance of AndFileFilter + * with the specified list of filters. + * + * @param fileFilters a List of IOFileFilter instances, copied, null ignored + * @since Commons IO 1.1 + */ + public AndFileFilter(final List fileFilters) { + if (fileFilters == null) { + this.fileFilters = new ArrayList(); + } else { + this.fileFilters = new ArrayList(fileFilters); + } + } + + /** + * Constructs a new file filter that ANDs the result of two other filters. + * + * @param filter1 the first filter, must not be null + * @param filter2 the second filter, must not be null + * @throws IllegalArgumentException if either filter is null + */ + public AndFileFilter(IOFileFilter filter1, IOFileFilter filter2) { + if (filter1 == null || filter2 == null) { + throw new IllegalArgumentException("The filters must not be null"); + } + this.fileFilters = new ArrayList(); + addFileFilter(filter1); + addFileFilter(filter2); + } + + /** + * {@inheritDoc} + */ + public void addFileFilter(final IOFileFilter ioFileFilter) { + this.fileFilters.add(ioFileFilter); + } + + /** + * {@inheritDoc} + */ + public List getFileFilters() { + return Collections.unmodifiableList(this.fileFilters); + } + + /** + * {@inheritDoc} + */ + public boolean removeFileFilter(final IOFileFilter ioFileFilter) { + return this.fileFilters.remove(ioFileFilter); + } + + /** + * {@inheritDoc} + */ + public void setFileFilters(final List fileFilters) { + this.fileFilters = new ArrayList(fileFilters); + } + + /** + * {@inheritDoc} + */ + public boolean accept(final File file) { + if (this.fileFilters.size() == 0) { + return false; + } + for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) { + IOFileFilter fileFilter = (IOFileFilter) iter.next(); + if (!fileFilter.accept(file)) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + public boolean accept(final File file, final String name) { + if (this.fileFilters.size() == 0) { + return false; + } + for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) { + IOFileFilter fileFilter = (IOFileFilter) iter.next(); + if (!fileFilter.accept(file, name)) { + return false; + } + } + return true; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (fileFilters != null) { + for (int i = 0; i < fileFilters.size(); i++) { + if (i > 0) { + buffer.append(","); + } + Object filter = fileFilters.get(i); + buffer.append(filter == null ? "null" : filter.toString()); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/CanReadFileFilter.java b/src/org/apache/commons/io/filefilter/CanReadFileFilter.java new file mode 100644 index 000000000..a9c132570 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/CanReadFileFilter.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts Files that can be read. + *

+ * Example, showing how to print out a list of the + * current directory's readable files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( CanReadFileFilter.CAN_READ );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * Example, showing how to print out a list of the + * current directory's un-readable files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( CanReadFileFilter.CANNOT_READ );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * Example, showing how to print out a list of the + * current directory's read-only files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( CanReadFileFilter.READ_ONLY );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 587916 $ + */ +public class CanReadFileFilter extends AbstractFileFilter implements Serializable { + + /** Singleton instance of readable filter */ + public static final IOFileFilter CAN_READ = new CanReadFileFilter(); + + /** Singleton instance of not readable filter */ + public static final IOFileFilter CANNOT_READ = new NotFileFilter(CAN_READ); + + /** Singleton instance of read-only filter */ + public static final IOFileFilter READ_ONLY = new AndFileFilter(CAN_READ, + CanWriteFileFilter.CANNOT_WRITE); + + /** + * Restrictive consructor. + */ + protected CanReadFileFilter() { + } + + /** + * Checks to see if the file can be read. + * + * @param file the File to check. + * @return true if the file can be + * read, otherwise false. + */ + public boolean accept(File file) { + return file.canRead(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/CanWriteFileFilter.java b/src/org/apache/commons/io/filefilter/CanWriteFileFilter.java new file mode 100644 index 000000000..da664f25c --- /dev/null +++ b/src/org/apache/commons/io/filefilter/CanWriteFileFilter.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts Files that can be written to. + *

+ * Example, showing how to print out a list of the + * current directory's writable files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( CanWriteFileFilter.CAN_WRITE );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * Example, showing how to print out a list of the + * current directory's un-writable files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( CanWriteFileFilter.CANNOT_WRITE );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * N.B. For read-only files, use + * CanReadFileFilter.READ_ONLY. + * + * @since Commons IO 1.3 + * @version $Revision: 587916 $ + */ +public class CanWriteFileFilter extends AbstractFileFilter implements Serializable { + + /** Singleton instance of writable filter */ + public static final IOFileFilter CAN_WRITE = new CanWriteFileFilter(); + + /** Singleton instance of not writable filter */ + public static final IOFileFilter CANNOT_WRITE = new NotFileFilter(CAN_WRITE); + + /** + * Restrictive consructor. + */ + protected CanWriteFileFilter() { + } + + /** + * Checks to see if the file can be written to. + * + * @param file the File to check + * @return true if the file can be + * written to, otherwise false. + */ + public boolean accept(File file) { + return file.canWrite(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/ConditionalFileFilter.java b/src/org/apache/commons/io/filefilter/ConditionalFileFilter.java new file mode 100644 index 000000000..ce1419ee8 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/ConditionalFileFilter.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.util.List; + +/** + * Defines operations for conditional file filters. + * + * @since Commons IO 1.1 + * @version $Revision: 437567 $ $Date: 2006-08-28 07:39:07 +0100 (Mon, 28 Aug 2006) $ + * + * @author Steven Caswell + */ +public interface ConditionalFileFilter { + + /** + * Adds the specified file filter to the list of file filters at the end of + * the list. + * + * @param ioFileFilter the filter to be added + * @since Commons IO 1.1 + */ + public void addFileFilter(IOFileFilter ioFileFilter); + + /** + * Returns this conditional file filter's list of file filters. + * + * @return the file filter list + * @since Commons IO 1.1 + */ + public List getFileFilters(); + + /** + * Removes the specified file filter. + * + * @param ioFileFilter filter to be removed + * @return true if the filter was found in the list, + * false otherwise + * @since Commons IO 1.1 + */ + public boolean removeFileFilter(IOFileFilter ioFileFilter); + + /** + * Sets the list of file filters, replacing any previously configured + * file filters on this filter. + * + * @param fileFilters the list of filters + * @since Commons IO 1.1 + */ + public void setFileFilters(List fileFilters); + +} diff --git a/src/org/apache/commons/io/filefilter/DelegateFileFilter.java b/src/org/apache/commons/io/filefilter/DelegateFileFilter.java new file mode 100644 index 000000000..c2d67c469 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/DelegateFileFilter.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.io.Serializable; + +/** + * This class turns a Java FileFilter or FilenameFilter into an IO FileFilter. + * + * @since Commons IO 1.0 + * @version $Revision: 591058 $ $Date: 2007-11-01 15:47:05 +0000 (Thu, 01 Nov 2007) $ + * + * @author Stephen Colebourne + */ +public class DelegateFileFilter extends AbstractFileFilter implements Serializable { + + /** The Filename filter */ + private final FilenameFilter filenameFilter; + /** The File filter */ + private final FileFilter fileFilter; + + /** + * Constructs a delegate file filter around an existing FilenameFilter. + * + * @param filter the filter to decorate + */ + public DelegateFileFilter(FilenameFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("The FilenameFilter must not be null"); + } + this.filenameFilter = filter; + this.fileFilter = null; + } + + /** + * Constructs a delegate file filter around an existing FileFilter. + * + * @param filter the filter to decorate + */ + public DelegateFileFilter(FileFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("The FileFilter must not be null"); + } + this.fileFilter = filter; + this.filenameFilter = null; + } + + /** + * Checks the filter. + * + * @param file the file to check + * @return true if the filter matches + */ + public boolean accept(File file) { + if (fileFilter != null) { + return fileFilter.accept(file); + } else { + return super.accept(file); + } + } + + /** + * Checks the filter. + * + * @param dir the directory + * @param name the filename in the directory + * @return true if the filter matches + */ + public boolean accept(File dir, String name) { + if (filenameFilter != null) { + return filenameFilter.accept(dir, name); + } else { + return super.accept(dir, name); + } + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + String delegate = (fileFilter != null ? fileFilter.toString() : filenameFilter.toString()); + return super.toString() + "(" + delegate + ")"; + } + +} diff --git a/src/org/apache/commons/io/filefilter/DirectoryFileFilter.java b/src/org/apache/commons/io/filefilter/DirectoryFileFilter.java new file mode 100644 index 000000000..3412e7bef --- /dev/null +++ b/src/org/apache/commons/io/filefilter/DirectoryFileFilter.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts Files that are directories. + *

+ * For example, here is how to print out a list of the + * current directory's subdirectories: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( DirectoryFileFilter.INSTANCE );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.0 + * @version $Revision: 587916 $ $Date: 2007-10-24 16:53:07 +0100 (Wed, 24 Oct 2007) $ + * + * @author Stephen Colebourne + * @author Peter Donald + */ +public class DirectoryFileFilter extends AbstractFileFilter implements Serializable { + + /** + * Singleton instance of directory filter. + * @since Commons IO 1.3 + */ + public static final IOFileFilter DIRECTORY = new DirectoryFileFilter(); + /** + * Singleton instance of directory filter. + * Please use the identical DirectoryFileFilter.DIRECTORY constant. + * The new name is more JDK 1.5 friendly as it doesn't clash with other + * values when using static imports. + */ + public static final IOFileFilter INSTANCE = DIRECTORY; + + /** + * Restrictive consructor. + */ + protected DirectoryFileFilter() { + } + + /** + * Checks to see if the file is a directory. + * + * @param file the File to check + * @return true if the file is a directory + */ + public boolean accept(File file) { + return file.isDirectory(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/EmptyFileFilter.java b/src/org/apache/commons/io/filefilter/EmptyFileFilter.java new file mode 100644 index 000000000..e88a862d4 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/EmptyFileFilter.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts files or directories that are empty. + *

+ * If the File is a directory it checks that + * it contains no files. + *

+ * Example, showing how to print out a list of the + * current directory's empty files/directories: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( EmptyFileFilter.EMPTY );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * Example, showing how to print out a list of the + * current directory's non-empty files/directories: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( EmptyFileFilter.NOT_EMPTY );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 587916 $ + */ +public class EmptyFileFilter extends AbstractFileFilter implements Serializable { + + /** Singleton instance of empty filter */ + public static final IOFileFilter EMPTY = new EmptyFileFilter(); + + /** Singleton instance of not-empty filter */ + public static final IOFileFilter NOT_EMPTY = new NotFileFilter(EMPTY); + + /** + * Restrictive consructor. + */ + protected EmptyFileFilter() { + } + + /** + * Checks to see if the file is empty. + * + * @param file the file or directory to check + * @return true if the file or directory + * is empty, otherwise false. + */ + public boolean accept(File file) { + if (file.isDirectory()) { + File[] files = file.listFiles(); + return (files == null || files.length == 0); + } else { + return (file.length() == 0); + } + } + +} diff --git a/src/org/apache/commons/io/filefilter/FalseFileFilter.java b/src/org/apache/commons/io/filefilter/FalseFileFilter.java new file mode 100644 index 000000000..8a87d4092 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/FalseFileFilter.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * A file filter that always returns false. + * + * @since Commons IO 1.0 + * @version $Revision: 587978 $ $Date: 2007-10-24 20:36:51 +0100 (Wed, 24 Oct 2007) $ + * + * @author Stephen Colebourne + */ +public class FalseFileFilter implements IOFileFilter, Serializable { + + /** + * Singleton instance of false filter. + * @since Commons IO 1.3 + */ + public static final IOFileFilter FALSE = new FalseFileFilter(); + /** + * Singleton instance of false filter. + * Please use the identical FalseFileFilter.FALSE constant. + * The new name is more JDK 1.5 friendly as it doesn't clash with other + * values when using static imports. + */ + public static final IOFileFilter INSTANCE = FALSE; + + /** + * Restrictive consructor. + */ + protected FalseFileFilter() { + } + + /** + * Returns false. + * + * @param file the file to check + * @return false + */ + public boolean accept(File file) { + return false; + } + + /** + * Returns false. + * + * @param dir the directory to check + * @param name the filename + * @return false + */ + public boolean accept(File dir, String name) { + return false; + } + +} diff --git a/src/org/apache/commons/io/filefilter/FileFileFilter.java b/src/org/apache/commons/io/filefilter/FileFileFilter.java new file mode 100644 index 000000000..0d49eddd4 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/FileFileFilter.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts Files that are files (not directories). + *

+ * For example, here is how to print out a list of the real files + * within the current directory: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( FileFileFilter.FILE );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 155419 $ $Date: 2007-10-24 16:53:07 +0100 (Wed, 24 Oct 2007) $ + */ +public class FileFileFilter extends AbstractFileFilter implements Serializable { + + /** Singleton instance of file filter */ + public static final IOFileFilter FILE = new FileFileFilter(); + + /** + * Restrictive consructor. + */ + protected FileFileFilter() { + } + + /** + * Checks to see if the file is a file. + * + * @param file the File to check + * @return true if the file is a file + */ + public boolean accept(File file) { + return file.isFile(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/FileFilterUtils.java b/src/org/apache/commons/io/filefilter/FileFilterUtils.java new file mode 100644 index 000000000..71c37b1d2 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/FileFilterUtils.java @@ -0,0 +1,361 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.util.Date; + +/** + * Useful utilities for working with file filters. It provides access to all + * file filter implementations in this package so you don't have to import + * every class you use. + * + * @since Commons IO 1.0 + * @version $Id: FileFilterUtils.java 609286 2008-01-06 10:01:26Z scolebourne $ + * + * @author Stephen Colebourne + * @author Jeremias Maerki + * @author Masato Tezuka + * @author Rahul Akolkar + */ +public class FileFilterUtils { + + /** + * FileFilterUtils is not normally instantiated. + */ + public FileFilterUtils() { + } + + //----------------------------------------------------------------------- + /** + * Returns a filter that returns true if the filename starts with the specified text. + * + * @param prefix the filename prefix + * @return a prefix checking filter + */ + public static IOFileFilter prefixFileFilter(String prefix) { + return new PrefixFileFilter(prefix); + } + + /** + * Returns a filter that returns true if the filename ends with the specified text. + * + * @param suffix the filename suffix + * @return a suffix checking filter + */ + public static IOFileFilter suffixFileFilter(String suffix) { + return new SuffixFileFilter(suffix); + } + + /** + * Returns a filter that returns true if the filename matches the specified text. + * + * @param name the filename + * @return a name checking filter + */ + public static IOFileFilter nameFileFilter(String name) { + return new NameFileFilter(name); + } + + /** + * Returns a filter that checks if the file is a directory. + * + * @return file filter that accepts only directories and not files + */ + public static IOFileFilter directoryFileFilter() { + return DirectoryFileFilter.DIRECTORY; + } + + /** + * Returns a filter that checks if the file is a file (and not a directory). + * + * @return file filter that accepts only files and not directories + */ + public static IOFileFilter fileFileFilter() { + return FileFileFilter.FILE; + } + + //----------------------------------------------------------------------- + /** + * Returns a filter that ANDs the two specified filters. + * + * @param filter1 the first filter + * @param filter2 the second filter + * @return a filter that ANDs the two specified filters + */ + public static IOFileFilter andFileFilter(IOFileFilter filter1, IOFileFilter filter2) { + return new AndFileFilter(filter1, filter2); + } + + /** + * Returns a filter that ORs the two specified filters. + * + * @param filter1 the first filter + * @param filter2 the second filter + * @return a filter that ORs the two specified filters + */ + public static IOFileFilter orFileFilter(IOFileFilter filter1, IOFileFilter filter2) { + return new OrFileFilter(filter1, filter2); + } + + /** + * Returns a filter that NOTs the specified filter. + * + * @param filter the filter to invert + * @return a filter that NOTs the specified filter + */ + public static IOFileFilter notFileFilter(IOFileFilter filter) { + return new NotFileFilter(filter); + } + + //----------------------------------------------------------------------- + /** + * Returns a filter that always returns true. + * + * @return a true filter + */ + public static IOFileFilter trueFileFilter() { + return TrueFileFilter.TRUE; + } + + /** + * Returns a filter that always returns false. + * + * @return a false filter + */ + public static IOFileFilter falseFileFilter() { + return FalseFileFilter.FALSE; + } + + //----------------------------------------------------------------------- + /** + * Returns an IOFileFilter that wraps the + * FileFilter instance. + * + * @param filter the filter to be wrapped + * @return a new filter that implements IOFileFilter + */ + public static IOFileFilter asFileFilter(FileFilter filter) { + return new DelegateFileFilter(filter); + } + + /** + * Returns an IOFileFilter that wraps the + * FilenameFilter instance. + * + * @param filter the filter to be wrapped + * @return a new filter that implements IOFileFilter + */ + public static IOFileFilter asFileFilter(FilenameFilter filter) { + return new DelegateFileFilter(filter); + } + + //----------------------------------------------------------------------- + /** + * Returns a filter that returns true if the file was last modified after + * the specified cutoff time. + * + * @param cutoff the time threshold + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(long cutoff) { + return new AgeFileFilter(cutoff); + } + + /** + * Returns a filter that filters files based on a cutoff time. + * + * @param cutoff the time threshold + * @param acceptOlder if true, older files get accepted, if false, newer + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(long cutoff, boolean acceptOlder) { + return new AgeFileFilter(cutoff, acceptOlder); + } + + /** + * Returns a filter that returns true if the file was last modified after + * the specified cutoff date. + * + * @param cutoffDate the time threshold + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(Date cutoffDate) { + return new AgeFileFilter(cutoffDate); + } + + /** + * Returns a filter that filters files based on a cutoff date. + * + * @param cutoffDate the time threshold + * @param acceptOlder if true, older files get accepted, if false, newer + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(Date cutoffDate, boolean acceptOlder) { + return new AgeFileFilter(cutoffDate, acceptOlder); + } + + /** + * Returns a filter that returns true if the file was last modified after + * the specified reference file. + * + * @param cutoffReference the file whose last modification + * time is usesd as the threshold age of the files + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(File cutoffReference) { + return new AgeFileFilter(cutoffReference); + } + + /** + * Returns a filter that filters files based on a cutoff reference file. + * + * @param cutoffReference the file whose last modification + * time is usesd as the threshold age of the files + * @param acceptOlder if true, older files get accepted, if false, newer + * @return an appropriately configured age file filter + * @since Commons IO 1.2 + */ + public static IOFileFilter ageFileFilter(File cutoffReference, boolean acceptOlder) { + return new AgeFileFilter(cutoffReference, acceptOlder); + } + + //----------------------------------------------------------------------- + /** + * Returns a filter that returns true if the file is bigger than a certain size. + * + * @param threshold the file size threshold + * @return an appropriately configured SizeFileFilter + * @since Commons IO 1.2 + */ + public static IOFileFilter sizeFileFilter(long threshold) { + return new SizeFileFilter(threshold); + } + + /** + * Returns a filter that filters based on file size. + * + * @param threshold the file size threshold + * @param acceptLarger if true, larger files get accepted, if false, smaller + * @return an appropriately configured SizeFileFilter + * @since Commons IO 1.2 + */ + public static IOFileFilter sizeFileFilter(long threshold, boolean acceptLarger) { + return new SizeFileFilter(threshold, acceptLarger); + } + + /** + * Returns a filter that accepts files whose size is >= minimum size + * and <= maximum size. + * + * @param minSizeInclusive the minimum file size (inclusive) + * @param maxSizeInclusive the maximum file size (inclusive) + * @return an appropriately configured IOFileFilter + * @since Commons IO 1.3 + */ + public static IOFileFilter sizeRangeFileFilter(long minSizeInclusive, long maxSizeInclusive ) { + IOFileFilter minimumFilter = new SizeFileFilter(minSizeInclusive, true); + IOFileFilter maximumFilter = new SizeFileFilter(maxSizeInclusive + 1L, false); + return new AndFileFilter(minimumFilter, maximumFilter); + } + + //----------------------------------------------------------------------- + /* Constructed on demand and then cached */ + private static IOFileFilter cvsFilter; + + /* Constructed on demand and then cached */ + private static IOFileFilter svnFilter; + + /** + * Decorates a filter to make it ignore CVS directories. + * Passing in null will return a filter that accepts everything + * except CVS directories. + * + * @param filter the filter to decorate, null means an unrestricted filter + * @return the decorated filter, never null + * @since Commons IO 1.1 (method existed but had bug in 1.0) + */ + public static IOFileFilter makeCVSAware(IOFileFilter filter) { + if (cvsFilter == null) { + cvsFilter = notFileFilter( + andFileFilter(directoryFileFilter(), nameFileFilter("CVS"))); + } + if (filter == null) { + return cvsFilter; + } else { + return andFileFilter(filter, cvsFilter); + } + } + + /** + * Decorates a filter to make it ignore SVN directories. + * Passing in null will return a filter that accepts everything + * except SVN directories. + * + * @param filter the filter to decorate, null means an unrestricted filter + * @return the decorated filter, never null + * @since Commons IO 1.1 + */ + public static IOFileFilter makeSVNAware(IOFileFilter filter) { + if (svnFilter == null) { + svnFilter = notFileFilter( + andFileFilter(directoryFileFilter(), nameFileFilter(".svn"))); + } + if (filter == null) { + return svnFilter; + } else { + return andFileFilter(filter, svnFilter); + } + } + + //----------------------------------------------------------------------- + /** + * Decorates a filter so that it only applies to directories and not to files. + * + * @param filter the filter to decorate, null means an unrestricted filter + * @return the decorated filter, never null + * @since Commons IO 1.3 + */ + public static IOFileFilter makeDirectoryOnly(IOFileFilter filter) { + if (filter == null) { + return DirectoryFileFilter.DIRECTORY; + } + return new AndFileFilter(DirectoryFileFilter.DIRECTORY, filter); + } + + /** + * Decorates a filter so that it only applies to files and not to directories. + * + * @param filter the filter to decorate, null means an unrestricted filter + * @return the decorated filter, never null + * @since Commons IO 1.3 + */ + public static IOFileFilter makeFileOnly(IOFileFilter filter) { + if (filter == null) { + return FileFileFilter.FILE; + } + return new AndFileFilter(FileFileFilter.FILE, filter); + } + +} diff --git a/src/org/apache/commons/io/filefilter/HiddenFileFilter.java b/src/org/apache/commons/io/filefilter/HiddenFileFilter.java new file mode 100644 index 000000000..244153d5e --- /dev/null +++ b/src/org/apache/commons/io/filefilter/HiddenFileFilter.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter accepts Files that are hidden. + *

+ * Example, showing how to print out a list of the + * current directory's hidden files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( HiddenFileFilter.HIDDEN );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + *

+ * Example, showing how to print out a list of the + * current directory's visible (i.e. not hidden) files: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( HiddenFileFilter.VISIBLE );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 587916 $ + */ +public class HiddenFileFilter extends AbstractFileFilter implements Serializable { + + /** Singleton instance of hidden filter */ + public static final IOFileFilter HIDDEN = new HiddenFileFilter(); + + /** Singleton instance of visible filter */ + public static final IOFileFilter VISIBLE = new NotFileFilter(HIDDEN); + + /** + * Restrictive consructor. + */ + protected HiddenFileFilter() { + } + + /** + * Checks to see if the file is hidden. + * + * @param file the File to check + * @return true if the file is + * hidden, otherwise false. + */ + public boolean accept(File file) { + return file.isHidden(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/IOFileFilter.java b/src/org/apache/commons/io/filefilter/IOFileFilter.java new file mode 100644 index 000000000..5ebd82751 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/IOFileFilter.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; + +/** + * An interface which brings the FileFilter and FilenameFilter + * interfaces together. + * + * @since Commons IO 1.0 + * @version $Revision: 471628 $ $Date: 2006-11-06 04:06:45 +0000 (Mon, 06 Nov 2006) $ + * + * @author Stephen Colebourne + */ +public interface IOFileFilter extends FileFilter, FilenameFilter { + + /** + * Checks to see if the File should be accepted by this filter. + *

+ * Defined in {@link java.io.FileFilter}. + * + * @param file the File to check + * @return true if this file matches the test + */ + public boolean accept(File file); + + /** + * Checks to see if the File should be accepted by this filter. + *

+ * Defined in {@link java.io.FilenameFilter}. + * + * @param dir the directory File to check + * @param name the filename within the directory to check + * @return true if this file matches the test + */ + public boolean accept(File dir, String name); + +} diff --git a/src/org/apache/commons/io/filefilter/NameFileFilter.java b/src/org/apache/commons/io/filefilter/NameFileFilter.java new file mode 100644 index 000000000..fa1c80f73 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/NameFileFilter.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +import org.apache.commons.io.IOCase; + +/** + * Filters filenames for a certain name. + *

+ * For example, to print all files and directories in the + * current directory whose name is Test: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( new NameFileFilter("Test") );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.0 + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * + * @author Stephen Colebourne + * @author Federico Barbieri + * @author Serge Knystautas + * @author Peter Donald + */ +public class NameFileFilter extends AbstractFileFilter implements Serializable { + + /** The filenames to search for */ + private final String[] names; + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Constructs a new case-sensitive name file filter for a single name. + * + * @param name the name to allow, must not be null + * @throws IllegalArgumentException if the name is null + */ + public NameFileFilter(String name) { + this(name, null); + } + + /** + * Construct a new name file filter specifying case-sensitivity. + * + * @param name the name to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the name is null + */ + public NameFileFilter(String name, IOCase caseSensitivity) { + if (name == null) { + throw new IllegalArgumentException("The wildcard must not be null"); + } + this.names = new String[] {name}; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new case-sensitive name file filter for an array of names. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param names the names to allow, must not be null + * @throws IllegalArgumentException if the names array is null + */ + public NameFileFilter(String[] names) { + this(names, null); + } + + /** + * Constructs a new name file filter for an array of names specifying case-sensitivity. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param names the names to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the names array is null + */ + public NameFileFilter(String[] names, IOCase caseSensitivity) { + if (names == null) { + throw new IllegalArgumentException("The array of names must not be null"); + } + this.names = names; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new case-sensitive name file filter for a list of names. + * + * @param names the names to allow, must not be null + * @throws IllegalArgumentException if the name list is null + * @throws ClassCastException if the list does not contain Strings + */ + public NameFileFilter(List names) { + this(names, null); + } + + /** + * Constructs a new name file filter for a list of names specifying case-sensitivity. + * + * @param names the names to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the name list is null + * @throws ClassCastException if the list does not contain Strings + */ + public NameFileFilter(List names, IOCase caseSensitivity) { + if (names == null) { + throw new IllegalArgumentException("The list of names must not be null"); + } + this.names = (String[]) names.toArray(new String[names.size()]); + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + //----------------------------------------------------------------------- + /** + * Checks to see if the filename matches. + * + * @param file the File to check + * @return true if the filename matches + */ + public boolean accept(File file) { + String name = file.getName(); + for (int i = 0; i < this.names.length; i++) { + if (caseSensitivity.checkEquals(name, names[i])) { + return true; + } + } + return false; + } + + /** + * Checks to see if the filename matches. + * + * @param file the File directory + * @param name the filename + * @return true if the filename matches + */ + public boolean accept(File file, String name) { + for (int i = 0; i < names.length; i++) { + if (caseSensitivity.checkEquals(name, names[i])) { + return true; + } + } + return false; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (names != null) { + for (int i = 0; i < names.length; i++) { + if (i > 0) { + buffer.append(","); + } + buffer.append(names[i]); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/NotFileFilter.java b/src/org/apache/commons/io/filefilter/NotFileFilter.java new file mode 100644 index 000000000..710c8ecf3 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/NotFileFilter.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * This filter produces a logical NOT of the filters specified. + * + * @since Commons IO 1.0 + * @version $Revision: 591058 $ $Date: 2007-11-01 15:47:05 +0000 (Thu, 01 Nov 2007) $ + * + * @author Stephen Colebourne + */ +public class NotFileFilter extends AbstractFileFilter implements Serializable { + + /** The filter */ + private final IOFileFilter filter; + + /** + * Constructs a new file filter that NOTs the result of another filters. + * + * @param filter the filter, must not be null + * @throws IllegalArgumentException if the filter is null + */ + public NotFileFilter(IOFileFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("The filter must not be null"); + } + this.filter = filter; + } + + /** + * Checks to see if both filters are true. + * + * @param file the File to check + * @return true if the filter returns false + */ + public boolean accept(File file) { + return ! filter.accept(file); + } + + /** + * Checks to see if both filters are true. + * + * @param file the File directory + * @param name the filename + * @return true if the filter returns false + */ + public boolean accept(File file, String name) { + return ! filter.accept(file, name); + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + return super.toString() + "(" + filter.toString() + ")"; + } + +} diff --git a/src/org/apache/commons/io/filefilter/OrFileFilter.java b/src/org/apache/commons/io/filefilter/OrFileFilter.java new file mode 100644 index 000000000..59cc0f2b0 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/OrFileFilter.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * A {@link java.io.FileFilter} providing conditional OR logic across a list of + * file filters. This filter returns true if any filters in the + * list return true. Otherwise, it returns false. + * Checking of the file filter list stops when the first filter returns + * true. + * + * @since Commons IO 1.0 + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * + * @author Steven Caswell + */ +public class OrFileFilter + extends AbstractFileFilter + implements ConditionalFileFilter, Serializable { + + /** The list of file filters. */ + private List fileFilters; + + /** + * Constructs a new instance of OrFileFilter. + * + * @since Commons IO 1.1 + */ + public OrFileFilter() { + this.fileFilters = new ArrayList(); + } + + /** + * Constructs a new instance of OrFileFilter + * with the specified filters. + * + * @param fileFilters the file filters for this filter, copied, null ignored + * @since Commons IO 1.1 + */ + public OrFileFilter(final List fileFilters) { + if (fileFilters == null) { + this.fileFilters = new ArrayList(); + } else { + this.fileFilters = new ArrayList(fileFilters); + } + } + + /** + * Constructs a new file filter that ORs the result of two other filters. + * + * @param filter1 the first filter, must not be null + * @param filter2 the second filter, must not be null + * @throws IllegalArgumentException if either filter is null + */ + public OrFileFilter(IOFileFilter filter1, IOFileFilter filter2) { + if (filter1 == null || filter2 == null) { + throw new IllegalArgumentException("The filters must not be null"); + } + this.fileFilters = new ArrayList(); + addFileFilter(filter1); + addFileFilter(filter2); + } + + /** + * {@inheritDoc} + */ + public void addFileFilter(final IOFileFilter ioFileFilter) { + this.fileFilters.add(ioFileFilter); + } + + /** + * {@inheritDoc} + */ + public List getFileFilters() { + return Collections.unmodifiableList(this.fileFilters); + } + + /** + * {@inheritDoc} + */ + public boolean removeFileFilter(IOFileFilter ioFileFilter) { + return this.fileFilters.remove(ioFileFilter); + } + + /** + * {@inheritDoc} + */ + public void setFileFilters(final List fileFilters) { + this.fileFilters = fileFilters; + } + + /** + * {@inheritDoc} + */ + public boolean accept(final File file) { + for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) { + IOFileFilter fileFilter = (IOFileFilter) iter.next(); + if (fileFilter.accept(file)) { + return true; + } + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean accept(final File file, final String name) { + for (Iterator iter = this.fileFilters.iterator(); iter.hasNext();) { + IOFileFilter fileFilter = (IOFileFilter) iter.next(); + if (fileFilter.accept(file, name)) { + return true; + } + } + return false; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (fileFilters != null) { + for (int i = 0; i < fileFilters.size(); i++) { + if (i > 0) { + buffer.append(","); + } + Object filter = fileFilters.get(i); + buffer.append(filter == null ? "null" : filter.toString()); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/PrefixFileFilter.java b/src/org/apache/commons/io/filefilter/PrefixFileFilter.java new file mode 100644 index 000000000..0b6fcb961 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/PrefixFileFilter.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +import org.apache.commons.io.IOCase; + +/** + * Filters filenames for a certain prefix. + *

+ * For example, to print all files and directories in the + * current directory whose name starts with Test: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( new PrefixFileFilter("Test") );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.0 + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * + * @author Stephen Colebourne + * @author Federico Barbieri + * @author Serge Knystautas + * @author Peter Donald + */ +public class PrefixFileFilter extends AbstractFileFilter implements Serializable { + + /** The filename prefixes to search for */ + private final String[] prefixes; + + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Constructs a new Prefix file filter for a single prefix. + * + * @param prefix the prefix to allow, must not be null + * @throws IllegalArgumentException if the prefix is null + */ + public PrefixFileFilter(String prefix) { + this(prefix, IOCase.SENSITIVE); + } + + /** + * Constructs a new Prefix file filter for a single prefix + * specifying case-sensitivity. + * + * @param prefix the prefix to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the prefix is null + * @since Commons IO 1.4 + */ + public PrefixFileFilter(String prefix, IOCase caseSensitivity) { + if (prefix == null) { + throw new IllegalArgumentException("The prefix must not be null"); + } + this.prefixes = new String[] {prefix}; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new Prefix file filter for any of an array of prefixes. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param prefixes the prefixes to allow, must not be null + * @throws IllegalArgumentException if the prefix array is null + */ + public PrefixFileFilter(String[] prefixes) { + this(prefixes, IOCase.SENSITIVE); + } + + /** + * Constructs a new Prefix file filter for any of an array of prefixes + * specifying case-sensitivity. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param prefixes the prefixes to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the prefix is null + * @since Commons IO 1.4 + */ + public PrefixFileFilter(String[] prefixes, IOCase caseSensitivity) { + if (prefixes == null) { + throw new IllegalArgumentException("The array of prefixes must not be null"); + } + this.prefixes = prefixes; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new Prefix file filter for a list of prefixes. + * + * @param prefixes the prefixes to allow, must not be null + * @throws IllegalArgumentException if the prefix list is null + * @throws ClassCastException if the list does not contain Strings + */ + public PrefixFileFilter(List prefixes) { + this(prefixes, IOCase.SENSITIVE); + } + + /** + * Constructs a new Prefix file filter for a list of prefixes + * specifying case-sensitivity. + * + * @param prefixes the prefixes to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the prefix list is null + * @throws ClassCastException if the list does not contain Strings + * @since Commons IO 1.4 + */ + public PrefixFileFilter(List prefixes, IOCase caseSensitivity) { + if (prefixes == null) { + throw new IllegalArgumentException("The list of prefixes must not be null"); + } + this.prefixes = (String[]) prefixes.toArray(new String[prefixes.size()]); + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Checks to see if the filename starts with the prefix. + * + * @param file the File to check + * @return true if the filename starts with one of our prefixes + */ + public boolean accept(File file) { + String name = file.getName(); + for (int i = 0; i < this.prefixes.length; i++) { + if (caseSensitivity.checkStartsWith(name, prefixes[i])) { + return true; + } + } + return false; + } + + /** + * Checks to see if the filename starts with the prefix. + * + * @param file the File directory + * @param name the filename + * @return true if the filename starts with one of our prefixes + */ + public boolean accept(File file, String name) { + for (int i = 0; i < prefixes.length; i++) { + if (caseSensitivity.checkStartsWith(name, prefixes[i])) { + return true; + } + } + return false; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (prefixes != null) { + for (int i = 0; i < prefixes.length; i++) { + if (i > 0) { + buffer.append(","); + } + buffer.append(prefixes[i]); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/RegexFileFilter.java b/src/org/apache/commons/io/filefilter/RegexFileFilter.java new file mode 100644 index 000000000..b49a1bd3b --- /dev/null +++ b/src/org/apache/commons/io/filefilter/RegexFileFilter.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.regex.Pattern; + +import org.apache.commons.io.IOCase; + +/** + * Filters files using supplied regular expression(s). + *

+ * See java.util.regex.Pattern for regex matching rules + *

+ * + *

+ * e.g. + *

+ * File dir = new File(".");
+ * FileFilter fileFilter = new RegexFileFilter("^.*[tT]est(-\\d+)?\\.java$");
+ * File[] files = dir.listFiles(fileFilter);
+ * for (int i = 0; i < files.length; i++) {
+ *   System.out.println(files[i]);
+ * }
+ * 
+ * + * @author Oliver Siegmar + * @version $Revision: 606381 $ + * @since Commons IO 1.4 + */ +public class RegexFileFilter extends AbstractFileFilter implements Serializable { + + /** The regular expression pattern that will be used to match filenames */ + private final Pattern pattern; + + /** + * Construct a new regular expression filter. + * + * @param pattern regular string expression to match + * @throws IllegalArgumentException if the pattern is null + */ + public RegexFileFilter(String pattern) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern is missing"); + } + + this.pattern = Pattern.compile(pattern); + } + + /** + * Construct a new regular expression filter with the specified flags case sensitivity. + * + * @param pattern regular string expression to match + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the pattern is null + */ + public RegexFileFilter(String pattern, IOCase caseSensitivity) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern is missing"); + } + int flags = 0; + if (caseSensitivity != null && !caseSensitivity.isCaseSensitive()) { + flags = Pattern.CASE_INSENSITIVE; + } + this.pattern = Pattern.compile(pattern, flags); + } + + /** + * Construct a new regular expression filter with the specified flags. + * + * @param pattern regular string expression to match + * @param flags pattern flags - e.g. {@link Pattern#CASE_INSENSITIVE} + * @throws IllegalArgumentException if the pattern is null + */ + public RegexFileFilter(String pattern, int flags) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern is missing"); + } + this.pattern = Pattern.compile(pattern, flags); + } + + /** + * Construct a new regular expression filter for a compiled regular expression + * + * @param pattern regular expression to match + * @throws IllegalArgumentException if the pattern is null + */ + public RegexFileFilter(Pattern pattern) { + if (pattern == null) { + throw new IllegalArgumentException("Pattern is missing"); + } + + this.pattern = pattern; + } + + /** + * Checks to see if the filename matches one of the regular expressions. + * + * @param dir the file directory + * @param name the filename + * @return true if the filename matches one of the regular expressions + */ + public boolean accept(File dir, String name) { + return (pattern.matcher(name).matches()); + } + +} diff --git a/src/org/apache/commons/io/filefilter/SizeFileFilter.java b/src/org/apache/commons/io/filefilter/SizeFileFilter.java new file mode 100644 index 000000000..614e4243f --- /dev/null +++ b/src/org/apache/commons/io/filefilter/SizeFileFilter.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * Filters files based on size, can filter either smaller files or + * files equal to or larger than a given threshold. + *

+ * For example, to print all files and directories in the + * current directory whose size is greater than 1 MB: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( new SizeFileFilter(1024 * 1024) );
+ * for ( int i = 0; i < files.length; i++ ) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @author Rahul Akolkar + * @version $Id: SizeFileFilter.java 591058 2007-11-01 15:47:05Z niallp $ + * @since Commons IO 1.2 + */ +public class SizeFileFilter extends AbstractFileFilter implements Serializable { + + /** The size threshold. */ + private final long size; + /** Whether the files accepted will be larger or smaller. */ + private final boolean acceptLarger; + + /** + * Constructs a new size file filter for files equal to or + * larger than a certain size. + * + * @param size the threshold size of the files + * @throws IllegalArgumentException if the size is negative + */ + public SizeFileFilter(long size) { + this(size, true); + } + + /** + * Constructs a new size file filter for files based on a certain size + * threshold. + * + * @param size the threshold size of the files + * @param acceptLarger if true, files equal to or larger are accepted, + * otherwise smaller ones (but not equal to) + * @throws IllegalArgumentException if the size is negative + */ + public SizeFileFilter(long size, boolean acceptLarger) { + if (size < 0) { + throw new IllegalArgumentException("The size must be non-negative"); + } + this.size = size; + this.acceptLarger = acceptLarger; + } + + //----------------------------------------------------------------------- + /** + * Checks to see if the size of the file is favorable. + *

+ * If size equals threshold and smaller files are required, + * file IS NOT selected. + * If size equals threshold and larger files are required, + * file IS selected. + * + * @param file the File to check + * @return true if the filename matches + */ + public boolean accept(File file) { + boolean smaller = file.length() < size; + return acceptLarger ? !smaller : smaller; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + String condition = acceptLarger ? ">=" : "<"; + return super.toString() + "(" + condition + size + ")"; + } + +} diff --git a/src/org/apache/commons/io/filefilter/SuffixFileFilter.java b/src/org/apache/commons/io/filefilter/SuffixFileFilter.java new file mode 100644 index 000000000..bee34402c --- /dev/null +++ b/src/org/apache/commons/io/filefilter/SuffixFileFilter.java @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +import org.apache.commons.io.IOCase; + +/** + * Filters files based on the suffix (what the filename ends with). + * This is used in retrieving all the files of a particular type. + *

+ * For example, to retrieve and print all *.java files + * in the current directory: + * + *

+ * File dir = new File(".");
+ * String[] files = dir.list( new SuffixFileFilter(".java") );
+ * for (int i = 0; i < files.length; i++) {
+ *     System.out.println(files[i]);
+ * }
+ * 
+ * + * @since Commons IO 1.0 + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * + * @author Stephen Colebourne + * @author Federico Barbieri + * @author Serge Knystautas + * @author Peter Donald + */ +public class SuffixFileFilter extends AbstractFileFilter implements Serializable { + + /** The filename suffixes to search for */ + private final String[] suffixes; + + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Constructs a new Suffix file filter for a single extension. + * + * @param suffix the suffix to allow, must not be null + * @throws IllegalArgumentException if the suffix is null + */ + public SuffixFileFilter(String suffix) { + this(suffix, IOCase.SENSITIVE); + } + + /** + * Constructs a new Suffix file filter for a single extension + * specifying case-sensitivity. + * + * @param suffix the suffix to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the suffix is null + * @since Commons IO 1.4 + */ + public SuffixFileFilter(String suffix, IOCase caseSensitivity) { + if (suffix == null) { + throw new IllegalArgumentException("The suffix must not be null"); + } + this.suffixes = new String[] {suffix}; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new Suffix file filter for an array of suffixs. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param suffixes the suffixes to allow, must not be null + * @throws IllegalArgumentException if the suffix array is null + */ + public SuffixFileFilter(String[] suffixes) { + this(suffixes, IOCase.SENSITIVE); + } + + /** + * Constructs a new Suffix file filter for an array of suffixs + * specifying case-sensitivity. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param suffixes the suffixes to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the suffix array is null + * @since Commons IO 1.4 + */ + public SuffixFileFilter(String[] suffixes, IOCase caseSensitivity) { + if (suffixes == null) { + throw new IllegalArgumentException("The array of suffixes must not be null"); + } + this.suffixes = suffixes; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Constructs a new Suffix file filter for a list of suffixes. + * + * @param suffixes the suffixes to allow, must not be null + * @throws IllegalArgumentException if the suffix list is null + * @throws ClassCastException if the list does not contain Strings + */ + public SuffixFileFilter(List suffixes) { + this(suffixes, IOCase.SENSITIVE); + } + + /** + * Constructs a new Suffix file filter for a list of suffixes + * specifying case-sensitivity. + * + * @param suffixes the suffixes to allow, must not be null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the suffix list is null + * @throws ClassCastException if the list does not contain Strings + * @since Commons IO 1.4 + */ + public SuffixFileFilter(List suffixes, IOCase caseSensitivity) { + if (suffixes == null) { + throw new IllegalArgumentException("The list of suffixes must not be null"); + } + this.suffixes = (String[]) suffixes.toArray(new String[suffixes.size()]); + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Checks to see if the filename ends with the suffix. + * + * @param file the File to check + * @return true if the filename ends with one of our suffixes + */ + public boolean accept(File file) { + String name = file.getName(); + for (int i = 0; i < this.suffixes.length; i++) { + if (caseSensitivity.checkEndsWith(name, suffixes[i])) { + return true; + } + } + return false; + } + + /** + * Checks to see if the filename ends with the suffix. + * + * @param file the File directory + * @param name the filename + * @return true if the filename ends with one of our suffixes + */ + public boolean accept(File file, String name) { + for (int i = 0; i < this.suffixes.length; i++) { + if (caseSensitivity.checkEndsWith(name, suffixes[i])) { + return true; + } + } + return false; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (suffixes != null) { + for (int i = 0; i < suffixes.length; i++) { + if (i > 0) { + buffer.append(","); + } + buffer.append(suffixes[i]); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/TrueFileFilter.java b/src/org/apache/commons/io/filefilter/TrueFileFilter.java new file mode 100644 index 000000000..be1b13a7e --- /dev/null +++ b/src/org/apache/commons/io/filefilter/TrueFileFilter.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; + +/** + * A file filter that always returns true. + * + * @since Commons IO 1.0 + * @version $Revision: 587978 $ $Date: 2007-10-24 20:36:51 +0100 (Wed, 24 Oct 2007) $ + * + * @author Stephen Colebourne + */ +public class TrueFileFilter implements IOFileFilter, Serializable { + + /** + * Singleton instance of true filter. + * @since Commons IO 1.3 + */ + public static final IOFileFilter TRUE = new TrueFileFilter(); + /** + * Singleton instance of true filter. + * Please use the identical TrueFileFilter.TRUE constant. + * The new name is more JDK 1.5 friendly as it doesn't clash with other + * values when using static imports. + */ + public static final IOFileFilter INSTANCE = TRUE; + + /** + * Restrictive consructor. + */ + protected TrueFileFilter() { + } + + /** + * Returns true. + * + * @param file the file to check + * @return true + */ + public boolean accept(File file) { + return true; + } + + /** + * Returns true. + * + * @param dir the directory to check + * @param name the filename + * @return true + */ + public boolean accept(File dir, String name) { + return true; + } + +} diff --git a/src/org/apache/commons/io/filefilter/WildcardFileFilter.java b/src/org/apache/commons/io/filefilter/WildcardFileFilter.java new file mode 100644 index 000000000..791fe985b --- /dev/null +++ b/src/org/apache/commons/io/filefilter/WildcardFileFilter.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOCase; + +/** + * Filters files using the supplied wildcards. + *

+ * This filter selects files and directories based on one or more wildcards. + * Testing is case-sensitive by default, but this can be configured. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple wildcard characters. + * This is the same as often found on Dos/Unix command lines. + * The extension check is case-sensitive by . + * See {@link FilenameUtils#wildcardMatchOnSystem} for more information. + *

+ * For example: + *

+ * File dir = new File(".");
+ * FileFilter fileFilter = new WildcardFileFilter("*test*.java~*~");
+ * File[] files = dir.listFiles(fileFilter);
+ * for (int i = 0; i < files.length; i++) {
+ *   System.out.println(files[i]);
+ * }
+ * 
+ * + * @author Jason Anderson + * @version $Revision: 155419 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * @since Commons IO 1.3 + */ +public class WildcardFileFilter extends AbstractFileFilter implements Serializable { + + /** The wildcards that will be used to match filenames. */ + private final String[] wildcards; + /** Whether the comparison is case sensitive. */ + private final IOCase caseSensitivity; + + /** + * Construct a new case-sensitive wildcard filter for a single wildcard. + * + * @param wildcard the wildcard to match + * @throws IllegalArgumentException if the pattern is null + */ + public WildcardFileFilter(String wildcard) { + this(wildcard, null); + } + + /** + * Construct a new wildcard filter for a single wildcard specifying case-sensitivity. + * + * @param wildcard the wildcard to match, not null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the pattern is null + */ + public WildcardFileFilter(String wildcard, IOCase caseSensitivity) { + if (wildcard == null) { + throw new IllegalArgumentException("The wildcard must not be null"); + } + this.wildcards = new String[] { wildcard }; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Construct a new case-sensitive wildcard filter for an array of wildcards. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param wildcards the array of wildcards to match + * @throws IllegalArgumentException if the pattern array is null + */ + public WildcardFileFilter(String[] wildcards) { + this(wildcards, null); + } + + /** + * Construct a new wildcard filter for an array of wildcards specifying case-sensitivity. + *

+ * The array is not cloned, so could be changed after constructing the + * instance. This would be inadvisable however. + * + * @param wildcards the array of wildcards to match, not null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the pattern array is null + */ + public WildcardFileFilter(String[] wildcards, IOCase caseSensitivity) { + if (wildcards == null) { + throw new IllegalArgumentException("The wildcard array must not be null"); + } + this.wildcards = wildcards; + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + /** + * Construct a new case-sensitive wildcard filter for a list of wildcards. + * + * @param wildcards the list of wildcards to match, not null + * @throws IllegalArgumentException if the pattern list is null + * @throws ClassCastException if the list does not contain Strings + */ + public WildcardFileFilter(List wildcards) { + this(wildcards, null); + } + + /** + * Construct a new wildcard filter for a list of wildcards specifying case-sensitivity. + * + * @param wildcards the list of wildcards to match, not null + * @param caseSensitivity how to handle case sensitivity, null means case-sensitive + * @throws IllegalArgumentException if the pattern list is null + * @throws ClassCastException if the list does not contain Strings + */ + public WildcardFileFilter(List wildcards, IOCase caseSensitivity) { + if (wildcards == null) { + throw new IllegalArgumentException("The wildcard list must not be null"); + } + this.wildcards = (String[]) wildcards.toArray(new String[wildcards.size()]); + this.caseSensitivity = (caseSensitivity == null ? IOCase.SENSITIVE : caseSensitivity); + } + + //----------------------------------------------------------------------- + /** + * Checks to see if the filename matches one of the wildcards. + * + * @param dir the file directory + * @param name the filename + * @return true if the filename matches one of the wildcards + */ + public boolean accept(File dir, String name) { + for (int i = 0; i < wildcards.length; i++) { + if (FilenameUtils.wildcardMatch(name, wildcards[i], caseSensitivity)) { + return true; + } + } + return false; + } + + /** + * Checks to see if the filename matches one of the wildcards. + * + * @param file the file to check + * @return true if the filename matches one of the wildcards + */ + public boolean accept(File file) { + String name = file.getName(); + for (int i = 0; i < wildcards.length; i++) { + if (FilenameUtils.wildcardMatch(name, wildcards[i], caseSensitivity)) { + return true; + } + } + return false; + } + + /** + * Provide a String representaion of this file filter. + * + * @return a String representaion + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(super.toString()); + buffer.append("("); + if (wildcards != null) { + for (int i = 0; i < wildcards.length; i++) { + if (i > 0) { + buffer.append(","); + } + buffer.append(wildcards[i]); + } + } + buffer.append(")"); + return buffer.toString(); + } + +} diff --git a/src/org/apache/commons/io/filefilter/WildcardFilter.java b/src/org/apache/commons/io/filefilter/WildcardFilter.java new file mode 100644 index 000000000..2a6e0dde0 --- /dev/null +++ b/src/org/apache/commons/io/filefilter/WildcardFilter.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.filefilter; + +import java.io.File; +import java.io.Serializable; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + +/** + * Filters files using the supplied wildcards. + *

+ * This filter selects files, but not directories, based on one or more wildcards + * and using case-sensitive comparison. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a + * single or multiple wildcard characters. + * This is the same as often found on Dos/Unix command lines. + * The extension check is case-sensitive. + * See {@link FilenameUtils#wildcardMatch} for more information. + *

+ * For example: + *

+ * File dir = new File(".");
+ * FileFilter fileFilter = new WildcardFilter("*test*.java~*~");
+ * File[] files = dir.listFiles(fileFilter);
+ * for (int i = 0; i < files.length; i++) {
+ *   System.out.println(files[i]);
+ * }
+ * 
+ * + * @author Jason Anderson + * @version $Revision: 606381 $ $Date: 2007-12-22 02:03:16 +0000 (Sat, 22 Dec 2007) $ + * @since Commons IO 1.1 + * @deprecated Use WilcardFileFilter. Deprecated as this class performs directory + * filtering which it shouldn't do, but that can't be removed due to compatability. + */ +public class WildcardFilter extends AbstractFileFilter implements Serializable { + + /** The wildcards that will be used to match filenames. */ + private final String[] wildcards; + + /** + * Construct a new case-sensitive wildcard filter for a single wildcard. + * + * @param wildcard the wildcard to match + * @throws IllegalArgumentException if the pattern is null + */ + public WildcardFilter(String wildcard) { + if (wildcard == null) { + throw new IllegalArgumentException("The wildcard must not be null"); + } + this.wildcards = new String[] { wildcard }; + } + + /** + * Construct a new case-sensitive wildcard filter for an array of wildcards. + * + * @param wildcards the array of wildcards to match + * @throws IllegalArgumentException if the pattern array is null + */ + public WildcardFilter(String[] wildcards) { + if (wildcards == null) { + throw new IllegalArgumentException("The wildcard array must not be null"); + } + this.wildcards = wildcards; + } + + /** + * Construct a new case-sensitive wildcard filter for a list of wildcards. + * + * @param wildcards the list of wildcards to match + * @throws IllegalArgumentException if the pattern list is null + * @throws ClassCastException if the list does not contain Strings + */ + public WildcardFilter(List wildcards) { + if (wildcards == null) { + throw new IllegalArgumentException("The wildcard list must not be null"); + } + this.wildcards = (String[]) wildcards.toArray(new String[wildcards.size()]); + } + + //----------------------------------------------------------------------- + /** + * Checks to see if the filename matches one of the wildcards. + * + * @param dir the file directory + * @param name the filename + * @return true if the filename matches one of the wildcards + */ + public boolean accept(File dir, String name) { + if (dir != null && new File(dir, name).isDirectory()) { + return false; + } + + for (int i = 0; i < wildcards.length; i++) { + if (FilenameUtils.wildcardMatch(name, wildcards[i])) { + return true; + } + } + + return false; + } + + /** + * Checks to see if the filename matches one of the wildcards. + * + * @param file the file to check + * @return true if the filename matches one of the wildcards + */ + public boolean accept(File file) { + if (file.isDirectory()) { + return false; + } + + for (int i = 0; i < wildcards.length; i++) { + if (FilenameUtils.wildcardMatch(file.getName(), wildcards[i])) { + return true; + } + } + + return false; + } + +} diff --git a/src/org/apache/commons/io/filefilter/package.html b/src/org/apache/commons/io/filefilter/package.html new file mode 100644 index 000000000..7a45f251d --- /dev/null +++ b/src/org/apache/commons/io/filefilter/package.html @@ -0,0 +1,143 @@ + + + + +

This package defines an interface (IOFileFilter) that combines both +{@link java.io.FileFilter} and {@link java.io.FilenameFilter}. Besides +that the package offers a series of ready-to-use implementations of the +IOFileFilter interface including implementation that allow you to combine +other such filters.

+

These filter can be used to list files or in {@link java.awt.FileDialog}, +for example.

+ +

There are a number of 'primitive' filters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectoryFilterOnly accept directories
PrefixFileFilterFilter based on a prefix
SuffixFileFilterFilter based on a suffix
NameFileFilterFilter based on a filename
WildcardFileFilterFilter based on wildcards
AgeFileFilterFilter based on last modified time of file
SizeFileFilterFilter based on file size
+ +

And there are five 'boolean' filters:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TrueFileFilterAccept all files
FalseFileFilterAccept no files
NotFileFilterApplies a logical NOT to an existing filter
AndFileFilterCombines two filters using a logical AND
OrFileFilterCombines two filter using a logical OR
+ +

These boolean FilenameFilters can be nested, to allow arbitrary expressions. +For example, here is how one could print all non-directory files in the +current directory, starting with "A", and ending in ".java" or ".class":

+ +
+  File dir = new File(".");
+  String[] files = dir.list( 
+    new AndFileFilter(
+      new AndFileFilter(
+        new PrefixFileFilter("A"),
+        new OrFileFilter(
+          new SuffixFileFilter(".class"),
+          new SuffixFileFilter(".java")
+        )
+      ),
+      new NotFileFilter(
+        new DirectoryFileFilter()
+      )
+    )
+  );
+  for ( int i=0; i<files.length; i++ ) {
+    System.out.println(files[i]);
+  }
+
+ +

This package also contains a utility class: +FileFilterUtils. It allows you to use all +file filters without having to put them in the import section. Here's how the +above example will look using FileFilterUtils:

+
+  File dir = new File(".");
+  String[] files = dir.list( 
+    FileFilterUtils.andFileFilter(
+      FileFilterUtils.andFileFilter(
+        FileFilterUtils.prefixFileFilter("A"),
+        FileFilterUtils.orFileFilter(
+          FileFilterUtils.suffixFileFilter(".class"),
+          FileFilterUtils.suffixFileFilter(".java")
+        )
+      ),
+      FileFilterUtils.notFileFilter(
+        FileFilterUtils.directoryFileFilter()
+      )
+    )
+  );
+  for ( int i=0; i<files.length; i++ ) {
+    System.out.println(files[i]);
+  }
+
+

There are a few other goodies in that class so please have a look at the +documentation in detail.

+ + diff --git a/src/org/apache/commons/io/input/AutoCloseInputStream.java b/src/org/apache/commons/io/input/AutoCloseInputStream.java new file mode 100644 index 000000000..bb62358f7 --- /dev/null +++ b/src/org/apache/commons/io/input/AutoCloseInputStream.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Proxy stream that closes and discards the underlying stream as soon as the + * end of input has been reached or when the stream is explicitly closed. + * Not even a reference to the underlying stream is kept after it has been + * closed, so any allocated in-memory buffers can be freed even if the + * client application still keeps a reference to the proxy stream. + *

+ * This class is typically used to release any resources related to an open + * stream as soon as possible even if the client application (by not explicitly + * closing the stream when no longer needed) or the underlying stream (by not + * releasing resources once the last byte has been read) do not do that. + * + * @version $Id: AutoCloseInputStream.java 610010 2008-01-08 14:50:59Z niallp $ + * @since Commons IO 1.4 + */ +public class AutoCloseInputStream extends ProxyInputStream { + + /** + * Creates an automatically closing proxy for the given input stream. + * + * @param in underlying input stream + */ + public AutoCloseInputStream(InputStream in) { + super(in); + } + + /** + * Closes the underlying input stream and replaces the reference to it + * with a {@link ClosedInputStream} instance. + *

+ * This method is automatically called by the read methods when the end + * of input has been reached. + *

+ * Note that it is safe to call this method any number of times. The original + * underlying input stream is closed and discarded only once when this + * method is first called. + * + * @throws IOException if the underlying input stream can not be closed + */ + public void close() throws IOException { + in.close(); + in = new ClosedInputStream(); + } + + /** + * Reads and returns a single byte from the underlying input stream. + * If the underlying stream returns -1, the {@link #close()} method is + * called to automatically close and discard the stream. + * + * @return next byte in the stream, or -1 if no more bytes are available + * @throws IOException if the stream could not be read or closed + */ + public int read() throws IOException { + int n = in.read(); + if (n == -1) { + close(); + } + return n; + } + + /** + * Reads and returns bytes from the underlying input stream to the given + * buffer. If the underlying stream returns -1, the {@link #close()} method + * i called to automatically close and discard the stream. + * + * @param b buffer to which bytes from the stream are written + * @return number of bytes read, or -1 if no more bytes are available + * @throws IOException if the stream could not be read or closed + */ + public int read(byte[] b) throws IOException { + int n = in.read(b); + if (n == -1) { + close(); + } + return n; + } + + /** + * Reads and returns bytes from the underlying input stream to the given + * buffer. If the underlying stream returns -1, the {@link #close()} method + * i called to automatically close and discard the stream. + * + * @param b buffer to which bytes from the stream are written + * @param off start offset within the buffer + * @param len maximum number of bytes to read + * @return number of bytes read, or -1 if no more bytes are available + * @throws IOException if the stream could not be read or closed + */ + public int read(byte[] b, int off, int len) throws IOException { + int n = in.read(b, off, len); + if (n == -1) { + close(); + } + return n; + } + + /** + * Ensures that the stream is closed before it gets garbage-collected. + * As mentioned in {@link #close()}, this is a no-op if the stream has + * already been closed. + * @throws Throwable if an error occurs + */ + protected void finalize() throws Throwable { + close(); + super.finalize(); + } + +} diff --git a/src/org/apache/commons/io/input/CharSequenceReader.java b/src/org/apache/commons/io/input/CharSequenceReader.java new file mode 100644 index 000000000..6ee11d87d --- /dev/null +++ b/src/org/apache/commons/io/input/CharSequenceReader.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.Reader; +import java.io.Serializable; + +/** + * {@link Reader} implementation that can read from String, StringBuffer, + * StringBuilder or CharBuffer. + *

+ * Note: Supports {@link #mark(int)} and {@link #reset()}. + * + * @version $Revision: 610516 $ $Date: 2008-01-09 19:05:05 +0000 (Wed, 09 Jan 2008) $ + * @since Commons IO 1.4 + */ +public class CharSequenceReader extends Reader implements Serializable { + + private final CharSequence charSequence; + private int idx; + private int mark; + + /** + * Construct a new instance with the specified character sequence. + * + * @param charSequence The character sequence, may be null + */ + public CharSequenceReader(CharSequence charSequence) { + this.charSequence = (charSequence != null ? charSequence : ""); + } + + /** + * Close resets the file back to the start and removes any marked position. + */ + public void close() { + idx = 0; + mark = 0; + } + + /** + * Mark the current position. + * + * @param readAheadLimit ignored + */ + public void mark(int readAheadLimit) { + mark = idx; + } + + /** + * Mark is supported (returns true). + * + * @return true + */ + public boolean markSupported() { + return true; + } + + /** + * Read a single character. + * + * @return the next character from the character sequence + * or -1 if the end has been reached. + */ + public int read() { + if (idx >= charSequence.length()) { + return -1; + } else { + return charSequence.charAt(idx++); + } + } + + /** + * Read the sepcified number of characters into the array. + * + * @param array The array to store the characters in + * @param offset The starting position in the array to store + * @param length The maximum number of characters to read + * @return The number of characters read or -1 if there are + * no more + */ + public int read(char[] array, int offset, int length) { + if (idx >= charSequence.length()) { + return -1; + } + if (array == null) { + throw new NullPointerException("Character array is missing"); + } + if (length < 0 || (offset + length) > array.length) { + throw new IndexOutOfBoundsException("Array Size=" + array.length + + ", offset=" + offset + ", length=" + length); + } + int count = 0; + for (int i = 0; i < length; i++) { + int c = read(); + if (c == -1) { + return count; + } + array[offset + i] = (char)c; + count++; + } + return count; + } + + /** + * Reset the reader to the last marked position (or the beginning if + * mark has not been called). + */ + public void reset() { + idx = mark; + } + + /** + * Skip the specified number of characters. + * + * @param n The number of characters to skip + * @return The actual number of characters skipped + */ + public long skip(long n) { + if (n < 0) { + throw new IllegalArgumentException( + "Number of characters to skip is less than zero: " + n); + } + if (idx >= charSequence.length()) { + return -1; + } + int dest = (int)Math.min(charSequence.length(), (idx + n)); + int count = dest - idx; + idx = dest; + return count; + } + + /** + * Return a String representation of the underlying + * character sequence. + * + * @return The contents of the character sequence + */ + public String toString() { + return charSequence.toString(); + } +} diff --git a/src/org/apache/commons/io/input/ClassLoaderObjectInputStream.java b/src/org/apache/commons/io/input/ClassLoaderObjectInputStream.java new file mode 100644 index 000000000..13d048946 --- /dev/null +++ b/src/org/apache/commons/io/input/ClassLoaderObjectInputStream.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.io.StreamCorruptedException; + +/** + * A special ObjectInputStream that loads a class based on a specified + * ClassLoader rather than the system default. + *

+ * This is useful in dynamic container environments. + * + * @author Paul Hammant + * @version $Id: ClassLoaderObjectInputStream.java 437567 2006-08-28 06:39:07Z bayard $ + * @since Commons IO 1.1 + */ +public class ClassLoaderObjectInputStream extends ObjectInputStream { + + /** The class loader to use. */ + private ClassLoader classLoader; + + /** + * Constructs a new ClassLoaderObjectInputStream. + * + * @param classLoader the ClassLoader from which classes should be loaded + * @param inputStream the InputStream to work on + * @throws IOException in case of an I/O error + * @throws StreamCorruptedException if the stream is corrupted + */ + public ClassLoaderObjectInputStream( + ClassLoader classLoader, InputStream inputStream) + throws IOException, StreamCorruptedException { + super(inputStream); + this.classLoader = classLoader; + } + + /** + * Resolve a class specified by the descriptor using the + * specified ClassLoader or the super ClassLoader. + * + * @param objectStreamClass descriptor of the class + * @return the Class object described by the ObjectStreamClass + * @throws IOException in case of an I/O error + * @throws ClassNotFoundException if the Class cannot be found + */ + protected Class resolveClass(ObjectStreamClass objectStreamClass) + throws IOException, ClassNotFoundException { + + Class clazz = Class.forName(objectStreamClass.getName(), false, classLoader); + + if (clazz != null) { + // the classloader knows of the class + return clazz; + } else { + // classloader knows not of class, let the super classloader do it + return super.resolveClass(objectStreamClass); + } + } +} diff --git a/src/org/apache/commons/io/input/CloseShieldInputStream.java b/src/org/apache/commons/io/input/CloseShieldInputStream.java new file mode 100644 index 000000000..2058beeb2 --- /dev/null +++ b/src/org/apache/commons/io/input/CloseShieldInputStream.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.InputStream; + +/** + * Proxy stream that prevents the underlying input stream from being closed. + *

+ * This class is typically used in cases where an input stream needs to be + * passed to a component that wants to explicitly close the stream even if + * more input would still be available to other components. + * + * @version $Id: CloseShieldInputStream.java 587913 2007-10-24 15:47:30Z niallp $ + * @since Commons IO 1.4 + */ +public class CloseShieldInputStream extends ProxyInputStream { + + /** + * Creates a proxy that shields the given input stream from being + * closed. + * + * @param in underlying input stream + */ + public CloseShieldInputStream(InputStream in) { + super(in); + } + + /** + * Replaces the underlying input stream with a {@link ClosedInputStream} + * sentinel. The original input stream will remain open, but this proxy + * will appear closed. + */ + public void close() { + in = new ClosedInputStream(); + } + +} diff --git a/src/org/apache/commons/io/input/ClosedInputStream.java b/src/org/apache/commons/io/input/ClosedInputStream.java new file mode 100644 index 000000000..86c83c903 --- /dev/null +++ b/src/org/apache/commons/io/input/ClosedInputStream.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.InputStream; + +/** + * Closed input stream. This stream returns -1 to all attempts to read + * something from the stream. + *

+ * Typically uses of this class include testing for corner cases in methods + * that accept input streams and acting as a sentinel value instead of a + * null input stream. + * + * @version $Id: ClosedInputStream.java 601751 2007-12-06 14:55:45Z niallp $ + * @since Commons IO 1.4 + */ +public class ClosedInputStream extends InputStream { + + /** + * A singleton. + */ + public static final ClosedInputStream CLOSED_INPUT_STREAM = new ClosedInputStream(); + + /** + * Returns -1 to indicate that the stream is closed. + * + * @return always -1 + */ + public int read() { + return -1; + } + +} diff --git a/src/org/apache/commons/io/input/CountingInputStream.java b/src/org/apache/commons/io/input/CountingInputStream.java new file mode 100644 index 000000000..2782276c8 --- /dev/null +++ b/src/org/apache/commons/io/input/CountingInputStream.java @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A decorating input stream that counts the number of bytes that have passed + * through the stream so far. + *

+ * A typical use case would be during debugging, to ensure that data is being + * read as expected. + * + * @author Marcelo Liberato + * @version $Id: CountingInputStream.java 471628 2006-11-06 04:06:45Z bayard $ + */ +public class CountingInputStream extends ProxyInputStream { + + /** The count of bytes that have passed. */ + private long count; + + /** + * Constructs a new CountingInputStream. + * + * @param in the InputStream to delegate to + */ + public CountingInputStream(InputStream in) { + super(in); + } + + //----------------------------------------------------------------------- + /** + * Reads a number of bytes into the byte array, keeping count of the + * number read. + * + * @param b the buffer into which the data is read, not null + * @return the total number of bytes read into the buffer, -1 if end of stream + * @throws IOException if an I/O error occurs + * @see java.io.InputStream#read(byte[]) + */ + public int read(byte[] b) throws IOException { + int found = super.read(b); + this.count += (found >= 0) ? found : 0; + return found; + } + + /** + * Reads a number of bytes into the byte array at a specific offset, + * keeping count of the number read. + * + * @param b the buffer into which the data is read, not null + * @param off the start offset in the buffer + * @param len the maximum number of bytes to read + * @return the total number of bytes read into the buffer, -1 if end of stream + * @throws IOException if an I/O error occurs + * @see java.io.InputStream#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + int found = super.read(b, off, len); + this.count += (found >= 0) ? found : 0; + return found; + } + + /** + * Reads the next byte of data adding to the count of bytes received + * if a byte is successfully read. + * + * @return the byte read, -1 if end of stream + * @throws IOException if an I/O error occurs + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + int found = super.read(); + this.count += (found >= 0) ? 1 : 0; + return found; + } + + /** + * Skips the stream over the specified number of bytes, adding the skipped + * amount to the count. + * + * @param length the number of bytes to skip + * @return the actual number of bytes skipped + * @throws IOException if an I/O error occurs + * @see java.io.InputStream#skip(long) + */ + public long skip(final long length) throws IOException { + final long skip = super.skip(length); + this.count += skip; + return skip; + } + + //----------------------------------------------------------------------- + /** + * The number of bytes that have passed through this stream. + *

+ * NOTE: From v1.3 this method throws an ArithmeticException if the + * count is greater than can be expressed by an int. + * See {@link #getByteCount()} for a method using a long. + * + * @return the number of bytes accumulated + * @throws ArithmeticException if the byte count is too large + */ + public synchronized int getCount() { + long result = getByteCount(); + if (result > Integer.MAX_VALUE) { + throw new ArithmeticException("The byte count " + result + " is too large to be converted to an int"); + } + return (int) result; + } + + /** + * Set the byte count back to 0. + *

+ * NOTE: From v1.3 this method throws an ArithmeticException if the + * count is greater than can be expressed by an int. + * See {@link #resetByteCount()} for a method using a long. + * + * @return the count previous to resetting + * @throws ArithmeticException if the byte count is too large + */ + public synchronized int resetCount() { + long result = resetByteCount(); + if (result > Integer.MAX_VALUE) { + throw new ArithmeticException("The byte count " + result + " is too large to be converted to an int"); + } + return (int) result; + } + + /** + * The number of bytes that have passed through this stream. + *

+ * NOTE: This method is an alternative for getCount() + * and was added because that method returns an integer which will + * result in incorrect count for files over 2GB. + * + * @return the number of bytes accumulated + * @since Commons IO 1.3 + */ + public synchronized long getByteCount() { + return this.count; + } + + /** + * Set the byte count back to 0. + *

+ * NOTE: This method is an alternative for resetCount() + * and was added because that method returns an integer which will + * result in incorrect count for files over 2GB. + * + * @return the count previous to resetting + * @since Commons IO 1.3 + */ + public synchronized long resetByteCount() { + long tmp = this.count; + this.count = 0; + return tmp; + } + +} diff --git a/src/org/apache/commons/io/input/DemuxInputStream.java b/src/org/apache/commons/io/input/DemuxInputStream.java new file mode 100644 index 000000000..1ae888916 --- /dev/null +++ b/src/org/apache/commons/io/input/DemuxInputStream.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Data written to this stream is forwarded to a stream that has been associated + * with this thread. + * + * @author Peter Donald + * @version $Revision: 437567 $ $Date: 2006-08-28 07:39:07 +0100 (Mon, 28 Aug 2006) $ + */ +public class DemuxInputStream + extends InputStream +{ + private InheritableThreadLocal m_streams = new InheritableThreadLocal(); + + /** + * Bind the specified stream to the current thread. + * + * @param input the stream to bind + * @return the InputStream that was previously active + */ + public InputStream bindStream( InputStream input ) + { + InputStream oldValue = getStream(); + m_streams.set( input ); + return oldValue; + } + + /** + * Closes stream associated with current thread. + * + * @throws IOException if an error occurs + */ + public void close() + throws IOException + { + InputStream input = getStream(); + if( null != input ) + { + input.close(); + } + } + + /** + * Read byte from stream associated with current thread. + * + * @return the byte read from stream + * @throws IOException if an error occurs + */ + public int read() + throws IOException + { + InputStream input = getStream(); + if( null != input ) + { + return input.read(); + } + else + { + return -1; + } + } + + /** + * Utility method to retrieve stream bound to current thread (if any). + * + * @return the input stream + */ + private InputStream getStream() + { + return (InputStream)m_streams.get(); + } +} diff --git a/src/org/apache/commons/io/input/NullInputStream.java b/src/org/apache/commons/io/input/NullInputStream.java new file mode 100644 index 000000000..7cee2c6d0 --- /dev/null +++ b/src/org/apache/commons/io/input/NullInputStream.java @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A functional, light weight {@link InputStream} that emulates + * a stream of a specified size. + *

+ * This implementation provides a light weight + * object for testing with an {@link InputStream} + * where the contents don't matter. + *

+ * One use case would be for testing the handling of + * large {@link InputStream} as it can emulate that + * scenario without the overhead of actually processing + * large numbers of bytes - significantly speeding up + * test execution times. + *

+ * This implementation returns zero from the method that + * reads a byte and leaves the array unchanged in the read + * methods that are passed a byte array. + * If alternative data is required the processByte() and + * processBytes() methods can be implemented to generate + * data, for example: + * + *

+ *  public class TestInputStream extends NullInputStream {
+ *      public TestInputStream(int size) {
+ *          super(size);
+ *      }
+ *      protected int processByte() {
+ *          return ... // return required value here
+ *      }
+ *      protected void processBytes(byte[] bytes, int offset, int length) {
+ *          for (int i = offset; i < length; i++) {
+ *              bytes[i] = ... // set array value here
+ *          }
+ *      }
+ *  }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 463529 $ + */ +public class NullInputStream extends InputStream { + + private long size; + private long position; + private long mark = -1; + private long readlimit; + private boolean eof; + private boolean throwEofException; + private boolean markSupported; + + /** + * Create an {@link InputStream} that emulates a specified size + * which supports marking and does not throw EOFException. + * + * @param size The size of the input stream to emulate. + */ + public NullInputStream(long size) { + this(size, true, false); + } + + /** + * Create an {@link InputStream} that emulates a specified + * size with option settings. + * + * @param size The size of the input stream to emulate. + * @param markSupported Whether this instance will support + * the mark() functionality. + * @param throwEofException Whether this implementation + * will throw an {@link EOFException} or return -1 when the + * end of file is reached. + */ + public NullInputStream(long size, boolean markSupported, boolean throwEofException) { + this.size = size; + this.markSupported = markSupported; + this.throwEofException = throwEofException; + } + + /** + * Return the current position. + * + * @return the current position. + */ + public long getPosition() { + return position; + } + + /** + * Return the size this {@link InputStream} emulates. + * + * @return The size of the input stream to emulate. + */ + public long getSize() { + return size; + } + + /** + * Return the number of bytes that can be read. + * + * @return The number of bytes that can be read. + */ + public int available() { + long avail = size - position; + if (avail <= 0) { + return 0; + } else if (avail > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int)avail; + } + } + + /** + * Close this input stream - resets the internal state to + * the initial values. + * + * @throws IOException If an error occurs. + */ + public void close() throws IOException { + eof = false; + position = 0; + mark = -1; + } + + /** + * Mark the current position. + * + * @param readlimit The number of bytes before this marked position + * is invalid. + * @throws UnsupportedOperationException if mark is not supported. + */ + public synchronized void mark(int readlimit) { + if (!markSupported) { + throw new UnsupportedOperationException("Mark not supported"); + } + mark = position; + this.readlimit = readlimit; + } + + /** + * Indicates whether mark is supported. + * + * @return Whether mark is supported or not. + */ + public boolean markSupported() { + return markSupported; + } + + /** + * Read a byte. + * + * @return Either The byte value returned by processByte() + * or -1 if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read() throws IOException { + if (eof) { + throw new IOException("Read after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position++; + return processByte(); + } + + /** + * Read some bytes into the specified array. + * + * @param bytes The byte array to read into + * @return The number of bytes read or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read(byte[] bytes) throws IOException { + return read(bytes, 0, bytes.length); + } + + /** + * Read the specified number bytes into an array. + * + * @param bytes The byte array to read into. + * @param offset The offset to start reading bytes into. + * @param length The number of bytes to read. + * @return The number of bytes read or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read(byte[] bytes, int offset, int length) throws IOException { + if (eof) { + throw new IOException("Read after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position += length; + int returnLength = length; + if (position > size) { + returnLength = length - (int)(position - size); + position = size; + } + processBytes(bytes, offset, returnLength); + return returnLength; + } + + /** + * Reset the stream to the point when mark was last called. + * + * @throws UnsupportedOperationException if mark is not supported. + * @throws IOException If no position has been marked + * or the read limit has been exceed since the last position was + * marked. + */ + public synchronized void reset() throws IOException { + if (!markSupported) { + throw new UnsupportedOperationException("Mark not supported"); + } + if (mark < 0) { + throw new IOException("No position has been marked"); + } + if (position > (mark + readlimit)) { + throw new IOException("Marked position [" + mark + + "] is no longer valid - passed the read limit [" + + readlimit + "]"); + } + position = mark; + eof = false; + } + + /** + * Skip a specified number of bytes. + * + * @param numberOfBytes The number of bytes to skip. + * @return The number of bytes skipped or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public long skip(long numberOfBytes) throws IOException { + if (eof) { + throw new IOException("Skip after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position += numberOfBytes; + long returnLength = numberOfBytes; + if (position > size) { + returnLength = numberOfBytes - (position - size); + position = size; + } + return returnLength; + } + + /** + * Return a byte value for the read() method. + *

+ * This implementation returns zero. + * + * @return This implementation always returns zero. + */ + protected int processByte() { + // do nothing - overridable by subclass + return 0; + } + + /** + * Process the bytes for the read(byte[], offset, length) + * method. + *

+ * This implementation leaves the byte array unchanged. + * + * @param bytes The byte array + * @param offset The offset to start at. + * @param length The number of bytes. + */ + protected void processBytes(byte[] bytes, int offset, int length) { + // do nothing - overridable by subclass + } + + /** + * Handle End of File. + * + * @return -1 if throwEofException is + * set to false + * @throws EOFException if throwEofException is set + * to true. + */ + private int doEndOfFile() throws EOFException { + eof = true; + if (throwEofException) { + throw new EOFException(); + } + return -1; + } + +} diff --git a/src/org/apache/commons/io/input/NullReader.java b/src/org/apache/commons/io/input/NullReader.java new file mode 100644 index 000000000..159e39021 --- /dev/null +++ b/src/org/apache/commons/io/input/NullReader.java @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; + +/** + * A functional, light weight {@link Reader} that emulates + * a reader of a specified size. + *

+ * This implementation provides a light weight + * object for testing with an {@link Reader} + * where the contents don't matter. + *

+ * One use case would be for testing the handling of + * large {@link Reader} as it can emulate that + * scenario without the overhead of actually processing + * large numbers of characters - significantly speeding up + * test execution times. + *

+ * This implementation returns a space from the method that + * reads a character and leaves the array unchanged in the read + * methods that are passed a character array. + * If alternative data is required the processChar() and + * processChars() methods can be implemented to generate + * data, for example: + * + *

+ *  public class TestReader extends NullReader {
+ *      public TestReader(int size) {
+ *          super(size);
+ *      }
+ *      protected char processChar() {
+ *          return ... // return required value here
+ *      }
+ *      protected void processChars(char[] chars, int offset, int length) {
+ *          for (int i = offset; i < length; i++) {
+ *              chars[i] = ... // set array value here
+ *          }
+ *      }
+ *  }
+ * 
+ * + * @since Commons IO 1.3 + * @version $Revision: 463529 $ + */ +public class NullReader extends Reader { + + private long size; + private long position; + private long mark = -1; + private long readlimit; + private boolean eof; + private boolean throwEofException; + private boolean markSupported; + + /** + * Create a {@link Reader} that emulates a specified size + * which supports marking and does not throw EOFException. + * + * @param size The size of the reader to emulate. + */ + public NullReader(long size) { + this(size, true, false); + } + + /** + * Create a {@link Reader} that emulates a specified + * size with option settings. + * + * @param size The size of the reader to emulate. + * @param markSupported Whether this instance will support + * the mark() functionality. + * @param throwEofException Whether this implementation + * will throw an {@link EOFException} or return -1 when the + * end of file is reached. + */ + public NullReader(long size, boolean markSupported, boolean throwEofException) { + this.size = size; + this.markSupported = markSupported; + this.throwEofException = throwEofException; + } + + /** + * Return the current position. + * + * @return the current position. + */ + public long getPosition() { + return position; + } + + /** + * Return the size this {@link Reader} emulates. + * + * @return The size of the reader to emulate. + */ + public long getSize() { + return size; + } + + /** + * Close this Reader - resets the internal state to + * the initial values. + * + * @throws IOException If an error occurs. + */ + public void close() throws IOException { + eof = false; + position = 0; + mark = -1; + } + + /** + * Mark the current position. + * + * @param readlimit The number of characters before this marked position + * is invalid. + * @throws UnsupportedOperationException if mark is not supported. + */ + public synchronized void mark(int readlimit) { + if (!markSupported) { + throw new UnsupportedOperationException("Mark not supported"); + } + mark = position; + this.readlimit = readlimit; + } + + /** + * Indicates whether mark is supported. + * + * @return Whether mark is supported or not. + */ + public boolean markSupported() { + return markSupported; + } + + /** + * Read a character. + * + * @return Either The character value returned by processChar() + * or -1 if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read() throws IOException { + if (eof) { + throw new IOException("Read after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position++; + return processChar(); + } + + /** + * Read some characters into the specified array. + * + * @param chars The character array to read into + * @return The number of characters read or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read(char[] chars) throws IOException { + return read(chars, 0, chars.length); + } + + /** + * Read the specified number characters into an array. + * + * @param chars The character array to read into. + * @param offset The offset to start reading characters into. + * @param length The number of characters to read. + * @return The number of characters read or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public int read(char[] chars, int offset, int length) throws IOException { + if (eof) { + throw new IOException("Read after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position += length; + int returnLength = length; + if (position > size) { + returnLength = length - (int)(position - size); + position = size; + } + processChars(chars, offset, returnLength); + return returnLength; + } + + /** + * Reset the stream to the point when mark was last called. + * + * @throws UnsupportedOperationException if mark is not supported. + * @throws IOException If no position has been marked + * or the read limit has been exceed since the last position was + * marked. + */ + public synchronized void reset() throws IOException { + if (!markSupported) { + throw new UnsupportedOperationException("Mark not supported"); + } + if (mark < 0) { + throw new IOException("No position has been marked"); + } + if (position > (mark + readlimit)) { + throw new IOException("Marked position [" + mark + + "] is no longer valid - passed the read limit [" + + readlimit + "]"); + } + position = mark; + eof = false; + } + + /** + * Skip a specified number of characters. + * + * @param numberOfChars The number of characters to skip. + * @return The number of characters skipped or -1 + * if the end of file has been reached and + * throwEofException is set to false. + * @throws EOFException if the end of file is reached and + * throwEofException is set to true. + * @throws IOException if trying to read past the end of file. + */ + public long skip(long numberOfChars) throws IOException { + if (eof) { + throw new IOException("Skip after end of file"); + } + if (position == size) { + return doEndOfFile(); + } + position += numberOfChars; + long returnLength = numberOfChars; + if (position > size) { + returnLength = numberOfChars - (position - size); + position = size; + } + return returnLength; + } + + /** + * Return a character value for the read() method. + *

+ * This implementation returns zero. + * + * @return This implementation always returns zero. + */ + protected int processChar() { + // do nothing - overridable by subclass + return 0; + } + + /** + * Process the characters for the read(char[], offset, length) + * method. + *

+ * This implementation leaves the character array unchanged. + * + * @param chars The character array + * @param offset The offset to start at. + * @param length The number of characters. + */ + protected void processChars(char[] chars, int offset, int length) { + // do nothing - overridable by subclass + } + + /** + * Handle End of File. + * + * @return -1 if throwEofException is + * set to false + * @throws EOFException if throwEofException is set + * to true. + */ + private int doEndOfFile() throws EOFException { + eof = true; + if (throwEofException) { + throw new EOFException(); + } + return -1; + } + +} diff --git a/src/org/apache/commons/io/input/ProxyInputStream.java b/src/org/apache/commons/io/input/ProxyInputStream.java new file mode 100644 index 000000000..a08ad92d0 --- /dev/null +++ b/src/org/apache/commons/io/input/ProxyInputStream.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. + *

+ * It is an alternative base class to FilterInputStream + * to increase reusability, because FilterInputStream changes the + * methods being called, such as read(byte[]) to read(byte[], int, int). + * + * @author Stephen Colebourne + * @version $Id: ProxyInputStream.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public abstract class ProxyInputStream extends FilterInputStream { + + /** + * Constructs a new ProxyInputStream. + * + * @param proxy the InputStream to delegate to + */ + public ProxyInputStream(InputStream proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'in' + } + + /** + * Invokes the delegate's read() method. + * @return the byte read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read() throws IOException { + return in.read(); + } + + /** + * Invokes the delegate's read(byte[]) method. + * @param bts the buffer to read the bytes into + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read(byte[] bts) throws IOException { + return in.read(bts); + } + + /** + * Invokes the delegate's read(byte[], int, int) method. + * @param bts the buffer to read the bytes into + * @param st The start offset + * @param end The number of bytes to read + * @return the number of bytes read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read(byte[] bts, int st, int end) throws IOException { + return in.read(bts, st, end); + } + + /** + * Invokes the delegate's skip(long) method. + * @param ln the number of bytes to skip + * @return the number of bytes to skipped or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public long skip(long ln) throws IOException { + return in.skip(ln); + } + + /** + * Invokes the delegate's available() method. + * @return the number of available bytes + * @throws IOException if an I/O error occurs + */ + public int available() throws IOException { + return in.available(); + } + + /** + * Invokes the delegate's close() method. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + in.close(); + } + + /** + * Invokes the delegate's mark(int) method. + * @param idx read ahead limit + */ + public synchronized void mark(int idx) { + in.mark(idx); + } + + /** + * Invokes the delegate's reset() method. + * @throws IOException if an I/O error occurs + */ + public synchronized void reset() throws IOException { + in.reset(); + } + + /** + * Invokes the delegate's markSupported() method. + * @return true if mark is supported, otherwise false + */ + public boolean markSupported() { + return in.markSupported(); + } + +} diff --git a/src/org/apache/commons/io/input/ProxyReader.java b/src/org/apache/commons/io/input/ProxyReader.java new file mode 100644 index 000000000..d55290f5a --- /dev/null +++ b/src/org/apache/commons/io/input/ProxyReader.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.FilterReader; +import java.io.IOException; +import java.io.Reader; + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. + *

+ * It is an alternative base class to FilterReader + * to increase reusability, because FilterReader changes the + * methods being called, such as read(char[]) to read(char[], int, int). + * + * @author Stephen Colebourne + * @version $Id: ProxyReader.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public abstract class ProxyReader extends FilterReader { + + /** + * Constructs a new ProxyReader. + * + * @param proxy the Reader to delegate to + */ + public ProxyReader(Reader proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'in' + } + + /** + * Invokes the delegate's read() method. + * @return the character read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read() throws IOException { + return in.read(); + } + + /** + * Invokes the delegate's read(char[]) method. + * @param chr the buffer to read the characters into + * @return the number of characters read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read(char[] chr) throws IOException { + return in.read(chr); + } + + /** + * Invokes the delegate's read(char[], int, int) method. + * @param chr the buffer to read the characters into + * @param st The start offset + * @param end The number of bytes to read + * @return the number of characters read or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public int read(char[] chr, int st, int end) throws IOException { + return in.read(chr, st, end); + } + + /** + * Invokes the delegate's skip(long) method. + * @param ln the number of bytes to skip + * @return the number of bytes to skipped or -1 if the end of stream + * @throws IOException if an I/O error occurs + */ + public long skip(long ln) throws IOException { + return in.skip(ln); + } + + /** + * Invokes the delegate's ready() method. + * @return true if the stream is ready to be read + * @throws IOException if an I/O error occurs + */ + public boolean ready() throws IOException { + return in.ready(); + } + + /** + * Invokes the delegate's close() method. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + in.close(); + } + + /** + * Invokes the delegate's mark(int) method. + * @param idx read ahead limit + * @throws IOException if an I/O error occurs + */ + public synchronized void mark(int idx) throws IOException { + in.mark(idx); + } + + /** + * Invokes the delegate's reset() method. + * @throws IOException if an I/O error occurs + */ + public synchronized void reset() throws IOException { + in.reset(); + } + + /** + * Invokes the delegate's markSupported() method. + * @return true if mark is supported, otherwise false + */ + public boolean markSupported() { + return in.markSupported(); + } + +} diff --git a/src/org/apache/commons/io/input/SwappedDataInputStream.java b/src/org/apache/commons/io/input/SwappedDataInputStream.java new file mode 100644 index 000000000..5b65b1eee --- /dev/null +++ b/src/org/apache/commons/io/input/SwappedDataInputStream.java @@ -0,0 +1,251 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.DataInput; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.EndianUtils; + +/** + * DataInput for systems relying on little endian data formats. + * When read, values will be changed from little endian to big + * endian formats for internal usage. + *

+ * Origin of code: Avalon Excalibur (IO) + * + * @author Peter Donald + * @version CVS $Revision: 610010 $ $Date: 2008-01-08 14:50:59 +0000 (Tue, 08 Jan 2008) $ + */ +public class SwappedDataInputStream extends ProxyInputStream + implements DataInput +{ + + /** + * Constructs a SwappedDataInputStream. + * + * @param input InputStream to read from + */ + public SwappedDataInputStream( InputStream input ) + { + super( input ); + } + + /** + * Return {@link #readByte()} == 0 + * @return the true if the byte read is zero, otherwise false + * @throws IOException if an I/O error occurs + * @throws EOFException if an end of file is reached unexpectedly + */ + public boolean readBoolean() + throws IOException, EOFException + { + return ( 0 == readByte() ); + } + + /** + * Invokes the delegate's read() method. + * @return the byte read or -1 if the end of stream + * @throws IOException if an I/O error occurs + * @throws EOFException if an end of file is reached unexpectedly + */ + public byte readByte() + throws IOException, EOFException + { + return (byte)in.read(); + } + + /** + * Reads a character delegating to {@link #readShort()}. + * @return the byte read or -1 if the end of stream + * @throws IOException if an I/O error occurs + * @throws EOFException if an end of file is reached unexpectedly + */ + public char readChar() + throws IOException, EOFException + { + return (char)readShort(); + } + + /** + * Delegates to {@link EndianUtils#readSwappedDouble(InputStream)}. + * @return the read long + * @throws IOException if an I/O error occurs + * @throws EOFException if an end of file is reached unexpectedly + */ + public double readDouble() + throws IOException, EOFException + { + return EndianUtils.readSwappedDouble( in ); + } + + /** + * Delegates to {@link EndianUtils#readSwappedFloat(InputStream)}. + * @return the read long + * @throws IOException if an I/O error occurs + * @throws EOFException if an end of file is reached unexpectedly + */ + public float readFloat() + throws IOException, EOFException + { + return EndianUtils.readSwappedFloat( in ); + } + + /** + * Invokes the delegate's read(byte[] data, int, int) method. + * + * @param data the buffer to read the bytes into + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public void readFully( byte[] data ) + throws IOException, EOFException + { + readFully( data, 0, data.length ); + } + + + /** + * Invokes the delegate's read(byte[] data, int, int) method. + * + * @param data the buffer to read the bytes into + * @param offset The start offset + * @param length The number of bytes to read + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public void readFully( byte[] data, int offset, int length ) + throws IOException, EOFException + { + int remaining = length; + + while( remaining > 0 ) + { + int location = offset + ( length - remaining ); + int count = read( data, location, remaining ); + + if( -1 == count ) + { + throw new EOFException(); + } + + remaining -= count; + } + } + + /** + * Delegates to {@link EndianUtils#readSwappedInteger(InputStream)}. + * @return the read long + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public int readInt() + throws IOException, EOFException + { + return EndianUtils.readSwappedInteger( in ); + } + + /** + * Not currently supported - throws {@link UnsupportedOperationException}. + * @return the line read + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public String readLine() + throws IOException, EOFException + { + throw new UnsupportedOperationException( + "Operation not supported: readLine()" ); + } + + /** + * Delegates to {@link EndianUtils#readSwappedLong(InputStream)}. + * @return the read long + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public long readLong() + throws IOException, EOFException + { + return EndianUtils.readSwappedLong( in ); + } + + /** + * Delegates to {@link EndianUtils#readSwappedShort(InputStream)}. + * @return the read long + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public short readShort() + throws IOException, EOFException + { + return EndianUtils.readSwappedShort( in ); + } + + /** + * Invokes the delegate's read() method. + * @return the byte read or -1 if the end of stream + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public int readUnsignedByte() + throws IOException, EOFException + { + return in.read(); + } + + /** + * Delegates to {@link EndianUtils#readSwappedUnsignedShort(InputStream)}. + * @return the read long + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public int readUnsignedShort() + throws IOException, EOFException + { + return EndianUtils.readSwappedUnsignedShort( in ); + } + + /** + * Not currently supported - throws {@link UnsupportedOperationException}. + * @return UTF String read + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public String readUTF() + throws IOException, EOFException + { + throw new UnsupportedOperationException( + "Operation not supported: readUTF()" ); + } + + /** + * Invokes the delegate's skip(int) method. + * @param count the number of bytes to skip + * @return the number of bytes to skipped or -1 if the end of stream + * @throws EOFException if an end of file is reached unexpectedly + * @throws IOException if an I/O error occurs + */ + public int skipBytes( int count ) + throws IOException, EOFException + { + return (int)in.skip( count ); + } + +} diff --git a/src/org/apache/commons/io/input/TeeInputStream.java b/src/org/apache/commons/io/input/TeeInputStream.java new file mode 100644 index 000000000..fed000ed6 --- /dev/null +++ b/src/org/apache/commons/io/input/TeeInputStream.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * InputStream proxy that transparently writes a copy of all bytes read + * from the proxied stream to a given OutputStream. Using {@link #skip(long)} + * or {@link #mark(int)}/{@link #reset()} on the stream will result on some + * bytes from the input stream being skipped or duplicated in the output + * stream. + *

+ * The proxied input stream is closed when the {@link #close()} method is + * called on this proxy. It is configurable whether the associated output + * stream will also closed. + * + * @version $Id: TeeInputStream.java 587913 2007-10-24 15:47:30Z niallp $ + * @since Commons IO 1.4 + */ +public class TeeInputStream extends ProxyInputStream { + + /** + * The output stream that will receive a copy of all bytes read from the + * proxied input stream. + */ + private final OutputStream branch; + + /** + * Flag for closing also the associated output stream when this + * stream is closed. + */ + private final boolean closeBranch; + + /** + * Creates a TeeInputStream that proxies the given {@link InputStream} + * and copies all read bytes to the given {@link OutputStream}. The given + * output stream will not be closed when this stream gets closed. + * + * @param input input stream to be proxied + * @param branch output stream that will receive a copy of all bytes read + */ + public TeeInputStream(InputStream input, OutputStream branch) { + this(input, branch, false); + } + + /** + * Creates a TeeInputStream that proxies the given {@link InputStream} + * and copies all read bytes to the given {@link OutputStream}. The given + * output stream will be closed when this stream gets closed if the + * closeBranch parameter is true. + * + * @param input input stream to be proxied + * @param branch output stream that will receive a copy of all bytes read + * @param closeBranch flag for closing also the output stream when this + * stream is closed + */ + public TeeInputStream( + InputStream input, OutputStream branch, boolean closeBranch) { + super(input); + this.branch = branch; + this.closeBranch = closeBranch; + } + + /** + * Closes the proxied input stream and, if so configured, the associated + * output stream. An exception thrown from one stream will not prevent + * closing of the other stream. + * + * @throws IOException if either of the streams could not be closed + */ + public void close() throws IOException { + try { + super.close(); + } finally { + if (closeBranch) { + branch.close(); + } + } + } + + /** + * Reads a single byte from the proxied input stream and writes it to + * the associated output stream. + * + * @return next byte from the stream, or -1 if the stream has ended + * @throws IOException if the stream could not be read (or written) + */ + public int read() throws IOException { + int ch = super.read(); + if (ch != -1) { + branch.write(ch); + } + return ch; + } + + /** + * Reads bytes from the proxied input stream and writes the read bytes + * to the associated output stream. + * + * @param bts byte buffer + * @param st start offset within the buffer + * @param end maximum number of bytes to read + * @return number of bytes read, or -1 if the stream has ended + * @throws IOException if the stream could not be read (or written) + */ + public int read(byte[] bts, int st, int end) throws IOException { + int n = super.read(bts, st, end); + if (n != -1) { + branch.write(bts, st, n); + } + return n; + } + + /** + * Reads bytes from the proxied input stream and writes the read bytes + * to the associated output stream. + * + * @param bts byte buffer + * @return number of bytes read, or -1 if the stream has ended + * @throws IOException if the stream could not be read (or written) + */ + public int read(byte[] bts) throws IOException { + int n = super.read(bts); + if (n != -1) { + branch.write(bts, 0, n); + } + return n; + } + +} diff --git a/src/org/apache/commons/io/input/package.html b/src/org/apache/commons/io/input/package.html new file mode 100644 index 000000000..9aa8b15ba --- /dev/null +++ b/src/org/apache/commons/io/input/package.html @@ -0,0 +1,25 @@ + + + + +

+This package provides implementations of input classes, such as +InputStream and Reader. +

+ + diff --git a/src/org/apache/commons/io/output/ByteArrayOutputStream.java b/src/org/apache/commons/io/output/ByteArrayOutputStream.java new file mode 100644 index 000000000..66d5e8dc3 --- /dev/null +++ b/src/org/apache/commons/io/output/ByteArrayOutputStream.java @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class implements an output stream in which the data is + * written into a byte array. The buffer automatically grows as data + * is written to it. + *

+ * The data can be retrieved using toByteArray() and + * toString(). + *

+ * Closing a ByteArrayOutputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + *

+ * This is an alternative implementation of the java.io.ByteArrayOutputStream + * class. The original implementation only allocates 32 bytes at the beginning. + * As this class is designed for heavy duty it starts at 1024 bytes. In contrast + * to the original it doesn't reallocate the whole memory block but allocates + * additional buffers. This way no buffers need to be garbage collected and + * the contents don't have to be copied to the new buffer. This class is + * designed to behave exactly like the original. The only exception is the + * deprecated toString(int) method that has been ignored. + * + * @author Jeremias Maerki + * @author Holger Hoffstatte + * @version $Id: ByteArrayOutputStream.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class ByteArrayOutputStream extends OutputStream { + + /** A singleton empty byte array. */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** The list of buffers, which grows and never reduces. */ + private List buffers = new ArrayList(); + /** The index of the current buffer. */ + private int currentBufferIndex; + /** The total count of bytes in all the filled buffers. */ + private int filledBufferSum; + /** The current buffer. */ + private byte[] currentBuffer; + /** The total count of bytes written. */ + private int count; + + /** + * Creates a new byte array output stream. The buffer capacity is + * initially 1024 bytes, though its size increases if necessary. + */ + public ByteArrayOutputStream() { + this(1024); + } + + /** + * Creates a new byte array output stream, with a buffer capacity of + * the specified size, in bytes. + * + * @param size the initial size + * @throws IllegalArgumentException if size is negative + */ + public ByteArrayOutputStream(int size) { + if (size < 0) { + throw new IllegalArgumentException( + "Negative initial size: " + size); + } + needNewBuffer(size); + } + + /** + * Return the appropriate byte[] buffer + * specified by index. + * + * @param index the index of the buffer required + * @return the buffer + */ + private byte[] getBuffer(int index) { + return (byte[]) buffers.get(index); + } + + /** + * Makes a new buffer available either by allocating + * a new one or re-cycling an existing one. + * + * @param newcount the size of the buffer if one is created + */ + private void needNewBuffer(int newcount) { + if (currentBufferIndex < buffers.size() - 1) { + //Recycling old buffer + filledBufferSum += currentBuffer.length; + + currentBufferIndex++; + currentBuffer = getBuffer(currentBufferIndex); + } else { + //Creating new buffer + int newBufferSize; + if (currentBuffer == null) { + newBufferSize = newcount; + filledBufferSum = 0; + } else { + newBufferSize = Math.max( + currentBuffer.length << 1, + newcount - filledBufferSum); + filledBufferSum += currentBuffer.length; + } + + currentBufferIndex++; + currentBuffer = new byte[newBufferSize]; + buffers.add(currentBuffer); + } + } + + /** + * Write the bytes to byte array. + * @param b the bytes to write + * @param off The start offset + * @param len The number of bytes to write + */ + public void write(byte[] b, int off, int len) { + if ((off < 0) + || (off > b.length) + || (len < 0) + || ((off + len) > b.length) + || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + synchronized (this) { + int newcount = count + len; + int remaining = len; + int inBufferPos = count - filledBufferSum; + while (remaining > 0) { + int part = Math.min(remaining, currentBuffer.length - inBufferPos); + System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part); + remaining -= part; + if (remaining > 0) { + needNewBuffer(newcount); + inBufferPos = 0; + } + } + count = newcount; + } + } + + /** + * Write a byte to byte array. + * @param b the byte to write + */ + public synchronized void write(int b) { + int inBufferPos = count - filledBufferSum; + if (inBufferPos == currentBuffer.length) { + needNewBuffer(count + 1); + inBufferPos = 0; + } + currentBuffer[inBufferPos] = (byte) b; + count++; + } + + /** + * Writes the entire contents of the specified input stream to this + * byte stream. Bytes from the input stream are read directly into the + * internal buffers of this streams. + * + * @param in the input stream to read from + * @return total number of bytes read from the input stream + * (and written to this stream) + * @throws IOException if an I/O error occurs while reading the input stream + * @since Commons IO 1.4 + */ + public synchronized int write(InputStream in) throws IOException { + int readCount = 0; + int inBufferPos = count - filledBufferSum; + int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); + while (n != -1) { + readCount += n; + inBufferPos += n; + count += n; + if (inBufferPos == currentBuffer.length) { + needNewBuffer(currentBuffer.length); + inBufferPos = 0; + } + n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos); + } + return readCount; + } + + /** + * Return the current size of the byte array. + * @return the current size of the byte array + */ + public synchronized int size() { + return count; + } + + /** + * Closing a ByteArrayOutputStream has no effect. The methods in + * this class can be called after the stream has been closed without + * generating an IOException. + * + * @throws IOException never (this method should not declare this exception + * but it has to now due to backwards compatability) + */ + public void close() throws IOException { + //nop + } + + /** + * @see java.io.ByteArrayOutputStream#reset() + */ + public synchronized void reset() { + count = 0; + filledBufferSum = 0; + currentBufferIndex = 0; + currentBuffer = getBuffer(currentBufferIndex); + } + + /** + * Writes the entire contents of this byte stream to the + * specified output stream. + * + * @param out the output stream to write to + * @throws IOException if an I/O error occurs, such as if the stream is closed + * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) + */ + public synchronized void writeTo(OutputStream out) throws IOException { + int remaining = count; + for (int i = 0; i < buffers.size(); i++) { + byte[] buf = getBuffer(i); + int c = Math.min(buf.length, remaining); + out.write(buf, 0, c); + remaining -= c; + if (remaining == 0) { + break; + } + } + } + + /** + * Gets the curent contents of this byte stream as a byte array. + * The result is independent of this stream. + * + * @return the current contents of this output stream, as a byte array + * @see java.io.ByteArrayOutputStream#toByteArray() + */ + public synchronized byte[] toByteArray() { + int remaining = count; + if (remaining == 0) { + return EMPTY_BYTE_ARRAY; + } + byte newbuf[] = new byte[remaining]; + int pos = 0; + for (int i = 0; i < buffers.size(); i++) { + byte[] buf = getBuffer(i); + int c = Math.min(buf.length, remaining); + System.arraycopy(buf, 0, newbuf, pos, c); + pos += c; + remaining -= c; + if (remaining == 0) { + break; + } + } + return newbuf; + } + + /** + * Gets the curent contents of this byte stream as a string. + * @return the contents of the byte array as a String + * @see java.io.ByteArrayOutputStream#toString() + */ + public String toString() { + return new String(toByteArray()); + } + + /** + * Gets the curent contents of this byte stream as a string + * using the specified encoding. + * + * @param enc the name of the character encoding + * @return the string converted from the byte array + * @throws UnsupportedEncodingException if the encoding is not supported + * @see java.io.ByteArrayOutputStream#toString(String) + */ + public String toString(String enc) throws UnsupportedEncodingException { + return new String(toByteArray(), enc); + } + +} diff --git a/src/org/apache/commons/io/output/CloseShieldOutputStream.java b/src/org/apache/commons/io/output/CloseShieldOutputStream.java new file mode 100644 index 000000000..63f44be40 --- /dev/null +++ b/src/org/apache/commons/io/output/CloseShieldOutputStream.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.OutputStream; + +/** + * Proxy stream that prevents the underlying output stream from being closed. + *

+ * This class is typically used in cases where an output stream needs to be + * passed to a component that wants to explicitly close the stream even if + * other components would still use the stream for output. + * + * @version $Id: CloseShieldOutputStream.java 587913 2007-10-24 15:47:30Z niallp $ + * @since Commons IO 1.4 + */ +public class CloseShieldOutputStream extends ProxyOutputStream { + + /** + * Creates a proxy that shields the given output stream from being + * closed. + * + * @param out underlying output stream + */ + public CloseShieldOutputStream(OutputStream out) { + super(out); + } + + /** + * Replaces the underlying output stream with a {@link ClosedOutputStream} + * sentinel. The original output stream will remain open, but this proxy + * will appear closed. + */ + public void close() { + out = new ClosedOutputStream(); + } + +} diff --git a/src/org/apache/commons/io/output/ClosedOutputStream.java b/src/org/apache/commons/io/output/ClosedOutputStream.java new file mode 100644 index 000000000..b585c0cf4 --- /dev/null +++ b/src/org/apache/commons/io/output/ClosedOutputStream.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Closed output stream. This stream throws an exception on all attempts to + * write something to the stream. + *

+ * Typically uses of this class include testing for corner cases in methods + * that accept an output stream and acting as a sentinel value instead of + * a null output stream. + * + * @version $Id: ClosedOutputStream.java 601751 2007-12-06 14:55:45Z niallp $ + * @since Commons IO 1.4 + */ +public class ClosedOutputStream extends OutputStream { + + /** + * A singleton. + */ + public static final ClosedOutputStream CLOSED_OUTPUT_STREAM = new ClosedOutputStream(); + + /** + * Throws an {@link IOException} to indicate that the stream is closed. + * + * @param b ignored + * @throws IOException always thrown + */ + public void write(int b) throws IOException { + throw new IOException("write(" + b + ") failed: stream is closed"); + } + +} diff --git a/src/org/apache/commons/io/output/CountingOutputStream.java b/src/org/apache/commons/io/output/CountingOutputStream.java new file mode 100644 index 000000000..672882860 --- /dev/null +++ b/src/org/apache/commons/io/output/CountingOutputStream.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A decorating output stream that counts the number of bytes that have passed + * through the stream so far. + *

+ * A typical use case would be during debugging, to ensure that data is being + * written as expected. + * + * @version $Id: CountingOutputStream.java 471628 2006-11-06 04:06:45Z bayard $ + */ +public class CountingOutputStream extends ProxyOutputStream { + + /** The count of bytes that have passed. */ + private long count; + + /** + * Constructs a new CountingOutputStream. + * + * @param out the OutputStream to write to + */ + public CountingOutputStream( OutputStream out ) { + super(out); + } + + //----------------------------------------------------------------------- + /** + * Writes the contents of the specified byte array to this output stream + * keeping count of the number of bytes written. + * + * @param b the bytes to write, not null + * @throws IOException if an I/O error occurs + * @see java.io.OutputStream#write(byte[]) + */ + public void write(byte[] b) throws IOException { + count += b.length; + super.write(b); + } + + /** + * Writes a portion of the specified byte array to this output stream + * keeping count of the number of bytes written. + * + * @param b the bytes to write, not null + * @param off the start offset in the buffer + * @param len the maximum number of bytes to write + * @throws IOException if an I/O error occurs + * @see java.io.OutputStream#write(byte[], int, int) + */ + public void write(byte[] b, int off, int len) throws IOException { + count += len; + super.write(b, off, len); + } + + /** + * Writes a single byte to the output stream adding to the count of the + * number of bytes written. + * + * @param b the byte to write + * @throws IOException if an I/O error occurs + * @see java.io.OutputStream#write(int) + */ + public void write(int b) throws IOException { + count++; + super.write(b); + } + + //----------------------------------------------------------------------- + /** + * The number of bytes that have passed through this stream. + *

+ * NOTE: From v1.3 this method throws an ArithmeticException if the + * count is greater than can be expressed by an int. + * See {@link #getByteCount()} for a method using a long. + * + * @return the number of bytes accumulated + * @throws ArithmeticException if the byte count is too large + */ + public synchronized int getCount() { + long result = getByteCount(); + if (result > Integer.MAX_VALUE) { + throw new ArithmeticException("The byte count " + result + " is too large to be converted to an int"); + } + return (int) result; + } + + /** + * Set the byte count back to 0. + *

+ * NOTE: From v1.3 this method throws an ArithmeticException if the + * count is greater than can be expressed by an int. + * See {@link #resetByteCount()} for a method using a long. + * + * @return the count previous to resetting + * @throws ArithmeticException if the byte count is too large + */ + public synchronized int resetCount() { + long result = resetByteCount(); + if (result > Integer.MAX_VALUE) { + throw new ArithmeticException("The byte count " + result + " is too large to be converted to an int"); + } + return (int) result; + } + + /** + * The number of bytes that have passed through this stream. + *

+ * NOTE: This method is an alternative for getCount(). + * It was added because that method returns an integer which will + * result in incorrect count for files over 2GB. + * + * @return the number of bytes accumulated + * @since Commons IO 1.3 + */ + public synchronized long getByteCount() { + return this.count; + } + + /** + * Set the byte count back to 0. + *

+ * NOTE: This method is an alternative for resetCount(). + * It was added because that method returns an integer which will + * result in incorrect count for files over 2GB. + * + * @return the count previous to resetting + * @since Commons IO 1.3 + */ + public synchronized long resetByteCount() { + long tmp = this.count; + this.count = 0; + return tmp; + } + +} diff --git a/src/org/apache/commons/io/output/DeferredFileOutputStream.java b/src/org/apache/commons/io/output/DeferredFileOutputStream.java new file mode 100644 index 000000000..b8a9e9607 --- /dev/null +++ b/src/org/apache/commons/io/output/DeferredFileOutputStream.java @@ -0,0 +1,269 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; + + +/** + * An output stream which will retain data in memory until a specified + * threshold is reached, and only then commit it to disk. If the stream is + * closed before the threshold is reached, the data will not be written to + * disk at all. + *

+ * This class originated in FileUpload processing. In this use case, you do + * not know in advance the size of the file being uploaded. If the file is small + * you want to store it in memory (for speed), but if the file is large you want + * to store it to file (to avoid memory issues). + * + * @author Martin Cooper + * @author gaxzerow + * + * @version $Id: DeferredFileOutputStream.java 606381 2007-12-22 02:03:16Z ggregory $ + */ +public class DeferredFileOutputStream + extends ThresholdingOutputStream +{ + + // ----------------------------------------------------------- Data members + + + /** + * The output stream to which data will be written prior to the theshold + * being reached. + */ + private ByteArrayOutputStream memoryOutputStream; + + + /** + * The output stream to which data will be written at any given time. This + * will always be one of memoryOutputStream or + * diskOutputStream. + */ + private OutputStream currentOutputStream; + + + /** + * The file to which output will be directed if the threshold is exceeded. + */ + private File outputFile; + + /** + * The temporary file prefix. + */ + private String prefix; + + /** + * The temporary file suffix. + */ + private String suffix; + + /** + * The directory to use for temporary files. + */ + private File directory; + + + /** + * True when close() has been called successfully. + */ + private boolean closed = false; + + // ----------------------------------------------------------- Constructors + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold, and save data to a file beyond that point. + * + * @param threshold The number of bytes at which to trigger an event. + * @param outputFile The file to which data is saved beyond the threshold. + */ + public DeferredFileOutputStream(int threshold, File outputFile) + { + super(threshold); + this.outputFile = outputFile; + + memoryOutputStream = new ByteArrayOutputStream(); + currentOutputStream = memoryOutputStream; + } + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold, and save data to a temporary file beyond that point. + * + * @param threshold The number of bytes at which to trigger an event. + * @param prefix Prefix to use for the temporary file. + * @param suffix Suffix to use for the temporary file. + * @param directory Temporary file directory. + * + * @since Commons IO 1.4 + */ + public DeferredFileOutputStream(int threshold, String prefix, String suffix, File directory) + { + this(threshold, (File)null); + if (prefix == null) { + throw new IllegalArgumentException("Temporary file prefix is missing"); + } + this.prefix = prefix; + this.suffix = suffix; + this.directory = directory; + } + + + // --------------------------------------- ThresholdingOutputStream methods + + + /** + * Returns the current output stream. This may be memory based or disk + * based, depending on the current state with respect to the threshold. + * + * @return The underlying output stream. + * + * @exception IOException if an error occurs. + */ + protected OutputStream getStream() throws IOException + { + return currentOutputStream; + } + + + /** + * Switches the underlying output stream from a memory based stream to one + * that is backed by disk. This is the point at which we realise that too + * much data is being written to keep in memory, so we elect to switch to + * disk-based storage. + * + * @exception IOException if an error occurs. + */ + protected void thresholdReached() throws IOException + { + if (prefix != null) { + outputFile = File.createTempFile(prefix, suffix, directory); + } + FileOutputStream fos = new FileOutputStream(outputFile); + memoryOutputStream.writeTo(fos); + currentOutputStream = fos; + memoryOutputStream = null; + } + + + // --------------------------------------------------------- Public methods + + + /** + * Determines whether or not the data for this output stream has been + * retained in memory. + * + * @return true if the data is available in memory; + * false otherwise. + */ + public boolean isInMemory() + { + return (!isThresholdExceeded()); + } + + + /** + * Returns the data for this output stream as an array of bytes, assuming + * that the data has been retained in memory. If the data was written to + * disk, this method returns null. + * + * @return The data for this output stream, or null if no such + * data is available. + */ + public byte[] getData() + { + if (memoryOutputStream != null) + { + return memoryOutputStream.toByteArray(); + } + return null; + } + + + /** + * Returns either the output file specified in the constructor or + * the temporary file created or null. + *

+ * If the constructor specifying the file is used then it returns that + * same output file, even when threashold has not been reached. + *

+ * If constructor specifying a temporary file prefix/suffix is used + * then the temporary file created once the threashold is reached is returned + * If the threshold was not reached then null is returned. + * + * @return The file for this output stream, or null if no such + * file exists. + */ + public File getFile() + { + return outputFile; + } + + + /** + * Closes underlying output stream, and mark this as closed + * + * @exception IOException if an error occurs. + */ + public void close() throws IOException + { + super.close(); + closed = true; + } + + + /** + * Writes the data from this output stream to the specified output stream, + * after it has been closed. + * + * @param out output stream to write to. + * @exception IOException if this stream is not yet closed or an error occurs. + */ + public void writeTo(OutputStream out) throws IOException + { + // we may only need to check if this is closed if we are working with a file + // but we should force the habit of closing wether we are working with + // a file or memory. + if (!closed) + { + throw new IOException("Stream not closed"); + } + + if(isInMemory()) + { + memoryOutputStream.writeTo(out); + } + else + { + FileInputStream fis = new FileInputStream(outputFile); + try { + IOUtils.copy(fis, out); + } finally { + IOUtils.closeQuietly(fis); + } + } + } +} diff --git a/src/org/apache/commons/io/output/DemuxOutputStream.java b/src/org/apache/commons/io/output/DemuxOutputStream.java new file mode 100644 index 000000000..086911118 --- /dev/null +++ b/src/org/apache/commons/io/output/DemuxOutputStream.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Data written to this stream is forwarded to a stream that has been associated + * with this thread. + * + * @author Peter Donald + * @version $Revision: 437567 $ $Date: 2006-08-28 07:39:07 +0100 (Mon, 28 Aug 2006) $ + */ +public class DemuxOutputStream + extends OutputStream +{ + private InheritableThreadLocal m_streams = new InheritableThreadLocal(); + + /** + * Bind the specified stream to the current thread. + * + * @param output the stream to bind + * @return the OutputStream that was previously active + */ + public OutputStream bindStream( OutputStream output ) + { + OutputStream stream = getStream(); + m_streams.set( output ); + return stream; + } + + /** + * Closes stream associated with current thread. + * + * @throws IOException if an error occurs + */ + public void close() + throws IOException + { + OutputStream output = getStream(); + if( null != output ) + { + output.close(); + } + } + + /** + * Flushes stream associated with current thread. + * + * @throws IOException if an error occurs + */ + public void flush() + throws IOException + { + OutputStream output = getStream(); + if( null != output ) + { + output.flush(); + } + } + + /** + * Writes byte to stream associated with current thread. + * + * @param ch the byte to write to stream + * @throws IOException if an error occurs + */ + public void write( int ch ) + throws IOException + { + OutputStream output = getStream(); + if( null != output ) + { + output.write( ch ); + } + } + + /** + * Utility method to retrieve stream bound to current thread (if any). + * + * @return the output stream + */ + private OutputStream getStream() + { + return (OutputStream)m_streams.get(); + } +} diff --git a/src/org/apache/commons/io/output/FileWriterWithEncoding.java b/src/org/apache/commons/io/output/FileWriterWithEncoding.java new file mode 100644 index 000000000..a8f89334b --- /dev/null +++ b/src/org/apache/commons/io/output/FileWriterWithEncoding.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +/** + * Writer of files that allows the encoding to be set. + *

+ * This class provides a simple alternative to FileWriter + * that allows an encoding to be set. Unfortunately, it cannot subclass + * FileWriter. + *

+ * By default, the file will be overwritten, but this may be changed to append. + *

+ * The encoding must be specified using either the name of the {@link Charset}, + * the {@link Charset}, or a {@link CharsetEncoder}. If the default encoding + * is required then use the {@link java.io.FileWriter} directly, rather than + * this implementation. + *

+ * + * + * @since Commons IO 1.4 + * @version $Id: FileWriterWithEncoding.java 611634 2008-01-13 20:35:00Z niallp $ + */ +public class FileWriterWithEncoding extends Writer { + // Cannot extend ProxyWriter, as requires writer to be + // known when super() is called + + /** The writer to decorate. */ + private final Writer out; + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, String encoding) throws IOException { + this(new File(filename), encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, String encoding, boolean append) throws IOException { + this(new File(filename), encoding, append); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, Charset encoding) throws IOException { + this(new File(filename), encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, Charset encoding, boolean append) throws IOException { + this(new File(filename), encoding, append); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, CharsetEncoder encoding) throws IOException { + this(new File(filename), encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param filename the name of the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file name or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(String filename, CharsetEncoder encoding, boolean append) throws IOException { + this(new File(filename), encoding, append); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, String encoding) throws IOException { + this(file, encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, String encoding, boolean append) throws IOException { + super(); + this.out = initWriter(file, encoding, append); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, Charset encoding) throws IOException { + this(file, encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, Charset encoding, boolean append) throws IOException { + super(); + this.out = initWriter(file, encoding, append); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, CharsetEncoder encoding) throws IOException { + this(file, encoding, false); + } + + /** + * Constructs a FileWriterWithEncoding with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file or encoding is null + * @throws IOException in case of an I/O error + */ + public FileWriterWithEncoding(File file, CharsetEncoder encoding, boolean append) throws IOException { + super(); + this.out = initWriter(file, encoding, append); + } + + //----------------------------------------------------------------------- + /** + * Initialise the wrapped file writer. + * Ensure that a cleanup occurs if the writer creation fails. + * + * @param file the file to be accessed + * @param encoding the encoding to use - may be Charset, CharsetEncoder or String + * @param append true to append + * @return the initialised writer + * @throws NullPointerException if the file or encoding is null + * @throws IOException if an error occurs + */ + private static Writer initWriter(File file, Object encoding, boolean append) throws IOException { + if (file == null) { + throw new NullPointerException("File is missing"); + } + if (encoding == null) { + throw new NullPointerException("Encoding is missing"); + } + boolean fileExistedAlready = file.exists(); + OutputStream stream = null; + Writer writer = null; + try { + stream = new FileOutputStream(file, append); + if (encoding instanceof Charset) { + writer = new OutputStreamWriter(stream, (Charset)encoding); + } else if (encoding instanceof CharsetEncoder) { + writer = new OutputStreamWriter(stream, (CharsetEncoder)encoding); + } else { + writer = new OutputStreamWriter(stream, (String)encoding); + } + } catch (IOException ex) { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(stream); + if (fileExistedAlready == false) { + FileUtils.deleteQuietly(file); + } + throw ex; + } catch (RuntimeException ex) { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(stream); + if (fileExistedAlready == false) { + FileUtils.deleteQuietly(file); + } + throw ex; + } + return writer; + } + + //----------------------------------------------------------------------- + /** + * Write a character. + * @param idx the character to write + * @throws IOException if an I/O error occurs + */ + public void write(int idx) throws IOException { + out.write(idx); + } + + /** + * Write the characters from an array. + * @param chr the characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr) throws IOException { + out.write(chr); + } + + /** + * Write the specified characters from an array. + * @param chr the characters to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr, int st, int end) throws IOException { + out.write(chr, st, end); + } + + /** + * Write the characters from a string. + * @param str the string to write + * @throws IOException if an I/O error occurs + */ + public void write(String str) throws IOException { + out.write(str); + } + + /** + * Write the specified characters from a string. + * @param str the string to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(String str, int st, int end) throws IOException { + out.write(str, st, end); + } + + /** + * Flush the stream. + * @throws IOException if an I/O error occurs + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Close the stream. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + out.close(); + } +} diff --git a/src/org/apache/commons/io/output/LockableFileWriter.java b/src/org/apache/commons/io/output/LockableFileWriter.java new file mode 100644 index 000000000..6b10bd282 --- /dev/null +++ b/src/org/apache/commons/io/output/LockableFileWriter.java @@ -0,0 +1,333 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +/** + * FileWriter that will create and honor lock files to allow simple + * cross thread file lock handling. + *

+ * This class provides a simple alternative to FileWriter + * that will use a lock file to prevent duplicate writes. + *

+ * By default, the file will be overwritten, but this may be changed to append. + * The lock directory may be specified, but defaults to the system property + * java.io.tmpdir. + * The encoding may also be specified, and defaults to the platform default. + * + * @author Scott Sanders + * @author Michael Salmon + * @author Jon S. Stevens + * @author Daniel Rall + * @author Stephen Colebourne + * @author Andy Lehane + * @version $Id: LockableFileWriter.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class LockableFileWriter extends Writer { + // Cannot extend ProxyWriter, as requires writer to be + // known when super() is called + + /** The extension for the lock file. */ + private static final String LCK = ".lck"; + + /** The writer to decorate. */ + private final Writer out; + /** The lock file. */ + private final File lockFile; + + /** + * Constructs a LockableFileWriter. + * If the file exists, it is overwritten. + * + * @param fileName the file to write to, not null + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(String fileName) throws IOException { + this(fileName, false, null); + } + + /** + * Constructs a LockableFileWriter. + * + * @param fileName file to write to, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(String fileName, boolean append) throws IOException { + this(fileName, append, null); + } + + /** + * Constructs a LockableFileWriter. + * + * @param fileName the file to write to, not null + * @param append true if content should be appended, false to overwrite + * @param lockDir the directory in which the lock file should be held + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(String fileName, boolean append, String lockDir) throws IOException { + this(new File(fileName), append, lockDir); + } + + /** + * Constructs a LockableFileWriter. + * If the file exists, it is overwritten. + * + * @param file the file to write to, not null + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(File file) throws IOException { + this(file, false, null); + } + + /** + * Constructs a LockableFileWriter. + * + * @param file the file to write to, not null + * @param append true if content should be appended, false to overwrite + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(File file, boolean append) throws IOException { + this(file, append, null); + } + + /** + * Constructs a LockableFileWriter. + * + * @param file the file to write to, not null + * @param append true if content should be appended, false to overwrite + * @param lockDir the directory in which the lock file should be held + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(File file, boolean append, String lockDir) throws IOException { + this(file, null, append, lockDir); + } + + /** + * Constructs a LockableFileWriter with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(File file, String encoding) throws IOException { + this(file, encoding, false, null); + } + + /** + * Constructs a LockableFileWriter with a file encoding. + * + * @param file the file to write to, not null + * @param encoding the encoding to use, null means platform default + * @param append true if content should be appended, false to overwrite + * @param lockDir the directory in which the lock file should be held + * @throws NullPointerException if the file is null + * @throws IOException in case of an I/O error + */ + public LockableFileWriter(File file, String encoding, boolean append, + String lockDir) throws IOException { + super(); + // init file to create/append + file = file.getAbsoluteFile(); + if (file.getParentFile() != null) { + FileUtils.forceMkdir(file.getParentFile()); + } + if (file.isDirectory()) { + throw new IOException("File specified is a directory"); + } + + // init lock file + if (lockDir == null) { + lockDir = System.getProperty("java.io.tmpdir"); + } + File lockDirFile = new File(lockDir); + FileUtils.forceMkdir(lockDirFile); + testLockDir(lockDirFile); + lockFile = new File(lockDirFile, file.getName() + LCK); + + // check if locked + createLock(); + + // init wrapped writer + out = initWriter(file, encoding, append); + } + + //----------------------------------------------------------------------- + /** + * Tests that we can write to the lock directory. + * + * @param lockDir the File representing the lock directory + * @throws IOException if we cannot write to the lock directory + * @throws IOException if we cannot find the lock file + */ + private void testLockDir(File lockDir) throws IOException { + if (!lockDir.exists()) { + throw new IOException( + "Could not find lockDir: " + lockDir.getAbsolutePath()); + } + if (!lockDir.canWrite()) { + throw new IOException( + "Could not write to lockDir: " + lockDir.getAbsolutePath()); + } + } + + /** + * Creates the lock file. + * + * @throws IOException if we cannot create the file + */ + private void createLock() throws IOException { + synchronized (LockableFileWriter.class) { + if (!lockFile.createNewFile()) { + throw new IOException("Can't write file, lock " + + lockFile.getAbsolutePath() + " exists"); + } + lockFile.deleteOnExit(); + } + } + + /** + * Initialise the wrapped file writer. + * Ensure that a cleanup occurs if the writer creation fails. + * + * @param file the file to be accessed + * @param encoding the encoding to use + * @param append true to append + * @return The initialised writer + * @throws IOException if an error occurs + */ + private Writer initWriter(File file, String encoding, boolean append) throws IOException { + boolean fileExistedAlready = file.exists(); + OutputStream stream = null; + Writer writer = null; + try { + if (encoding == null) { + writer = new FileWriter(file.getAbsolutePath(), append); + } else { + stream = new FileOutputStream(file.getAbsolutePath(), append); + writer = new OutputStreamWriter(stream, encoding); + } + } catch (IOException ex) { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(stream); + lockFile.delete(); + if (fileExistedAlready == false) { + file.delete(); + } + throw ex; + } catch (RuntimeException ex) { + IOUtils.closeQuietly(writer); + IOUtils.closeQuietly(stream); + lockFile.delete(); + if (fileExistedAlready == false) { + file.delete(); + } + throw ex; + } + return writer; + } + + //----------------------------------------------------------------------- + /** + * Closes the file writer. + * + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + try { + out.close(); + } finally { + lockFile.delete(); + } + } + + //----------------------------------------------------------------------- + /** + * Write a character. + * @param idx the character to write + * @throws IOException if an I/O error occurs + */ + public void write(int idx) throws IOException { + out.write(idx); + } + + /** + * Write the characters from an array. + * @param chr the characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr) throws IOException { + out.write(chr); + } + + /** + * Write the specified characters from an array. + * @param chr the characters to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr, int st, int end) throws IOException { + out.write(chr, st, end); + } + + /** + * Write the characters from a string. + * @param str the string to write + * @throws IOException if an I/O error occurs + */ + public void write(String str) throws IOException { + out.write(str); + } + + /** + * Write the specified characters from a string. + * @param str the string to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(String str, int st, int end) throws IOException { + out.write(str, st, end); + } + + /** + * Flush the stream. + * @throws IOException if an I/O error occurs + */ + public void flush() throws IOException { + out.flush(); + } + +} diff --git a/src/org/apache/commons/io/output/NullOutputStream.java b/src/org/apache/commons/io/output/NullOutputStream.java new file mode 100644 index 000000000..7e3cdaf2c --- /dev/null +++ b/src/org/apache/commons/io/output/NullOutputStream.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * This OutputStream writes all data to the famous /dev/null. + *

+ * This output stream has no destination (file/socket etc.) and all + * bytes written to it are ignored and lost. + * + * @author Jeremias Maerki + * @version $Id: NullOutputStream.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class NullOutputStream extends OutputStream { + + /** + * A singleton. + */ + public static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream(); + + /** + * Does nothing - output to /dev/null. + * @param b The bytes to write + * @param off The start offset + * @param len The number of bytes to write + */ + public void write(byte[] b, int off, int len) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param b The byte to write + */ + public void write(int b) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param b The bytes to write + * @throws IOException never + */ + public void write(byte[] b) throws IOException { + //to /dev/null + } + +} diff --git a/src/org/apache/commons/io/output/NullWriter.java b/src/org/apache/commons/io/output/NullWriter.java new file mode 100644 index 000000000..aed52aba8 --- /dev/null +++ b/src/org/apache/commons/io/output/NullWriter.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.Writer; + +/** + * This {@link Writer} writes all data to the famous /dev/null. + *

+ * This Writer has no destination (file/socket etc.) and all + * characters written to it are ignored and lost. + * + * @version $Id: NullWriter.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class NullWriter extends Writer { + + /** + * A singleton. + */ + public static final NullWriter NULL_WRITER = new NullWriter(); + + /** + * Constructs a new NullWriter. + */ + public NullWriter() { + } + + /** + * Does nothing - output to /dev/null. + * @param idx The character to write + */ + public void write(int idx) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param chr The characters to write + */ + public void write(char[] chr) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param chr The characters to write + * @param st The start offset + * @param end The number of characters to write + */ + public void write(char[] chr, int st, int end) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param str The string to write + */ + public void write(String str) { + //to /dev/null + } + + /** + * Does nothing - output to /dev/null. + * @param str The string to write + * @param st The start offset + * @param end The number of characters to write + */ + public void write(String str, int st, int end) { + //to /dev/null + } + + /** @see java.io.Writer#flush() */ + public void flush() { + //to /dev/null + } + + /** @see java.io.Writer#close() */ + public void close() { + //to /dev/null + } + +} diff --git a/src/org/apache/commons/io/output/ProxyOutputStream.java b/src/org/apache/commons/io/output/ProxyOutputStream.java new file mode 100644 index 000000000..b63d72317 --- /dev/null +++ b/src/org/apache/commons/io/output/ProxyOutputStream.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. It is an alternative base class to FilterOutputStream + * to increase reusability. + * + * @author Stephen Colebourne + * @version $Id: ProxyOutputStream.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class ProxyOutputStream extends FilterOutputStream { + + /** + * Constructs a new ProxyOutputStream. + * + * @param proxy the OutputStream to delegate to + */ + public ProxyOutputStream(OutputStream proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'out' + } + + /** + * Invokes the delegate's write(int) method. + * @param idx the byte to write + * @throws IOException if an I/O error occurs + */ + public void write(int idx) throws IOException { + out.write(idx); + } + + /** + * Invokes the delegate's write(byte[]) method. + * @param bts the bytes to write + * @throws IOException if an I/O error occurs + */ + public void write(byte[] bts) throws IOException { + out.write(bts); + } + + /** + * Invokes the delegate's write(byte[]) method. + * @param bts the bytes to write + * @param st The start offset + * @param end The number of bytes to write + * @throws IOException if an I/O error occurs + */ + public void write(byte[] bts, int st, int end) throws IOException { + out.write(bts, st, end); + } + + /** + * Invokes the delegate's flush() method. + * @throws IOException if an I/O error occurs + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Invokes the delegate's close() method. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + out.close(); + } + +} diff --git a/src/org/apache/commons/io/output/ProxyWriter.java b/src/org/apache/commons/io/output/ProxyWriter.java new file mode 100644 index 000000000..fbec62885 --- /dev/null +++ b/src/org/apache/commons/io/output/ProxyWriter.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * A Proxy stream which acts as expected, that is it passes the method + * calls on to the proxied stream and doesn't change which methods are + * being called. It is an alternative base class to FilterWriter + * to increase reusability, because FilterWriter changes the + * methods being called, such as write(char[]) to write(char[], int, int) + * and write(String) to write(String, int, int). + * + * @author Stephen Colebourne + * @version $Id: ProxyWriter.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class ProxyWriter extends FilterWriter { + + /** + * Constructs a new ProxyWriter. + * + * @param proxy the Writer to delegate to + */ + public ProxyWriter(Writer proxy) { + super(proxy); + // the proxy is stored in a protected superclass variable named 'out' + } + + /** + * Invokes the delegate's write(int) method. + * @param idx the character to write + * @throws IOException if an I/O error occurs + */ + public void write(int idx) throws IOException { + out.write(idx); + } + + /** + * Invokes the delegate's write(char[]) method. + * @param chr the characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr) throws IOException { + out.write(chr); + } + + /** + * Invokes the delegate's write(char[], int, int) method. + * @param chr the characters to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(char[] chr, int st, int end) throws IOException { + out.write(chr, st, end); + } + + /** + * Invokes the delegate's write(String) method. + * @param str the string to write + * @throws IOException if an I/O error occurs + */ + public void write(String str) throws IOException { + out.write(str); + } + + /** + * Invokes the delegate's write(String) method. + * @param str the string to write + * @param st The start offset + * @param end The number of characters to write + * @throws IOException if an I/O error occurs + */ + public void write(String str, int st, int end) throws IOException { + out.write(str, st, end); + } + + /** + * Invokes the delegate's flush() method. + * @throws IOException if an I/O error occurs + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Invokes the delegate's close() method. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + out.close(); + } + +} diff --git a/src/org/apache/commons/io/output/TeeOutputStream.java b/src/org/apache/commons/io/output/TeeOutputStream.java new file mode 100644 index 000000000..ee957fb3b --- /dev/null +++ b/src/org/apache/commons/io/output/TeeOutputStream.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Classic splitter of OutputStream. Named after the unix 'tee' + * command. It allows a stream to be branched off so there + * are now two streams. + * + * @version $Id: TeeOutputStream.java 610010 2008-01-08 14:50:59Z niallp $ + */ +public class TeeOutputStream extends ProxyOutputStream { + + /** the second OutputStream to write to */ + protected OutputStream branch; + + /** + * Constructs a TeeOutputStream. + * @param out the main OutputStream + * @param branch the second OutputStream + */ + public TeeOutputStream( OutputStream out, OutputStream branch ) { + super(out); + this.branch = branch; + } + + /** + * Write the bytes to both streams. + * @param b the bytes to write + * @throws IOException if an I/O error occurs + */ + public synchronized void write(byte[] b) throws IOException { + super.write(b); + this.branch.write(b); + } + + /** + * Write the specified bytes to both streams. + * @param b the bytes to write + * @param off The start offset + * @param len The number of bytes to write + * @throws IOException if an I/O error occurs + */ + public synchronized void write(byte[] b, int off, int len) throws IOException { + super.write(b, off, len); + this.branch.write(b, off, len); + } + + /** + * Write a byte to both streams. + * @param b the byte to write + * @throws IOException if an I/O error occurs + */ + public synchronized void write(int b) throws IOException { + super.write(b); + this.branch.write(b); + } + + /** + * Flushes both streams. + * @throws IOException if an I/O error occurs + */ + public void flush() throws IOException { + super.flush(); + this.branch.flush(); + } + + /** + * Closes both streams. + * @throws IOException if an I/O error occurs + */ + public void close() throws IOException { + super.close(); + this.branch.close(); + } + +} diff --git a/src/org/apache/commons/io/output/ThresholdingOutputStream.java b/src/org/apache/commons/io/output/ThresholdingOutputStream.java new file mode 100644 index 000000000..fa69a804c --- /dev/null +++ b/src/org/apache/commons/io/output/ThresholdingOutputStream.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.output; + +import java.io.IOException; +import java.io.OutputStream; + + +/** + * An output stream which triggers an event when a specified number of bytes of + * data have been written to it. The event can be used, for example, to throw + * an exception if a maximum has been reached, or to switch the underlying + * stream type when the threshold is exceeded. + *

+ * This class overrides all OutputStream methods. However, these + * overrides ultimately call the corresponding methods in the underlying output + * stream implementation. + *

+ * NOTE: This implementation may trigger the event before the threshold + * is actually reached, since it triggers when a pending write operation would + * cause the threshold to be exceeded. + * + * @author Martin Cooper + * + * @version $Id: ThresholdingOutputStream.java 540714 2007-05-22 19:39:44Z niallp $ + */ +public abstract class ThresholdingOutputStream + extends OutputStream +{ + + // ----------------------------------------------------------- Data members + + + /** + * The threshold at which the event will be triggered. + */ + private int threshold; + + + /** + * The number of bytes written to the output stream. + */ + private long written; + + + /** + * Whether or not the configured threshold has been exceeded. + */ + private boolean thresholdExceeded; + + + // ----------------------------------------------------------- Constructors + + + /** + * Constructs an instance of this class which will trigger an event at the + * specified threshold. + * + * @param threshold The number of bytes at which to trigger an event. + */ + public ThresholdingOutputStream(int threshold) + { + this.threshold = threshold; + } + + + // --------------------------------------------------- OutputStream methods + + + /** + * Writes the specified byte to this output stream. + * + * @param b The byte to be written. + * + * @exception IOException if an error occurs. + */ + public void write(int b) throws IOException + { + checkThreshold(1); + getStream().write(b); + written++; + } + + + /** + * Writes b.length bytes from the specified byte array to this + * output stream. + * + * @param b The array of bytes to be written. + * + * @exception IOException if an error occurs. + */ + public void write(byte b[]) throws IOException + { + checkThreshold(b.length); + getStream().write(b); + written += b.length; + } + + + /** + * Writes len bytes from the specified byte array starting at + * offset off to this output stream. + * + * @param b The byte array from which the data will be written. + * @param off The start offset in the byte array. + * @param len The number of bytes to write. + * + * @exception IOException if an error occurs. + */ + public void write(byte b[], int off, int len) throws IOException + { + checkThreshold(len); + getStream().write(b, off, len); + written += len; + } + + + /** + * Flushes this output stream and forces any buffered output bytes to be + * written out. + * + * @exception IOException if an error occurs. + */ + public void flush() throws IOException + { + getStream().flush(); + } + + + /** + * Closes this output stream and releases any system resources associated + * with this stream. + * + * @exception IOException if an error occurs. + */ + public void close() throws IOException + { + try + { + flush(); + } + catch (IOException ignored) + { + // ignore + } + getStream().close(); + } + + + // --------------------------------------------------------- Public methods + + + /** + * Returns the threshold, in bytes, at which an event will be triggered. + * + * @return The threshold point, in bytes. + */ + public int getThreshold() + { + return threshold; + } + + + /** + * Returns the number of bytes that have been written to this output stream. + * + * @return The number of bytes written. + */ + public long getByteCount() + { + return written; + } + + + /** + * Determines whether or not the configured threshold has been exceeded for + * this output stream. + * + * @return true if the threshold has been reached; + * false otherwise. + */ + public boolean isThresholdExceeded() + { + return (written > threshold); + } + + + // ------------------------------------------------------ Protected methods + + + /** + * Checks to see if writing the specified number of bytes would cause the + * configured threshold to be exceeded. If so, triggers an event to allow + * a concrete implementation to take action on this. + * + * @param count The number of bytes about to be written to the underlying + * output stream. + * + * @exception IOException if an error occurs. + */ + protected void checkThreshold(int count) throws IOException + { + if (!thresholdExceeded && (written + count > threshold)) + { + thresholdExceeded = true; + thresholdReached(); + } + } + + /** + * Resets the byteCount to zero. You can call this from + * {@link #thresholdReached()} if you want the event to be triggered again. + */ + protected void resetByteCount() + { + this.thresholdExceeded = false; + this.written = 0; + } + + // ------------------------------------------------------- Abstract methods + + + /** + * Returns the underlying output stream, to which the corresponding + * OutputStream methods in this class will ultimately delegate. + * + * @return The underlying output stream. + * + * @exception IOException if an error occurs. + */ + protected abstract OutputStream getStream() throws IOException; + + + /** + * Indicates that the configured threshold has been reached, and that a + * subclass should take whatever action necessary on this event. This may + * include changing the underlying output stream. + * + * @exception IOException if an error occurs. + */ + protected abstract void thresholdReached() throws IOException; +} diff --git a/src/org/apache/commons/io/output/package.html b/src/org/apache/commons/io/output/package.html new file mode 100644 index 000000000..db2cbce59 --- /dev/null +++ b/src/org/apache/commons/io/output/package.html @@ -0,0 +1,25 @@ + + + + +

+This package provides implementations of output classes, such as +OutputStream and Writer. +

+ + diff --git a/src/org/apache/commons/io/overview.html b/src/org/apache/commons/io/overview.html new file mode 100644 index 000000000..31311b5e9 --- /dev/null +++ b/src/org/apache/commons/io/overview.html @@ -0,0 +1,32 @@ + + + + +

+The commons-io component contains utility classes, +filters, streams, readers and writers. +

+

+These classes aim to add to the standard JDK IO classes. +The utilities provide convenience wrappers around the JDK, simplifying +various operations into pre-tested units of code. +The filters and streams provide useful implementations that perhaps should +be in the JDK itself. +

+ + diff --git a/src/org/apache/commons/io/package.html b/src/org/apache/commons/io/package.html new file mode 100644 index 000000000..e5ba9b0aa --- /dev/null +++ b/src/org/apache/commons/io/package.html @@ -0,0 +1,47 @@ + + + + +

+This package defines utility classes for working with streams, readers, +writers and files. The most commonly used classes are described here: +

+

+IOUtils is the most frequently used class. +It provides operations to read, write, copy and close streams. +

+

+FileUtils provides operations based around the JDK File class. +These include reading, writing, copying, comparing and deleting. +

+

+FilenameUtils provides utilities based on filenames. +This utility class manipulates filenames without using File objects. +It aims to simplify the transition between Windows and Unix. +Before using this class however, you should consider whether you should +be using File objects. +

+

+FileSystemUtils allows access to the filing system in ways the JDK +does not support. At present this allows you to get the free space on a drive. +

+

+EndianUtils swaps data between Big-Endian and Little-Endian formats. +

+ + diff --git a/src/org/apache/james/mime4j/AbstractContentHandler.java b/src/org/apache/james/mime4j/AbstractContentHandler.java new file mode 100644 index 000000000..06c9b90a0 --- /dev/null +++ b/src/org/apache/james/mime4j/AbstractContentHandler.java @@ -0,0 +1,113 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Abstract ContentHandler with default implementations of all + * the methods of the ContentHandler interface. + * + * The default is to todo nothing. + * + * + * @version $Id: AbstractContentHandler.java,v 1.3 2004/10/02 12:41:10 ntherning Exp $ + */ +public abstract class AbstractContentHandler implements ContentHandler { + + /** + * @see org.apache.james.mime4j.ContentHandler#endMultipart() + */ + public void endMultipart() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startMultipart(org.apache.james.mime4j.BodyDescriptor) + */ + public void startMultipart(BodyDescriptor bd) { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#body(org.apache.james.mime4j.BodyDescriptor, java.io.InputStream) + */ + public void body(BodyDescriptor bd, InputStream is) throws IOException { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endBodyPart() + */ + public void endBodyPart() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endHeader() + */ + public void endHeader() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endMessage() + */ + public void endMessage() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#epilogue(java.io.InputStream) + */ + public void epilogue(InputStream is) throws IOException { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#field(java.lang.String) + */ + public void field(String fieldData) { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#preamble(java.io.InputStream) + */ + public void preamble(InputStream is) throws IOException { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startBodyPart() + */ + public void startBodyPart() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startHeader() + */ + public void startHeader() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startMessage() + */ + public void startMessage() { + } + + /** + * @see org.apache.james.mime4j.ContentHandler#raw(java.io.InputStream) + */ + public void raw(InputStream is) throws IOException { + } +} diff --git a/src/org/apache/james/mime4j/BodyDescriptor.java b/src/org/apache/james/mime4j/BodyDescriptor.java new file mode 100644 index 000000000..515658a8d --- /dev/null +++ b/src/org/apache/james/mime4j/BodyDescriptor.java @@ -0,0 +1,410 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Encapsulates the values of the MIME-specific header fields + * (which starts with Content-). + * + * + * @version $Id: BodyDescriptor.java,v 1.4 2005/02/11 10:08:37 ntherning Exp $ + */ +public class BodyDescriptor { + private static Log log = LogFactory.getLog(BodyDescriptor.class); + + private String mimeType = "text/plain"; + private String boundary = null; + private String charset = "us-ascii"; + private String transferEncoding = "7bit"; + private Map parameters = new HashMap(); + private boolean contentTypeSet = false; + private boolean contentTransferEncSet = false; + + /** + * Creates a new root BodyDescriptor instance. + */ + public BodyDescriptor() { + this(null); + } + + /** + * Creates a new BodyDescriptor instance. + * + * @param parent the descriptor of the parent or null if this + * is the root descriptor. + */ + public BodyDescriptor(BodyDescriptor parent) { + if (parent != null && parent.isMimeType("multipart/digest")) { + mimeType = "message/rfc822"; + } else { + mimeType = "text/plain"; + } + } + + /** + * Should be called for each Content- header field of + * a MIME message or part. + * + * @param name the field name. + * @param value the field value. + */ + public void addField(String name, String value) { + + name = name.trim().toLowerCase(); + + if (name.equals("content-transfer-encoding") && !contentTransferEncSet) { + contentTransferEncSet = true; + + value = value.trim().toLowerCase(); + if (value.length() > 0) { + transferEncoding = value; + } + + } else if (name.equals("content-type") && !contentTypeSet) { + contentTypeSet = true; + + value = value.trim(); + + /* + * Unfold Content-Type value + */ + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\r' || c == '\n') { + continue; + } + sb.append(c); + } + + Map params = getHeaderParams(sb.toString()); + + String main = (String) params.get(""); + if (main != null) { + main = main.toLowerCase().trim(); + int index = main.indexOf('/'); + boolean valid = false; + if (index != -1) { + String type = main.substring(0, index).trim(); + String subtype = main.substring(index + 1).trim(); + if (type.length() > 0 && subtype.length() > 0) { + main = type + "/" + subtype; + valid = true; + } + } + + if (!valid) { + main = null; + } + } + String b = (String) params.get("boundary"); + + if (main != null + && ((main.startsWith("multipart/") && b != null) + || !main.startsWith("multipart/"))) { + + mimeType = main; + } + + if (isMultipart()) { + boundary = b; + } + + String c = (String) params.get("charset"); + if (c != null) { + c = c.trim(); + if (c.length() > 0) { + charset = c.toLowerCase(); + } + } + + /* + * Add all other parameters to parameters. + */ + parameters.putAll(params); + parameters.remove(""); + parameters.remove("boundary"); + parameters.remove("charset"); + } + } + + private Map getHeaderParams(String headerValue) { + Map result = new HashMap(); + + // split main value and parameters + String main; + String rest; + if (headerValue.indexOf(";") == -1) { + main = headerValue; + rest = null; + } else { + main = headerValue.substring(0, headerValue.indexOf(";")); + rest = headerValue.substring(main.length() + 1); + } + + result.put("", main); + if (rest != null) { + char[] chars = rest.toCharArray(); + StringBuffer paramName = new StringBuffer(); + StringBuffer paramValue = new StringBuffer(); + + final byte READY_FOR_NAME = 0; + final byte IN_NAME = 1; + final byte READY_FOR_VALUE = 2; + final byte IN_VALUE = 3; + final byte IN_QUOTED_VALUE = 4; + final byte VALUE_DONE = 5; + final byte ERROR = 99; + + byte state = READY_FOR_NAME; + boolean escaped = false; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + switch (state) { + case ERROR: + if (c == ';') + state = READY_FOR_NAME; + break; + + case READY_FOR_NAME: + if (c == '=') { + log.error("Expected header param name, got '='"); + state = ERROR; + break; + } + + paramName = new StringBuffer(); + paramValue = new StringBuffer(); + + state = IN_NAME; + // fall-through + + case IN_NAME: + if (c == '=') { + if (paramName.length() == 0) + state = ERROR; + else + state = READY_FOR_VALUE; + break; + } + + // not '='... just add to name + paramName.append(c); + break; + + case READY_FOR_VALUE: + boolean fallThrough = false; + switch (c) { + case ' ': + case '\t': + break; // ignore spaces, especially before '"' + + case '"': + state = IN_QUOTED_VALUE; + break; + + default: + state = IN_VALUE; + fallThrough = true; + break; + } + if (!fallThrough) + break; + + // fall-through + + case IN_VALUE: + fallThrough = false; + switch (c) { + case ';': + case ' ': + case '\t': + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString().trim()); + state = VALUE_DONE; + fallThrough = true; + break; + default: + paramValue.append(c); + break; + } + if (!fallThrough) + break; + + case VALUE_DONE: + switch (c) { + case ';': + state = READY_FOR_NAME; + break; + + case ' ': + case '\t': + break; + + default: + state = ERROR; + break; + } + break; + + case IN_QUOTED_VALUE: + switch (c) { + case '"': + if (!escaped) { + // don't trim quoted strings; the spaces could be intentional. + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString()); + state = VALUE_DONE; + } else { + escaped = false; + paramValue.append(c); + } + break; + + case '\\': + if (escaped) { + paramValue.append('\\'); + } + escaped = !escaped; + break; + + default: + if (escaped) { + paramValue.append('\\'); + } + escaped = false; + paramValue.append(c); + break; + } + break; + + } + } + + // done looping. check if anything is left over. + if (state == IN_VALUE) { + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString().trim()); + } + } + + return result; + } + + + public boolean isMimeType(String mimeType) { + return this.mimeType.equals(mimeType.toLowerCase()); + } + + /** + * Return true if the BodyDescriptor belongs to a message + * + * @return + */ + public boolean isMessage() { + return mimeType.equals("message/rfc822"); + } + + /** + * Retrun true if the BodyDescripotro belogns to a multipart + * + * @return + */ + public boolean isMultipart() { + return mimeType.startsWith("multipart/"); + } + + /** + * Return the MimeType + * + * @return mimeType + */ + public String getMimeType() { + return mimeType; + } + + /** + * Return the boundary + * + * @return boundary + */ + public String getBoundary() { + return boundary; + } + + /** + * Return the charset + * + * @return charset + */ + public String getCharset() { + return charset; + } + + /** + * Return all parameters for the BodyDescriptor + * + * @return parameters + */ + public Map getParameters() { + return parameters; + } + + /** + * Return the TransferEncoding + * + * @return transferEncoding + */ + public String getTransferEncoding() { + return transferEncoding; + } + + /** + * Return true if it's base64 encoded + * + * @return + * + */ + public boolean isBase64Encoded() { + return "base64".equals(transferEncoding); + } + + /** + * Return true if it's quoted-printable + * @return + */ + public boolean isQuotedPrintableEncoded() { + return "quoted-printable".equals(transferEncoding); + } + + public String toString() { + return mimeType; + } +} diff --git a/src/org/apache/james/mime4j/CloseShieldInputStream.java b/src/org/apache/james/mime4j/CloseShieldInputStream.java new file mode 100644 index 000000000..94995d110 --- /dev/null +++ b/src/org/apache/james/mime4j/CloseShieldInputStream.java @@ -0,0 +1,129 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.InputStream; +import java.io.IOException; + +/** + * InputStream that shields its underlying input stream from + * being closed. + * + * + * @version $Id: CloseShieldInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $ + */ +public class CloseShieldInputStream extends InputStream { + + /** + * Underlying InputStream + */ + private InputStream is; + + public CloseShieldInputStream(InputStream is) { + this.is = is; + } + + public InputStream getUnderlyingStream() { + return is; + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + checkIfClosed(); + return is.read(); + } + + /** + * @see java.io.InputStream#available() + */ + public int available() throws IOException { + checkIfClosed(); + return is.available(); + } + + + /** + * Set the underlying InputStream to null + */ + public void close() throws IOException { + is = null; + } + + /** + * @see java.io.FilterInputStream#reset() + */ + public synchronized void reset() throws IOException { + checkIfClosed(); + is.reset(); + } + + /** + * @see java.io.FilterInputStream#markSupported() + */ + public boolean markSupported() { + if (is == null) + return false; + return is.markSupported(); + } + + /** + * @see java.io.FilterInputStream#mark(int) + */ + public synchronized void mark(int readlimit) { + if (is != null) + is.mark(readlimit); + } + + /** + * @see java.io.FilterInputStream#skip(long) + */ + public long skip(long n) throws IOException { + checkIfClosed(); + return is.skip(n); + } + + /** + * @see java.io.FilterInputStream#read(byte[]) + */ + public int read(byte b[]) throws IOException { + checkIfClosed(); + return is.read(b); + } + + /** + * @see java.io.FilterInputStream#read(byte[], int, int) + */ + public int read(byte b[], int off, int len) throws IOException { + checkIfClosed(); + return is.read(b, off, len); + } + + /** + * Check if the underlying InputStream is null. If so throw an Exception + * + * @throws IOException if the underlying InputStream is null + */ + private void checkIfClosed() throws IOException { + if (is == null) + throw new IOException("Stream is closed"); + } +} diff --git a/src/org/apache/james/mime4j/ContentHandler.java b/src/org/apache/james/mime4j/ContentHandler.java new file mode 100644 index 000000000..946c89401 --- /dev/null +++ b/src/org/apache/james/mime4j/ContentHandler.java @@ -0,0 +1,177 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; + +/** + *

+ * Receives notifications of the content of a plain RFC822 or MIME message. + * Implement this interface and register an instance of that implementation + * with a MimeStreamParser instance using its + * {@link org.apache.james.mime4j.MimeStreamParser#setContentHandler(ContentHandler)} + * method. The parser uses the ContentHandler instance to report + * basic message-related events like the start and end of the body of a + * part in a multipart MIME entity. + *

+ *

+ * Events will be generated in the order the corresponding elements occur in + * the message stream parsed by the parser. E.g.: + *

+ *      startMessage()
+ *          startHeader()
+ *              field(...)
+ *              field(...)
+ *              ...
+ *          endHeader()
+ *          startMultipart()
+ *              preamble(...)
+ *              startBodyPart()
+ *                  startHeader()
+ *                      field(...)
+ *                      field(...)
+ *                      ...
+ *                  endHeader()
+ *                  body()
+ *              endBodyPart()
+ *              startBodyPart()
+ *                  startHeader()
+ *                      field(...)
+ *                      field(...)
+ *                      ...
+ *                  endHeader()
+ *                  body()
+ *              endBodyPart()
+ *              epilogue(...)
+ *          endMultipart()
+ *      endMessage()
+ * 
+ * The above shows an example of a MIME message consisting of a multipart + * body containing two body parts. + *

+ *

+ * See MIME RFCs 2045-2049 for more information on the structure of MIME + * messages and RFC 822 and 2822 for the general structure of Internet mail + * messages. + *

+ * + * + * @version $Id: ContentHandler.java,v 1.3 2004/10/02 12:41:10 ntherning Exp $ + */ +public interface ContentHandler { + /** + * Called when a new message starts (a top level message or an embedded + * rfc822 message). + */ + void startMessage(); + + /** + * Called when a message ends. + */ + void endMessage(); + + /** + * Called when a new body part starts inside a + * multipart/* entity. + */ + void startBodyPart(); + + /** + * Called when a body part ends. + */ + void endBodyPart(); + + /** + * Called when a header (of a message or body part) is about to be parsed. + */ + void startHeader(); + + /** + * Called for each field of a header. + * + * @param fieldData the raw contents of the field + * (Field-Name: field value). The value will not be + * unfolded. + */ + void field(String fieldData); + + /** + * Called when there are no more header fields in a message or body part. + */ + void endHeader(); + + /** + * Called for the preamble (whatever comes before the first body part) + * of a multipart/* entity. + * + * @param is used to get the contents of the preamble. + * @throws IOException should be thrown on I/O errors. + */ + void preamble(InputStream is) throws IOException; + + /** + * Called for the epilogue (whatever comes after the final body part) + * of a multipart/* entity. + * + * @param is used to get the contents of the epilogue. + * @throws IOException should be thrown on I/O errors. + */ + void epilogue(InputStream is) throws IOException; + + /** + * Called when the body of a multipart entity is about to be parsed. + * + * @param bd encapsulates the values (either read from the + * message stream or, if not present, determined implictly + * as described in the + * MIME rfc:s) of the Content-Type and + * Content-Transfer-Encoding header fields. + */ + void startMultipart(BodyDescriptor bd); + + /** + * Called when the body of an entity has been parsed. + */ + void endMultipart(); + + /** + * Called when the body of a discrete (non-multipart) entity is about to + * be parsed. + * + * @param bd see {@link #startMultipart(BodyDescriptor)} + * @param is the contents of the body. NOTE: this is the raw body contents + * - it will not be decoded if encoded. The bd + * parameter should be used to determine how the stream data + * should be decoded. + * @throws IOException should be thrown on I/O errors. + */ + void body(BodyDescriptor bd, InputStream is) throws IOException; + + /** + * Called when a new entity (message or body part) starts and the + * parser is in raw mode. + * + * @param is the raw contents of the entity. + * @throws IOException should be thrown on I/O errors. + * @see MimeStreamParser#setRaw(boolean) + */ + void raw(InputStream is) throws IOException; +} diff --git a/src/org/apache/james/mime4j/EOLConvertingInputStream.java b/src/org/apache/james/mime4j/EOLConvertingInputStream.java new file mode 100644 index 000000000..7d5009ca5 --- /dev/null +++ b/src/org/apache/james/mime4j/EOLConvertingInputStream.java @@ -0,0 +1,108 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * InputStream which converts \r + * bytes not followed by \n and \n not + * preceded by \r to \r\n. + * + * + * @version $Id: EOLConvertingInputStream.java,v 1.4 2004/11/29 13:15:42 ntherning Exp $ + */ +public class EOLConvertingInputStream extends InputStream { + /** Converts single '\r' to '\r\n' */ + public static final int CONVERT_CR = 1; + /** Converts single '\n' to '\r\n' */ + public static final int CONVERT_LF = 2; + /** Converts single '\r' and '\n' to '\r\n' */ + public static final int CONVERT_BOTH = 3; + + private PushbackInputStream in = null; + private int previous = 0; + private int flags = CONVERT_BOTH; + + /** + * Creates a new EOLConvertingInputStream + * instance converting bytes in the given InputStream. + * The flag CONVERT_BOTH is the default. + * + * @param in the InputStream to read from. + */ + public EOLConvertingInputStream(InputStream in) { + this(in, CONVERT_BOTH); + } + /** + * Creates a new EOLConvertingInputStream + * instance converting bytes in the given InputStream. + * + * @param in the InputStream to read from. + * @param flags one of CONVERT_CR, CONVERT_LF or + * CONVERT_BOTH. + */ + public EOLConvertingInputStream(InputStream in, int flags) { + super(); + + this.in = new PushbackInputStream(in, 2); + this.flags = flags; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + in.close(); + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + int b = in.read(); + + if (b == -1) { + return -1; + } + + if ((flags & CONVERT_CR) != 0 && b == '\r') { + int c = in.read(); + if (c != -1) { + in.unread(c); + } + if (c != '\n') { + in.unread('\n'); + } + } else if ((flags & CONVERT_LF) != 0 && b == '\n' && previous != '\r') { + b = '\r'; + in.unread('\n'); + } + + previous = b; + + return b; + } + +} diff --git a/src/org/apache/james/mime4j/MimeBoundaryInputStream.java b/src/org/apache/james/mime4j/MimeBoundaryInputStream.java new file mode 100644 index 000000000..0fffb7820 --- /dev/null +++ b/src/org/apache/james/mime4j/MimeBoundaryInputStream.java @@ -0,0 +1,184 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * Stream that constrains itself to a single MIME body part. + * After the stream ends (i.e. read() returns -1) {@link #hasMoreParts()} + * can be used to determine if a final boundary has been seen or not. + * If {@link #parentEOF()} is true an unexpected end of stream + * has been detected in the parent stream. + * + * + * + * @version $Id: MimeBoundaryInputStream.java,v 1.2 2004/11/29 13:15:42 ntherning Exp $ + */ +public class MimeBoundaryInputStream extends InputStream { + + private PushbackInputStream s = null; + private byte[] boundary = null; + private boolean first = true; + private boolean eof = false; + private boolean parenteof = false; + private boolean moreParts = true; + + /** + * Creates a new MimeBoundaryInputStream. + * @param s The underlying stream. + * @param boundary Boundary string (not including leading hyphens). + */ + public MimeBoundaryInputStream(InputStream s, String boundary) + throws IOException { + + this.s = new PushbackInputStream(s, boundary.length() + 4); + + boundary = "--" + boundary; + this.boundary = new byte[boundary.length()]; + for (int i = 0; i < this.boundary.length; i++) { + this.boundary[i] = (byte) boundary.charAt(i); + } + + /* + * By reading one byte we will update moreParts to be as expected + * before any bytes have been read. + */ + int b = read(); + if (b != -1) { + this.s.unread(b); + } + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + s.close(); + } + + /** + * Determines if the underlying stream has more parts (this stream has + * not seen an end boundary). + * + * @return true if there are more parts in the underlying + * stream, false otherwise. + */ + public boolean hasMoreParts() { + return moreParts; + } + + /** + * Determines if the parent stream has reached EOF + * + * @return true if EOF has been reached for the parent stream, + * false otherwise. + */ + public boolean parentEOF() { + return parenteof; + } + + /** + * Consumes all unread bytes of this stream. After a call to this method + * this stream will have reached EOF. + * + * @throws IOException on I/O errors. + */ + public void consume() throws IOException { + while (read() != -1) { + } + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + if (eof) { + return -1; + } + + if (first) { + first = false; + if (matchBoundary()) { + return -1; + } + } + + int b1 = s.read(); + int b2 = s.read(); + + if (b1 == '\r' && b2 == '\n') { + if (matchBoundary()) { + return -1; + } + } + + if (b2 != -1) { + s.unread(b2); + } + + parenteof = b1 == -1; + eof = parenteof; + + return b1; + } + + private boolean matchBoundary() throws IOException { + + for (int i = 0; i < boundary.length; i++) { + int b = s.read(); + if (b != boundary[i]) { + if (b != -1) { + s.unread(b); + } + for (int j = i - 1; j >= 0; j--) { + s.unread(boundary[j]); + } + return false; + } + } + + /* + * We have a match. Is it an end boundary? + */ + int prev = s.read(); + int curr = s.read(); + moreParts = !(prev == '-' && curr == '-'); + do { + if (curr == '\n' && prev == '\r') { + break; + } + prev = curr; + } while ((curr = s.read()) != -1); + + if (curr == -1) { + moreParts = false; + parenteof = true; + } + + eof = true; + + return true; + } +} diff --git a/src/org/apache/james/mime4j/MimeStreamParser.java b/src/org/apache/james/mime4j/MimeStreamParser.java new file mode 100644 index 000000000..2e486adc3 --- /dev/null +++ b/src/org/apache/james/mime4j/MimeStreamParser.java @@ -0,0 +1,320 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; +import java.util.BitSet; +import java.util.LinkedList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.decoder.Base64InputStream; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; + +/** + *

+ * Parses MIME (or RFC822) message streams of bytes or characters and reports + * parsing events to a ContentHandler instance. + *

+ *

+ * Typical usage:
+ *

+ *      ContentHandler handler = new MyHandler();
+ *      MimeStreamParser parser = new MimeStreamParser();
+ *      parser.setContentHandler(handler);
+ *      parser.parse(new BufferedInputStream(new FileInputStream("mime.msg")));
+ * 
+ * NOTE: All lines must end with CRLF + * (\r\n). If you are unsure of the line endings in your stream + * you should wrap it in a {@link org.apache.james.mime4j.EOLConvertingInputStream} instance. + * + * + * @version $Id: MimeStreamParser.java,v 1.8 2005/02/11 10:12:02 ntherning Exp $ + */ +public class MimeStreamParser { + private static final Log log = LogFactory.getLog(MimeStreamParser.class); + + private static BitSet fieldChars = null; + + private RootInputStream rootStream = null; + private LinkedList bodyDescriptors = new LinkedList(); + private ContentHandler handler = null; + private boolean raw = false; + + static { + fieldChars = new BitSet(); + for (int i = 0x21; i <= 0x39; i++) { + fieldChars.set(i); + } + for (int i = 0x3b; i <= 0x7e; i++) { + fieldChars.set(i); + } + } + + /** + * Creates a new MimeStreamParser instance. + */ + public MimeStreamParser() { + } + + /** + * Parses a stream of bytes containing a MIME message. + * + * @param is the stream to parse. + * @throws IOException on I/O errors. + */ + public void parse(InputStream is) throws IOException { + rootStream = new RootInputStream(is); + parseMessage(rootStream); + } + + /** + * Determines if this parser is currently in raw mode. + * + * @return true if in raw mode, false + * otherwise. + * @see #setRaw(boolean) + */ + public boolean isRaw() { + return raw; + } + + /** + * Enables or disables raw mode. In raw mode all future entities + * (messages or body parts) in the stream will be reported to the + * {@link ContentHandler#raw(InputStream)} handler method only. + * The stream will contain the entire unparsed entity contents + * including header fields and whatever is in the body. + * + * @param raw true enables raw mode, false + * disables it. + */ + public void setRaw(boolean raw) { + this.raw = raw; + } + + /** + * Finishes the parsing and stops reading lines. + * NOTE: No more lines will be parsed but the parser + * will still call + * {@link ContentHandler#endMultipart()}, + * {@link ContentHandler#endBodyPart()}, + * {@link ContentHandler#endMessage()}, etc to match previous calls + * to + * {@link ContentHandler#startMultipart(BodyDescriptor)}, + * {@link ContentHandler#startBodyPart()}, + * {@link ContentHandler#startMessage()}, etc. + */ + public void stop() { + rootStream.truncate(); + } + + /** + * Parses an entity which consists of a header followed by a body containing + * arbitrary data, body parts or an embedded message. + * + * @param is the stream to parse. + * @throws IOException on I/O errors. + */ + private void parseEntity(InputStream is) throws IOException { + BodyDescriptor bd = parseHeader(is); + + if (bd.isMultipart()) { + bodyDescriptors.addFirst(bd); + + handler.startMultipart(bd); + + MimeBoundaryInputStream tempIs = + new MimeBoundaryInputStream(is, bd.getBoundary()); + handler.preamble(new CloseShieldInputStream(tempIs)); + tempIs.consume(); + + while (tempIs.hasMoreParts()) { + tempIs = new MimeBoundaryInputStream(is, bd.getBoundary()); + parseBodyPart(tempIs); + tempIs.consume(); + if (tempIs.parentEOF()) { + if (log.isWarnEnabled()) { + log.warn("Line " + rootStream.getLineNumber() + + ": Body part ended prematurely. " + + "Higher level boundary detected or " + + "EOF reached."); + } + break; + } + } + + handler.epilogue(new CloseShieldInputStream(is)); + + handler.endMultipart(); + + bodyDescriptors.removeFirst(); + + } else if (bd.isMessage()) { + if (bd.isBase64Encoded()) { + log.warn("base64 encoded message/rfc822 detected"); + is = new EOLConvertingInputStream( + new Base64InputStream(is)); + } else if (bd.isQuotedPrintableEncoded()) { + log.warn("quoted-printable encoded message/rfc822 detected"); + is = new EOLConvertingInputStream( + new QuotedPrintableInputStream(is)); + } + bodyDescriptors.addFirst(bd); + parseMessage(is); + bodyDescriptors.removeFirst(); + } else { + handler.body(bd, new CloseShieldInputStream(is)); + } + + /* + * Make sure the stream has been consumed. + */ + while (is.read() != -1) { + } + } + + private void parseMessage(InputStream is) throws IOException { + if (raw) { + handler.raw(new CloseShieldInputStream(is)); + } else { + handler.startMessage(); + parseEntity(is); + handler.endMessage(); + } + } + + private void parseBodyPart(InputStream is) throws IOException { + if (raw) { + handler.raw(new CloseShieldInputStream(is)); + } else { + handler.startBodyPart(); + parseEntity(is); + handler.endBodyPart(); + } + } + + /** + * Parses a header. + * + * @param is the stream to parse. + * @return a BodyDescriptor describing the body following + * the header. + */ + private BodyDescriptor parseHeader(InputStream is) throws IOException { + BodyDescriptor bd = new BodyDescriptor(bodyDescriptors.isEmpty() + ? null : (BodyDescriptor) bodyDescriptors.getFirst()); + + handler.startHeader(); + + int lineNumber = rootStream.getLineNumber(); + + StringBuffer sb = new StringBuffer(); + int curr = 0; + int prev = 0; + while ((curr = is.read()) != -1) { + if (curr == '\n' && (prev == '\n' || prev == 0)) { + /* + * [\r]\n[\r]\n or an immediate \r\n have been seen. + */ + sb.deleteCharAt(sb.length() - 1); + break; + } + sb.append((char) curr); + prev = curr == '\r' ? prev : curr; + } + + if (curr == -1 && log.isWarnEnabled()) { + log.warn("Line " + rootStream.getLineNumber() + + ": Unexpected end of headers detected. " + + "Boundary detected in header or EOF reached."); + } + + int start = 0; + int pos = 0; + int startLineNumber = lineNumber; + while (pos < sb.length()) { + while (pos < sb.length() && sb.charAt(pos) != '\r') { + pos++; + } + if (pos < sb.length() - 1 && sb.charAt(pos + 1) != '\n') { + pos++; + continue; + } + + if (pos >= sb.length() - 2 || fieldChars.get(sb.charAt(pos + 2))) { + + /* + * field should be the complete field data excluding the + * trailing \r\n. + */ + String field = sb.substring(start, pos); + start = pos + 2; + + /* + * Check for a valid field. + */ + int index = field.indexOf(':'); + boolean valid = false; + if (index != -1 && fieldChars.get(field.charAt(0))) { + valid = true; + String fieldName = field.substring(0, index).trim(); + for (int i = 0; i < fieldName.length(); i++) { + if (!fieldChars.get(fieldName.charAt(i))) { + valid = false; + break; + } + } + + if (valid) { + handler.field(field); + bd.addField(fieldName, field.substring(index + 1)); + } + } + + if (!valid && log.isWarnEnabled()) { + log.warn("Line " + startLineNumber + + ": Ignoring invalid field: '" + field.trim() + "'"); + } + + startLineNumber = lineNumber; + } + + pos += 2; + lineNumber++; + } + + handler.endHeader(); + + return bd; + } + + /** + * Sets the ContentHandler to use when reporting + * parsing events. + * + * @param h the ContentHandler. + */ + public void setContentHandler(ContentHandler h) { + this.handler = h; + } + +} diff --git a/src/org/apache/james/mime4j/RootInputStream.java b/src/org/apache/james/mime4j/RootInputStream.java new file mode 100644 index 000000000..fa848df18 --- /dev/null +++ b/src/org/apache/james/mime4j/RootInputStream.java @@ -0,0 +1,111 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStream used by the parser to wrap the original user + * supplied stream. This stream keeps track of the current line number and + * can also be truncated. When truncated the stream will appear to have + * reached end of file. This is used by the parser's + * {@link org.apache.james.mime4j.MimeStreamParser#stop()} method. + * + * + * @version $Id: RootInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $ + */ +class RootInputStream extends InputStream { + private InputStream is = null; + private int lineNumber = 1; + private int prev = -1; + private boolean truncated = false; + + /** + * Creates a new RootInputStream. + * + * @param in the stream to read from. + */ + public RootInputStream(InputStream is) { + this.is = is; + } + + /** + * Gets the current line number starting at 1 + * (the number of \r\n read so far plus 1). + * + * @return the current line number. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Truncates this InputStream. After this call any + * call to {@link #read()}, {@link #read(byte[]) or + * {@link #read(byte[], int, int)} will return + * -1 as if end-of-file had been reached. + */ + public void truncate() { + this.truncated = true; + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + if (truncated) { + return -1; + } + + int b = is.read(); + if (prev == '\r' && b == '\n') { + lineNumber++; + } + prev = b; + return b; + } + + /** + * + * @see java.io.InputStream#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + if (truncated) { + return -1; + } + + int n = is.read(b, off, len); + for (int i = off; i < off + n; i++) { + if (prev == '\r' && b[i] == '\n') { + lineNumber++; + } + prev = b[i]; + } + return n; + } + + /** + * @see java.io.InputStream#read(byte[]) + */ + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } +} diff --git a/src/org/apache/james/mime4j/SimpleContentHandler.java b/src/org/apache/james/mime4j/SimpleContentHandler.java new file mode 100644 index 000000000..13f1fd2c6 --- /dev/null +++ b/src/org/apache/james/mime4j/SimpleContentHandler.java @@ -0,0 +1,100 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j; + +import org.apache.james.mime4j.decoder.Base64InputStream; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; +import org.apache.james.mime4j.field.Field; +import org.apache.james.mime4j.message.Header; + +import java.io.InputStream; +import java.io.IOException; + +/** + * Abstract implementation of ContentHandler that automates common + * tasks. Currently performs header parsing and applies content-transfer + * decoding to body parts. + * + * + */ +public abstract class SimpleContentHandler extends AbstractContentHandler { + + /** + * Called after headers are parsed. + */ + public abstract void headers(Header header); + + /** + * Called when the body of a discrete (non-multipart) entity is encountered. + + * @param bd encapsulates the values (either read from the + * message stream or, if not present, determined implictly + * as described in the + * MIME rfc:s) of the Content-Type and + * Content-Transfer-Encoding header fields. + * @param is the contents of the body. Base64 or quoted-printable + * decoding will be applied transparently. + * @throws IOException should be thrown on I/O errors. + */ + public abstract void bodyDecoded(BodyDescriptor bd, InputStream is) throws IOException; + + + /* Implement introduced callbacks. */ + + private Header currHeader; + + /** + * @see org.apache.james.mime4j.AbstractContentHandler#startHeader() + */ + public final void startHeader() { + currHeader = new Header(); + } + + /** + * @see org.apache.james.mime4j.AbstractContentHandler#field(java.lang.String) + */ + public final void field(String fieldData) { + currHeader.addField(Field.parse(fieldData)); + } + + /** + * @see org.apache.james.mime4j.AbstractContentHandler#endHeader() + */ + public final void endHeader() { + Header tmp = currHeader; + currHeader = null; + headers(tmp); + } + + /** + * @see org.apache.james.mime4j.AbstractContentHandler#body(org.apache.james.mime4j.BodyDescriptor, java.io.InputStream) + */ + public final void body(BodyDescriptor bd, InputStream is) throws IOException { + if (bd.isBase64Encoded()) { + bodyDecoded(bd, new Base64InputStream(is)); + } + else if (bd.isQuotedPrintableEncoded()) { + bodyDecoded(bd, new QuotedPrintableInputStream(is)); + } + else { + bodyDecoded(bd, is); + } + } +} diff --git a/src/org/apache/james/mime4j/decoder/Base64InputStream.java b/src/org/apache/james/mime4j/decoder/Base64InputStream.java new file mode 100644 index 000000000..930b982c4 --- /dev/null +++ b/src/org/apache/james/mime4j/decoder/Base64InputStream.java @@ -0,0 +1,146 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.decoder; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Performs Base-64 decoding on an underlying stream. + * + * + * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $ + */ +public class Base64InputStream extends InputStream { + private static Log log = LogFactory.getLog(Base64InputStream.class); + + private final InputStream s; + private final ByteQueue byteq = new ByteQueue(3); + private boolean done = false; + + public Base64InputStream(InputStream s) { + this.s = s; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + s.close(); + } + + public int read() throws IOException { + if (byteq.count() == 0) { + fillBuffer(); + if (byteq.count() == 0) { + return -1; + } + } + + byte val = byteq.dequeue(); + if (val >= 0) + return val; + else + return val & 0xFF; + } + + /** + * Retrieve data from the underlying stream, decode it, + * and put the results in the byteq. + * @throws IOException + */ + private void fillBuffer() throws IOException { + byte[] data = new byte[4]; + int pos = 0; + + int i; + while (!done) { + switch (i = s.read()) { + case -1: + if (pos > 0) { + log.warn("Unexpected EOF in MIME parser, dropping " + + pos + " sextets"); + } + return; + case '=': + decodeAndEnqueue(data, pos); + done = true; + break; + default: + byte sX = TRANSLATION[i]; + if (sX < 0) + continue; + data[pos++] = sX; + if (pos == data.length) { + decodeAndEnqueue(data, pos); + return; + } + break; + } + } + } + + private void decodeAndEnqueue(byte[] data, int len) { + int accum = 0; + accum |= data[0] << 18; + accum |= data[1] << 12; + accum |= data[2] << 6; + accum |= data[3]; + + byte b1 = (byte)(accum >>> 16); + byteq.enqueue(b1); + + if (len > 2) { + byte b2 = (byte)((accum >>> 8) & 0xFF); + byteq.enqueue(b2); + + if (len > 3) { + byte b3 = (byte)(accum & 0xFF); + byteq.enqueue(b3); + } + } + } + + private static byte[] TRANSLATION = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */ + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xF0 */ + }; + + +} diff --git a/src/org/apache/james/mime4j/decoder/ByteQueue.java b/src/org/apache/james/mime4j/decoder/ByteQueue.java new file mode 100644 index 000000000..68e7d3380 --- /dev/null +++ b/src/org/apache/james/mime4j/decoder/ByteQueue.java @@ -0,0 +1,62 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.decoder; + +import java.util.Iterator; + +public class ByteQueue { + + private UnboundedFifoByteBuffer buf; + private int initialCapacity = -1; + + public ByteQueue() { + buf = new UnboundedFifoByteBuffer(); + } + + public ByteQueue(int initialCapacity) { + buf = new UnboundedFifoByteBuffer(initialCapacity); + this.initialCapacity = initialCapacity; + } + + public void enqueue(byte b) { + buf.add(b); + } + + public byte dequeue() { + return buf.remove(); + } + + public int count() { + return buf.size(); + } + + public void clear() { + if (initialCapacity != -1) + buf = new UnboundedFifoByteBuffer(initialCapacity); + else + buf = new UnboundedFifoByteBuffer(); + } + + public Iterator iterator() { + return buf.iterator(); + } + + +} diff --git a/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/src/org/apache/james/mime4j/decoder/DecoderUtil.java new file mode 100644 index 000000000..9bd2c512d --- /dev/null +++ b/src/org/apache/james/mime4j/decoder/DecoderUtil.java @@ -0,0 +1,276 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.decoder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.CharsetUtil; + +/** + * Static methods for decoding strings, byte arrays and encoded words. + * + * + * @version $Id: DecoderUtil.java,v 1.3 2005/02/07 15:33:59 ntherning Exp $ + */ +public class DecoderUtil { + private static Log log = LogFactory.getLog(DecoderUtil.class); + + /** + * Decodes a string containing quoted-printable encoded data. + * + * @param s the string to decode. + * @return the decoded bytes. + */ + public static byte[] decodeBaseQuotedPrintable(String s) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try { + byte[] bytes = s.getBytes("US-ASCII"); + + QuotedPrintableInputStream is = new QuotedPrintableInputStream( + new ByteArrayInputStream(bytes)); + + int b = 0; + while ((b = is.read()) != -1) { + baos.write(b); + } + } catch (IOException e) { + /* + * This should never happen! + */ + log.error(e); + } + + return baos.toByteArray(); + } + + /** + * Decodes a string containing base64 encoded data. + * + * @param s the string to decode. + * @return the decoded bytes. + */ + public static byte[] decodeBase64(String s) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try { + byte[] bytes = s.getBytes("US-ASCII"); + + Base64InputStream is = new Base64InputStream( + new ByteArrayInputStream(bytes)); + + int b = 0; + while ((b = is.read()) != -1) { + baos.write(b); + } + } catch (IOException e) { + /* + * This should never happen! + */ + log.error(e); + } + + return baos.toByteArray(); + } + + /** + * Decodes an encoded word encoded with the 'B' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedWord the encoded word to decode. + * @param charset the Java charset to use. + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + public static String decodeB(String encodedWord, String charset) + throws UnsupportedEncodingException { + + return new String(decodeBase64(encodedWord), charset); + } + + /** + * Decodes an encoded word encoded with the 'Q' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedWord the encoded word to decode. + * @param charset the Java charset to use. + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + public static String decodeQ(String encodedWord, String charset) + throws UnsupportedEncodingException { + + /* + * Replace _ with =20 + */ + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < encodedWord.length(); i++) { + char c = encodedWord.charAt(i); + if (c == '_') { + sb.append("=20"); + } else { + sb.append(c); + } + } + + return new String(decodeBaseQuotedPrintable(sb.toString()), charset); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. + * Encoded words in have the form + * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for + * quoted-printable and 'B' or 'b' for Base64. + * + * @param body the string to decode. + * @return the decoded string. + */ + public static String decodeEncodedWords(String body) { + StringBuffer sb = new StringBuffer(); + + int p1 = 0; + int p2 = 0; + + try { + + /* + * Encoded words in headers have the form + * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for + * quoted printable and 'B' and 'b' for Base64 + */ + + while (p2 < body.length()) { + /* + * Find beginning of first encoded word + */ + p1 = body.indexOf("=?", p2); + if (p1 == -1) { + /* + * None found. Emit the rest of the header and exit. + */ + sb.append(body.substring(p2)); + break; + } + + /* + * p2 points to the previously found end marker or the start + * of the entire header text. Append the text between that + * marker and the one pointed to by p1. + */ + if (p1 - p2 > 0) { + sb.append(body.substring(p2, p1)); + } + + /* + * Find the first and second '?':s after the marker pointed to + * by p1. + */ + int t1 = body.indexOf('?', p1 + 2); + int t2 = t1 != -1 ? body.indexOf('?', t1 + 1) : -1; + + /* + * Find this words end marker. + */ + p2 = t2 != -1 ? body.indexOf("?=", t2 + 1) : -1; + if (p2 == -1) { + if (t2 != -1 && (body.length() - 1 == t2 || body.charAt(t2 + 1) == '=')) { + /* + * Treat "=?charset?enc?" and "=?charset?enc?=" as + * empty strings. + */ + p2 = t2; + } else { + /* + * No end marker was found. Append the rest of the + * header and exit. + */ + sb.append(body.substring(p1)); + break; + } + } + + /* + * [p1+2, t1] -> charset + * [t1+1, t2] -> encoding + * [t2+1, p2] -> encoded word + */ + + String decodedWord = null; + if (t2 == p2) { + /* + * The text is empty + */ + decodedWord = ""; + } else { + + String mimeCharset = body.substring(p1 + 2, t1); + String enc = body.substring(t1 + 1, t2); + String encodedWord = body.substring(t2 + 1, p2); + + /* + * Convert the MIME charset to a corresponding Java one. + */ + String charset = CharsetUtil.toJavaCharset(mimeCharset); + if (charset == null) { + decodedWord = body.substring(p1, p2 + 2); + if (log.isWarnEnabled()) { + log.warn("MIME charset '" + mimeCharset + + "' in header field doesn't have a " + +"corresponding Java charset"); + } + } else if (!CharsetUtil.isDecodingSupported(charset)) { + decodedWord = body.substring(p1, p2 + 2); + if (log.isWarnEnabled()) { + log.warn("Current JDK doesn't support decoding " + + "of charset '" + charset + + "' (MIME charset '" + + mimeCharset + "')"); + } + } else { + if (enc.equalsIgnoreCase("Q")) { + decodedWord = DecoderUtil.decodeQ(encodedWord, charset); + } else if (enc.equalsIgnoreCase("B")) { + decodedWord = DecoderUtil.decodeB(encodedWord, charset); + } else { + decodedWord = encodedWord; + if (log.isWarnEnabled()) { + log.warn("Warning: Unknown encoding in " + + "header field '" + enc + "'"); + } + } + } + } + p2 += 2; + sb.append(decodedWord); + } + } catch (Throwable t) { + log.error("Decoding header field body '" + body + "'", t); + } + + return sb.toString(); + } +} diff --git a/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java b/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java new file mode 100644 index 000000000..eb3f09c9a --- /dev/null +++ b/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java @@ -0,0 +1,227 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.decoder; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Performs Quoted-Printable decoding on an underlying stream. + * + * + * + * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $ + */ +public class QuotedPrintableInputStream extends InputStream { + private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class); + + private InputStream stream; + ByteQueue byteq = new ByteQueue(); + ByteQueue pushbackq = new ByteQueue(); + private byte state = 0; + + public QuotedPrintableInputStream(InputStream stream) { + this.stream = stream; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + stream.close(); + } + + public int read() throws IOException { + fillBuffer(); + if (byteq.count() == 0) + return -1; + else { + byte val = byteq.dequeue(); + if (val >= 0) + return val; + else + return val & 0xFF; + } + } + + /** + * Pulls bytes out of the underlying stream and places them in the + * pushback queue. This is necessary (vs. reading from the + * underlying stream directly) to detect and filter out "transport + * padding" whitespace, i.e., all whitespace that appears immediately + * before a CRLF. + * + * @throws IOException Underlying stream threw IOException. + */ + private void populatePushbackQueue() throws IOException { + //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!"); + + if (pushbackq.count() != 0) + return; + + while (true) { + int i = stream.read(); + switch (i) { + case -1: + // stream is done + pushbackq.clear(); // discard any whitespace preceding EOF + return; + case ' ': + case '\t': + pushbackq.enqueue((byte)i); + break; + case '\r': + case '\n': + pushbackq.clear(); // discard any whitespace preceding EOL + pushbackq.enqueue((byte)i); + return; + default: + pushbackq.enqueue((byte)i); + return; + } + } + } + + /** + * Causes the pushback queue to get populated if it is empty, then + * consumes and decodes bytes out of it until one or more bytes are + * in the byte queue. This decoding step performs the actual QP + * decoding. + * + * @throws IOException Underlying stream threw IOException. + */ + private void fillBuffer() throws IOException { + byte msdChar = 0; // first digit of escaped num + while (byteq.count() == 0) { + if (pushbackq.count() == 0) { + populatePushbackQueue(); + if (pushbackq.count() == 0) + return; + } + + byte b = (byte)pushbackq.dequeue(); + + switch (state) { + case 0: // start state, no bytes pending + if (b != '=') { + byteq.enqueue(b); + break; // state remains 0 + } else { + state = 1; + break; + } + case 1: // encountered "=" so far + if (b == '\r') { + state = 2; + break; + } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { + state = 3; + msdChar = b; // save until next digit encountered + break; + } else if (b == '=') { + /* + * Special case when == is encountered. + * Emit one = and stay in this state. + */ + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; got =="); + } + byteq.enqueue((byte)'='); + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected \\r or " + + "[0-9A-Z], got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue(b); + break; + } + case 2: // encountered "=\r" so far + if (b == '\n') { + state = 0; + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected " + + (int)'\n' + ", got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue((byte)'\r'); + byteq.enqueue(b); + break; + } + case 3: // encountered = so far; expecting another to complete the octet + if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { + byte msd = asciiCharToNumericValue(msdChar); + byte low = asciiCharToNumericValue(b); + state = 0; + byteq.enqueue((byte)((msd << 4) | low)); + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected " + + "[0-9A-Z], got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue(msdChar); + byteq.enqueue(b); + break; + } + default: // should never happen + log.error("Illegal state: " + state); + state = 0; + byteq.enqueue(b); + break; + } + } + } + + /** + * Converts '0' => 0, 'A' => 10, etc. + * @param c ASCII character value. + * @return Numeric value of hexadecimal character. + */ + private byte asciiCharToNumericValue(byte c) { + if (c >= '0' && c <= '9') { + return (byte)(c - '0'); + } else if (c >= 'A' && c <= 'Z') { + return (byte)(0xA + (c - 'A')); + } else if (c >= 'a' && c <= 'z') { + return (byte)(0xA + (c - 'a')); + } else { + /* + * This should never happen since all calls to this method + * are preceded by a check that c is in [0-9A-Za-z] + */ + throw new IllegalArgumentException((char) c + + " is not a hexadecimal digit"); + } + } + +} diff --git a/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java b/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java new file mode 100644 index 000000000..dc32caf36 --- /dev/null +++ b/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java @@ -0,0 +1,272 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.decoder; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * UnboundedFifoByteBuffer is a very efficient buffer implementation. + * According to performance testing, it exhibits a constant access time, but it + * also outperforms ArrayList when used for the same purpose. + *

+ * The removal order of an UnboundedFifoByteBuffer is based on the insertion + * order; elements are removed in the same order in which they were added. + * The iteration order is the same as the removal order. + *

+ * The {@link #remove()} and {@link #get()} operations perform in constant time. + * The {@link #add(Object)} operation performs in amortized constant time. All + * other operations perform in linear time or worse. + *

+ * Note that this implementation is not synchronized. The following can be + * used to provide synchronized access to your UnboundedFifoByteBuffer: + *

+ *   Buffer fifo = BufferUtils.synchronizedBuffer(new UnboundedFifoByteBuffer());
+ * 
+ *

+ * This buffer prevents null objects from being added. + * + * @since Commons Collections 3.0 (previously in main package v2.1) + * @version $Revision: 1.1 $ $Date: 2004/08/24 06:52:02 $ + * + * + * + * + * + * + */ +class UnboundedFifoByteBuffer { + + protected byte[] buffer; + protected int head; + protected int tail; + + /** + * Constructs an UnboundedFifoByteBuffer with the default number of elements. + * It is exactly the same as performing the following: + * + *

+     *   new UnboundedFifoByteBuffer(32);
+     * 
+ */ + public UnboundedFifoByteBuffer() { + this(32); + } + + /** + * Constructs an UnboundedFifoByteBuffer with the specified number of elements. + * The integer must be a positive integer. + * + * @param initialSize the initial size of the buffer + * @throws IllegalArgumentException if the size is less than 1 + */ + public UnboundedFifoByteBuffer(int initialSize) { + if (initialSize <= 0) { + throw new IllegalArgumentException("The size must be greater than 0"); + } + buffer = new byte[initialSize + 1]; + head = 0; + tail = 0; + } + + /** + * Returns the number of elements stored in the buffer. + * + * @return this buffer's size + */ + public int size() { + int size = 0; + + if (tail < head) { + size = buffer.length - head + tail; + } else { + size = tail - head; + } + + return size; + } + + /** + * Returns true if this buffer is empty; false otherwise. + * + * @return true if this buffer is empty + */ + public boolean isEmpty() { + return (size() == 0); + } + + /** + * Adds the given element to this buffer. + * + * @param b the byte to add + * @return true, always + */ + public boolean add(final byte b) { + + if (size() + 1 >= buffer.length) { + byte[] tmp = new byte[((buffer.length - 1) * 2) + 1]; + + int j = 0; + for (int i = head; i != tail;) { + tmp[j] = buffer[i]; + buffer[i] = 0; + + j++; + i++; + if (i == buffer.length) { + i = 0; + } + } + + buffer = tmp; + head = 0; + tail = j; + } + + buffer[tail] = b; + tail++; + if (tail >= buffer.length) { + tail = 0; + } + return true; + } + + /** + * Returns the next object in the buffer. + * + * @return the next object in the buffer + * @throws BufferUnderflowException if this buffer is empty + */ + public byte get() { + if (isEmpty()) { + throw new IllegalStateException("The buffer is already empty"); + } + + return buffer[head]; + } + + /** + * Removes the next object from the buffer + * + * @return the removed object + * @throws BufferUnderflowException if this buffer is empty + */ + public byte remove() { + if (isEmpty()) { + throw new IllegalStateException("The buffer is already empty"); + } + + byte element = buffer[head]; + + head++; + if (head >= buffer.length) { + head = 0; + } + + return element; + } + + /** + * Increments the internal index. + * + * @param index the index to increment + * @return the updated index + */ + private int increment(int index) { + index++; + if (index >= buffer.length) { + index = 0; + } + return index; + } + + /** + * Decrements the internal index. + * + * @param index the index to decrement + * @return the updated index + */ + private int decrement(int index) { + index--; + if (index < 0) { + index = buffer.length - 1; + } + return index; + } + + /** + * Returns an iterator over this buffer's elements. + * + * @return an iterator over this buffer's elements + */ + public Iterator iterator() { + return new Iterator() { + + private int index = head; + private int lastReturnedIndex = -1; + + public boolean hasNext() { + return index != tail; + + } + + public Object next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + lastReturnedIndex = index; + index = increment(index); + return new Byte(buffer[lastReturnedIndex]); + } + + public void remove() { + if (lastReturnedIndex == -1) { + throw new IllegalStateException(); + } + + // First element can be removed quickly + if (lastReturnedIndex == head) { + UnboundedFifoByteBuffer.this.remove(); + lastReturnedIndex = -1; + return; + } + + // Other elements require us to shift the subsequent elements + int i = lastReturnedIndex + 1; + while (i != tail) { + if (i >= buffer.length) { + buffer[i - 1] = buffer[0]; + i = 0; + } else { + buffer[i - 1] = buffer[i]; + i++; + } + } + + lastReturnedIndex = -1; + tail = decrement(tail); + buffer[tail] = 0; + index = decrement(index); + } + + }; + } + +} \ No newline at end of file diff --git a/src/org/apache/james/mime4j/field/AddressListField.java b/src/org/apache/james/mime4j/field/AddressListField.java new file mode 100644 index 000000000..0e4297c76 --- /dev/null +++ b/src/org/apache/james/mime4j/field/AddressListField.java @@ -0,0 +1,63 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.parser.ParseException; + +public class AddressListField extends Field { + private AddressList addressList; + private ParseException parseException; + + protected AddressListField(String name, String body, String raw, AddressList addressList, ParseException parseException) { + super(name, body, raw); + this.addressList = addressList; + this.parseException = parseException; + } + + public AddressList getAddressList() { + return addressList; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + AddressList addressList = null; + ParseException parseException = null; + try { + addressList = AddressList.parse(body); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new AddressListField(name, body, raw, addressList, parseException); + } + } +} diff --git a/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java b/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java new file mode 100644 index 000000000..eb6151377 --- /dev/null +++ b/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java @@ -0,0 +1,88 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + + + +/** + * Represents a Content-Transfer-Encoding field. + * + * + * @version $Id: ContentTransferEncodingField.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public class ContentTransferEncodingField extends Field { + /** + * The 7bit encoding. + */ + public static final String ENC_7BIT = "7bit"; + /** + * The 8bit encoding. + */ + public static final String ENC_8BIT = "8bit"; + /** + * The binary encoding. + */ + public static final String ENC_BINARY = "binary"; + /** + * The quoted-printable encoding. + */ + public static final String ENC_QUOTED_PRINTABLE = "quoted-printable"; + /** + * The base64 encoding. + */ + public static final String ENC_BASE64 = "base64"; + + private String encoding; + + protected ContentTransferEncodingField(String name, String body, String raw, String encoding) { + super(name, body, raw); + this.encoding = encoding; + } + + /** + * Gets the encoding defined in this field. + * + * @return the encoding or an empty string if not set. + */ + public String getEncoding() { + return encoding; + } + + /** + * Gets the encoding of the given field if. Returns the default + * 7bit if not set or if + * f is null. + * + * @return the encoding. + */ + public static String getEncoding(ContentTransferEncodingField f) { + if (f != null && f.getEncoding().length() != 0) { + return f.getEncoding(); + } + return ENC_7BIT; + } + + public static class Parser implements FieldParser { + public Field parse(final String name, final String body, final String raw) { + final String encoding = body.trim().toLowerCase(); + return new ContentTransferEncodingField(name, body, raw, encoding); + } + } +} diff --git a/src/org/apache/james/mime4j/field/ContentTypeField.java b/src/org/apache/james/mime4j/field/ContentTypeField.java new file mode 100644 index 000000000..51616419d --- /dev/null +++ b/src/org/apache/james/mime4j/field/ContentTypeField.java @@ -0,0 +1,256 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.field.contenttype.parser.ContentTypeParser; +import org.apache.james.mime4j.field.contenttype.parser.ParseException; +import org.apache.james.mime4j.field.contenttype.parser.TokenMgrError; + +/** + * Represents a Content-Type field. + * + *

TODO: Remove dependency on Java 1.4 regexps

+ * + * + * @version $Id: ContentTypeField.java,v 1.6 2005/01/27 14:16:31 ntherning Exp $ + */ +public class ContentTypeField extends Field { + + /** + * The prefix of all multipart MIME types. + */ + public static final String TYPE_MULTIPART_PREFIX = "multipart/"; + /** + * The multipart/digest MIME type. + */ + public static final String TYPE_MULTIPART_DIGEST = "multipart/digest"; + /** + * The text/plain MIME type. + */ + public static final String TYPE_TEXT_PLAIN = "text/plain"; + /** + * The message/rfc822 MIME type. + */ + public static final String TYPE_MESSAGE_RFC822 = "message/rfc822"; + /** + * The name of the boundary parameter. + */ + public static final String PARAM_BOUNDARY = "boundary"; + /** + * The name of the charset parameter. + */ + public static final String PARAM_CHARSET = "charset"; + + private String mimeType = ""; + private Map parameters = null; + private ParseException parseException; + + protected ContentTypeField(String name, String body, String raw, String mimeType, Map parameters, ParseException parseException) { + super(name, body, raw); + this.mimeType = mimeType; + this.parameters = parameters; + this.parseException = parseException; + } + + /** + * Gets the exception that was raised during parsing of + * the field value, if any; otherwise, null. + */ + public ParseException getParseException() { + return parseException; + } + + /** + * Gets the MIME type defined in this Content-Type field. + * + * @return the MIME type or an empty string if not set. + */ + public String getMimeType() { + return mimeType; + } + + /** + * Gets the MIME type defined in the child's + * Content-Type field or derives a MIME type from the parent + * if child is null or hasn't got a MIME type value set. + * If child's MIME type is multipart but no boundary + * has been set the MIME type of child will be derived from + * the parent. + * + * @param child the child. + * @param parent the parent. + * @return the MIME type. + */ + public static String getMimeType(ContentTypeField child, + ContentTypeField parent) { + + if (child == null || child.getMimeType().length() == 0 + || child.isMultipart() && child.getBoundary() == null) { + + if (parent != null && parent.isMimeType(TYPE_MULTIPART_DIGEST)) { + return TYPE_MESSAGE_RFC822; + } else { + return TYPE_TEXT_PLAIN; + } + } + + return child.getMimeType(); + } + + /** + * Gets the value of a parameter. Parameter names are case-insensitive. + * + * @param name the name of the parameter to get. + * @return the parameter value or null if not set. + */ + public String getParameter(String name) { + return parameters != null + ? (String) parameters.get(name.toLowerCase()) + : null; + } + + /** + * Gets all parameters. + * + * @return the parameters. + */ + public Map getParameters() { + return parameters != null + ? Collections.unmodifiableMap(parameters) + : Collections.EMPTY_MAP; + } + + /** + * Gets the value of the boundary parameter if set. + * + * @return the boundary parameter value or null + * if not set. + */ + public String getBoundary() { + return getParameter(PARAM_BOUNDARY); + } + + /** + * Gets the value of the charset parameter if set. + * + * @return the charset parameter value or null + * if not set. + */ + public String getCharset() { + return getParameter(PARAM_CHARSET); + } + + /** + * Gets the value of the charset parameter if set for the + * given field. Returns the default us-ascii if not set or if + * f is null. + * + * @return the charset parameter value. + */ + public static String getCharset(ContentTypeField f) { + if (f != null) { + if (f.getCharset() != null && f.getCharset().length() > 0) { + return f.getCharset(); + } + } + return "us-ascii"; + } + + /** + * Determines if the MIME type of this field matches the given one. + * + * @param mimeType the MIME type to match against. + * @return true if the MIME type of this field matches, + * false otherwise. + */ + public boolean isMimeType(String mimeType) { + return this.mimeType.equalsIgnoreCase(mimeType); + } + + /** + * Determines if the MIME type of this field is multipart/*. + * + * @return true if this field is has a multipart/* + * MIME type, false otherwise. + */ + public boolean isMultipart() { + return mimeType.startsWith(TYPE_MULTIPART_PREFIX); + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + ParseException parseException = null; + String mimeType = ""; + Map parameters = null; + + ContentTypeParser parser = new ContentTypeParser(new StringReader(body)); + try { + parser.parseAll(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + catch (TokenMgrError e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = new ParseException(e.getMessage()); + } + + try { + final String type = parser.getType(); + final String subType = parser.getSubType(); + + if (type != null && subType != null) { + mimeType = (type + "/" + parser.getSubType()).toLowerCase(); + + ArrayList paramNames = parser.getParamNames(); + ArrayList paramValues = parser.getParamValues(); + + if (paramNames != null && paramValues != null) { + for (int i = 0; i < paramNames.size() && i < paramValues.size(); i++) { + if (parameters == null) + parameters = new HashMap((int)(paramNames.size() * 1.3 + 1)); + String paramName = ((String)paramNames.get(i)).toLowerCase(); + String paramValue = ((String)paramValues.get(i)); + parameters.put(paramName, paramValue); + } + } + } + } + catch (NullPointerException npe) { + } + return new ContentTypeField(name, body, raw, mimeType, parameters, parseException); + } + } +} diff --git a/src/org/apache/james/mime4j/field/DateTimeField.java b/src/org/apache/james/mime4j/field/DateTimeField.java new file mode 100644 index 000000000..a7b24b5bd --- /dev/null +++ b/src/org/apache/james/mime4j/field/DateTimeField.java @@ -0,0 +1,65 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.field.datetime.DateTime; +import org.apache.james.mime4j.field.datetime.parser.ParseException; + +import java.util.Date; + +public class DateTimeField extends Field { + private Date date; + private ParseException parseException; + + protected DateTimeField(String name, String body, String raw, Date date, ParseException parseException) { + super(name, body, raw); + this.date = date; + this.parseException = parseException; + } + + public Date getDate() { + return date; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + Date date = null; + ParseException parseException = null; + try { + date = DateTime.parse(body).getDate(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new DateTimeField(name, body, raw, date, parseException); + } + } +} diff --git a/src/org/apache/james/mime4j/field/DefaultFieldParser.java b/src/org/apache/james/mime4j/field/DefaultFieldParser.java new file mode 100644 index 000000000..84fcdcb2d --- /dev/null +++ b/src/org/apache/james/mime4j/field/DefaultFieldParser.java @@ -0,0 +1,45 @@ +/* + * Copyright 2006 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field; + +public class DefaultFieldParser extends DelegatingFieldParser { + + public DefaultFieldParser() { + setFieldParser(Field.CONTENT_TRANSFER_ENCODING, new ContentTransferEncodingField.Parser()); + setFieldParser(Field.CONTENT_TYPE, new ContentTypeField.Parser()); + + final DateTimeField.Parser dateTimeParser = new DateTimeField.Parser(); + setFieldParser(Field.DATE, dateTimeParser); + setFieldParser(Field.RESENT_DATE, dateTimeParser); + + final MailboxListField.Parser mailboxListParser = new MailboxListField.Parser(); + setFieldParser(Field.FROM, mailboxListParser); + setFieldParser(Field.RESENT_FROM, mailboxListParser); + + final MailboxField.Parser mailboxParser = new MailboxField.Parser(); + setFieldParser(Field.SENDER, mailboxParser); + setFieldParser(Field.RESENT_SENDER, mailboxParser); + + final AddressListField.Parser addressListParser = new AddressListField.Parser(); + setFieldParser(Field.TO, addressListParser); + setFieldParser(Field.RESENT_TO, addressListParser); + setFieldParser(Field.CC, addressListParser); + setFieldParser(Field.RESENT_CC, addressListParser); + setFieldParser(Field.BCC, addressListParser); + setFieldParser(Field.RESENT_BCC, addressListParser); + setFieldParser(Field.REPLY_TO, addressListParser); + } +} diff --git a/src/org/apache/james/mime4j/field/DelegatingFieldParser.java b/src/org/apache/james/mime4j/field/DelegatingFieldParser.java new file mode 100644 index 000000000..e7787a336 --- /dev/null +++ b/src/org/apache/james/mime4j/field/DelegatingFieldParser.java @@ -0,0 +1,47 @@ +/* + * Copyright 2006 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field; + +import java.util.HashMap; +import java.util.Map; + +public class DelegatingFieldParser implements FieldParser { + + private Map parsers = new HashMap(); + private FieldParser defaultParser = new UnstructuredField.Parser(); + + /** + * Sets the parser used for the field named name. + * @param name the name of the field + * @param parser the parser for fields named name + */ + public void setFieldParser(final String name, final FieldParser parser) { + parsers.put(name.toLowerCase(), parser); + } + + public FieldParser getParser(final String name) { + final FieldParser field = (FieldParser) parsers.get(name.toLowerCase()); + if(field==null) { + return defaultParser; + } + return field; + } + + public Field parse(final String name, final String body, final String raw) { + final FieldParser parser = getParser(name); + return parser.parse(name, body, raw); + } +} diff --git a/src/org/apache/james/mime4j/field/Field.java b/src/org/apache/james/mime4j/field/Field.java new file mode 100644 index 000000000..7c2a20dc8 --- /dev/null +++ b/src/org/apache/james/mime4j/field/Field.java @@ -0,0 +1,192 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The base class of all field classes. + * + * + * @version $Id: Field.java,v 1.6 2004/10/25 07:26:46 ntherning Exp $ + */ +public abstract class Field { + public static final String SENDER = "Sender"; + public static final String FROM = "From"; + public static final String TO = "To"; + public static final String CC = "Cc"; + public static final String BCC = "Bcc"; + public static final String REPLY_TO = "Reply-To"; + public static final String RESENT_SENDER = "Resent-Sender"; + public static final String RESENT_FROM = "Resent-From"; + public static final String RESENT_TO = "Resent-To"; + public static final String RESENT_CC = "Resent-Cc"; + public static final String RESENT_BCC = "Resent-Bcc"; + + public static final String DATE = "Date"; + public static final String RESENT_DATE = "Resent-Date"; + + public static final String SUBJECT = "Subject"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_TRANSFER_ENCODING = + "Content-Transfer-Encoding"; + + private static final String FIELD_NAME_PATTERN = + "^([\\x21-\\x39\\x3b-\\x7e]+)[ \t]*:"; + private static final Pattern fieldNamePattern = + Pattern.compile(FIELD_NAME_PATTERN); + + private static final DefaultFieldParser parser = new DefaultFieldParser(); + + private final String name; + private final String body; + private final String raw; + + protected Field(final String name, final String body, final String raw) { + this.name = name; + this.body = body; + this.raw = raw; + } + + /** + * Parses the given string and returns an instance of the + * Field class. The type of the class returned depends on + * the field name: + * + * + * + * + * + * + *
Field nameClass returnedContent-Typeorg.apache.james.mime4j.field.ContentTypeFieldotherorg.apache.james.mime4j.field.UnstructuredField
+ * + * @param s the string to parse. + * @return a Field instance. + * @throws IllegalArgumentException on parse errors. + */ + public static Field parse(final String raw) { + + /* + * Unfold the field. + */ + final String unfolded = raw.replaceAll("\r|\n", ""); + + /* + * Split into name and value. + */ + final Matcher fieldMatcher = fieldNamePattern.matcher(unfolded); + if (!fieldMatcher.find()) { + throw new IllegalArgumentException("Invalid field in string"); + } + final String name = fieldMatcher.group(1); + + String body = unfolded.substring(fieldMatcher.end()); + if (body.length() > 0 && body.charAt(0) == ' ') { + body = body.substring(1); + } + + return parser.parse(name, body, raw); + } + + /** + * Gets the default parser used to parse fields. + * @return the default field parser + */ + public static DefaultFieldParser getParser() { + return parser; + } + + /** + * Gets the name of the field (Subject, + * From, etc). + * + * @return the field name. + */ + public String getName() { + return name; + } + + /** + * Gets the original raw field string. + * + * @return the original raw field string. + */ + public String getRaw() { + return raw; + } + + /** + * Gets the unfolded, unparsed and possibly encoded (see RFC 2047) field + * body string. + * + * @return the unfolded unparsed field body string. + */ + public String getBody() { + return body; + } + + /** + * Determines if this is a Content-Type field. + * + * @return true if this is a Content-Type field, + * false otherwise. + */ + public boolean isContentType() { + return CONTENT_TYPE.equalsIgnoreCase(name); + } + + /** + * Determines if this is a Subject field. + * + * @return true if this is a Subject field, + * false otherwise. + */ + public boolean isSubject() { + return SUBJECT.equalsIgnoreCase(name); + } + + /** + * Determines if this is a From field. + * + * @return true if this is a From field, + * false otherwise. + */ + public boolean isFrom() { + return FROM.equalsIgnoreCase(name); + } + + /** + * Determines if this is a To field. + * + * @return true if this is a To field, + * false otherwise. + */ + public boolean isTo() { + return TO.equalsIgnoreCase(name); + } + + /** + * @see #getRaw() + */ + public String toString() { + return raw; + } +} diff --git a/src/org/apache/james/mime4j/field/FieldParser.java b/src/org/apache/james/mime4j/field/FieldParser.java new file mode 100644 index 000000000..4f33c9e26 --- /dev/null +++ b/src/org/apache/james/mime4j/field/FieldParser.java @@ -0,0 +1,21 @@ +/* + * Copyright 2006 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field; + +public interface FieldParser { + + Field parse(final String name, final String body, final String raw); +} diff --git a/src/org/apache/james/mime4j/field/MailboxField.java b/src/org/apache/james/mime4j/field/MailboxField.java new file mode 100644 index 000000000..96cca551f --- /dev/null +++ b/src/org/apache/james/mime4j/field/MailboxField.java @@ -0,0 +1,68 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.Mailbox; +import org.apache.james.mime4j.field.address.MailboxList; +import org.apache.james.mime4j.field.address.parser.ParseException; + +public class MailboxField extends Field { + private final Mailbox mailbox; + private final ParseException parseException; + + protected MailboxField(final String name, final String body, final String raw, final Mailbox mailbox, final ParseException parseException) { + super(name, body, raw); + this.mailbox = mailbox; + this.parseException = parseException; + } + + public Mailbox getMailbox() { + return mailbox; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + Mailbox mailbox = null; + ParseException parseException = null; + try { + MailboxList mailboxList = AddressList.parse(body).flatten(); + if (mailboxList.size() > 0) { + mailbox = mailboxList.get(0); + } + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new MailboxField(name, body, raw, mailbox, parseException); + } + } +} diff --git a/src/org/apache/james/mime4j/field/MailboxListField.java b/src/org/apache/james/mime4j/field/MailboxListField.java new file mode 100644 index 000000000..efb18cd35 --- /dev/null +++ b/src/org/apache/james/mime4j/field/MailboxListField.java @@ -0,0 +1,65 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.MailboxList; +import org.apache.james.mime4j.field.address.parser.ParseException; + +public class MailboxListField extends Field { + + private MailboxList mailboxList; + private ParseException parseException; + + protected MailboxListField(final String name, final String body, final String raw, final MailboxList mailboxList, final ParseException parseException) { + super(name, body, raw); + this.mailboxList = mailboxList; + this.parseException = parseException; + } + + public MailboxList getMailboxList() { + return mailboxList; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + MailboxList mailboxList = null; + ParseException parseException = null; + try { + mailboxList = AddressList.parse(body).flatten(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new MailboxListField(name, body, raw, mailboxList, parseException); + } + } +} diff --git a/src/org/apache/james/mime4j/field/UnstructuredField.java b/src/org/apache/james/mime4j/field/UnstructuredField.java new file mode 100644 index 000000000..5e2adf9f2 --- /dev/null +++ b/src/org/apache/james/mime4j/field/UnstructuredField.java @@ -0,0 +1,49 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field; + +import org.apache.james.mime4j.decoder.DecoderUtil; + + +/** + * Simple unstructured field such as Subject. + * + * + * @version $Id: UnstructuredField.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $ + */ +public class UnstructuredField extends Field { + private String value; + + protected UnstructuredField(String name, String body, String raw, String value) { + super(name, body, raw); + this.value = value; + } + + public String getValue() { + return value; + } + + public static class Parser implements FieldParser { + public Field parse(final String name, final String body, final String raw) { + final String value = DecoderUtil.decodeEncodedWords(body); + return new UnstructuredField(name, body, raw, value); + } + } +} diff --git a/src/org/apache/james/mime4j/field/address/Address.java b/src/org/apache/james/mime4j/field/address/Address.java new file mode 100644 index 000000000..47cee0ba0 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/Address.java @@ -0,0 +1,52 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * The abstract base for classes that represent RFC2822 addresses. + * This includes groups and mailboxes. + * + * Currently, no public methods are introduced on this class. + * + * + */ +public abstract class Address { + + /** + * Adds any mailboxes represented by this address + * into the given ArrayList. Note that this method + * has default (package) access, so a doAddMailboxesTo + * method is needed to allow the behavior to be + * overridden by subclasses. + */ + final void addMailboxesTo(ArrayList results) { + doAddMailboxesTo(results); + } + + /** + * Adds any mailboxes represented by this address + * into the given ArrayList. Must be overridden by + * concrete subclasses. + */ + protected abstract void doAddMailboxesTo(ArrayList results); + +} diff --git a/src/org/apache/james/mime4j/field/address/AddressList.java b/src/org/apache/james/mime4j/field/address/AddressList.java new file mode 100644 index 000000000..75072bde4 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/AddressList.java @@ -0,0 +1,140 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import org.apache.james.mime4j.field.address.parser.AddressListParser; +import org.apache.james.mime4j.field.address.parser.ParseException; + +import java.io.StringReader; +import java.util.ArrayList; + +/** + * An immutable, random-access list of Address objects. + * + * + */ +public class AddressList { + + private ArrayList addresses; + + /** + * @param addresses An ArrayList that contains only Address objects. + * @param dontCopy true iff it is not possible for the addresses ArrayList to be modified by someone else. + */ + public AddressList(ArrayList addresses, boolean dontCopy) { + if (addresses != null) + this.addresses = (dontCopy ? addresses : (ArrayList) addresses.clone()); + else + this.addresses = new ArrayList(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return addresses.size(); + } + + /** + * Gets an address. + */ + public Address get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return (Address) addresses.get(index); + } + + /** + * Returns a flat list of all mailboxes represented + * in this address list. Use this if you don't care + * about grouping. + */ + public MailboxList flatten() { + // in the common case, all addresses are mailboxes + boolean groupDetected = false; + for (int i = 0; i < size(); i++) { + if (!(get(i) instanceof Mailbox)) { + groupDetected = true; + break; + } + } + + if (!groupDetected) + return new MailboxList(addresses, true); + + ArrayList results = new ArrayList(); + for (int i = 0; i < size(); i++) { + Address addr = get(i); + addr.addMailboxesTo(results); + } + + // copy-on-construct this time, because subclasses + // could have held onto a reference to the results + return new MailboxList(results, false); + } + + /** + * Dumps a representation of this address list to + * stdout, for debugging purposes. + */ + public void print() { + for (int i = 0; i < size(); i++) { + Address addr = get(i); + System.out.println(addr.toString()); + } + } + + + + /** + * Parse the address list string, such as the value + * of a From, To, Cc, Bcc, Sender, or Reply-To + * header. + * + * The string MUST be unfolded already. + */ + public static AddressList parse(String rawAddressList) throws ParseException { + AddressListParser parser = new AddressListParser(new StringReader(rawAddressList)); + return Builder.getInstance().buildAddressList(parser.parse()); + } + + /** + * Test console. + */ + public static void main(String[] args) throws Exception { + java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); + while (true) { + try { + System.out.print("> "); + String line = reader.readLine(); + if (line.length() == 0 || line.toLowerCase().equals("exit") || line.toLowerCase().equals("quit")) { + System.out.println("Goodbye."); + return; + } + AddressList list = parse(line); + list.print(); + } + catch(Exception e) { + e.printStackTrace(); + Thread.sleep(300); + } + } + } +} diff --git a/src/org/apache/james/mime4j/field/address/Builder.java b/src/org/apache/james/mime4j/field/address/Builder.java new file mode 100644 index 000000000..3699afed7 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/Builder.java @@ -0,0 +1,244 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; +import java.util.Iterator; + +import org.apache.james.mime4j.decoder.DecoderUtil; +import org.apache.james.mime4j.field.address.parser.*; +import org.apache.james.mime4j.field.address.parser.ASTaddr_spec; +import org.apache.james.mime4j.field.address.parser.ASTaddress; +import org.apache.james.mime4j.field.address.parser.ASTaddress_list; +import org.apache.james.mime4j.field.address.parser.ASTangle_addr; +import org.apache.james.mime4j.field.address.parser.ASTdomain; +import org.apache.james.mime4j.field.address.parser.ASTgroup_body; +import org.apache.james.mime4j.field.address.parser.ASTlocal_part; +import org.apache.james.mime4j.field.address.parser.ASTmailbox; +import org.apache.james.mime4j.field.address.parser.ASTname_addr; +import org.apache.james.mime4j.field.address.parser.ASTphrase; +import org.apache.james.mime4j.field.address.parser.ASTroute; +import org.apache.james.mime4j.field.address.parser.Node; +import org.apache.james.mime4j.field.address.parser.SimpleNode; +import org.apache.james.mime4j.field.address.parser.Token; + +/** + * Transforms the JJTree-generated abstract syntax tree + * into a graph of org.apache.james.mime4j.field.address objects. + * + * + */ +class Builder { + + private static Builder singleton = new Builder(); + + public static Builder getInstance() { + return singleton; + } + + + + public AddressList buildAddressList(ASTaddress_list node) { + ArrayList list = new ArrayList(); + for (int i = 0; i < node.jjtGetNumChildren(); i++) { + ASTaddress childNode = (ASTaddress) node.jjtGetChild(i); + Address address = buildAddress(childNode); + list.add(address); + } + return new AddressList(list, true); + } + + private Address buildAddress(ASTaddress node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + if (n instanceof ASTaddr_spec) { + return buildAddrSpec((ASTaddr_spec)n); + } + else if (n instanceof ASTangle_addr) { + return buildAngleAddr((ASTangle_addr)n); + } + else if (n instanceof ASTphrase) { + String name = buildString((ASTphrase)n, false); + Node n2 = it.nextNode(); + if (n2 instanceof ASTgroup_body) { + return new Group(name, buildGroupBody((ASTgroup_body)n2)); + } + else if (n2 instanceof ASTangle_addr) { + name = DecoderUtil.decodeEncodedWords(name); + return new NamedMailbox(name, buildAngleAddr((ASTangle_addr)n2)); + } + else { + throw new IllegalStateException(); + } + } + else { + throw new IllegalStateException(); + } + } + + + + private MailboxList buildGroupBody(ASTgroup_body node) { + ArrayList results = new ArrayList(); + ChildNodeIterator it = new ChildNodeIterator(node); + while (it.hasNext()) { + Node n = it.nextNode(); + if (n instanceof ASTmailbox) + results.add(buildMailbox((ASTmailbox)n)); + else + throw new IllegalStateException(); + } + return new MailboxList(results, true); + } + + private Mailbox buildMailbox(ASTmailbox node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + if (n instanceof ASTaddr_spec) { + return buildAddrSpec((ASTaddr_spec)n); + } + else if (n instanceof ASTangle_addr) { + return buildAngleAddr((ASTangle_addr)n); + } + else if (n instanceof ASTname_addr) { + return buildNameAddr((ASTname_addr)n); + } + else { + throw new IllegalStateException(); + } + } + + private NamedMailbox buildNameAddr(ASTname_addr node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + String name; + if (n instanceof ASTphrase) { + name = buildString((ASTphrase)n, false); + } + else { + throw new IllegalStateException(); + } + + n = it.nextNode(); + if (n instanceof ASTangle_addr) { + name = DecoderUtil.decodeEncodedWords(name); + return new NamedMailbox(name, buildAngleAddr((ASTangle_addr) n)); + } + else { + throw new IllegalStateException(); + } + } + + private Mailbox buildAngleAddr(ASTangle_addr node) { + ChildNodeIterator it = new ChildNodeIterator(node); + DomainList route = null; + Node n = it.nextNode(); + if (n instanceof ASTroute) { + route = buildRoute((ASTroute)n); + n = it.nextNode(); + } + else if (n instanceof ASTaddr_spec) + ; // do nothing + else + throw new IllegalStateException(); + + if (n instanceof ASTaddr_spec) + return buildAddrSpec(route, (ASTaddr_spec)n); + else + throw new IllegalStateException(); + } + + private DomainList buildRoute(ASTroute node) { + ArrayList results = new ArrayList(node.jjtGetNumChildren()); + ChildNodeIterator it = new ChildNodeIterator(node); + while (it.hasNext()) { + Node n = it.nextNode(); + if (n instanceof ASTdomain) + results.add(buildString((ASTdomain)n, true)); + else + throw new IllegalStateException(); + } + return new DomainList(results, true); + } + + private Mailbox buildAddrSpec(ASTaddr_spec node) { + return buildAddrSpec(null, node); + } + private Mailbox buildAddrSpec(DomainList route, ASTaddr_spec node) { + ChildNodeIterator it = new ChildNodeIterator(node); + String localPart = buildString((ASTlocal_part)it.nextNode(), true); + String domain = buildString((ASTdomain)it.nextNode(), true); + return new Mailbox(route, localPart, domain); + } + + + private String buildString(SimpleNode node, boolean stripSpaces) { + Token head = node.firstToken; + Token tail = node.lastToken; + StringBuffer out = new StringBuffer(); + + while (head != tail) { + out.append(head.image); + head = head.next; + if (!stripSpaces) + addSpecials(out, head.specialToken); + } + out.append(tail.image); + + return out.toString(); + } + + private void addSpecials(StringBuffer out, Token specialToken) { + if (specialToken != null) { + addSpecials(out, specialToken.specialToken); + out.append(specialToken.image); + } + } + + private static class ChildNodeIterator implements Iterator { + + private SimpleNode simpleNode; + private int index; + private int len; + + public ChildNodeIterator(SimpleNode simpleNode) { + this.simpleNode = simpleNode; + this.len = simpleNode.jjtGetNumChildren(); + this.index = 0; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasNext() { + return index < len; + } + + public Object next() { + return nextNode(); + } + + public Node nextNode() { + return simpleNode.jjtGetChild(index++); + } + + } +} diff --git a/src/org/apache/james/mime4j/field/address/DomainList.java b/src/org/apache/james/mime4j/field/address/DomainList.java new file mode 100644 index 000000000..23c377e9e --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/DomainList.java @@ -0,0 +1,76 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * An immutable, random-access list of Strings (that + * are supposedly domain names or domain literals). + * + * + */ +public class DomainList { + private ArrayList domains; + + /** + * @param domains An ArrayList that contains only String objects. + * @param dontCopy true iff it is not possible for the domains ArrayList to be modified by someone else. + */ + public DomainList(ArrayList domains, boolean dontCopy) { + if (domains != null) + this.domains = (dontCopy ? domains : (ArrayList) domains.clone()); + else + this.domains = new ArrayList(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return domains.size(); + } + + /** + * Gets the domain name or domain literal at the + * specified index. + * @throws IndexOutOfBoundsException If index is < 0 or >= size(). + */ + public String get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return (String) domains.get(index); + } + + /** + * Returns the list of domains formatted as a route + * string (not including the trailing ':'). + */ + public String toRouteString() { + StringBuffer out = new StringBuffer(); + for (int i = 0; i < domains.size(); i++) { + out.append("@"); + out.append(get(i)); + if (i + 1 < domains.size()) + out.append(","); + } + return out.toString(); + } +} diff --git a/src/org/apache/james/mime4j/field/address/Group.java b/src/org/apache/james/mime4j/field/address/Group.java new file mode 100644 index 000000000..eb4002708 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/Group.java @@ -0,0 +1,73 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * A named group of zero or more mailboxes. + * + * + */ +public class Group extends Address { + private String name; + private MailboxList mailboxList; + + /** + * @param name The group name. + * @param mailboxes The mailboxes in this group. + */ + public Group(String name, MailboxList mailboxes) { + this.name = name; + this.mailboxList = mailboxes; + } + + /** + * Returns the group name. + */ + public String getName() { + return name; + } + + /** + * Returns the mailboxes in this group. + */ + public MailboxList getMailboxes() { + return mailboxList; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(name); + buf.append(":"); + for (int i = 0; i < mailboxList.size(); i++) { + buf.append(mailboxList.get(i).toString()); + if (i + 1 < mailboxList.size()) + buf.append(","); + } + buf.append(";"); + return buf.toString(); + } + + protected void doAddMailboxesTo(ArrayList results) { + for (int i = 0; i < mailboxList.size(); i++) + results.add(mailboxList.get(i)); + } +} diff --git a/src/org/apache/james/mime4j/field/address/Mailbox.java b/src/org/apache/james/mime4j/field/address/Mailbox.java new file mode 100644 index 000000000..57668abfe --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/Mailbox.java @@ -0,0 +1,119 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * Represents a single e-mail address. + * + * + */ +public class Mailbox extends Address { + private DomainList route; + private String localPart; + private String domain; + + /** + * Creates a mailbox without a route. Routes are obsolete. + * @param localPart The part of the e-mail address to the left of the "@". + * @param domain The part of the e-mail address to the right of the "@". + */ + public Mailbox(String localPart, String domain) { + this(null, localPart, domain); + } + + /** + * Creates a mailbox with a route. Routes are obsolete. + * @param route The zero or more domains that make up the route. Can be null. + * @param localPart The part of the e-mail address to the left of the "@". + * @param domain The part of the e-mail address to the right of the "@". + */ + public Mailbox(DomainList route, String localPart, String domain) { + this.route = route; + this.localPart = localPart; + this.domain = domain; + } + + /** + * Returns the route list. + */ + public DomainList getRoute() { + return route; + } + + /** + * Returns the left part of the e-mail address + * (before "@"). + */ + public String getLocalPart() { + return localPart; + } + + /** + * Returns the right part of the e-mail address + * (after "@"). + */ + public String getDomain() { + return domain; + } + + /** + * Formats the address as a string, not including + * the route. + * + * @see #getAddressString(boolean) + */ + public String getAddressString() { + return getAddressString(false); + } + + /** + * Note that this value may not be usable + * for transport purposes, only display purposes. + * + * For example, if the unparsed address was + * + * <"Joe Cheng"@joecheng.com> + * + * this method would return + * + * + * + * which is not valid for transport; the local part + * would need to be re-quoted. + * + * @param includeRoute true if the route should be included if it exists. + */ + public String getAddressString(boolean includeRoute) { + return "<" + (!includeRoute || route == null ? "" : route.toRouteString() + ":") + + localPart + + (domain == null ? "" : "@") + + domain + ">"; + } + + protected final void doAddMailboxesTo(ArrayList results) { + results.add(this); + } + + public String toString() { + return getAddressString(); + } +} diff --git a/src/org/apache/james/mime4j/field/address/MailboxList.java b/src/org/apache/james/mime4j/field/address/MailboxList.java new file mode 100644 index 000000000..b25264da3 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/MailboxList.java @@ -0,0 +1,71 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * An immutable, random-access list of Mailbox objects. + * + * + */ +public class MailboxList { + + private ArrayList mailboxes; + + /** + * @param mailboxes An ArrayList that contains only Mailbox objects. + * @param dontCopy true iff it is not possible for the mailboxes ArrayList to be modified by someone else. + */ + public MailboxList(ArrayList mailboxes, boolean dontCopy) { + if (mailboxes != null) + this.mailboxes = (dontCopy ? mailboxes : (ArrayList) mailboxes.clone()); + else + this.mailboxes = new ArrayList(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return mailboxes.size(); + } + + /** + * Gets an address. + */ + public Mailbox get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return (Mailbox) mailboxes.get(index); + } + + /** + * Dumps a representation of this mailbox list to + * stdout, for debugging purposes. + */ + public void print() { + for (int i = 0; i < size(); i++) { + Mailbox mailbox = get(i); + System.out.println(mailbox.toString()); + } + } + +} diff --git a/src/org/apache/james/mime4j/field/address/NamedMailbox.java b/src/org/apache/james/mime4j/field/address/NamedMailbox.java new file mode 100644 index 000000000..dea0d821c --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/NamedMailbox.java @@ -0,0 +1,70 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address; + +/** + * A Mailbox that has a name/description. + * + * + */ +public class NamedMailbox extends Mailbox { + private String name; + + /** + * @see Mailbox#Mailbox(String, String) + */ + public NamedMailbox(String name, String localPart, String domain) { + super(localPart, domain); + this.name = name; + } + + /** + * @see Mailbox#Mailbox(DomainList, String, String) + */ + public NamedMailbox(String name, DomainList route, String localPart, String domain) { + super(route, localPart, domain); + this.name = name; + } + + /** + * Creates a named mailbox based on an unnamed mailbox. + */ + public NamedMailbox(String name, Mailbox baseMailbox) { + super(baseMailbox.getRoute(), baseMailbox.getLocalPart(), baseMailbox.getDomain()); + this.name = name; + } + + /** + * Returns the name of the mailbox. + */ + public String getName() { + return this.name; + } + + /** + * Same features (or problems) as Mailbox.getAddressString(boolean), + * only more so. + * + * @see Mailbox#getAddressString(boolean) + */ + public String getAddressString(boolean includeRoute) { + return (name == null ? "" : name + " ") + super.getAddressString(includeRoute); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java b/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java new file mode 100644 index 000000000..4d56d000b --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddr_spec.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddr_spec extends SimpleNode { + public ASTaddr_spec(int id) { + super(id); + } + + public ASTaddr_spec(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java b/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java new file mode 100644 index 000000000..47bdeda8e --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddress.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddress extends SimpleNode { + public ASTaddress(int id) { + super(id); + } + + public ASTaddress(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java b/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java new file mode 100644 index 000000000..737840e38 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddress_list.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddress_list extends SimpleNode { + public ASTaddress_list(int id) { + super(id); + } + + public ASTaddress_list(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java b/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java new file mode 100644 index 000000000..8cb8f421f --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTangle_addr.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTangle_addr extends SimpleNode { + public ASTangle_addr(int id) { + super(id); + } + + public ASTangle_addr(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java b/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java new file mode 100644 index 000000000..b52664386 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTdomain.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTdomain extends SimpleNode { + public ASTdomain(int id) { + super(id); + } + + public ASTdomain(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java b/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java new file mode 100644 index 000000000..f6017b9fc --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTgroup_body.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTgroup_body extends SimpleNode { + public ASTgroup_body(int id) { + super(id); + } + + public ASTgroup_body(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java b/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java new file mode 100644 index 000000000..5c244fa3e --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTlocal_part.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTlocal_part extends SimpleNode { + public ASTlocal_part(int id) { + super(id); + } + + public ASTlocal_part(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java b/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java new file mode 100644 index 000000000..aeb469da1 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTmailbox.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTmailbox extends SimpleNode { + public ASTmailbox(int id) { + super(id); + } + + public ASTmailbox(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java b/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java new file mode 100644 index 000000000..846c73167 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTname_addr.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTname_addr extends SimpleNode { + public ASTname_addr(int id) { + super(id); + } + + public ASTname_addr(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java b/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java new file mode 100644 index 000000000..7d711c529 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTphrase.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTphrase extends SimpleNode { + public ASTphrase(int id) { + super(id); + } + + public ASTphrase(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ASTroute.java b/src/org/apache/james/mime4j/field/address/parser/ASTroute.java new file mode 100644 index 000000000..54ea11523 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ASTroute.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTroute.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTroute extends SimpleNode { + public ASTroute(int id) { + super(id); + } + + public ASTroute(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java b/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java new file mode 100644 index 000000000..6cf08ac1d --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java @@ -0,0 +1,977 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParser.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants, AddressListParserConstants {/*@bgen(jjtree)*/ + protected JJTAddressListParserState jjtree = new JJTAddressListParserState();public static void main(String args[]) throws ParseException { + while (true) { + try { + AddressListParser parser = new AddressListParser(System.in); + parser.parseLine(); + ((SimpleNode)parser.jjtree.rootNode()).dump("> "); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static void log(String msg) { + System.out.print(msg); + } + + public ASTaddress_list parse() throws ParseException { + try { + parseAll(); + return (ASTaddress_list)jjtree.rootNode(); + } catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + } + + + void jjtreeOpenNodeScope(Node n) { + ((SimpleNode)n).firstToken = getToken(1); + } + + void jjtreeCloseNodeScope(Node n) { + ((SimpleNode)n).lastToken = getToken(0); + } + + final public void parseLine() throws ParseException { + address_list(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + } + + final public void parseAll() throws ParseException { + address_list(); + jj_consume_token(0); + } + + final public void address_list() throws ParseException { + /*@bgen(jjtree) address_list */ + ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + address(); + break; + default: + jj_la1[1] = jj_gen; + ; + } + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[2] = jj_gen; + break label_1; + } + jj_consume_token(3); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + address(); + break; + default: + jj_la1[3] = jj_gen; + ; + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void address() throws ParseException { + /*@bgen(jjtree) address */ + ASTaddress jjtn000 = new ASTaddress(JJTADDRESS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + if (jj_2_1(2147483647)) { + addr_spec(); + } else { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + angle_addr(); + break; + case DOTATOM: + case QUOTEDSTRING: + phrase(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + group_body(); + break; + case 6: + angle_addr(); + break; + default: + jj_la1[4] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[5] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void mailbox() throws ParseException { + /*@bgen(jjtree) mailbox */ + ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + if (jj_2_2(2147483647)) { + addr_spec(); + } else { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + angle_addr(); + break; + case DOTATOM: + case QUOTEDSTRING: + name_addr(); + break; + default: + jj_la1[6] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void name_addr() throws ParseException { + /*@bgen(jjtree) name_addr */ + ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + phrase(); + angle_addr(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void group_body() throws ParseException { + /*@bgen(jjtree) group_body */ + ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(4); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + mailbox(); + break; + default: + jj_la1[7] = jj_gen; + ; + } + label_2: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[8] = jj_gen; + break label_2; + } + jj_consume_token(3); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + mailbox(); + break; + default: + jj_la1[9] = jj_gen; + ; + } + } + jj_consume_token(5); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void angle_addr() throws ParseException { + /*@bgen(jjtree) angle_addr */ + ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(6); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 8: + route(); + break; + default: + jj_la1[10] = jj_gen; + ; + } + addr_spec(); + jj_consume_token(7); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void route() throws ParseException { + /*@bgen(jjtree) route */ + ASTroute jjtn000 = new ASTroute(JJTROUTE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(8); + domain(); + label_3: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + case 8: + ; + break; + default: + jj_la1[11] = jj_gen; + break label_3; + } + label_4: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[12] = jj_gen; + break label_4; + } + jj_consume_token(3); + } + jj_consume_token(8); + domain(); + } + jj_consume_token(4); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void phrase() throws ParseException { + /*@bgen(jjtree) phrase */ + ASTphrase jjtn000 = new ASTphrase(JJTPHRASE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + label_5: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + case QUOTEDSTRING: + ; + break; + default: + jj_la1[14] = jj_gen; + break label_5; + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void addr_spec() throws ParseException { + /*@bgen(jjtree) addr_spec */ + ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + local_part(); + jj_consume_token(8); + domain(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void local_part() throws ParseException { + /*@bgen(jjtree) local_part */ + ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000);Token t; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + label_6: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + case DOTATOM: + case QUOTEDSTRING: + ; + break; + default: + jj_la1[16] = jj_gen; + break label_6; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + t = jj_consume_token(9); + break; + default: + jj_la1[17] = jj_gen; + ; + } + if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING) + {if (true) throw new ParseException("Words in local part must be separated by '.'");} + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[18] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void domain() throws ParseException { + /*@bgen(jjtree) domain */ + ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000);Token t; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + label_7: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + case DOTATOM: + ; + break; + default: + jj_la1[19] = jj_gen; + break label_7; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + t = jj_consume_token(9); + break; + default: + jj_la1[20] = jj_gen; + ; + } + if (t.image.charAt(t.image.length() - 1) != '.') + {if (true) throw new ParseException("Atoms in domain names must be separated by '.'");} + t = jj_consume_token(DOTATOM); + } + break; + case DOMAINLITERAL: + jj_consume_token(DOMAINLITERAL); + break; + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final private boolean jj_2_1(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_1(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + final private boolean jj_2_2(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_2(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(1, xla); } + } + + final private boolean jj_3R_11() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(9)) jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_scan_token(14)) { + jj_scanpos = xsp; + if (jj_scan_token(31)) return true; + } + return false; + } + + final private boolean jj_3R_13() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(9)) jj_scanpos = xsp; + if (jj_scan_token(DOTATOM)) return true; + return false; + } + + final private boolean jj_3R_8() { + if (jj_3R_9()) return true; + if (jj_scan_token(8)) return true; + if (jj_3R_10()) return true; + return false; + } + + final private boolean jj_3_1() { + if (jj_3R_8()) return true; + return false; + } + + final private boolean jj_3R_12() { + if (jj_scan_token(DOTATOM)) return true; + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_13()) { jj_scanpos = xsp; break; } + } + return false; + } + + final private boolean jj_3R_10() { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_12()) { + jj_scanpos = xsp; + if (jj_scan_token(18)) return true; + } + return false; + } + + final private boolean jj_3_2() { + if (jj_3R_8()) return true; + return false; + } + + final private boolean jj_3R_9() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(14)) { + jj_scanpos = xsp; + if (jj_scan_token(31)) return true; + } + while (true) { + xsp = jj_scanpos; + if (jj_3R_11()) { jj_scanpos = xsp; break; } + } + return false; + } + + public AddressListParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; + public boolean lookingAhead = false; + private boolean jj_semLA; + private int jj_gen; + final private int[] jj_la1 = new int[22]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_0(); + jj_la1_1(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x80004040,0x8,0x80004040,0x50,0x80004040,0x80004040,0x80004040,0x8,0x80004040,0x100,0x108,0x8,0x80004000,0x80004000,0x80004000,0x80004200,0x200,0x80004000,0x4200,0x200,0x44000,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + final private JJCalls[] jj_2_rtns = new JJCalls[2]; + private boolean jj_rescan = false; + private int jj_gc = 0; + + public AddressListParser(java.io.InputStream stream) { + this(stream, null); + } + public AddressListParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new AddressListParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public AddressListParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new AddressListParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public AddressListParser(AddressListParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(AddressListParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) c.first = null; + c = c.next; + } + } + } + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + static private final class LookaheadSuccess extends java.lang.Error { } + final private LookaheadSuccess jj_ls = new LookaheadSuccess(); + final private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) jj_add_error_token(kind, i); + } + if (jj_scanpos.kind != kind) return true; + if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls; + return false; + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = lookingAhead ? jj_scanpos : token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) return; + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + boolean exists = false; + for (java.util.Enumeration e = jj_expentries.elements(); e.hasMoreElements();) { + int[] oldentry = (int[])(e.nextElement()); + if (oldentry.length == jj_expentry.length) { + exists = true; + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + exists = false; + break; + } + } + if (exists) break; + } + } + if (!exists) jj_expentries.addElement(jj_expentry); + if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[34]; + for (int i = 0; i < 34; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 22; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1< jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + case 1: jj_3_2(); break; + } + } + p = p.next; + } while (p != null); + } catch(LookaheadSuccess ls) { } + } + jj_rescan = false; + } + + final private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj b/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj new file mode 100644 index 000000000..685988634 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj @@ -0,0 +1,595 @@ +/*@bgen(jjtree) Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParser.jj */ +/*@egen*//**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + + +/** + * RFC2822 address list parser. + * + * Created 9/17/2004 + * by Joe Cheng + */ + +options { + STATIC=false; + LOOKAHEAD=1; + //DEBUG_PARSER=true; + //DEBUG_TOKEN_MANAGER=true; +} + +PARSER_BEGIN(AddressListParser) +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants/*@egen*/ {/*@bgen(jjtree)*/ + protected JJTAddressListParserState jjtree = new JJTAddressListParserState(); + +/*@egen*/ + public static void main(String args[]) throws ParseException { + while (true) { + try { + AddressListParser parser = new AddressListParser(System.in); + parser.parseLine(); + ((SimpleNode)parser.jjtree.rootNode()).dump("> "); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static void log(String msg) { + System.out.print(msg); + } + + public ASTaddress_list parse() throws ParseException { + try { + parseAll(); + return (ASTaddress_list)jjtree.rootNode(); + } catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + } + + + void jjtreeOpenNodeScope(Node n) { + ((SimpleNode)n).firstToken = getToken(1); + } + + void jjtreeCloseNodeScope(Node n) { + ((SimpleNode)n).lastToken = getToken(0); + } +} + +PARSER_END(AddressListParser) + +void parseLine() : +{} +{ + address_list() ["\r"] "\n" +} + +void parseAll() : +{} +{ + address_list() +} + +void address_list() : +{/*@bgen(jjtree) address_list */ + ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) address_list */ + try { +/*@egen*/ + [ address() ] + ( + "," + [ address() ] + )*/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void address() : +{/*@bgen(jjtree) address */ + ASTaddress jjtn000 = new ASTaddress(JJTADDRESS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) address */ + try { +/*@egen*/ + LOOKAHEAD(2147483647) + addr_spec() +| angle_addr() +| ( phrase() (group_body() | angle_addr()) )/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void mailbox() : +{/*@bgen(jjtree) mailbox */ + ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) mailbox */ + try { +/*@egen*/ + LOOKAHEAD(2147483647) + addr_spec() +| angle_addr() +| name_addr()/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void name_addr() : +{/*@bgen(jjtree) name_addr */ + ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) name_addr */ + try { +/*@egen*/ + phrase() angle_addr()/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void group_body() : +{/*@bgen(jjtree) group_body */ + ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) group_body */ + try { +/*@egen*/ + ":" + [ mailbox() ] + ( + "," + [ mailbox() ] + )* + ";"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void angle_addr() : +{/*@bgen(jjtree) angle_addr */ + ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) angle_addr */ + try { +/*@egen*/ + "<" [ route() ] addr_spec() ">"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void route() : +{/*@bgen(jjtree) route */ + ASTroute jjtn000 = new ASTroute(JJTROUTE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) route */ + try { +/*@egen*/ + "@" domain() ( (",")* "@" domain() )* ":"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void phrase() : +{/*@bgen(jjtree) phrase */ + ASTphrase jjtn000 = new ASTphrase(JJTPHRASE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) phrase */ +try { +/*@egen*/ +( +| +)+/*@bgen(jjtree)*/ +} finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } +} +/*@egen*/ +} + +void addr_spec() : +{/*@bgen(jjtree) addr_spec */ + ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) addr_spec */ + try { +/*@egen*/ + ( local_part() "@" domain() )/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void local_part() : +{/*@bgen(jjtree) local_part */ + ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/ Token t; } +{/*@bgen(jjtree) local_part */ + try { +/*@egen*/ + ( t= | t= ) + ( [t="."] + { + if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING) + throw new ParseException("Words in local part must be separated by '.'"); + } + ( t= | t= ) + )*/*@bgen(jjtree)*/ + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void domain() : +{/*@bgen(jjtree) domain */ + ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/ Token t; } +{/*@bgen(jjtree) domain */ + try { +/*@egen*/ + ( t= + ( [t="."] + { + if (t.image.charAt(t.image.length() - 1) != '.') + throw new ParseException("Atoms in domain names must be separated by '.'"); + } + t= + )* + ) +| /*@bgen(jjtree)*/ + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +SPECIAL_TOKEN : +{ + < WS: ( [" ", "\t"] )+ > +} + +TOKEN : +{ + < #ALPHA: ["a" - "z", "A" - "Z"] > +| < #DIGIT: ["0" - "9"] > +| < #ATEXT: ( | + | "!" | "#" | "$" | "%" + | "&" | "'" | "*" | "+" + | "-" | "/" | "=" | "?" + | "^" | "_" | "`" | "{" + | "|" | "}" | "~" + )> +| < DOTATOM: ( | "." )* > +} + +TOKEN_MGR_DECLS : +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; +} + +MORE : +{ + // domain literal + "[" : INDOMAINLITERAL +} + + +MORE : +{ + < > { image.deleteCharAt(image.length() - 2); } +| < ~["[", "]", "\\"] > +} + + +TOKEN : +{ + < DOMAINLITERAL: "]" > { matchedToken.image = image.toString(); }: DEFAULT +} + +MORE : +{ + // starts a comment + "(" : INCOMMENT +} + + +SKIP : +{ + // ends a comment + < COMMENT: ")" > : DEFAULT + // if this is ever changed to not be a SKIP, need + // to make sure matchedToken.token = token.toString() + // is called. +} + + +MORE : +{ + < > { image.deleteCharAt(image.length() - 2); } +| "(" { commentNest = 1; } : NESTED_COMMENT +| < > +} + + +MORE : +{ + < > { image.deleteCharAt(image.length() - 2); } +| "(" { ++commentNest; } +| ")" { --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); } +| < > +} + + +// QUOTED STRINGS + +MORE : +{ + "\"" { image.deleteCharAt(image.length() - 1); } : INQUOTEDSTRING +} + + +MORE : +{ + < > { image.deleteCharAt(image.length() - 2); } +| < (~["\"", "\\"])+ > +} + + +TOKEN : +{ + < QUOTEDSTRING: "\"" > { matchedToken.image = image.substring(0, image.length() - 1); } : DEFAULT +} + +// GLOBALS + +<*> +TOKEN : +{ + < #QUOTEDPAIR: "\\" > +| < #ANY: ~[] > +} + +// ERROR! +/* + +<*> +TOKEN : +{ + < UNEXPECTED_CHAR: > +} + +*/ \ No newline at end of file diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java b/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java new file mode 100644 index 000000000..a21ad35db --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java @@ -0,0 +1,76 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserConstants.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserConstants { + + int EOF = 0; + int WS = 10; + int ALPHA = 11; + int DIGIT = 12; + int ATEXT = 13; + int DOTATOM = 14; + int DOMAINLITERAL = 18; + int COMMENT = 20; + int QUOTEDSTRING = 31; + int QUOTEDPAIR = 32; + int ANY = 33; + + int DEFAULT = 0; + int INDOMAINLITERAL = 1; + int INCOMMENT = 2; + int NESTED_COMMENT = 3; + int INQUOTEDSTRING = 4; + + String[] tokenImage = { + "", + "\"\\r\"", + "\"\\n\"", + "\",\"", + "\":\"", + "\";\"", + "\"<\"", + "\">\"", + "\"@\"", + "\".\"", + "", + "", + "", + "", + "", + "\"[\"", + "", + "", + "\"]\"", + "\"(\"", + "\")\"", + "", + "\"(\"", + "", + "", + "\"(\"", + "\")\"", + "", + "\"\\\"\"", + "", + "", + "\"\\\"\"", + "", + "", + }; + +} diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java b/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java new file mode 100644 index 000000000..df8974a3b --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java @@ -0,0 +1,1009 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserTokenManager.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +public class AddressListParserTokenManager implements AddressListParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStopAtPos(0, 2); + case 13: + return jjStopAtPos(0, 1); + case 34: + return jjStopAtPos(0, 28); + case 40: + return jjStopAtPos(0, 19); + case 44: + return jjStopAtPos(0, 3); + case 46: + return jjStopAtPos(0, 9); + case 58: + return jjStopAtPos(0, 4); + case 59: + return jjStopAtPos(0, 5); + case 60: + return jjStopAtPos(0, 6); + case 62: + return jjStopAtPos(0, 7); + case 64: + return jjStopAtPos(0, 8); + case 91: + return jjStopAtPos(0, 15); + default : + return jjMoveNfa_0(1, 0); + } +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0xa3ffacfa00000000L & l) != 0L) + { + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 10) + kind = 10; + jjCheckNAdd(0); + } + break; + case 0: + if ((0x100000200L & l) == 0L) + break; + kind = 10; + jjCheckNAdd(0); + break; + case 2: + if ((0xa3ffecfa00000000L & l) == 0L) + break; + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 2: + if ((0x7fffffffc7fffffeL & l) == 0L) + break; + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 22); + case 41: + return jjStopAtPos(0, 20); + default : + return jjMoveNfa_2(0, 0); + } +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 23) + kind = 23; + break; + case 1: + if (kind > 21) + kind = 21; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 23) + kind = 23; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 21) + kind = 21; + break; + case 2: + if (kind > 23) + kind = 23; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 23) + kind = 23; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 21) + kind = 21; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_4(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_4(int pos, long active0) +{ + return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_4(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_4(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_4() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 31); + default : + return jjMoveNfa_4(0, 0); + } +} +private final int jjMoveNfa_4(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0xfffffffbffffffffL & l) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + case 1: + if (kind > 29) + kind = 29; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 29) + kind = 29; + break; + case 2: + if ((0xffffffffefffffffL & l) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 29) + kind = 29; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 25); + case 41: + return jjStopAtPos(0, 26); + default : + return jjMoveNfa_3(0, 0); + } +} +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 27) + kind = 27; + break; + case 1: + if (kind > 24) + kind = 24; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 27) + kind = 27; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 24) + kind = 24; + break; + case 2: + if (kind > 27) + kind = 27; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 27) + kind = 27; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 24) + kind = 24; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 93: + return jjStopAtPos(0, 18); + default : + return jjMoveNfa_1(0, 0); + } +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 17) + kind = 17; + break; + case 1: + if (kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffc7ffffffL & l) != 0L) + { + if (kind > 17) + kind = 17; + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 16) + kind = 16; + break; + case 2: + if ((0xffffffffc7ffffffL & l) != 0L && kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 17) + kind = 17; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\54", "\72", "\73", "\74", "\76", "\100", "\56", null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INDOMAINLITERAL", + "INCOMMENT", + "NESTED_COMMENT", + "INQUOTEDSTRING", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, 0, 2, 0, -1, 3, -1, -1, + -1, -1, -1, 4, -1, -1, 0, -1, -1, +}; +static final long[] jjtoToken = { + 0x800443ffL, +}; +static final long[] jjtoSkip = { + 0x100400L, +}; +static final long[] jjtoSpecial = { + 0x400L, +}; +static final long[] jjtoMore = { + 0x7feb8000L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[3]; +private final int[] jjstateSet = new int[6]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public AddressListParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public AddressListParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 3; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 5 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + case 4: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_4(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 16 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 21 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 22 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 24 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 25 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 26 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + case 28 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 1); + break; + case 29 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 18 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.toString(); + break; + case 31 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.substring(0, image.length() - 1); + break; + default : + break; + } +} +} diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java b/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java new file mode 100644 index 000000000..5987f19d8 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java @@ -0,0 +1,35 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java */ + +package org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserTreeConstants +{ + public int JJTVOID = 0; + public int JJTADDRESS_LIST = 1; + public int JJTADDRESS = 2; + public int JJTMAILBOX = 3; + public int JJTNAME_ADDR = 4; + public int JJTGROUP_BODY = 5; + public int JJTANGLE_ADDR = 6; + public int JJTROUTE = 7; + public int JJTPHRASE = 8; + public int JJTADDR_SPEC = 9; + public int JJTLOCAL_PART = 10; + public int JJTDOMAIN = 11; + + + public String[] jjtNodeName = { + "void", + "address_list", + "address", + "mailbox", + "name_addr", + "group_body", + "angle_addr", + "route", + "phrase", + "addr_spec", + "local_part", + "domain", + }; +} diff --git a/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java b/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java new file mode 100644 index 000000000..8ec2fe7d2 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java */ + +package org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserVisitor +{ + public Object visit(SimpleNode node, Object data); + public Object visit(ASTaddress_list node, Object data); + public Object visit(ASTaddress node, Object data); + public Object visit(ASTmailbox node, Object data); + public Object visit(ASTname_addr node, Object data); + public Object visit(ASTgroup_body node, Object data); + public Object visit(ASTangle_addr node, Object data); + public Object visit(ASTroute node, Object data); + public Object visit(ASTphrase node, Object data); + public Object visit(ASTaddr_spec node, Object data); + public Object visit(ASTlocal_part node, Object data); + public Object visit(ASTdomain node, Object data); +} diff --git a/src/org/apache/james/mime4j/field/address/parser/BaseNode.java b/src/org/apache/james/mime4j/field/address/parser/BaseNode.java new file mode 100644 index 000000000..42fe3db0c --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/BaseNode.java @@ -0,0 +1,30 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.address.parser; + +import org.apache.james.mime4j.field.address.parser.Node; +import org.apache.james.mime4j.field.address.parser.Token; + +public abstract class BaseNode implements Node { + + public Token firstToken; + public Token lastToken; + +} \ No newline at end of file diff --git a/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java b/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java new file mode 100644 index 000000000..cca539483 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java @@ -0,0 +1,123 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java */ + +package org.apache.james.mime4j.field.address.parser; + +class JJTAddressListParserState { + private java.util.Stack nodes; + private java.util.Stack marks; + + private int sp; // number of nodes on stack + private int mk; // current mark + private boolean node_created; + + JJTAddressListParserState() { + nodes = new java.util.Stack(); + marks = new java.util.Stack(); + sp = 0; + mk = 0; + } + + /* Determines whether the current node was actually closed and + pushed. This should only be called in the final user action of a + node scope. */ + boolean nodeCreated() { + return node_created; + } + + /* Call this to reinitialize the node stack. It is called + automatically by the parser's ReInit() method. */ + void reset() { + nodes.removeAllElements(); + marks.removeAllElements(); + sp = 0; + mk = 0; + } + + /* Returns the root node of the AST. It only makes sense to call + this after a successful parse. */ + Node rootNode() { + return (Node)nodes.elementAt(0); + } + + /* Pushes a node on to the stack. */ + void pushNode(Node n) { + nodes.push(n); + ++sp; + } + + /* Returns the node on the top of the stack, and remove it from the + stack. */ + Node popNode() { + if (--sp < mk) { + mk = ((Integer)marks.pop()).intValue(); + } + return (Node)nodes.pop(); + } + + /* Returns the node currently on the top of the stack. */ + Node peekNode() { + return (Node)nodes.peek(); + } + + /* Returns the number of children on the stack in the current node + scope. */ + int nodeArity() { + return sp - mk; + } + + + void clearNodeScope(Node n) { + while (sp > mk) { + popNode(); + } + mk = ((Integer)marks.pop()).intValue(); + } + + + void openNodeScope(Node n) { + marks.push(new Integer(mk)); + mk = sp; + n.jjtOpen(); + } + + + /* A definite node is constructed from a specified number of + children. That number of nodes are popped from the stack and + made the children of the definite node. Then the definite node + is pushed on to the stack. */ + void closeNodeScope(Node n, int num) { + mk = ((Integer)marks.pop()).intValue(); + while (num-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, num); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } + + + /* A conditional node is constructed if its condition is true. All + the nodes that have been pushed since the node was opened are + made children of the the conditional node, which is then pushed + on to the stack. If the condition is false the node is not + constructed and they are left on the stack. */ + void closeNodeScope(Node n, boolean condition) { + if (condition) { + int a = nodeArity(); + mk = ((Integer)marks.pop()).intValue(); + while (a-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, a); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } else { + mk = ((Integer)marks.pop()).intValue(); + node_created = false; + } + } +} diff --git a/src/org/apache/james/mime4j/field/address/parser/Node.java b/src/org/apache/james/mime4j/field/address/parser/Node.java new file mode 100644 index 000000000..158892016 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/Node.java @@ -0,0 +1,37 @@ +/* Generated By:JJTree: Do not edit this line. Node.java */ + +package org.apache.james.mime4j.field.address.parser; + +/* All AST nodes must implement this interface. It provides basic + machinery for constructing the parent and child relationships + between nodes. */ + +public interface Node { + + /** This method is called after the node has been made the current + node. It indicates that child nodes can now be added to it. */ + public void jjtOpen(); + + /** This method is called after all the child nodes have been + added. */ + public void jjtClose(); + + /** This pair of methods are used to inform the node of its + parent. */ + public void jjtSetParent(Node n); + public Node jjtGetParent(); + + /** This method tells the node to add its argument to the node's + list of children. */ + public void jjtAddChild(Node n, int i); + + /** This method returns a child node. The children are numbered + from zero, left to right. */ + public Node jjtGetChild(int i); + + /** Return the number of children the node has. */ + public int jjtGetNumChildren(); + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data); +} diff --git a/src/org/apache/james/mime4j/field/address/parser/ParseException.java b/src/org/apache/james/mime4j/field/address/parser/ParseException.java new file mode 100644 index 000000000..939c6cfed --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java b/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java new file mode 100644 index 000000000..957bbeabc --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java b/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java new file mode 100644 index 000000000..9bf537e60 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java @@ -0,0 +1,87 @@ +/* Generated By:JJTree: Do not edit this line. SimpleNode.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class SimpleNode extends org.apache.james.mime4j.field.address.parser.BaseNode implements Node { + protected Node parent; + protected Node[] children; + protected int id; + protected AddressListParser parser; + + public SimpleNode(int i) { + id = i; + } + + public SimpleNode(AddressListParser p, int i) { + this(i); + parser = p; + } + + public void jjtOpen() { + } + + public void jjtClose() { + } + + public void jjtSetParent(Node n) { parent = n; } + public Node jjtGetParent() { return parent; } + + public void jjtAddChild(Node n, int i) { + if (children == null) { + children = new Node[i + 1]; + } else if (i >= children.length) { + Node c[] = new Node[i + 1]; + System.arraycopy(children, 0, c, 0, children.length); + children = c; + } + children[i] = n; + } + + public Node jjtGetChild(int i) { + return children[i]; + } + + public int jjtGetNumChildren() { + return (children == null) ? 0 : children.length; + } + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + /** Accept the visitor. **/ + public Object childrenAccept(AddressListParserVisitor visitor, Object data) { + if (children != null) { + for (int i = 0; i < children.length; ++i) { + children[i].jjtAccept(visitor, data); + } + } + return data; + } + + /* You can override these two methods in subclasses of SimpleNode to + customize the way the node appears when the tree is dumped. If + your output uses more than one line you should override + toString(String), otherwise overriding toString() is probably all + you need to do. */ + + public String toString() { return AddressListParserTreeConstants.jjtNodeName[id]; } + public String toString(String prefix) { return prefix + toString(); } + + /* Override this method if you want to customize how the node dumps + out its children. */ + + public void dump(String prefix) { + System.out.println(toString(prefix)); + if (children != null) { + for (int i = 0; i < children.length; ++i) { + SimpleNode n = (SimpleNode)children[i]; + if (n != null) { + n.dump(prefix + " "); + } + } + } + } +} + diff --git a/src/org/apache/james/mime4j/field/address/parser/Token.java b/src/org/apache/james/mime4j/field/address/parser/Token.java new file mode 100644 index 000000000..0228aac3e --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java b/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java new file mode 100644 index 000000000..c06a44cf3 --- /dev/null +++ b/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.address.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java b/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java new file mode 100644 index 000000000..64e829a51 --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java @@ -0,0 +1,267 @@ +/* Generated By:JavaCC: Do not edit this line. ContentTypeParser.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; + +import java.util.ArrayList; + +public class ContentTypeParser implements ContentTypeParserConstants { + + private String type; + private String subtype; + private ArrayList paramNames = new ArrayList(); + private ArrayList paramValues = new ArrayList(); + + public String getType() { return type; } + public String getSubType() { return subtype; } + public ArrayList getParamNames() { return paramNames; } + public ArrayList getParamValues() { return paramValues; } + + public static void main(String args[]) throws ParseException { + while (true) { + try { + ContentTypeParser parser = new ContentTypeParser(System.in); + parser.parseLine(); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + final public void parseLine() throws ParseException { + parse(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + } + + final public void parseAll() throws ParseException { + parse(); + jj_consume_token(0); + } + + final public void parse() throws ParseException { + Token type; + Token subtype; + type = jj_consume_token(ATOKEN); + jj_consume_token(3); + subtype = jj_consume_token(ATOKEN); + this.type = type.image; + this.subtype = subtype.image; + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + ; + break; + default: + jj_la1[1] = jj_gen; + break label_1; + } + jj_consume_token(4); + parameter(); + } + } + + final public void parameter() throws ParseException { + Token attrib; + String val; + attrib = jj_consume_token(ATOKEN); + jj_consume_token(5); + val = value(); + paramNames.add(attrib.image); + paramValues.add(val); + } + + final public String value() throws ParseException { + Token t; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ATOKEN: + t = jj_consume_token(ATOKEN); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return t.image;} + throw new Error("Missing return statement in function"); + } + + public ContentTypeParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[3]; + static private int[] jj_la1_0; + static { + jj_la1_0(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x10,0x280000,}; + } + + public ContentTypeParser(java.io.InputStream stream) { + this(stream, null); + } + public ContentTypeParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new ContentTypeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public ContentTypeParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new ContentTypeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public ContentTypeParser(ContentTypeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(ContentTypeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[24]; + for (int i = 0; i < 24; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 3; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<", + "\"\\r\"", + "\"\\n\"", + "\"/\"", + "\";\"", + "\"=\"", + "", + "\"(\"", + "\")\"", + "", + "\"(\"", + "", + "", + "\"(\"", + "\")\"", + "", + "\"\\\"\"", + "", + "", + "\"\\\"\"", + "", + "", + "", + "", + }; + +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java b/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java new file mode 100644 index 000000000..05d940db5 --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java @@ -0,0 +1,877 @@ +/* Generated By:JavaCC: Do not edit this line. ContentTypeParserTokenManager.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; +import java.util.ArrayList; + +public class ContentTypeParserTokenManager implements ContentTypeParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStartNfaWithStates_0(0, 2, 2); + case 13: + return jjStartNfaWithStates_0(0, 1, 2); + case 34: + return jjStopAtPos(0, 16); + case 40: + return jjStopAtPos(0, 7); + case 47: + return jjStopAtPos(0, 3); + case 59: + return jjStopAtPos(0, 4); + case 61: + return jjStopAtPos(0, 5); + default : + return jjMoveNfa_0(3, 0); + } +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + if ((0x3ff6cfafffffdffL & l) != 0L) + { + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 6) + kind = 6; + jjCheckNAdd(0); + } + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 20) + kind = 20; + jjCheckNAdd(1); + } + break; + case 0: + if ((0x100000200L & l) == 0L) + break; + kind = 6; + jjCheckNAdd(0); + break; + case 1: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAdd(1); + break; + case 2: + if ((0x3ff6cfafffffdffL & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((0xffffffffc7fffffeL & l) == 0L) + break; + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 10); + case 41: + return jjStopAtPos(0, 8); + default : + return jjMoveNfa_1(0, 0); + } +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 11) + kind = 11; + break; + case 1: + if (kind > 9) + kind = 9; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 11) + kind = 11; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 9) + kind = 9; + break; + case 2: + if (kind > 11) + kind = 11; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 11) + kind = 11; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 9) + kind = 9; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 19); + default : + return jjMoveNfa_3(0, 0); + } +} +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0xfffffffbffffffffL & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + case 1: + if (kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 17) + kind = 17; + break; + case 2: + if ((0xffffffffefffffffL & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 13); + case 41: + return jjStopAtPos(0, 14); + default : + return jjMoveNfa_2(0, 0); + } +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 15) + kind = 15; + break; + case 1: + if (kind > 12) + kind = 12; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 15) + kind = 15; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 12) + kind = 12; + break; + case 2: + if (kind > 15) + kind = 15; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 15) + kind = 15; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 12) + kind = 12; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\57", "\73", "\75", null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INCOMMENT", + "NESTED_COMMENT", + "INQUOTEDSTRING", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, 3, -1, -1, 0, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x38003fL, +}; +static final long[] jjtoSkip = { + 0x140L, +}; +static final long[] jjtoSpecial = { + 0x40L, +}; +static final long[] jjtoMore = { + 0x7fe80L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[3]; +private final int[] jjstateSet = new int[6]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public ContentTypeParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public ContentTypeParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 3; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 4 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 9 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 10 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 12 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 13 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 14 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + case 16 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 1); + break; + case 17 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 19 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.substring(0, image.length() - 1); + break; + default : + break; + } +} +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java b/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java new file mode 100644 index 000000000..2c5dc8275 --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java b/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java new file mode 100644 index 000000000..c139e4f38 --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/Token.java b/src/org/apache/james/mime4j/field/contenttype/parser/Token.java new file mode 100644 index 000000000..5bef6cf02 --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java b/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java new file mode 100644 index 000000000..4a490efac --- /dev/null +++ b/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.contenttype.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/src/org/apache/james/mime4j/field/datetime/DateTime.java b/src/org/apache/james/mime4j/field/datetime/DateTime.java new file mode 100644 index 000000000..bf00ca753 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/DateTime.java @@ -0,0 +1,127 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.field.datetime; + +import org.apache.james.mime4j.field.datetime.parser.DateTimeParser; +import org.apache.james.mime4j.field.datetime.parser.ParseException; +import org.apache.james.mime4j.field.datetime.parser.TokenMgrError; + +import java.util.Date; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.GregorianCalendar; +import java.io.StringReader; + +public class DateTime { + private final Date date; + private final int year; + private final int month; + private final int day; + private final int hour; + private final int minute; + private final int second; + private final int timeZone; + + public DateTime(String yearString, int month, int day, int hour, int minute, int second, int timeZone) { + this.year = convertToYear(yearString); + this.date = convertToDate(year, month, day, hour, minute, second, timeZone); + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = second; + this.timeZone = timeZone; + } + + private int convertToYear(String yearString) { + int year = Integer.parseInt(yearString); + switch (yearString.length()) { + case 1: + case 2: + if (year >= 0 && year < 50) + return 2000 + year; + else + return 1900 + year; + case 3: + return 1900 + year; + default: + return year; + } + } + + public static Date convertToDate(int year, int month, int day, int hour, int minute, int second, int timeZone) { + Calendar c = new GregorianCalendar(TimeZone.getTimeZone("GMT+0")); + c.set(year, month - 1, day, hour, minute, second); + c.set(Calendar.MILLISECOND, 0); + + if (timeZone != Integer.MIN_VALUE) { + int minutes = ((timeZone / 100) * 60) + timeZone % 100; + c.add(Calendar.MINUTE, -1 * minutes); + } + + return c.getTime(); + } + + public Date getDate() { + return date; + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + public int getSecond() { + return second; + } + + public int getTimeZone() { + return timeZone; + } + + public void print() { + System.out.println(getYear() + " " + getMonth() + " " + getDay() + "; " + getHour() + " " + getMinute() + " " + getSecond() + " " + getTimeZone()); + } + + + public static DateTime parse(String dateString) throws ParseException { + try { + return new DateTimeParser(new StringReader(dateString)).parseAll(); + } + catch (TokenMgrError err) { + throw new ParseException(err.getMessage()); + } + } +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java b/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java new file mode 100644 index 000000000..94b517208 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java @@ -0,0 +1,570 @@ +/* Generated By:JavaCC: Do not edit this line. DateTimeParser.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; + +import org.apache.james.mime4j.field.datetime.DateTime; + +import java.util.Calendar; + +public class DateTimeParser implements DateTimeParserConstants { + private static final boolean ignoreMilitaryZoneOffset = true; + + public static void main(String args[]) throws ParseException { + while (true) { + try { + DateTimeParser parser = new DateTimeParser(System.in); + parser.parseLine(); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static int parseDigits(Token token) { + return Integer.parseInt(token.image, 10); + } + + private static int getMilitaryZoneOffset(char c) { + if (ignoreMilitaryZoneOffset) + return 0; + + c = Character.toUpperCase(c); + + switch (c) { + case 'A': return 1; + case 'B': return 2; + case 'C': return 3; + case 'D': return 4; + case 'E': return 5; + case 'F': return 6; + case 'G': return 7; + case 'H': return 8; + case 'I': return 9; + case 'K': return 10; + case 'L': return 11; + case 'M': return 12; + + case 'N': return -1; + case 'O': return -2; + case 'P': return -3; + case 'Q': return -4; + case 'R': return -5; + case 'S': return -6; + case 'T': return -7; + case 'U': return -8; + case 'V': return -9; + case 'W': return -10; + case 'X': return -11; + case 'Y': return -12; + + case 'Z': return 0; + default: return 0; + } + } + + private static class Time { + private int hour; + private int minute; + private int second; + private int zone; + + public Time(int hour, int minute, int second, int zone) { + this.hour = hour; + this.minute = minute; + this.second = second; + this.zone = zone; + } + + public int getHour() { return hour; } + public int getMinute() { return minute; } + public int getSecond() { return second; } + public int getZone() { return zone; } + } + + private static class Date { + private String year; + private int month; + private int day; + + public Date(String year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + public String getYear() { return year; } + public int getMonth() { return month; } + public int getDay() { return day; } + } + + final public DateTime parseLine() throws ParseException { + DateTime dt; + dt = date_time(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + {if (true) return dt;} + throw new Error("Missing return statement in function"); + } + + final public DateTime parseAll() throws ParseException { + DateTime dt; + dt = date_time(); + jj_consume_token(0); + {if (true) return dt;} + throw new Error("Missing return statement in function"); + } + + final public DateTime date_time() throws ParseException { + Date d; Time t; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + day_of_week(); + jj_consume_token(3); + break; + default: + jj_la1[1] = jj_gen; + ; + } + d = date(); + t = time(); + {if (true) return new DateTime( + d.getYear(), + d.getMonth(), + d.getDay(), + t.getHour(), + t.getMinute(), + t.getSecond(), + t.getZone());} // time zone offset + + throw new Error("Missing return statement in function"); + } + + final public String day_of_week() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + jj_consume_token(4); + break; + case 5: + jj_consume_token(5); + break; + case 6: + jj_consume_token(6); + break; + case 7: + jj_consume_token(7); + break; + case 8: + jj_consume_token(8); + break; + case 9: + jj_consume_token(9); + break; + case 10: + jj_consume_token(10); + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return token.image;} + throw new Error("Missing return statement in function"); + } + + final public Date date() throws ParseException { + int d, m; String y; + d = day(); + m = month(); + y = year(); + {if (true) return new Date(y, m, d);} + throw new Error("Missing return statement in function"); + } + + final public int day() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int month() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 11: + jj_consume_token(11); + {if (true) return 1;} + break; + case 12: + jj_consume_token(12); + {if (true) return 2;} + break; + case 13: + jj_consume_token(13); + {if (true) return 3;} + break; + case 14: + jj_consume_token(14); + {if (true) return 4;} + break; + case 15: + jj_consume_token(15); + {if (true) return 5;} + break; + case 16: + jj_consume_token(16); + {if (true) return 6;} + break; + case 17: + jj_consume_token(17); + {if (true) return 7;} + break; + case 18: + jj_consume_token(18); + {if (true) return 8;} + break; + case 19: + jj_consume_token(19); + {if (true) return 9;} + break; + case 20: + jj_consume_token(20); + {if (true) return 10;} + break; + case 21: + jj_consume_token(21); + {if (true) return 11;} + break; + case 22: + jj_consume_token(22); + {if (true) return 12;} + break; + default: + jj_la1[3] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); + } + + final public String year() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return t.image;} + throw new Error("Missing return statement in function"); + } + + final public Time time() throws ParseException { + int h, m, s=0, z; + h = hour(); + jj_consume_token(23); + m = minute(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 23: + jj_consume_token(23); + s = second(); + break; + default: + jj_la1[4] = jj_gen; + ; + } + z = zone(); + {if (true) return new Time(h, m, s, z);} + throw new Error("Missing return statement in function"); + } + + final public int hour() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int minute() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int second() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int zone() throws ParseException { + Token t, u; int z; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OFFSETDIR: + t = jj_consume_token(OFFSETDIR); + u = jj_consume_token(DIGITS); + z=parseDigits(u)*(t.image.equals("-") ? -1 : 1); + break; + case 25: + case 26: + case 27: + case 28: + case 29: + case 30: + case 31: + case 32: + case 33: + case 34: + case MILITARY_ZONE: + z = obs_zone(); + break; + default: + jj_la1[5] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return z;} + throw new Error("Missing return statement in function"); + } + + final public int obs_zone() throws ParseException { + Token t; int z; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 25: + jj_consume_token(25); + z=0; + break; + case 26: + jj_consume_token(26); + z=0; + break; + case 27: + jj_consume_token(27); + z=-5; + break; + case 28: + jj_consume_token(28); + z=-4; + break; + case 29: + jj_consume_token(29); + z=-6; + break; + case 30: + jj_consume_token(30); + z=-5; + break; + case 31: + jj_consume_token(31); + z=-7; + break; + case 32: + jj_consume_token(32); + z=-6; + break; + case 33: + jj_consume_token(33); + z=-8; + break; + case 34: + jj_consume_token(34); + z=-7; + break; + case MILITARY_ZONE: + t = jj_consume_token(MILITARY_ZONE); + z=getMilitaryZoneOffset(t.image.charAt(0)); + break; + default: + jj_la1[6] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return z * 100;} + throw new Error("Missing return statement in function"); + } + + public DateTimeParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[7]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_0(); + jj_la1_1(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x7f0,0x7f0,0x7ff800,0x800000,0xff000000,0xfe000000,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0xf,0xf,}; + } + + public DateTimeParser(java.io.InputStream stream) { + this(stream, null); + } + public DateTimeParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new DateTimeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public DateTimeParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new DateTimeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public DateTimeParser(DateTimeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(DateTimeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector jj_expentries = new java.util.Vector(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[49]; + for (int i = 0; i < 49; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 7; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<", + "\"\\r\"", + "\"\\n\"", + "\",\"", + "\"Mon\"", + "\"Tue\"", + "\"Wed\"", + "\"Thu\"", + "\"Fri\"", + "\"Sat\"", + "\"Sun\"", + "\"Jan\"", + "\"Feb\"", + "\"Mar\"", + "\"Apr\"", + "\"May\"", + "\"Jun\"", + "\"Jul\"", + "\"Aug\"", + "\"Sep\"", + "\"Oct\"", + "\"Nov\"", + "\"Dec\"", + "\":\"", + "", + "\"UT\"", + "\"GMT\"", + "\"EST\"", + "\"EDT\"", + "\"CST\"", + "\"CDT\"", + "\"MST\"", + "\"MDT\"", + "\"PST\"", + "\"PDT\"", + "", + "", + "\"(\"", + "\")\"", + "", + "\"(\"", + "", + "", + "\"(\"", + "\")\"", + "", + "", + "", + "", + }; + +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java b/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java new file mode 100644 index 000000000..e75998cf2 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java @@ -0,0 +1,882 @@ +/* Generated By:JavaCC: Do not edit this line. DateTimeParserTokenManager.java */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; +import org.apache.james.mime4j.field.datetime.DateTime; +import java.util.Calendar; + +public class DateTimeParserTokenManager implements DateTimeParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x7fe7cf7f0L) != 0L) + { + jjmatchedKind = 35; + return -1; + } + return -1; + case 1: + if ((active0 & 0x7fe7cf7f0L) != 0L) + { + if (jjmatchedPos == 0) + { + jjmatchedKind = 35; + jjmatchedPos = 0; + } + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStopAtPos(0, 2); + case 13: + return jjStopAtPos(0, 1); + case 40: + return jjStopAtPos(0, 37); + case 44: + return jjStopAtPos(0, 3); + case 58: + return jjStopAtPos(0, 23); + case 65: + return jjMoveStringLiteralDfa1_0(0x44000L); + case 67: + return jjMoveStringLiteralDfa1_0(0x60000000L); + case 68: + return jjMoveStringLiteralDfa1_0(0x400000L); + case 69: + return jjMoveStringLiteralDfa1_0(0x18000000L); + case 70: + return jjMoveStringLiteralDfa1_0(0x1100L); + case 71: + return jjMoveStringLiteralDfa1_0(0x4000000L); + case 74: + return jjMoveStringLiteralDfa1_0(0x30800L); + case 77: + return jjMoveStringLiteralDfa1_0(0x18000a010L); + case 78: + return jjMoveStringLiteralDfa1_0(0x200000L); + case 79: + return jjMoveStringLiteralDfa1_0(0x100000L); + case 80: + return jjMoveStringLiteralDfa1_0(0x600000000L); + case 83: + return jjMoveStringLiteralDfa1_0(0x80600L); + case 84: + return jjMoveStringLiteralDfa1_0(0xa0L); + case 85: + return jjMoveStringLiteralDfa1_0(0x2000000L); + case 87: + return jjMoveStringLiteralDfa1_0(0x40L); + default : + return jjMoveNfa_0(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_0(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(0, active0); + return 1; + } + switch(curChar) + { + case 68: + return jjMoveStringLiteralDfa2_0(active0, 0x550000000L); + case 77: + return jjMoveStringLiteralDfa2_0(active0, 0x4000000L); + case 83: + return jjMoveStringLiteralDfa2_0(active0, 0x2a8000000L); + case 84: + if ((active0 & 0x2000000L) != 0L) + return jjStopAtPos(1, 25); + break; + case 97: + return jjMoveStringLiteralDfa2_0(active0, 0xaa00L); + case 99: + return jjMoveStringLiteralDfa2_0(active0, 0x100000L); + case 101: + return jjMoveStringLiteralDfa2_0(active0, 0x481040L); + case 104: + return jjMoveStringLiteralDfa2_0(active0, 0x80L); + case 111: + return jjMoveStringLiteralDfa2_0(active0, 0x200010L); + case 112: + return jjMoveStringLiteralDfa2_0(active0, 0x4000L); + case 114: + return jjMoveStringLiteralDfa2_0(active0, 0x100L); + case 117: + return jjMoveStringLiteralDfa2_0(active0, 0x70420L); + default : + break; + } + return jjStartNfa_0(0, active0); +} +private final int jjMoveStringLiteralDfa2_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_0(0, old0); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(1, active0); + return 2; + } + switch(curChar) + { + case 84: + if ((active0 & 0x4000000L) != 0L) + return jjStopAtPos(2, 26); + else if ((active0 & 0x8000000L) != 0L) + return jjStopAtPos(2, 27); + else if ((active0 & 0x10000000L) != 0L) + return jjStopAtPos(2, 28); + else if ((active0 & 0x20000000L) != 0L) + return jjStopAtPos(2, 29); + else if ((active0 & 0x40000000L) != 0L) + return jjStopAtPos(2, 30); + else if ((active0 & 0x80000000L) != 0L) + return jjStopAtPos(2, 31); + else if ((active0 & 0x100000000L) != 0L) + return jjStopAtPos(2, 32); + else if ((active0 & 0x200000000L) != 0L) + return jjStopAtPos(2, 33); + else if ((active0 & 0x400000000L) != 0L) + return jjStopAtPos(2, 34); + break; + case 98: + if ((active0 & 0x1000L) != 0L) + return jjStopAtPos(2, 12); + break; + case 99: + if ((active0 & 0x400000L) != 0L) + return jjStopAtPos(2, 22); + break; + case 100: + if ((active0 & 0x40L) != 0L) + return jjStopAtPos(2, 6); + break; + case 101: + if ((active0 & 0x20L) != 0L) + return jjStopAtPos(2, 5); + break; + case 103: + if ((active0 & 0x40000L) != 0L) + return jjStopAtPos(2, 18); + break; + case 105: + if ((active0 & 0x100L) != 0L) + return jjStopAtPos(2, 8); + break; + case 108: + if ((active0 & 0x20000L) != 0L) + return jjStopAtPos(2, 17); + break; + case 110: + if ((active0 & 0x10L) != 0L) + return jjStopAtPos(2, 4); + else if ((active0 & 0x400L) != 0L) + return jjStopAtPos(2, 10); + else if ((active0 & 0x800L) != 0L) + return jjStopAtPos(2, 11); + else if ((active0 & 0x10000L) != 0L) + return jjStopAtPos(2, 16); + break; + case 112: + if ((active0 & 0x80000L) != 0L) + return jjStopAtPos(2, 19); + break; + case 114: + if ((active0 & 0x2000L) != 0L) + return jjStopAtPos(2, 13); + else if ((active0 & 0x4000L) != 0L) + return jjStopAtPos(2, 14); + break; + case 116: + if ((active0 & 0x200L) != 0L) + return jjStopAtPos(2, 9); + else if ((active0 & 0x100000L) != 0L) + return jjStopAtPos(2, 20); + break; + case 117: + if ((active0 & 0x80L) != 0L) + return jjStopAtPos(2, 7); + break; + case 118: + if ((active0 & 0x200000L) != 0L) + return jjStopAtPos(2, 21); + break; + case 121: + if ((active0 & 0x8000L) != 0L) + return jjStopAtPos(2, 15); + break; + default : + break; + } + return jjStartNfa_0(1, active0); +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 4; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 46) + kind = 46; + jjCheckNAdd(3); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 36) + kind = 36; + jjCheckNAdd(2); + } + else if ((0x280000000000L & l) != 0L) + { + if (kind > 24) + kind = 24; + } + break; + case 2: + if ((0x100000200L & l) == 0L) + break; + kind = 36; + jjCheckNAdd(2); + break; + case 3: + if ((0x3ff000000000000L & l) == 0L) + break; + kind = 46; + jjCheckNAdd(3); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x7fffbfe07fffbfeL & l) != 0L) + kind = 35; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 4 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 40); + case 41: + return jjStopAtPos(0, 38); + default : + return jjMoveNfa_1(0, 0); + } +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 41) + kind = 41; + break; + case 1: + if (kind > 39) + kind = 39; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 41) + kind = 41; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 39) + kind = 39; + break; + case 2: + if (kind > 41) + kind = 41; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 41) + kind = 41; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 39) + kind = 39; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 43); + case 41: + return jjStopAtPos(0, 44); + default : + return jjMoveNfa_2(0, 0); + } +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 45) + kind = 45; + break; + case 1: + if (kind > 42) + kind = 42; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 45) + kind = 45; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 42) + kind = 42; + break; + case 2: + if (kind > 45) + kind = 45; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 45) + kind = 45; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 42) + kind = 42; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\54", "\115\157\156", "\124\165\145", "\127\145\144", +"\124\150\165", "\106\162\151", "\123\141\164", "\123\165\156", "\112\141\156", +"\106\145\142", "\115\141\162", "\101\160\162", "\115\141\171", "\112\165\156", +"\112\165\154", "\101\165\147", "\123\145\160", "\117\143\164", "\116\157\166", +"\104\145\143", "\72", null, "\125\124", "\107\115\124", "\105\123\124", "\105\104\124", +"\103\123\124", "\103\104\124", "\115\123\124", "\115\104\124", "\120\123\124", +"\120\104\124", null, null, null, null, null, null, null, null, null, null, null, null, null, +null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INCOMMENT", + "NESTED_COMMENT", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x400fffffffffL, +}; +static final long[] jjtoSkip = { + 0x5000000000L, +}; +static final long[] jjtoSpecial = { + 0x1000000000L, +}; +static final long[] jjtoMore = { + 0x3fa000000000L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[4]; +private final int[] jjstateSet = new int[8]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public DateTimeParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public DateTimeParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 4; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 3 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 39 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 40 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 42 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 43 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 44 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + default : + break; + } +} +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java b/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java new file mode 100644 index 000000000..418699107 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java b/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java new file mode 100644 index 000000000..d44a9018b --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/Token.java b/src/org/apache/james/mime4j/field/datetime/parser/Token.java new file mode 100644 index 000000000..52d101ed0 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java b/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java new file mode 100644 index 000000000..973255070 --- /dev/null +++ b/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.james.mime4j.field.datetime.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? " " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/src/org/apache/james/mime4j/message/AbstractBody.java b/src/org/apache/james/mime4j/message/AbstractBody.java new file mode 100644 index 000000000..d2647ec77 --- /dev/null +++ b/src/org/apache/james/mime4j/message/AbstractBody.java @@ -0,0 +1,47 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + + +/** + * Abstract Body implementation providing the parent + * functionality required by bodies. + * + * + * @version $Id: AbstractBody.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public abstract class AbstractBody implements Body { + private Entity parent = null; + + /** + * @see org.apache.james.mime4j.message.Body#getParent() + */ + public Entity getParent() { + return parent; + } + + /** + * @see org.apache.james.mime4j.message.Body#setParent(org.apache.james.mime4j.message.Entity) + */ + public void setParent(Entity parent) { + this.parent = parent; + } + +} diff --git a/src/org/apache/james/mime4j/message/BinaryBody.java b/src/org/apache/james/mime4j/message/BinaryBody.java new file mode 100644 index 000000000..bfc992a8f --- /dev/null +++ b/src/org/apache/james/mime4j/message/BinaryBody.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * Interface implemented by bodies containing binary data. + * + * + * @version $Id: BinaryBody.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public interface BinaryBody extends Body { + + /** + * Gets a InputStream which reads the bytes of the + * body. + * + * @return the stream. + * @throws IOException on I/O errors. + */ + InputStream getInputStream() throws IOException; +} diff --git a/src/org/apache/james/mime4j/message/Body.java b/src/org/apache/james/mime4j/message/Body.java new file mode 100644 index 000000000..54b8948db --- /dev/null +++ b/src/org/apache/james/mime4j/message/Body.java @@ -0,0 +1,54 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Encapsulates the body of an entity (see RFC 2045). + * + * + * @version $Id: Body.java,v 1.4 2004/10/04 15:36:43 ntherning Exp $ + */ +public interface Body { + + /** + * Gets the parent of this body. + * + * @return the parent. + */ + Entity getParent(); + + /** + * Sets the parent of this body. + * + * @param parent the parent. + */ + void setParent(Entity parent); + + /** + * Writes this body to the given stream in MIME message format. + * + * @param out the stream to write to. + * @throws IOException on I/O errors. + */ + void writeTo(OutputStream out) throws IOException; +} diff --git a/src/org/apache/james/mime4j/message/BodyPart.java b/src/org/apache/james/mime4j/message/BodyPart.java new file mode 100644 index 000000000..474030d7f --- /dev/null +++ b/src/org/apache/james/mime4j/message/BodyPart.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.OutputStream; + + +/** + * Represents a MIME body part (see RFC 2045). + * + * + * @version $Id: BodyPart.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public class BodyPart extends Entity { + + /** + * + * @see org.apache.james.mime4j.message.Entity#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + getHeader().writeTo(out); + getBody().writeTo(out); + } +} diff --git a/src/org/apache/james/mime4j/message/Entity.java b/src/org/apache/james/mime4j/message/Entity.java new file mode 100644 index 000000000..96f2a4875 --- /dev/null +++ b/src/org/apache/james/mime4j/message/Entity.java @@ -0,0 +1,170 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.james.mime4j.field.ContentTransferEncodingField; +import org.apache.james.mime4j.field.ContentTypeField; +import org.apache.james.mime4j.field.Field; + +/** + * MIME entity. An entity has a header and a body (see RFC 2045). + * + * + * @version $Id: Entity.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public abstract class Entity { + private Header header = null; + private Body body = null; + private Entity parent = null; + + /** + * Gets the parent entity of this entity. + * Returns null if this is the root entity. + * + * @return the parent or null. + */ + public Entity getParent() { + return parent; + } + + /** + * Sets the parent entity of this entity. + * + * @param parent the parent entity or null if + * this will be the root entity. + */ + public void setParent(Entity parent) { + this.parent = parent; + } + + /** + * Gets the entity header. + * + * @return the header. + */ + public Header getHeader() { + return header; + } + + /** + * Sets the entity header. + * + * @param header the header. + */ + public void setHeader(Header header) { + this.header = header; + } + + /** + * Gets the body of this entity. + * + * @return the body, + */ + public Body getBody() { + return body; + } + + /** + * Sets the body of this entity. + * + * @param body the body. + */ + public void setBody(Body body) { + this.body = body; + body.setParent(this); + } + + /** + * Determines the MIME type of this Entity. The MIME type + * is derived by looking at the parent's Content-Type field if no + * Content-Type field is set for this Entity. + * + * @return the MIME type. + */ + public String getMimeType() { + ContentTypeField child = + (ContentTypeField) getHeader().getField(Field.CONTENT_TYPE); + ContentTypeField parent = getParent() != null + ? (ContentTypeField) getParent().getHeader(). + getField(Field.CONTENT_TYPE) + : null; + + return ContentTypeField.getMimeType(child, parent); + } + + /** + * Determines the MIME character set encoding of this Entity. + * + * @return the MIME character set encoding. + */ + public String getCharset() { + return ContentTypeField.getCharset( + (ContentTypeField) getHeader().getField(Field.CONTENT_TYPE)); + } + + /** + * Determines the transfer encoding of this Entity. + * + * @return the transfer encoding. + */ + public String getContentTransferEncoding() { + ContentTransferEncodingField f = (ContentTransferEncodingField) + getHeader().getField(Field.CONTENT_TRANSFER_ENCODING); + + return ContentTransferEncodingField.getEncoding(f); + } + + /** + * Determines if the MIME type of this Entity matches the + * given one. MIME types are case-insensitive. + * + * @param type the MIME type to match against. + * @return true on match, false otherwise. + */ + public boolean isMimeType(String type) { + return getMimeType().equalsIgnoreCase(type); + } + + /** + * Determines if the MIME type of this Entity is + * multipart/*. Since multipart-entities must have + * a boundary parameter in the Content-Type field this + * method returns false if no boundary exists. + * + * @return true on match, false otherwise. + */ + public boolean isMultipart() { + ContentTypeField f = + (ContentTypeField) getHeader().getField(Field.CONTENT_TYPE); + return f != null && f.getBoundary() != null + && getMimeType().startsWith(ContentTypeField.TYPE_MULTIPART_PREFIX); + } + + /** + * Write the content to the given outputstream + * + * @param out the outputstream to write to + * @throws IOException + */ + public abstract void writeTo(OutputStream out) throws IOException; +} diff --git a/src/org/apache/james/mime4j/message/Header.java b/src/org/apache/james/mime4j/message/Header.java new file mode 100644 index 000000000..90a0ef480 --- /dev/null +++ b/src/org/apache/james/mime4j/message/Header.java @@ -0,0 +1,155 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.james.mime4j.AbstractContentHandler; +import org.apache.james.mime4j.MimeStreamParser; +import org.apache.james.mime4j.field.ContentTypeField; +import org.apache.james.mime4j.field.Field; +import org.apache.james.mime4j.util.CharsetUtil; + + +/** + * The header of an entity (see RFC 2045). + * + * + * @version $Id: Header.java,v 1.3 2004/10/04 15:36:44 ntherning Exp $ + */ +public class Header { + private List fields = new LinkedList(); + private HashMap fieldMap = new HashMap(); + + /** + * Creates a new empty Header. + */ + public Header() { + } + + /** + * Creates a new Header from the specified stream. + * + * @param is the stream to read the header from. + */ + public Header(InputStream is) throws IOException { + final MimeStreamParser parser = new MimeStreamParser(); + parser.setContentHandler(new AbstractContentHandler() { + public void endHeader() { + parser.stop(); + } + public void field(String fieldData) { + addField(Field.parse(fieldData)); + } + }); + parser.parse(is); + } + + /** + * Adds a field to the end of the list of fields. + * + * @param field the field to add. + */ + public void addField(Field field) { + List values = (List) fieldMap.get(field.getName().toLowerCase()); + if (values == null) { + values = new LinkedList(); + fieldMap.put(field.getName().toLowerCase(), values); + } + values.add(field); + fields.add(field); + } + + /** + * Gets the fields of this header. The returned list will not be + * modifiable. + * + * @return the list of Field objects. + */ + public List getFields() { + return Collections.unmodifiableList(fields); + } + + /** + * Gets a Field given a field name. If there are multiple + * such fields defined in this header the first one will be returned. + * + * @param name the field name (e.g. From, Subject). + * @return the field or null if none found. + */ + public Field getField(String name) { + List l = (List) fieldMap.get(name.toLowerCase()); + if (l != null && !l.isEmpty()) { + return (Field) l.get(0); + } + return null; + } + + /** + * Gets all Fields having the specified field name. + * + * @param name the field name (e.g. From, Subject). + * @return the list of fields. + */ + public List getFields(String name) { + List l = (List) fieldMap.get(name.toLowerCase()); + return Collections.unmodifiableList(l); + } + + /** + * Return Header Object as String representation. Each headerline is + * seperated by "\r\n" + * + * @return headers + */ + public String toString() { + StringBuffer str = new StringBuffer(); + for (Iterator it = fields.iterator(); it.hasNext();) { + str.append(it.next().toString()); + str.append("\r\n"); + } + return str.toString(); + } + + + /** + * Write the Header to the given OutputStream + * + * @param out the OutputStream to write to + * @throws IOException + */ + public void writeTo(OutputStream out) throws IOException { + String charString = ((ContentTypeField) getField(Field.CONTENT_TYPE)).getCharset(); + + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, CharsetUtil.getCharset(charString)),8192); + writer.write(toString()+ "\r\n"); + writer.flush(); + } + +} diff --git a/src/org/apache/james/mime4j/message/MemoryBinaryBody.java b/src/org/apache/james/mime4j/message/MemoryBinaryBody.java new file mode 100644 index 000000000..0db44e199 --- /dev/null +++ b/src/org/apache/james/mime4j/message/MemoryBinaryBody.java @@ -0,0 +1,90 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.TempPath; +import org.apache.james.mime4j.util.TempStorage; + + +/** + * Binary body backed by a {@link org.apache.james.mime4j.util.TempFile}. + * + * + * @version $Id: TempFileBinaryBody.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +class MemoryBinaryBody extends AbstractBody implements BinaryBody { + private static Log log = LogFactory.getLog(MemoryBinaryBody.class); + + private Entity parent = null; + private byte[] tempFile = null; + + /** + * Use the given InputStream to build the TemporyFileBinaryBody + * + * @param is the InputStream to use as source + * @throws IOException + */ + public MemoryBinaryBody(InputStream is) throws IOException { + + TempPath tempPath = TempStorage.getInstance().getRootTempPath(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.copy(is, out); + out.close(); + tempFile = out.toByteArray(); + } + + /** + * @see org.apache.james.mime4j.message.AbstractBody#getParent() + */ + public Entity getParent() { + return parent; + } + + /** + * @see org.apache.james.mime4j.message.AbstractBody#setParent(org.apache.james.mime4j.message.Entity) + */ + public void setParent(Entity parent) { + this.parent = parent; + } + + /** + * @see org.apache.james.mime4j.message.BinaryBody#getInputStream() + */ + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(tempFile); + } + + /** + * @see org.apache.james.mime4j.message.Body#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + IOUtils.copy(getInputStream(),out); + } +} diff --git a/src/org/apache/james/mime4j/message/MemoryTextBody.java b/src/org/apache/james/mime4j/message/MemoryTextBody.java new file mode 100644 index 000000000..e5e9d9d72 --- /dev/null +++ b/src/org/apache/james/mime4j/message/MemoryTextBody.java @@ -0,0 +1,116 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.CharsetUtil; +import org.apache.james.mime4j.util.TempPath; +import org.apache.james.mime4j.util.TempStorage; + + +/** + * Text body backed by a {@link org.apache.james.mime4j.util.TempFile}. + * + * + * @version $Id: TempFileTextBody.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $ + */ +class MemoryTextBody extends AbstractBody implements TextBody { + private static Log log = LogFactory.getLog(MemoryTextBody.class); + + private String mimeCharset = null; + private byte[] tempFile = null; + + public MemoryTextBody(InputStream is) throws IOException { + this(is, null); + } + + public MemoryTextBody(InputStream is, String mimeCharset) + throws IOException { + + this.mimeCharset = mimeCharset; + + TempPath tempPath = TempStorage.getInstance().getRootTempPath(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + IOUtils.copy(is, out); + out.close(); + tempFile = out.toByteArray(); + } + + /** + * @see org.apache.james.mime4j.message.TextBody#getReader() + */ + public Reader getReader() throws UnsupportedEncodingException, IOException { + String javaCharset = null; + if (mimeCharset != null) { + javaCharset = CharsetUtil.toJavaCharset(mimeCharset); + } + + if (javaCharset == null) { + javaCharset = "ISO-8859-1"; + + if (log.isWarnEnabled()) { + if (mimeCharset == null) { + log.warn("No MIME charset specified. Using " + javaCharset + + " instead."); + } else { + log.warn("MIME charset '" + mimeCharset + "' has no " + + "corresponding Java charset. Using " + javaCharset + + " instead."); + } + } + } + /* + if (log.isWarnEnabled()) { + if (mimeCharset == null) { + log.warn("No MIME charset specified. Using the " + + "platform's default charset."); + } else { + log.warn("MIME charset '" + mimeCharset + "' has no " + + "corresponding Java charset. Using the " + + "platform's default charset."); + } + } + + return new InputStreamReader(tempFile.getInputStream()); + }*/ + + return new InputStreamReader(new ByteArrayInputStream(tempFile), javaCharset); + } + + + /** + * @see org.apache.james.mime4j.message.Body#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + IOUtils.copy(new ByteArrayInputStream(tempFile), out); + } +} diff --git a/src/org/apache/james/mime4j/message/Message.java b/src/org/apache/james/mime4j/message/Message.java new file mode 100644 index 000000000..fc28c586a --- /dev/null +++ b/src/org/apache/james/mime4j/message/Message.java @@ -0,0 +1,256 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Stack; + +import org.apache.james.mime4j.BodyDescriptor; +import org.apache.james.mime4j.ContentHandler; +import org.apache.james.mime4j.MimeStreamParser; +import org.apache.james.mime4j.decoder.Base64InputStream; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; +import org.apache.james.mime4j.field.Field; +import org.apache.james.mime4j.field.UnstructuredField; + + +/** + * Represents a MIME message. The following code parses a stream into a + * Message object. + * + *
+ *      Message msg = new Message(new BufferedInputStream(
+ *                                      new FileInputStream("mime.msg")));
+ * 
+ * + * + * + * @version $Id: Message.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public class Message extends Entity implements Body { + + /** + * Creates a new empty Message. + */ + public Message() { + } + + /** + * Parses the specified MIME message stream into a Message + * instance. + * + * @param is the stream to parse. + * @throws IOException on I/O errors. + */ + public Message(InputStream is) throws IOException { + MimeStreamParser parser = new MimeStreamParser(); + parser.setContentHandler(new MessageBuilder()); + parser.parse(is); + } + + + /** + * Gets the Subject field. + * + * @return the Subject field or null if it + * doesn't exist. + */ + public UnstructuredField getSubject() { + return (UnstructuredField) getHeader().getField(Field.SUBJECT); + } + + /** + * + * @see org.apache.james.mime4j.message.Entity#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + getHeader().writeTo(out); + + Body body = getBody(); + if (body instanceof Multipart) { + Multipart mp = (Multipart) body; + mp.writeTo(out); + } else { + body.writeTo(out); + } + } + + + private class MessageBuilder implements ContentHandler { + private Stack stack = new Stack(); + + public MessageBuilder() { + } + + private void expect(Class c) { + if (!c.isInstance(stack.peek())) { + throw new IllegalStateException("Internal stack error: " + + "Expected '" + c.getName() + "' found '" + + stack.peek().getClass().getName() + "'"); + } + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startMessage() + */ + public void startMessage() { + if (stack.isEmpty()) { + stack.push(Message.this); + } else { + expect(Entity.class); + Message m = new Message(); + ((Entity) stack.peek()).setBody(m); + stack.push(m); + } + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endMessage() + */ + public void endMessage() { + expect(Message.class); + stack.pop(); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startHeader() + */ + public void startHeader() { + stack.push(new Header()); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#field(java.lang.String) + */ + public void field(String fieldData) { + expect(Header.class); + ((Header) stack.peek()).addField(Field.parse(fieldData)); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endHeader() + */ + public void endHeader() { + expect(Header.class); + Header h = (Header) stack.pop(); + expect(Entity.class); + ((Entity) stack.peek()).setHeader(h); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startMultipart(org.apache.james.mime4j.BodyDescriptor) + */ + public void startMultipart(BodyDescriptor bd) { + expect(Entity.class); + + Entity e = (Entity) stack.peek(); + Multipart multiPart = new Multipart(); + e.setBody(multiPart); + stack.push(multiPart); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#body(org.apache.james.mime4j.BodyDescriptor, java.io.InputStream) + */ + public void body(BodyDescriptor bd, InputStream is) throws IOException { + expect(Entity.class); + + String enc = bd.getTransferEncoding(); + if ("base64".equals(enc)) { + is = new Base64InputStream(is); + } else if ("quoted-printable".equals(enc)) { + is = new QuotedPrintableInputStream(is); + } + + Body body = null; + if (bd.getMimeType().startsWith("text/")) { + body = new MemoryTextBody(is, bd.getCharset()); + } else { + body = new MemoryBinaryBody(is); + } + + ((Entity) stack.peek()).setBody(body); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endMultipart() + */ + public void endMultipart() { + stack.pop(); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#startBodyPart() + */ + public void startBodyPart() { + expect(Multipart.class); + + BodyPart bodyPart = new BodyPart(); + ((Multipart) stack.peek()).addBodyPart(bodyPart); + stack.push(bodyPart); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#endBodyPart() + */ + public void endBodyPart() { + expect(BodyPart.class); + stack.pop(); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#epilogue(java.io.InputStream) + */ + public void epilogue(InputStream is) throws IOException { + expect(Multipart.class); + StringBuffer sb = new StringBuffer(); + int b; + while ((b = is.read()) != -1) { + sb.append((char) b); + } + ((Multipart) stack.peek()).setEpilogue(sb.toString()); + } + + /** + * @see org.apache.james.mime4j.ContentHandler#preamble(java.io.InputStream) + */ + public void preamble(InputStream is) throws IOException { + expect(Multipart.class); + StringBuffer sb = new StringBuffer(); + int b; + while ((b = is.read()) != -1) { + sb.append((char) b); + } + ((Multipart) stack.peek()).setPreamble(sb.toString()); + } + + /** + * TODO: Implement me + * + * @see org.apache.james.mime4j.ContentHandler#raw(java.io.InputStream) + */ + public void raw(InputStream is) throws IOException { + throw new UnsupportedOperationException("Not supported"); + } + + } +} diff --git a/src/org/apache/james/mime4j/message/Multipart.java b/src/org/apache/james/mime4j/message/Multipart.java new file mode 100644 index 000000000..3c4b519c4 --- /dev/null +++ b/src/org/apache/james/mime4j/message/Multipart.java @@ -0,0 +1,203 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.apache.james.mime4j.field.ContentTypeField; +import org.apache.james.mime4j.field.Field; +import org.apache.james.mime4j.util.CharsetUtil; + +/** + * Represents a MIME multipart body (see RFC 2045).A multipart body has a + * ordered list of body parts. The multipart body also has a preamble and + * epilogue. The preamble consists of whatever characters appear before the + * first body part while the epilogue consists of whatever characters come + * after the last body part. + * + * + * @version $Id: Multipart.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public class Multipart implements Body { + private String preamble = ""; + private String epilogue = ""; + private List bodyParts = new LinkedList(); + private Entity parent = null; + private String subType = "alternative"; + + /** + * Creates a new empty Multipart instance. + */ + public Multipart() { + } + + /** + * Gets the multipart sub-type. E.g. alternative (the default) + * or parallel. See RFC 2045 for common sub-types and their + * meaning. + * + * @return the multipart sub-type. + */ + public String getSubType() { + return subType; + } + + /** + * Sets the multipart sub-type. E.g. alternative + * or parallel. See RFC 2045 for common sub-types and their + * meaning. + * + * @param subType the sub-type. + */ + public void setSubType(String subType) { + this.subType = subType; + } + + /** + * @see org.apache.james.mime4j.message.Body#getParent() + */ + public Entity getParent() { + return parent; + } + + /** + * @see org.apache.james.mime4j.message.Body#setParent(org.apache.james.mime4j.message.Entity) + */ + public void setParent(Entity parent) { + this.parent = parent; + for (Iterator it = bodyParts.iterator(); it.hasNext();) { + ((BodyPart) it.next()).setParent(parent); + } + } + + /** + * Gets the epilogue. + * + * @return the epilogue. + */ + public String getEpilogue() { + return epilogue; + } + + /** + * Sets the epilogue. + * + * @param epilogue the epilogue. + */ + public void setEpilogue(String epilogue) { + this.epilogue = epilogue; + } + + /** + * Gets the list of body parts. The list is immutable. + * + * @return the list of BodyPart objects. + */ + public List getBodyParts() { + return Collections.unmodifiableList(bodyParts); + } + + /** + * Sets the list of body parts. + * + * @param bodyParts the new list of BodyPart objects. + */ + public void setBodyParts(List bodyParts) { + this.bodyParts = bodyParts; + for (Iterator it = bodyParts.iterator(); it.hasNext();) { + ((BodyPart) it.next()).setParent(parent); + } + } + + /** + * Adds a body part to the end of the list of body parts. + * + * @param bodyPart the body part. + */ + public void addBodyPart(BodyPart bodyPart) { + bodyParts.add(bodyPart); + bodyPart.setParent(parent); + } + + /** + * Gets the preamble. + * + * @return the preamble. + */ + public String getPreamble() { + return preamble; + } + + /** + * Sets the preamble. + * + * @param preamble the preamble. + */ + public void setPreamble(String preamble) { + this.preamble = preamble; + } + + /** + * + * @see org.apache.james.mime4j.message.Body#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + String boundary = getBoundary(); + List bodyParts = getBodyParts(); + + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, CharsetUtil.getCharset(getCharset())),8192); + + writer.write(getPreamble() + "\r\n"); + + for (int i = 0; i < bodyParts.size(); i++) { + writer.write(boundary + "\r\n"); + ((BodyPart) bodyParts.get(i)).writeTo(out); + } + + writer.write(getEpilogue() + "\r\n"); + writer.write(boundary + "--" + "\r\n"); + + } + + /** + * Return the boundory of the parent Entity + * + * @return boundery + */ + private String getBoundary() { + Entity e = getParent(); + ContentTypeField cField = (ContentTypeField) e.getHeader().getField( + Field.CONTENT_TYPE); + return cField.getBoundary(); + } + + private String getCharset() { + Entity e = getParent(); + String charString = ((ContentTypeField) e.getHeader().getField(Field.CONTENT_TYPE)).getCharset(); + return charString; + } +} diff --git a/src/org/apache/james/mime4j/message/TempFileBinaryBody.java b/src/org/apache/james/mime4j/message/TempFileBinaryBody.java new file mode 100644 index 000000000..3ded83db8 --- /dev/null +++ b/src/org/apache/james/mime4j/message/TempFileBinaryBody.java @@ -0,0 +1,89 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.TempFile; +import org.apache.james.mime4j.util.TempPath; +import org.apache.james.mime4j.util.TempStorage; + + +/** + * Binary body backed by a {@link org.apache.james.mime4j.util.TempFile}. + * + * + * @version $Id: TempFileBinaryBody.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +class TempFileBinaryBody extends AbstractBody implements BinaryBody { + private static Log log = LogFactory.getLog(TempFileBinaryBody.class); + + private Entity parent = null; + private TempFile tempFile = null; + + /** + * Use the given InputStream to build the TemporyFileBinaryBody + * + * @param is the InputStream to use as source + * @throws IOException + */ + public TempFileBinaryBody(InputStream is) throws IOException { + + TempPath tempPath = TempStorage.getInstance().getRootTempPath(); + tempFile = tempPath.createTempFile("attachment", ".bin"); + + OutputStream out = tempFile.getOutputStream(); + IOUtils.copy(is, out); + out.close(); + } + + /** + * @see org.apache.james.mime4j.message.AbstractBody#getParent() + */ + public Entity getParent() { + return parent; + } + + /** + * @see org.apache.james.mime4j.message.AbstractBody#setParent(org.apache.james.mime4j.message.Entity) + */ + public void setParent(Entity parent) { + this.parent = parent; + } + + /** + * @see org.apache.james.mime4j.message.BinaryBody#getInputStream() + */ + public InputStream getInputStream() throws IOException { + return tempFile.getInputStream(); + } + + /** + * @see org.apache.james.mime4j.message.Body#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + IOUtils.copy(getInputStream(),out); + } +} diff --git a/src/org/apache/james/mime4j/message/TempFileTextBody.java b/src/org/apache/james/mime4j/message/TempFileTextBody.java new file mode 100644 index 000000000..0b3b70c25 --- /dev/null +++ b/src/org/apache/james/mime4j/message/TempFileTextBody.java @@ -0,0 +1,115 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.james.mime4j.util.CharsetUtil; +import org.apache.james.mime4j.util.TempFile; +import org.apache.james.mime4j.util.TempPath; +import org.apache.james.mime4j.util.TempStorage; + + +/** + * Text body backed by a {@link org.apache.james.mime4j.util.TempFile}. + * + * + * @version $Id: TempFileTextBody.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $ + */ +class TempFileTextBody extends AbstractBody implements TextBody { + private static Log log = LogFactory.getLog(TempFileTextBody.class); + + private String mimeCharset = null; + private TempFile tempFile = null; + + public TempFileTextBody(InputStream is) throws IOException { + this(is, null); + } + + public TempFileTextBody(InputStream is, String mimeCharset) + throws IOException { + + this.mimeCharset = mimeCharset; + + TempPath tempPath = TempStorage.getInstance().getRootTempPath(); + tempFile = tempPath.createTempFile("attachment", ".txt"); + + OutputStream out = tempFile.getOutputStream(); + IOUtils.copy(is, out); + out.close(); + } + + /** + * @see org.apache.james.mime4j.message.TextBody#getReader() + */ + public Reader getReader() throws UnsupportedEncodingException, IOException { + String javaCharset = null; + if (mimeCharset != null) { + javaCharset = CharsetUtil.toJavaCharset(mimeCharset); + } + + if (javaCharset == null) { + javaCharset = "ISO-8859-1"; + + if (log.isWarnEnabled()) { + if (mimeCharset == null) { + log.warn("No MIME charset specified. Using " + javaCharset + + " instead."); + } else { + log.warn("MIME charset '" + mimeCharset + "' has no " + + "corresponding Java charset. Using " + javaCharset + + " instead."); + } + } + } + /* + if (log.isWarnEnabled()) { + if (mimeCharset == null) { + log.warn("No MIME charset specified. Using the " + + "platform's default charset."); + } else { + log.warn("MIME charset '" + mimeCharset + "' has no " + + "corresponding Java charset. Using the " + + "platform's default charset."); + } + } + + return new InputStreamReader(tempFile.getInputStream()); + }*/ + + return new InputStreamReader(tempFile.getInputStream(), javaCharset); + } + + + /** + * @see org.apache.james.mime4j.message.Body#writeTo(java.io.OutputStream) + */ + public void writeTo(OutputStream out) throws IOException { + IOUtils.copy(tempFile.getInputStream(), out); + } +} diff --git a/src/org/apache/james/mime4j/message/TextBody.java b/src/org/apache/james/mime4j/message/TextBody.java new file mode 100644 index 000000000..4fe714466 --- /dev/null +++ b/src/org/apache/james/mime4j/message/TextBody.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.message; + +import java.io.IOException; +import java.io.Reader; + + +/** + * Encapsulates the contents of a text/* entity body. + * + * + * @version $Id: TextBody.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public interface TextBody extends Body { + + /** + * Gets a Reader which may be used to read out the contents + * of this body. + * + * @return the Reader. + * @throws IOException on I/O errors. + */ + Reader getReader() throws IOException; +} diff --git a/src/org/apache/james/mime4j/util/CharsetUtil.java b/src/org/apache/james/mime4j/util/CharsetUtil.java new file mode 100644 index 000000000..f5026b1f8 --- /dev/null +++ b/src/org/apache/james/mime4j/util/CharsetUtil.java @@ -0,0 +1,1178 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.TreeSet; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Utility class for working with character sets. It is somewhat similar to + * the Java 1.4 java.nio.charset.Charset class but knows many + * more aliases and is compatible with Java 1.3. It will use a simple detection + * mechanism to detect what character sets the current VM supports. This will + * be a sub-set of the character sets listed in the + * + * Java 1.5 (J2SE5.0) Supported Encodings document. + *

+ * The + * IANA Character Sets document has been used to determine the preferred + * MIME character set names and to get a list of known aliases. + *

+ * This is a complete list of the character sets known to this class: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Canonical (Java) nameMIME preferredAliases
ASCIIUS-ASCIIANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ISO646-US us IBM367 cp367 csASCII ascii7 646 iso_646.irv:1983
Big5Big5csBig5 CN-Big5 BIG-FIVE BIGFIVE
Big5_HKSCSBig5-HKSCSbig5hkscs
Big5_Solaris?
Cp037IBM037ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl csIBM037
Cp1006?
Cp1025?
Cp1026IBM1026csIBM1026
Cp1046?
Cp1047IBM1047IBM-1047
Cp1097?
Cp1098?
Cp1112?
Cp1122?
Cp1123?
Cp1124?
Cp1140IBM01140CCSID01140 CP01140 ebcdic-us-37+euro
Cp1141IBM01141CCSID01141 CP01141 ebcdic-de-273+euro
Cp1142IBM01142CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro
Cp1143IBM01143CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro
Cp1144IBM01144CCSID01144 CP01144 ebcdic-it-280+euro
Cp1145IBM01145CCSID01145 CP01145 ebcdic-es-284+euro
Cp1146IBM01146CCSID01146 CP01146 ebcdic-gb-285+euro
Cp1147IBM01147CCSID01147 CP01147 ebcdic-fr-297+euro
Cp1148IBM01148CCSID01148 CP01148 ebcdic-international-500+euro
Cp1149IBM01149CCSID01149 CP01149 ebcdic-is-871+euro
Cp1250windows-1250
Cp1251windows-1251
Cp1252windows-1252
Cp1253windows-1253
Cp1254windows-1254
Cp1255windows-1255
Cp1256windows-1256
Cp1257windows-1257
Cp1258windows-1258
Cp1381?
Cp1383?
Cp273IBM273csIBM273
Cp277IBM277EBCDIC-CP-DK EBCDIC-CP-NO csIBM277
Cp278IBM278CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278
Cp280IBM280ebcdic-cp-it csIBM280
Cp284IBM284ebcdic-cp-es csIBM284
Cp285IBM285ebcdic-cp-gb csIBM285
Cp297IBM297ebcdic-cp-fr csIBM297
Cp33722?
Cp420IBM420ebcdic-cp-ar1 csIBM420
Cp424IBM424ebcdic-cp-he csIBM424
Cp437IBM437437 csPC8CodePage437
Cp500IBM500ebcdic-cp-be ebcdic-cp-ch csIBM500
Cp737?
Cp775IBM775csPC775Baltic
Cp838IBM-Thai
Cp850IBM850850 csPC850Multilingual
Cp852IBM852852 csPCp852
Cp855IBM855855 csIBM855
Cp856?
Cp857IBM857857 csIBM857
Cp858IBM00858CCSID00858 CP00858 PC-Multilingual-850+euro
Cp860IBM860860 csIBM860
Cp861IBM861861 cp-is csIBM861
Cp862IBM862862 csPC862LatinHebrew
Cp863IBM863863 csIBM863
Cp864IBM864cp864 csIBM864
Cp865IBM865865 csIBM865
Cp866IBM866866 csIBM866
Cp868IBM868cp-ar csIBM868
Cp869IBM869cp-gr csIBM869
Cp870IBM870ebcdic-cp-roece ebcdic-cp-yu csIBM870
Cp871IBM871ebcdic-cp-is csIBM871
Cp875?
Cp918IBM918ebcdic-cp-ar2 csIBM918
Cp921?
Cp922?
Cp930?
Cp933?
Cp935?
Cp937?
Cp939?
Cp942?
Cp942C?
Cp943?
Cp943C?
Cp948?
Cp949?
Cp949C?
Cp950?
Cp964?
Cp970?
EUC_CNGB2312x-EUC-CN csGB2312 euccn euc-cn gb2312-80 gb2312-1980 CN-GB CN-GB-ISOIR165
EUC_JPEUC-JPcsEUCPkdFmtJapanese Extended_UNIX_Code_Packed_Format_for_Japanese eucjis x-eucjp eucjp x-euc-jp
EUC_JP_LINUX?
EUC_JP_Solaris?
EUC_KREUC-KRcsEUCKR ksc5601 5601 ksc5601_1987 ksc_5601 ksc5601-1987 ks_c_5601-1987 euckr
EUC_TWEUC-TWx-EUC-TW cns11643 euctw
GB18030GB18030gb18030-2000
GBKwindows-936CP936 MS936 ms_936 x-mswin-936
ISCII91?x-ISCII91 iscii
ISO2022CNISO-2022-CN
ISO2022JPISO-2022-JPcsISO2022JP JIS jis_encoding csjisencoding
ISO2022KRISO-2022-KRcsISO2022KR
ISO2022_CN_CNS?
ISO2022_CN_GB?
ISO8859_1ISO-8859-1ISO_8859-1:1987 iso-ir-100 ISO_8859-1 latin1 l1 IBM819 CP819 csISOLatin1 8859_1 819 IBM-819 ISO8859-1 ISO_8859_1
ISO8859_13ISO-8859-13
ISO8859_15ISO-8859-15ISO_8859-15 Latin-9 8859_15 csISOlatin9 IBM923 cp923 923 L9 IBM-923 ISO8859-15 LATIN9 LATIN0 csISOlatin0 ISO8859_15_FDIS
ISO8859_2ISO-8859-2ISO_8859-2:1987 iso-ir-101 ISO_8859-2 latin2 l2 csISOLatin2 8859_2 iso8859_2
ISO8859_3ISO-8859-3ISO_8859-3:1988 iso-ir-109 ISO_8859-3 latin3 l3 csISOLatin3 8859_3
ISO8859_4ISO-8859-4ISO_8859-4:1988 iso-ir-110 ISO_8859-4 latin4 l4 csISOLatin4 8859_4
ISO8859_5ISO-8859-5ISO_8859-5:1988 iso-ir-144 ISO_8859-5 cyrillic csISOLatinCyrillic 8859_5
ISO8859_6ISO-8859-6ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ECMA-114 ASMO-708 arabic csISOLatinArabic 8859_6
ISO8859_7ISO-8859-7ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ELOT_928 ECMA-118 greek greek8 csISOLatinGreek 8859_7 sun_eu_greek
ISO8859_8ISO-8859-8ISO_8859-8:1988 iso-ir-138 ISO_8859-8 hebrew csISOLatinHebrew 8859_8
ISO8859_9ISO-8859-9ISO_8859-9:1989 iso-ir-148 ISO_8859-9 latin5 l5 csISOLatin5 8859_9
JISAutoDetect?
JIS_C6626-1983JIS_C6626-1983x-JIS0208 JIS0208 csISO87JISX0208 x0208 JIS_X0208-1983 iso-ir-87
JIS_X0201JIS_X0201X0201 JIS0201 csHalfWidthKatakana
JIS_X0212-1990JIS_X0212-1990iso-ir-159 x0212 JIS0212 csISO159JISX02121990
KOI8_RKOI8-RcsKOI8R koi8
MS874windows-874cp874
MS932Windows-31Jwindows-932 csWindows31J x-ms-cp932
MS949windows-949windows949 ms_949 x-windows-949
MS950windows-950x-windows-950
MS950_HKSCS
MacArabic?
MacCentralEurope?
MacCroatian?
MacCyrillic?
MacDingbat?
MacGreekMacGreek
MacHebrew?
MacIceland?
MacRomanMacRomanMacintosh MAC csMacintosh
MacRomania?
MacSymbol?
MacThai?
MacTurkish?
MacUkraine?
SJISShift_JISMS_Kanji csShiftJIS shift-jis x-sjis pck
TIS620TIS-620
UTF-16UTF-16UTF_16
UTF8UTF-8
UnicodeBig?
UnicodeBigUnmarkedUTF-16BEX-UTF-16BE UTF_16BE ISO-10646-UCS-2
UnicodeLittle?
UnicodeLittleUnmarkedUTF-16LEUTF_16LE X-UTF-16LE
x-Johabjohabjohab cp1361 ms1361 ksc5601-1992 ksc5601_1992
x-iso-8859-11?
+ * + * + * @version $Id: CharsetUtil.java,v 1.1 2004/10/25 07:26:46 ntherning Exp $ + */ +public class CharsetUtil { + private static Log log = LogFactory.getLog(CharsetUtil.class); + + private static class Charset implements Comparable { + private String canonical = null; + private String mime = null; + private String[] aliases = null; + + private Charset(String canonical, String mime, String[] aliases) { + this.canonical = canonical; + this.mime = mime; + this.aliases = aliases; + } + + public int compareTo(Object o) { + Charset c = (Charset) o; + return this.canonical.compareTo(c.canonical); + } + } + + private static Charset[] JAVA_CHARSETS = { + new Charset("ISO8859_1", "ISO-8859-1", + new String[] {"ISO_8859-1:1987", "iso-ir-100", "ISO_8859-1", + "latin1", "l1", "IBM819", "CP819", + "csISOLatin1", "8859_1", "819", "IBM-819", + "ISO8859-1", "ISO_8859_1"}), + new Charset("ISO8859_2", "ISO-8859-2", + new String[] {"ISO_8859-2:1987", "iso-ir-101", "ISO_8859-2", + "latin2", "l2", "csISOLatin2", "8859_2", + "iso8859_2"}), + new Charset("ISO8859_3", "ISO-8859-3", new String[] {"ISO_8859-3:1988", "iso-ir-109", "ISO_8859-3", "latin3", "l3", "csISOLatin3", "8859_3"}), + new Charset("ISO8859_4", "ISO-8859-4", + new String[] {"ISO_8859-4:1988", "iso-ir-110", "ISO_8859-4", + "latin4", "l4", "csISOLatin4", "8859_4"}), + new Charset("ISO8859_5", "ISO-8859-5", + new String[] {"ISO_8859-5:1988", "iso-ir-144", "ISO_8859-5", + "cyrillic", "csISOLatinCyrillic", "8859_5"}), + new Charset("ISO8859_6", "ISO-8859-6", new String[] {"ISO_8859-6:1987", "iso-ir-127", "ISO_8859-6", "ECMA-114", "ASMO-708", "arabic", "csISOLatinArabic", "8859_6"}), + new Charset("ISO8859_7", "ISO-8859-7", + new String[] {"ISO_8859-7:1987", "iso-ir-126", "ISO_8859-7", + "ELOT_928", "ECMA-118", "greek", "greek8", + "csISOLatinGreek", "8859_7", "sun_eu_greek"}), + new Charset("ISO8859_8", "ISO-8859-8", new String[] {"ISO_8859-8:1988", "iso-ir-138", "ISO_8859-8", "hebrew", "csISOLatinHebrew", "8859_8"}), + new Charset("ISO8859_9", "ISO-8859-9", + new String[] {"ISO_8859-9:1989", "iso-ir-148", "ISO_8859-9", + "latin5", "l5", "csISOLatin5", "8859_9"}), + + new Charset("ISO8859_13", "ISO-8859-13", new String[] {}), + new Charset("ISO8859_15", "ISO-8859-15", + new String[] {"ISO_8859-15", "Latin-9", "8859_15", + "csISOlatin9", "IBM923", "cp923", "923", "L9", + "IBM-923", "ISO8859-15", "LATIN9", "LATIN0", + "csISOlatin0", "ISO8859_15_FDIS"}), + new Charset("KOI8_R", "KOI8-R", new String[] {"csKOI8R", "koi8"}), + new Charset("ASCII", "US-ASCII", + new String[] {"ANSI_X3.4-1968", "iso-ir-6", + "ANSI_X3.4-1986", "ISO_646.irv:1991", + "ISO646-US", "us", "IBM367", "cp367", + "csASCII", "ascii7", "646", "iso_646.irv:1983"}), + new Charset("UTF8", "UTF-8", new String[] {}), + new Charset("UTF-16", "UTF-16", new String[] {"UTF_16"}), + new Charset("UnicodeBigUnmarked", "UTF-16BE", new String[] {"X-UTF-16BE", "UTF_16BE", "ISO-10646-UCS-2"}), + new Charset("UnicodeLittleUnmarked", "UTF-16LE", new String[] {"UTF_16LE", "X-UTF-16LE"}), + new Charset("Big5", "Big5", new String[] {"csBig5", "CN-Big5", "BIG-FIVE", "BIGFIVE"}), + new Charset("Big5_HKSCS", "Big5-HKSCS", new String[] {"big5hkscs"}), + new Charset("EUC_JP", "EUC-JP", + new String[] {"csEUCPkdFmtJapanese", + "Extended_UNIX_Code_Packed_Format_for_Japanese", + "eucjis", "x-eucjp", "eucjp", "x-euc-jp"}), + new Charset("EUC_KR", "EUC-KR", + new String[] {"csEUCKR", "ksc5601", "5601", "ksc5601_1987", + "ksc_5601", "ksc5601-1987", "ks_c_5601-1987", + "euckr"}), + new Charset("GB18030", "GB18030", new String[] {"gb18030-2000"}), + new Charset("EUC_CN", "GB2312", new String[] {"x-EUC-CN", "csGB2312", "euccn", "euc-cn", "gb2312-80", "gb2312-1980", "CN-GB", "CN-GB-ISOIR165"}), + new Charset("GBK", "windows-936", new String[] {"CP936", "MS936", "ms_936", "x-mswin-936"}), + + new Charset("Cp037", "IBM037", new String[] {"ebcdic-cp-us", "ebcdic-cp-ca", "ebcdic-cp-wt", "ebcdic-cp-nl", "csIBM037"}), + new Charset("Cp273", "IBM273", new String[] {"csIBM273"}), + new Charset("Cp277", "IBM277", new String[] {"EBCDIC-CP-DK", "EBCDIC-CP-NO", "csIBM277"}), + new Charset("Cp278", "IBM278", new String[] {"CP278", "ebcdic-cp-fi", "ebcdic-cp-se", "csIBM278"}), + new Charset("Cp280", "IBM280", new String[] {"ebcdic-cp-it", "csIBM280"}), + new Charset("Cp284", "IBM284", new String[] {"ebcdic-cp-es", "csIBM284"}), + new Charset("Cp285", "IBM285", new String[] {"ebcdic-cp-gb", "csIBM285"}), + new Charset("Cp297", "IBM297", new String[] {"ebcdic-cp-fr", "csIBM297"}), + new Charset("Cp420", "IBM420", new String[] {"ebcdic-cp-ar1", "csIBM420"}), + new Charset("Cp424", "IBM424", new String[] {"ebcdic-cp-he", "csIBM424"}), + new Charset("Cp437", "IBM437", new String[] {"437", "csPC8CodePage437"}), + new Charset("Cp500", "IBM500", new String[] {"ebcdic-cp-be", "ebcdic-cp-ch", "csIBM500"}), + new Charset("Cp775", "IBM775", new String[] {"csPC775Baltic"}), + new Charset("Cp838", "IBM-Thai", new String[] {}), + new Charset("Cp850", "IBM850", new String[] {"850", "csPC850Multilingual"}), + new Charset("Cp852", "IBM852", new String[] {"852", "csPCp852"}), + new Charset("Cp855", "IBM855", new String[] {"855", "csIBM855"}), + new Charset("Cp857", "IBM857", new String[] {"857", "csIBM857"}), + new Charset("Cp858", "IBM00858", + new String[] {"CCSID00858", "CP00858", + "PC-Multilingual-850+euro"}), + new Charset("Cp860", "IBM860", new String[] {"860", "csIBM860"}), + new Charset("Cp861", "IBM861", new String[] {"861", "cp-is", "csIBM861"}), + new Charset("Cp862", "IBM862", new String[] {"862", "csPC862LatinHebrew"}), + new Charset("Cp863", "IBM863", new String[] {"863", "csIBM863"}), + new Charset("Cp864", "IBM864", new String[] {"cp864", "csIBM864"}), + new Charset("Cp865", "IBM865", new String[] {"865", "csIBM865"}), + new Charset("Cp866", "IBM866", new String[] {"866", "csIBM866"}), + new Charset("Cp868", "IBM868", new String[] {"cp-ar", "csIBM868"}), + new Charset("Cp869", "IBM869", new String[] {"cp-gr", "csIBM869"}), + new Charset("Cp870", "IBM870", new String[] {"ebcdic-cp-roece", "ebcdic-cp-yu", "csIBM870"}), + new Charset("Cp871", "IBM871", new String[] {"ebcdic-cp-is", "csIBM871"}), + new Charset("Cp918", "IBM918", new String[] {"ebcdic-cp-ar2", "csIBM918"}), + new Charset("Cp1026", "IBM1026", new String[] {"csIBM1026"}), + new Charset("Cp1047", "IBM1047", new String[] {"IBM-1047"}), + new Charset("Cp1140", "IBM01140", + new String[] {"CCSID01140", "CP01140", + "ebcdic-us-37+euro"}), + new Charset("Cp1141", "IBM01141", + new String[] {"CCSID01141", "CP01141", + "ebcdic-de-273+euro"}), + new Charset("Cp1142", "IBM01142", new String[] {"CCSID01142", "CP01142", "ebcdic-dk-277+euro", "ebcdic-no-277+euro"}), + new Charset("Cp1143", "IBM01143", new String[] {"CCSID01143", "CP01143", "ebcdic-fi-278+euro", "ebcdic-se-278+euro"}), + new Charset("Cp1144", "IBM01144", new String[] {"CCSID01144", "CP01144", "ebcdic-it-280+euro"}), + new Charset("Cp1145", "IBM01145", new String[] {"CCSID01145", "CP01145", "ebcdic-es-284+euro"}), + new Charset("Cp1146", "IBM01146", new String[] {"CCSID01146", "CP01146", "ebcdic-gb-285+euro"}), + new Charset("Cp1147", "IBM01147", new String[] {"CCSID01147", "CP01147", "ebcdic-fr-297+euro"}), + new Charset("Cp1148", "IBM01148", new String[] {"CCSID01148", "CP01148", "ebcdic-international-500+euro"}), + new Charset("Cp1149", "IBM01149", new String[] {"CCSID01149", "CP01149", "ebcdic-is-871+euro"}), + new Charset("Cp1250", "windows-1250", new String[] {}), + new Charset("Cp1251", "windows-1251", new String[] {}), + new Charset("Cp1252", "windows-1252", new String[] {}), + new Charset("Cp1253", "windows-1253", new String[] {}), + new Charset("Cp1254", "windows-1254", new String[] {}), + new Charset("Cp1255", "windows-1255", new String[] {}), + new Charset("Cp1256", "windows-1256", new String[] {}), + new Charset("Cp1257", "windows-1257", new String[] {}), + new Charset("Cp1258", "windows-1258", new String[] {}), + new Charset("ISO2022CN", "ISO-2022-CN", new String[] {}), + new Charset("ISO2022JP", "ISO-2022-JP", new String[] {"csISO2022JP", "JIS", "jis_encoding", "csjisencoding"}), + new Charset("ISO2022KR", "ISO-2022-KR", new String[] {"csISO2022KR"}), + new Charset("JIS_X0201", "JIS_X0201", new String[] {"X0201", "JIS0201", "csHalfWidthKatakana"}), + new Charset("JIS_X0212-1990", "JIS_X0212-1990", new String[] {"iso-ir-159", "x0212", "JIS0212", "csISO159JISX02121990"}), + new Charset("JIS_C6626-1983", "JIS_C6626-1983", new String[] {"x-JIS0208", "JIS0208", "csISO87JISX0208", "x0208", "JIS_X0208-1983", "iso-ir-87"}), + new Charset("SJIS", "Shift_JIS", new String[] {"MS_Kanji", "csShiftJIS", "shift-jis", "x-sjis", "pck"}), + new Charset("TIS620", "TIS-620", new String[] {}), + new Charset("MS932", "Windows-31J", new String[] {"windows-932", "csWindows31J", "x-ms-cp932"}), + new Charset("EUC_TW", "EUC-TW", new String[] {"x-EUC-TW", "cns11643", "euctw"}), + new Charset("x-Johab", "johab", new String[] {"johab", "cp1361", "ms1361", "ksc5601-1992", "ksc5601_1992"}), + new Charset("MS950_HKSCS", "", new String[] {}), + new Charset("MS874", "windows-874", new String[] {"cp874"}), + new Charset("MS949", "windows-949", new String[] {"windows949", "ms_949", "x-windows-949"}), + new Charset("MS950", "windows-950", new String[] {"x-windows-950"}), + + new Charset("Cp737", null, new String[] {}), + new Charset("Cp856", null, new String[] {}), + new Charset("Cp875", null, new String[] {}), + new Charset("Cp921", null, new String[] {}), + new Charset("Cp922", null, new String[] {}), + new Charset("Cp930", null, new String[] {}), + new Charset("Cp933", null, new String[] {}), + new Charset("Cp935", null, new String[] {}), + new Charset("Cp937", null, new String[] {}), + new Charset("Cp939", null, new String[] {}), + new Charset("Cp942", null, new String[] {}), + new Charset("Cp942C", null, new String[] {}), + new Charset("Cp943", null, new String[] {}), + new Charset("Cp943C", null, new String[] {}), + new Charset("Cp948", null, new String[] {}), + new Charset("Cp949", null, new String[] {}), + new Charset("Cp949C", null, new String[] {}), + new Charset("Cp950", null, new String[] {}), + new Charset("Cp964", null, new String[] {}), + new Charset("Cp970", null, new String[] {}), + new Charset("Cp1006", null, new String[] {}), + new Charset("Cp1025", null, new String[] {}), + new Charset("Cp1046", null, new String[] {}), + new Charset("Cp1097", null, new String[] {}), + new Charset("Cp1098", null, new String[] {}), + new Charset("Cp1112", null, new String[] {}), + new Charset("Cp1122", null, new String[] {}), + new Charset("Cp1123", null, new String[] {}), + new Charset("Cp1124", null, new String[] {}), + new Charset("Cp1381", null, new String[] {}), + new Charset("Cp1383", null, new String[] {}), + new Charset("Cp33722", null, new String[] {}), + new Charset("Big5_Solaris", null, new String[] {}), + new Charset("EUC_JP_LINUX", null, new String[] {}), + new Charset("EUC_JP_Solaris", null, new String[] {}), + new Charset("ISCII91", null, new String[] {"x-ISCII91", "iscii"}), + new Charset("ISO2022_CN_CNS", null, new String[] {}), + new Charset("ISO2022_CN_GB", null, new String[] {}), + new Charset("x-iso-8859-11", null, new String[] {}), + new Charset("JISAutoDetect", null, new String[] {}), + new Charset("MacArabic", null, new String[] {}), + new Charset("MacCentralEurope", null, new String[] {}), + new Charset("MacCroatian", null, new String[] {}), + new Charset("MacCyrillic", null, new String[] {}), + new Charset("MacDingbat", null, new String[] {}), + new Charset("MacGreek", "MacGreek", new String[] {}), + new Charset("MacHebrew", null, new String[] {}), + new Charset("MacIceland", null, new String[] {}), + new Charset("MacRoman", "MacRoman", new String[] {"Macintosh", "MAC", "csMacintosh"}), + new Charset("MacRomania", null, new String[] {}), + new Charset("MacSymbol", null, new String[] {}), + new Charset("MacThai", null, new String[] {}), + new Charset("MacTurkish", null, new String[] {}), + new Charset("MacUkraine", null, new String[] {}), + new Charset("UnicodeBig", null, new String[] {}), + new Charset("UnicodeLittle", null, new String[] {}) + }; + + /** + * Contains the canonical names of character sets which can be used to + * decode bytes into Java chars. + */ + private static TreeSet decodingSupported = null; + + /** + * Contains the canonical names of character sets which can be used to + * encode Java chars into bytes. + */ + private static TreeSet encodingSupported = null; + + /** + * Maps character set names to Charset objects. All possible names of + * a charset will be mapped to the Charset. + */ + private static HashMap charsetMap = null; + + static { + decodingSupported = new TreeSet(); + encodingSupported = new TreeSet(); + byte[] dummy = new byte[] {'d', 'u', 'm', 'm', 'y'}; + for (int i = 0; i < JAVA_CHARSETS.length; i++) { + try { + String s = new String(dummy, JAVA_CHARSETS[i].canonical); + decodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase()); + } catch (UnsupportedOperationException e) { + } catch (UnsupportedEncodingException e) { + } + try { + "dummy".getBytes(JAVA_CHARSETS[i].canonical); + encodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase()); + } catch (UnsupportedOperationException e) { + } catch (UnsupportedEncodingException e) { + } + } + + charsetMap = new HashMap(); + for (int i = 0; i < JAVA_CHARSETS.length; i++) { + Charset c = JAVA_CHARSETS[i]; + charsetMap.put(c.canonical.toLowerCase(), c); + if (c.mime != null) { + charsetMap.put(c.mime.toLowerCase(), c); + } + if (c.aliases != null) { + for (int j = 0; j < c.aliases.length; j++) { + charsetMap.put(c.aliases[j].toLowerCase(), c); + } + } + } + + if (log.isDebugEnabled()) { + log.debug("Character sets which support decoding: " + + decodingSupported); + log.debug("Character sets which support encoding: " + + encodingSupported); + } + } + + /** + * Determines if the VM supports encoding (chars to bytes) the + * specified character set. NOTE: the given character set name may + * not be known to the VM even if this method returns true. + * Use {@link #toJavaCharset(String)} to get the canonical Java character + * set name. + * + * @param charsetName the characters set name. + * @return true if encoding is supported, false + * otherwise. + */ + public static boolean isEncodingSupported(String charsetName) { + return encodingSupported.contains(charsetName.toLowerCase()); + } + + /** + * Determines if the VM supports decoding (bytes to chars) the + * specified character set. NOTE: the given character set name may + * not be known to the VM even if this method returns true. + * Use {@link #toJavaCharset(String)} to get the canonical Java character + * set name. + * + * @param charsetName the characters set name. + * @return true if decoding is supported, false + * otherwise. + */ + public static boolean isDecodingSupported(String charsetName) { + return decodingSupported.contains(charsetName.toLowerCase()); + } + + /** + * Gets the preferred MIME character set name for the specified + * character set or null if not known. + * + * @param charsetName the character set name to look for. + * @return the MIME preferred name or null if not known. + */ + public static String toMimeCharset(String charsetName) { + Charset c = (Charset) charsetMap.get(charsetName.toLowerCase()); + if (c != null) { + return c.mime; + } + return null; + } + + /** + * Gets the canonical Java character set name for the specified + * character set or null if not known. This should be + * called before doing any conversions using the Java API. NOTE: + * you must use {@link #isEncodingSupported(String)} or + * {@link #isDecodingSupported(String)} to make sure the returned + * Java character set is supported by the current VM. + * + * @param charsetName the character set name to look for. + * @return the canonical Java name or null if not known. + */ + public static String toJavaCharset(String charsetName) { + Charset c = (Charset) charsetMap.get(charsetName.toLowerCase()); + if (c != null) { + return c.canonical; + } + return null; + } + + public static java.nio.charset.Charset getCharset(String charsetName) { + String defaultCharset = "ISO-8859-1"; + + // Use the default chareset if given charset is null + if(charsetName == null) charsetName = defaultCharset; + + try { + return java.nio.charset.Charset.forName(charsetName); + } catch (IllegalCharsetNameException e) { + log.info("Illegal charset " + charsetName + ", fallback to " + defaultCharset + ": " + e); + // Use default charset on exception + return java.nio.charset.Charset.forName(defaultCharset); + } catch (UnsupportedCharsetException ex) { + log.info("Unsupported charset " + charsetName + ", fallback to " + defaultCharset + ": " + ex); + // Use default charset on exception + return java.nio.charset.Charset.forName(defaultCharset); + } + + } + /* + * Uncomment the code below and run the main method to regenerate the + * Javadoc table above when the known charsets change. + */ + + /* + private static String dumpHtmlTable() { + LinkedList l = new LinkedList(Arrays.asList(JAVA_CHARSETS)); + Collections.sort(l); + StringBuffer sb = new StringBuffer(); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + + for (Iterator it = l.iterator(); it.hasNext();) { + Charset c = (Charset) it.next(); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + sb.append(" * \n"); + } + sb.append(" *
Canonical (Java) nameMIME preferredAliases
" + c.canonical + "" + (c.mime == null ? "?" : c.mime)+ ""); + for (int i = 0; c.aliases != null && i < c.aliases.length; i++) { + sb.append(c.aliases[i] + " "); + } + sb.append("
\n"); + return sb.toString(); + } + + public static void main(String[] args) { + System.out.println(dumpHtmlTable()); + }*/ +} diff --git a/src/org/apache/james/mime4j/util/PartialInputStream.java b/src/org/apache/james/mime4j/util/PartialInputStream.java new file mode 100644 index 000000000..a5ac0596d --- /dev/null +++ b/src/org/apache/james/mime4j/util/PartialInputStream.java @@ -0,0 +1,63 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + + +package org.apache.james.mime4j.util; + +import java.io.InputStream; +import java.io.IOException; + +public class PartialInputStream extends PositionInputStream { + private final long limit; + + public PartialInputStream(InputStream inputStream, long offset, long length) throws IOException { + super(inputStream); + inputStream.skip(offset); + this.limit = offset + length; + } + + public int available() throws IOException { + return Math.min(super.available(), getBytesLeft()); + } + + public int read() throws IOException { + if (limit > position) + return super.read(); + else + return -1; + } + + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + public int read(byte b[], int off, int len) throws IOException { + len = Math.min(len, getBytesLeft()); + return super.read(b, off, len); //To change body of overridden methods use File | Settings | File Templates. + } + + public long skip(long n) throws IOException { + n = Math.min(n, getBytesLeft()); + return super.skip(n); //To change body of overridden methods use File | Settings | File Templates. + } + + private int getBytesLeft() { + return (int)Math.min(Integer.MAX_VALUE, limit - position); + } +} diff --git a/src/org/apache/james/mime4j/util/PositionInputStream.java b/src/org/apache/james/mime4j/util/PositionInputStream.java new file mode 100644 index 000000000..9fcd21d0c --- /dev/null +++ b/src/org/apache/james/mime4j/util/PositionInputStream.java @@ -0,0 +1,87 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + + +package org.apache.james.mime4j.util; + +import java.io.InputStream; +import java.io.IOException; + +public class PositionInputStream extends InputStream { + + private final InputStream inputStream; + protected long position = 0; + private long markedPosition = 0; + + public PositionInputStream(InputStream inputStream) { + this.inputStream = inputStream; + } + + public long getPosition() { + return position; + } + + public int available() throws IOException { + return inputStream.available(); + } + + public int read() throws IOException { + int b = inputStream.read(); + if (b != -1) + position++; + return b; + } + + public void close() throws IOException { + inputStream.close(); + } + + public void reset() throws IOException { + inputStream.reset(); + position = markedPosition; + } + + public boolean markSupported() { + return inputStream.markSupported(); + } + + public void mark(int readlimit) { + inputStream.mark(readlimit); + markedPosition = position; + } + + public long skip(long n) throws IOException { + final long c = inputStream.skip(n); + position += c; + return c; + } + + public int read(byte b[]) throws IOException { + final int c = inputStream.read(b); + position += c; + return c; + } + + public int read(byte b[], int off, int len) throws IOException { + final int c = inputStream.read(b, off, len); + position += c; + return c; + } + +} diff --git a/src/org/apache/james/mime4j/util/SimpleTempStorage.java b/src/org/apache/james/mime4j/util/SimpleTempStorage.java new file mode 100644 index 000000000..7ca0371a2 --- /dev/null +++ b/src/org/apache/james/mime4j/util/SimpleTempStorage.java @@ -0,0 +1,236 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Random; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * @version $Id: SimpleTempStorage.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public class SimpleTempStorage extends TempStorage { + private static Log log = LogFactory.getLog(SimpleTempStorage.class); + + private TempPath rootPath = null; + private Random random = new Random(); + + /** + * Creates a new SimpleTempStorageManager instance. + */ + public SimpleTempStorage() { + rootPath = new SimpleTempPath(System.getProperty("java.io.tmpdir")); + } + + private TempPath createTempPath(TempPath parent, String prefix) + throws IOException { + + if (prefix == null) { + prefix = ""; + } + + File p = null; + int count = 1000; + do { + long n = Math.abs(random.nextLong()); + p = new File(parent.getAbsolutePath(), prefix + n); + count--; + } while (p.exists() && count > 0); + + if (p.exists() || !p.mkdirs()) { + log.error("Unable to mkdirs on " + p.getAbsolutePath()); + throw new IOException("Creating dir '" + + p.getAbsolutePath() + "' failed."); + } + + return new SimpleTempPath(p); + } + + private TempFile createTempFile(TempPath parent, String prefix, + String suffix) throws IOException { + + if (prefix == null) { + prefix = ""; + } + if (suffix == null) { + suffix = ".tmp"; + } + + File f = null; + + int count = 1000; + synchronized (this) { + do { + long n = Math.abs(random.nextLong()); + f = new File(parent.getAbsolutePath(), prefix + n + suffix); + count--; + } while (f.exists() && count > 0); + + if (f.exists()) { + throw new IOException("Creating temp file failed: " + + "Unable to find unique file name"); + } + + try { + f.createNewFile(); + } catch (IOException e) { + throw new IOException("Creating dir '" + + f.getAbsolutePath() + "' failed."); + } + } + + return new SimpleTempFile(f); + } + + /** + * @see org.apache.james.mime4j.util.TempStorage#getRootTempPath() + */ + public TempPath getRootTempPath() { + return rootPath; + } + + private class SimpleTempPath implements TempPath { + private File path = null; + + private SimpleTempPath(String path) { + this.path = new File(path); + } + + private SimpleTempPath(File path) { + this.path = path; + } + + /** + * @see org.apache.james.mime4j.util.TempPath#createTempFile() + */ + public TempFile createTempFile() throws IOException { + return SimpleTempStorage.this.createTempFile(this, null, null); + } + + /** + * @see org.apache.james.mime4j.util.TempPath#createTempFile(java.lang.String, java.lang.String) + */ + public TempFile createTempFile(String prefix, String suffix) + throws IOException { + + return SimpleTempStorage.this.createTempFile(this, prefix, suffix); + } + + /** + * @see org.apache.james.mime4j.util.TempPath#createTempFile(java.lang.String, java.lang.String, boolean) + */ + public TempFile createTempFile(String prefix, String suffix, + boolean allowInMemory) + throws IOException { + + return SimpleTempStorage.this.createTempFile(this, prefix, suffix); + } + + /** + * @see org.apache.james.mime4j.util.TempPath#getAbsolutePath() + */ + public String getAbsolutePath() { + return path.getAbsolutePath(); + } + + /** + * Do nothing + */ + public void delete() { + } + + /** + * @see org.apache.james.mime4j.util.TempPath#createTempPath() + */ + public TempPath createTempPath() throws IOException { + return SimpleTempStorage.this.createTempPath(this, null); + } + + /** + * @see org.apache.james.mime4j.util.TempPath#createTempPath(java.lang.String) + */ + public TempPath createTempPath(String prefix) throws IOException { + return SimpleTempStorage.this.createTempPath(this, prefix); + } + + } + + private class SimpleTempFile implements TempFile { + private File file = null; + + private SimpleTempFile(File file) { + this.file = file; + this.file.deleteOnExit(); + } + + /** + * @see org.apache.james.mime4j.util.TempFile#getInputStream() + */ + public InputStream getInputStream() throws IOException { + return new BufferedInputStream(new FileInputStream(file)); + } + + /** + * @see org.apache.james.mime4j.util.TempFile#getOutputStream() + */ + public OutputStream getOutputStream() throws IOException { + return new BufferedOutputStream(new FileOutputStream(file)); + } + + /** + * @see org.apache.james.mime4j.util.TempFile#getAbsolutePath() + */ + public String getAbsolutePath() { + return file.getAbsolutePath(); + } + + /** + * Do nothing + */ + public void delete() { + // Not implementated + } + + /** + * @see org.apache.james.mime4j.util.TempFile#isInMemory() + */ + public boolean isInMemory() { + return false; + } + + /** + * @see org.apache.james.mime4j.util.TempFile#length() + */ + public long length() { + return file.length(); + } + + } +} diff --git a/src/org/apache/james/mime4j/util/TempFile.java b/src/org/apache/james/mime4j/util/TempFile.java new file mode 100644 index 000000000..f67e1e93e --- /dev/null +++ b/src/org/apache/james/mime4j/util/TempFile.java @@ -0,0 +1,84 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @version $Id: TempFile.java,v 1.3 2004/10/02 12:41:11 ntherning Exp $ + */ +public interface TempFile { + /** + * Gets an InputStream to read bytes from this temporary file. + * NOTE: The stream should NOT be wrapped in + * BufferedInputStream by the caller. If the implementing + * TempFile creates a FileInputStream or any + * other stream which would benefit from being buffered it's the + * TempFile's responsibility to wrap it. + * + * @return the stream. + * @throws IOException + */ + InputStream getInputStream() throws IOException; + + /** + * Gets an OutputStream to write bytes to this temporary file. + * NOTE: The stream should NOT be wrapped in + * BufferedOutputStream by the caller. If the implementing + * TempFile creates a FileOutputStream or any + * other stream which would benefit from being buffered it's the + * TempFile's responsibility to wrap it. + * + * @return the stream. + * @throws IOException + */ + OutputStream getOutputStream() throws IOException; + + /** + * Returns the absolute path including file name of this + * TempFile. The path may be null if this is + * an in-memory file. + * + * @return the absolute path. + */ + String getAbsolutePath(); + + /** + * Deletes this file as soon as possible. + */ + void delete(); + + /** + * Determines if this is an in-memory file. + * + * @return true if this file is currently in memory, + * false otherwise. + */ + boolean isInMemory(); + + /** + * Gets the length of this temporary file. + * + * @return the length. + */ + long length(); +} diff --git a/src/org/apache/james/mime4j/util/TempPath.java b/src/org/apache/james/mime4j/util/TempPath.java new file mode 100644 index 000000000..3b55aa6db --- /dev/null +++ b/src/org/apache/james/mime4j/util/TempPath.java @@ -0,0 +1,73 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import java.io.IOException; + +/** + * + * @version $Id: TempPath.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public interface TempPath { + TempPath createTempPath() throws IOException; + TempPath createTempPath(String prefix) throws IOException; + + /** + * Creates a new temporary file. Wheter it will be be created in memory + * or on disk is up to to the implementation. + * The prefix will be empty and the suffix will be + * .tmp if created on disk. + * + * @return the temporary file. + */ + TempFile createTempFile() throws IOException; + + /** + * Creates a new temporary file. Wheter it will be be created in memory + * or on disk is up to to the implementation. + * The prefix and suffix can be set by the user. + * + * @param prefix the prefix to use. null gives no prefix. + * @param suffix the suffix to use. null gives + * .tmp. + * @return the temporary file. + */ + TempFile createTempFile(String prefix, String suffix) throws IOException; + + /** + * Creates a new temporary file. Wheter it will be be created in memory + * or on disk can be specified using the allowInMemory + * parameter. If the implementation doesn't support in-memory files + * the new file will be created on disk. + * The prefix and suffix can be set by the user. + * + * @param prefix the prefix to use. null gives no prefix. + * @param suffix the suffix to use. null gives + * .tmp. + * @param allowInMemory if true the file MIGHT be created in + * memory if supported by the implentation. If false the + * file MUST be created on disk. + * @return the temporary file. + */ + TempFile createTempFile(String prefix, String suffix, + boolean allowInMemory) throws IOException; + String getAbsolutePath(); + void delete(); +} diff --git a/src/org/apache/james/mime4j/util/TempStorage.java b/src/org/apache/james/mime4j/util/TempStorage.java new file mode 100644 index 000000000..ca951531a --- /dev/null +++ b/src/org/apache/james/mime4j/util/TempStorage.java @@ -0,0 +1,70 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.mime4j.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * + * @version $Id: TempStorage.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public abstract class TempStorage { + private static Log log = LogFactory.getLog(TempStorage.class); + private static TempStorage inst = null; + + static { + + String clazz = System.getProperty("org.apache.james.mime4j.tempStorage"); + try { + + if (inst != null) { + inst = (TempStorage) Class.forName(clazz).newInstance(); + } + + } catch (Throwable t) { + log.warn("Unable to create or instantiate TempStorage class '" + + clazz + "' using SimpleTempStorage instead", t); + } + + if (inst == null) { + inst = new SimpleTempStorage(); + } + } + + /** + * Gets the root temporary path which should be used to + * create new temporary paths or files. + * + * @return the root temporary path. + */ + public abstract TempPath getRootTempPath(); + + public static TempStorage getInstance() { + return inst; + } + + public static void setInstance(TempStorage inst) { + if (inst == null) { + throw new NullPointerException("inst"); + } + TempStorage.inst = inst; + } +}