From 1a09b3477716df298cb71b13634495722ed98e6c Mon Sep 17 00:00:00 2001 From: "jwz%mozilla.org" Date: Wed, 9 Sep 1998 00:52:38 +0000 Subject: [PATCH] vivat grendel! git-svn-id: svn://10.0.0.236/trunk@9581 18797224-902f-48f8-a5cc-f745e15eee43 --- mozilla/grendel/Main.java | 49 + mozilla/grendel/Makefile | 55 + mozilla/grendel/README | 5 + mozilla/grendel/SelfTest.java | 245 +++ mozilla/grendel/TestFolderViewer.java | 279 +++ mozilla/grendel/addressbook/AddressBook.java | 692 ++++++ mozilla/grendel/addressbook/Makefile | 28 + mozilla/grendel/addressbook/Menus.properties | 109 + .../grendel/addressbook/NewCardDialog.java | 88 + mozilla/grendel/addressbook/README | 6 + .../addressbook/addresscard/ACS_Personal.java | 209 ++ .../addressbook/addresscard/AC_Attribute.java | 105 + .../addressbook/addresscard/AC_Exception.java | 34 + .../addressbook/addresscard/AC_ID.java | 29 + .../addressbook/addresscard/AC_IDSet.java | 47 + .../addressbook/addresscard/IAttribute.java | 46 + .../addresscard/IAttributeSet.java | 53 + .../addressbook/addresscard/ICard.java | 44 + .../addressbook/addresscard/ICardSet.java | 54 + .../addressbook/addresscard/ICardSource.java | 62 + .../addressbook/addresscard/IQuerySet.java | 31 + .../addressbook/addresscard/IQueryString.java | 31 + .../addressbook/addresscard/ITerm.java | 31 + .../addresscard/LDAP_Attribute.java | 71 + .../addresscard/LDAP_AttributeSet.java | 85 + .../addressbook/addresscard/LDAP_Card.java | 76 + .../addressbook/addresscard/LDAP_CardSet.java | 87 + .../addressbook/addresscard/LDAP_Server.java | 275 +++ .../grendel/addressbook/addresscard/Makefile | 45 + .../addressbook/addresscard/SelfTest.java | 500 +++++ .../addressbook/addresscard/TermAnd.java | 57 + .../addressbook/addresscard/TermEqual.java | 42 + .../addressbook/addresscard/TermNotEqual.java | 46 + .../addressbook/addresscard/TermOr.java | 74 + mozilla/grendel/calypso/Makefile | 23 + mozilla/grendel/calypso/README | 7 + mozilla/grendel/calypso/util/Abacus.java | 289 +++ .../calypso/util/ArrayEnumeration.java | 38 + mozilla/grendel/calypso/util/Assert.java | 116 + .../grendel/calypso/util/AssertionError.java | 33 + .../calypso/util/AssertionException.java | 33 + mozilla/grendel/calypso/util/Atom.java | 222 ++ .../grendel/calypso/util/AtomHashtable.java | 193 ++ .../calypso/util/AttributeValuePair.java | 109 + mozilla/grendel/calypso/util/ByteBuf.java | 918 ++++++++ .../grendel/calypso/util/ByteLineBuffer.java | 346 +++ .../util/ByteToCharConverterEnumeration.java | 85 + .../calypso/util/CacheOutputStream.java | 409 ++++ mozilla/grendel/calypso/util/CharArray.java | 41 + .../calypso/util/CharArrayIterator.java | 123 ++ .../util/CharToByteConverterEnumeration.java | 85 + .../util/ClasspathEntryEnumeration.java | 234 ++ mozilla/grendel/calypso/util/Comparer.java | 30 + mozilla/grendel/calypso/util/ConfigUtils.java | 72 + mozilla/grendel/calypso/util/Counter.java | 47 + .../grendel/calypso/util/DataExtension.java | 102 + .../calypso/util/DefaultPreferences.java | 56 + .../calypso/util/EnumerationEnumerator.java | 63 + .../grendel/calypso/util/HashtableBase.java | 592 +++++ .../grendel/calypso/util/HashtableLite.java | 340 +++ .../calypso/util/HashtableRecycler.java | 117 + mozilla/grendel/calypso/util/IDMap.java | 294 +++ .../util/LineBufferingInputStream.java | 185 ++ .../grendel/calypso/util/ListenerList.java | 184 ++ .../calypso/util/ListenerListState.java | 46 + mozilla/grendel/calypso/util/Makefile | 83 + .../grendel/calypso/util/MemoryManager.java | 39 + .../grendel/calypso/util/MemoryMonitor.java | 111 + .../grendel/calypso/util/MemoryPressure.java | 41 + mozilla/grendel/calypso/util/NetworkDate.java | 1071 +++++++++ .../grendel/calypso/util/NullEnumeration.java | 43 + .../calypso/util/NullJavaEnumeration.java | 43 + mozilla/grendel/calypso/util/Preferences.java | 69 + .../grendel/calypso/util/PreferencesBase.java | 170 ++ .../calypso/util/PreferencesFactory.java | 39 + .../calypso/util/PrefetchEnumeration.java | 88 + mozilla/grendel/calypso/util/QSort.java | 108 + mozilla/grendel/calypso/util/RWLock.java | 312 +++ .../util/RandomAccessFileWithByteLines.java | 174 ++ mozilla/grendel/calypso/util/Recycler.java | 201 ++ mozilla/grendel/calypso/util/SelfTest.java | 126 ++ .../grendel/calypso/util/SelfTestAtom.java | 53 + .../calypso/util/SelfTestException.java | 33 + .../grendel/calypso/util/SelfTestIDMap.java | 78 + .../grendel/calypso/util/SelfTestRWLock.java | 343 +++ .../grendel/calypso/util/SignedInteger.java | 175 ++ .../calypso/util/SingleEnumeration.java | 48 + mozilla/grendel/calypso/util/StringBuf.java | 742 +++++++ .../calypso/util/StringBufRecycler.java | 57 + mozilla/grendel/calypso/util/StringUtils.java | 151 ++ mozilla/grendel/calypso/util/TempFile.java | 158 ++ mozilla/grendel/calypso/util/TimeBomb.java | 47 + .../grendel/calypso/util/URLClassLoader.java | 341 +++ mozilla/grendel/calypso/util/Vec.java | 138 ++ .../grendel/calypso/util/VectorRecycler.java | 119 + mozilla/grendel/calypso/util/WeakLink.java | 38 + .../grendel/calypso/util/WeakLinkArray.java | 95 + .../util/WeakLinkArrayEnumeration.java | 45 + mozilla/grendel/composition/AddressBar.java | 61 + .../grendel/composition/AddressDialog.java | 389 ++++ mozilla/grendel/composition/AddressList.java | 917 ++++++++ .../composition/AddressListBeanInfo.java | 72 + mozilla/grendel/composition/Addressee.java | 118 + .../grendel/composition/AttachmentsList.java | 374 ++++ .../composition/AttachmentsListBeanInfo.java | 127 ++ mozilla/grendel/composition/Composition.java | 177 ++ .../grendel/composition/CompositionPanel.java | 867 ++++++++ .../composition/CompositionPanelListener.java | 32 + .../grendel/composition/CompositionTest.java | 104 + mozilla/grendel/composition/Makefile | 36 + .../grendel/composition/MenuLabels.properties | 85 + mozilla/grendel/composition/NSTabbedPane.java | 139 ++ mozilla/grendel/composition/OptionsPanel.java | 109 + mozilla/grendel/composition/README | 5 + .../grendel/composition/TestDataSource2.java | 111 + mozilla/grendel/composition/contenthandlers | 2 + .../images/AttachmentsListIcon16.gif | Bin 0 -> 101 bytes .../images/AttachmentsListIcon32.gif | Bin 0 -> 87 bytes mozilla/grendel/composition/images/README | 16 + .../composition/images/address-disabled.gif | Bin 0 -> 206 bytes .../composition/images/address-pressed.gif | Bin 0 -> 228 bytes .../composition/images/address-rollover.gif | Bin 0 -> 319 bytes .../grendel/composition/images/address.gif | Bin 0 -> 232 bytes .../composition/images/attach-disabled.gif | Bin 0 -> 127 bytes .../composition/images/attach-pressed.gif | Bin 0 -> 129 bytes .../composition/images/attach-rollover.gif | Bin 0 -> 190 bytes mozilla/grendel/composition/images/attach.gif | Bin 0 -> 145 bytes mozilla/grendel/composition/images/card.gif | Bin 0 -> 99 bytes mozilla/grendel/composition/images/logo.gif | Bin 0 -> 1289 bytes .../composition/images/quote-disabled.gif | Bin 0 -> 168 bytes .../composition/images/quote-pressed.gif | Bin 0 -> 195 bytes .../composition/images/quote-rollover.gif | Bin 0 -> 188 bytes mozilla/grendel/composition/images/quote.gif | Bin 0 -> 180 bytes .../composition/images/save-disabled.gif | Bin 0 -> 195 bytes .../composition/images/save-pressed.gif | Bin 0 -> 221 bytes .../composition/images/save-rollover.gif | Bin 0 -> 226 bytes mozilla/grendel/composition/images/save.gif | Bin 0 -> 218 bytes .../composition/images/security-disabled.gif | Bin 0 -> 179 bytes .../composition/images/security-pressed.gif | Bin 0 -> 200 bytes .../composition/images/security-rollover.gif | Bin 0 -> 207 bytes .../grendel/composition/images/security.gif | Bin 0 -> 204 bytes .../composition/images/send-disabled.gif | Bin 0 -> 191 bytes .../composition/images/send-pressed.gif | Bin 0 -> 212 bytes .../composition/images/send-rollover.gif | Bin 0 -> 231 bytes mozilla/grendel/composition/images/send.gif | Bin 0 -> 235 bytes .../composition/images/small_address.gif | Bin 0 -> 177 bytes .../composition/images/small_attachments.gif | Bin 0 -> 102 bytes .../composition/images/small_otpions.gif | Bin 0 -> 167 bytes .../composition/images/spelling-disabled.gif | Bin 0 -> 200 bytes .../composition/images/spelling-pressed.gif | Bin 0 -> 161 bytes .../composition/images/spelling-rollover.gif | Bin 0 -> 245 bytes .../grendel/composition/images/spelling.gif | Bin 0 -> 226 bytes .../composition/images/stop-disabled.gif | Bin 0 -> 216 bytes .../composition/images/stop-pressed.gif | Bin 0 -> 254 bytes .../composition/images/stop-rollover.gif | Bin 0 -> 269 bytes mozilla/grendel/composition/images/stop.gif | Bin 0 -> 262 bytes mozilla/grendel/composition/menus.xml | 67 + mozilla/grendel/dnd/DnDConstants.java | 31 + mozilla/grendel/dnd/DragSource.java | 102 + mozilla/grendel/dnd/DragSourceContext.java | 192 ++ mozilla/grendel/dnd/DragSourceDragEvent.java | 47 + mozilla/grendel/dnd/DragSourceDropEvent.java | 43 + mozilla/grendel/dnd/DragSourceListener.java | 32 + mozilla/grendel/dnd/DropTarget.java | 88 + mozilla/grendel/dnd/DropTargetComponent.java | 28 + mozilla/grendel/dnd/DropTargetContext.java | 63 + mozilla/grendel/dnd/DropTargetDragEvent.java | 41 + mozilla/grendel/dnd/DropTargetDropEvent.java | 49 + mozilla/grendel/dnd/DropTargetEvent.java | 50 + mozilla/grendel/dnd/DropTargetListener.java | 31 + .../dnd/InvalidDnDOperationException.java | 25 + mozilla/grendel/dnd/Makefile | 36 + .../filters/DeleteFilterActionFactory.java | 72 + mozilla/grendel/filters/FilterBase.java | 61 + mozilla/grendel/filters/FilterFactory.java | 38 + mozilla/grendel/filters/FilterMaster.java | 226 ++ .../grendel/filters/FilterRulesParser.java | 354 +++ .../filters/FilterSyntaxException.java | 34 + mozilla/grendel/filters/IFilter.java | 32 + mozilla/grendel/filters/IFilterAction.java | 30 + .../grendel/filters/IFilterActionFactory.java | 40 + .../grendel/filters/IFilterTermFactory.java | 38 + mozilla/grendel/filters/Makefile | 34 + .../filters/MoveFilterActionFactory.java | 134 ++ mozilla/grendel/filters/README | 13 + .../grendel/filters/SubjectTermFactory.java | 62 + .../grendel/integrator/CompositionShell.java | 263 +++ .../grendel/integrator/CompositionView.java | 158 ++ mozilla/grendel/integrator/FolderCtx.java | 290 +++ mozilla/grendel/integrator/FolderView.java | 240 ++ mozilla/grendel/integrator/Makefile | 29 + .../grendel/integrator/MenuLabels.properties | 134 ++ mozilla/grendel/integrator/SessionView.java | 148 ++ mozilla/grendel/integrator/Shell.java | 311 +++ mozilla/grendel/integrator/StoreCtx.java | 54 + mozilla/grendel/integrator/menus.xml | 79 + .../grendel/mime/HeaderCharsetDecoder.java | 72 + mozilla/grendel/mime/IMimeObject.java | 75 + mozilla/grendel/mime/IMimeOperator.java | 61 + mozilla/grendel/mime/IMimeParser.java | 55 + mozilla/grendel/mime/Makefile | 34 + mozilla/grendel/mime/README | 7 + mozilla/grendel/mime/TestMime.java | 108 + mozilla/grendel/mime/encoder/Makefile | 29 + .../mime/encoder/MimeBase64Decoder.java | 181 ++ .../mime/encoder/MimeBase64Encoder.java | 123 ++ mozilla/grendel/mime/encoder/MimeEncoder.java | 53 + .../encoder/MimeQuotedPrintableDecoder.java | 164 ++ .../encoder/MimeQuotedPrintableEncoder.java | 170 ++ .../grendel/mime/encoder/MimeUUDecoder.java | 196 ++ .../grendel/mime/encoder/MimeUUEncoder.java | 125 ++ mozilla/grendel/mime/encoder/README | 6 + mozilla/grendel/mime/extractor/Makefile | 23 + .../MimeExtractorOperatorFactory.java | 73 + mozilla/grendel/mime/extractor/README | 7 + .../mime/html/BriefHeaderFormatter.java | 31 + .../mime/html/FullHeaderFormatter.java | 72 + .../grendel/mime/html/HeaderFormatter.java | 214 ++ mozilla/grendel/mime/html/Makefile | 43 + .../mime/html/MimeContainerOperator.java | 57 + .../mime/html/MimeExternalBodyOperator.java | 227 ++ .../mime/html/MimeExternalObjectOperator.java | 151 ++ .../grendel/mime/html/MimeHTMLOperator.java | 111 + .../mime/html/MimeHTMLOperatorFactory.java | 45 + .../grendel/mime/html/MimeLeafOperator.java | 38 + .../mime/html/MimeMessageOperator.java | 50 + .../MimeMultipartAlternativeOperator.java | 92 + .../MimeMultipartAppleDoubleOperator.java | 71 + .../html/MimeMultipartRelatedOperator.java | 45 + .../html/MimeMultipartSignedOperator.java | 45 + .../mime/html/MimeTextEnrichedOperator.java | 38 + .../mime/html/MimeTextHTMLOperator.java | 54 + .../grendel/mime/html/MimeTextOperator.java | 60 + .../mime/html/MimeTextRichtextOperator.java | 47 + .../mime/html/MimeTextVCardOperator.java | 45 + .../mime/html/NormalHeaderFormatter.java | 192 ++ mozilla/grendel/mime/html/README | 7 + .../grendel/mime/html/TextHTMLConverter.java | 267 +++ mozilla/grendel/mime/parser/Makefile | 32 + .../grendel/mime/parser/MimeContainer.java | 236 ++ mozilla/grendel/mime/parser/MimeDwimText.java | 256 +++ mozilla/grendel/mime/parser/MimeLeaf.java | 103 + .../mime/parser/MimeMessageExternalBody.java | 47 + .../mime/parser/MimeMessageRFC822.java | 111 + .../grendel/mime/parser/MimeMultipart.java | 292 +++ .../mime/parser/MimeMultipartDigest.java | 40 + mozilla/grendel/mime/parser/MimeObject.java | 129 ++ .../mime/parser/MimeParserFactory.java | 42 + .../mime/parser/MimeXSunAttachment.java | 268 +++ mozilla/grendel/mime/parser/README | 6 + mozilla/grendel/prefs/MailServerPrefs.java | 67 + .../grendel/prefs/MailServerPrefsEditor.java | 358 +++ mozilla/grendel/prefs/Makefile | 30 + mozilla/grendel/prefs/PrefDialogs.xml | 94 + mozilla/grendel/prefs/PrefLabels.properties | 42 + mozilla/grendel/prefs/Prefs.java | 154 ++ mozilla/grendel/prefs/PrefsBeanInfo.java | 77 + mozilla/grendel/prefs/UIPrefs.java | 36 + mozilla/grendel/prefs/UIPrefsEditor.java | 219 ++ mozilla/grendel/prefs/UserPrefs.java | 62 + mozilla/grendel/prefs/UserPrefsEditor.java | 196 ++ mozilla/grendel/rules.mk | 58 + mozilla/grendel/search/AndPanel.java | 56 + mozilla/grendel/search/AttributeLayout.java | 126 ++ mozilla/grendel/search/AttributePanel.java | 121 + mozilla/grendel/search/ISearchAttribute.java | 78 + mozilla/grendel/search/ISearchResults.java | 26 + mozilla/grendel/search/ISearchable.java | 37 + mozilla/grendel/search/MailSearch.java | 236 ++ mozilla/grendel/search/Makefile | 37 + mozilla/grendel/search/NaryTermPanel.java | 69 + mozilla/grendel/search/NoopPanel.java | 39 + mozilla/grendel/search/OperatorLayout.java | 135 ++ mozilla/grendel/search/OrPanel.java | 56 + mozilla/grendel/search/ResultsFrame.java | 43 + mozilla/grendel/search/SearchFrame.java | 116 + mozilla/grendel/search/SearchPanel.java | 204 ++ mozilla/grendel/search/TermPanel.java | 136 ++ mozilla/grendel/storage/BerkeleyFolder.java | 1582 ++++++++++++++ mozilla/grendel/storage/BerkeleyMessage.java | 227 ++ mozilla/grendel/storage/BerkeleyStore.java | 113 + mozilla/grendel/storage/ByteString.java | 114 + mozilla/grendel/storage/ByteStringTable.java | 152 ++ mozilla/grendel/storage/Constants.java | 74 + .../storage/DotTerminatedInputStream.java | 153 ++ mozilla/grendel/storage/FilterFolder.java | 182 ++ .../grendel/storage/FilterFolderFactory.java | 37 + mozilla/grendel/storage/FolderBase.java | 145 ++ mozilla/grendel/storage/FolderExtra.java | 35 + .../grendel/storage/FolderExtraFactory.java | 57 + mozilla/grendel/storage/MailDrop.java | 55 + .../grendel/storage/MailDropException.java | 37 + mozilla/grendel/storage/MailSummaryFile.java | 356 +++ .../storage/MailSummaryFileCheddar.java | 295 +++ .../storage/MailSummaryFileFactory.java | 105 + .../storage/MailSummaryFileGrendel.java | 534 +++++ mozilla/grendel/storage/Makefile | 78 + mozilla/grendel/storage/MasterBase.java | 95 + mozilla/grendel/storage/MessageBase.java | 727 ++++++ mozilla/grendel/storage/MessageExtra.java | 71 + .../grendel/storage/MessageExtraFactory.java | 37 + .../grendel/storage/MessageExtraWrapper.java | 269 +++ mozilla/grendel/storage/MessageID.java | 114 + mozilla/grendel/storage/MessageIDTable.java | 84 + mozilla/grendel/storage/MessageReadOnly.java | 301 +++ mozilla/grendel/storage/NNTPConnection.java | 910 ++++++++ mozilla/grendel/storage/NNTPException.java | 35 + mozilla/grendel/storage/NewsFolder.java | 540 +++++ mozilla/grendel/storage/NewsFolderRoot.java | 207 ++ mozilla/grendel/storage/NewsMessage.java | 125 ++ mozilla/grendel/storage/NewsRC.java | 457 ++++ mozilla/grendel/storage/NewsRCLine.java | 97 + mozilla/grendel/storage/NewsSet.java | 1338 ++++++++++++ mozilla/grendel/storage/NewsStore.java | 297 +++ mozilla/grendel/storage/Obarray.java | 155 ++ mozilla/grendel/storage/PackedIntArray.java | 227 ++ .../grendel/storage/ParseBerkeleyFolder.java | 485 ++++ .../ParseBerkeleyFolderAndExpunge.java | 284 +++ .../ParseBerkeleyFolderWithSummary.java | 153 ++ .../storage/PartialFileInputStream.java | 75 + mozilla/grendel/storage/PopFolder.java | 292 +++ mozilla/grendel/storage/PopMailDrop.java | 518 +++++ mozilla/grendel/storage/PopMessage.java | 265 +++ mozilla/grendel/storage/PopStore.java | 339 +++ mozilla/grendel/storage/README | 9 + .../storage/SearchResultsFolderFactory.java | 44 + mozilla/grendel/storage/SelfTest.java | 250 +++ mozilla/grendel/storage/TestBerkeley.java | 54 + mozilla/grendel/storage/UnixDotLock.java | 452 ++++ .../addressparser/AddressCorrector.java | 91 + .../storage/addressparser/AddressToken.java | 135 ++ .../storage/addressparser/GroupToken.java | 48 + .../addressparser/MailAddressToken.java | 60 + .../storage/addressparser/MailboxToken.java | 249 +++ .../grendel/storage/addressparser/Makefile | 37 + mozilla/grendel/storage/addressparser/P1.java | 94 + mozilla/grendel/storage/addressparser/P2.java | 65 + mozilla/grendel/storage/addressparser/README | 6 + .../addressparser/RFC822AddressList.java | 158 ++ .../addressparser/RFC822AddressParser.java | 966 ++++++++ .../storage/addressparser/RFC822Mailbox.java | 94 + .../addressparser/RFC822MailboxList.java | 317 +++ .../addressparser/RFC822ParserException.java | 48 + .../storage/addressparser/RFC822Token.java | 116 + .../addressparser/RFC822Tokenizer.java | 622 ++++++ .../RouteAddrNoPhraseException.java | 200 ++ mozilla/grendel/storage/folderstats.el | 232 ++ .../grendel/storage/intertwingle/BGDB.java | 211 ++ .../grendel/storage/intertwingle/BaseDB.java | 38 + mozilla/grendel/storage/intertwingle/DB.java | 44 + .../grendel/storage/intertwingle/Dump.java | 34 + .../grendel/storage/intertwingle/HackDB.java | 197 ++ mozilla/grendel/storage/intertwingle/Makefile | 31 + .../storage/intertwingle/PhoneTest.java | 156 ++ mozilla/grendel/storage/intertwingle/README | 10 + .../storage/intertwingle/SelfTest.java | 71 + .../storage/intertwingle/SimpleDB.java | 500 +++++ .../grendel/storage/intertwingle/Twingle.java | 195 ++ mozilla/grendel/storage/selftestdata/Inbox | 449 ++++ mozilla/grendel/ui/ActionFactory.java | 214 ++ mozilla/grendel/ui/BiffIcon.java | 84 + mozilla/grendel/ui/BiffThread.java | 133 ++ mozilla/grendel/ui/DialogAuthenticator.java | 267 +++ mozilla/grendel/ui/EditHostDialog.java | 194 ++ mozilla/grendel/ui/FolderCombo.java | 174 ++ mozilla/grendel/ui/FolderFrame.java | 149 ++ .../grendel/ui/FolderListTransferable.java | 67 + mozilla/grendel/ui/FolderPanel.java | 1072 +++++++++ mozilla/grendel/ui/FolderPanelListener.java | 63 + mozilla/grendel/ui/GeneralDialog.java | 75 + mozilla/grendel/ui/GeneralFrame.java | 344 +++ mozilla/grendel/ui/GeneralPanel.java | 113 + mozilla/grendel/ui/Labels.properties | 110 + mozilla/grendel/ui/Makefile | 56 + mozilla/grendel/ui/MasterPanel.java | 851 ++++++++ mozilla/grendel/ui/MasterPanelListener.java | 44 + mozilla/grendel/ui/MenuLabels.properties | 192 ++ mozilla/grendel/ui/Menus.properties | 106 + mozilla/grendel/ui/MessageCellRenderer.java | 66 + mozilla/grendel/ui/MessageDisplayManager.java | 113 + mozilla/grendel/ui/MessageFrame.java | 129 ++ mozilla/grendel/ui/MessageHeader.java | 32 + .../grendel/ui/MessageListTransferable.java | 67 + mozilla/grendel/ui/MessageModel.java | 282 +++ mozilla/grendel/ui/MessagePanel.java | 440 ++++ mozilla/grendel/ui/MessagePanelListener.java | 34 + .../ui/MultiMessageDisplayManager.java | 199 ++ mozilla/grendel/ui/NewFolderDialog.java | 179 ++ mozilla/grendel/ui/PrefsDialog.java | 63 + mozilla/grendel/ui/ProgressFactory.java | 201 ++ mozilla/grendel/ui/ProgressFrame.java | 171 ++ mozilla/grendel/ui/README | 7 + mozilla/grendel/ui/SimpleAuthenticator.java | 37 + mozilla/grendel/ui/SimpleMessageHeader.java | 216 ++ mozilla/grendel/ui/StoreFactory.java | 253 +++ mozilla/grendel/ui/UIFactory.java | 118 + .../ui/UnifiedMessageDisplayManager.java | 465 ++++ mozilla/grendel/ui/Util.java | 298 +++ mozilla/grendel/ui/dialogs.xml | 70 + mozilla/grendel/ui/images/GrendelIcon16.gif | Bin 0 -> 134 bytes mozilla/grendel/ui/images/GrendelIcon32.gif | Bin 0 -> 210 bytes mozilla/grendel/ui/images/README | 16 + .../ui/images/animation/AnimHuge00.gif | Bin 0 -> 1350 bytes .../ui/images/animation/AnimHuge01.gif | Bin 0 -> 1350 bytes .../ui/images/animation/AnimHuge02.gif | Bin 0 -> 1350 bytes .../ui/images/animation/AnimHuge03.gif | Bin 0 -> 1352 bytes .../ui/images/animation/AnimHuge04.gif | Bin 0 -> 1352 bytes .../ui/images/animation/AnimHuge05.gif | Bin 0 -> 1364 bytes .../ui/images/animation/AnimHuge06.gif | Bin 0 -> 1364 bytes .../ui/images/animation/AnimHuge07.gif | Bin 0 -> 1368 bytes .../ui/images/animation/AnimHuge08.gif | Bin 0 -> 1368 bytes .../ui/images/animation/AnimHuge09.gif | Bin 0 -> 1384 bytes .../ui/images/animation/AnimHuge10.gif | Bin 0 -> 1384 bytes .../ui/images/animation/AnimHuge11.gif | Bin 0 -> 1385 bytes .../ui/images/animation/AnimHuge12.gif | Bin 0 -> 1385 bytes .../ui/images/animation/AnimHuge13.gif | Bin 0 -> 1404 bytes .../ui/images/animation/AnimHuge14.gif | Bin 0 -> 1404 bytes .../ui/images/animation/AnimHuge15.gif | Bin 0 -> 1422 bytes .../ui/images/animation/AnimHuge16.gif | Bin 0 -> 1422 bytes .../ui/images/animation/AnimHuge17.gif | Bin 0 -> 1428 bytes .../ui/images/animation/AnimHuge18.gif | Bin 0 -> 1428 bytes .../ui/images/animation/AnimHuge19.gif | Bin 0 -> 1475 bytes .../ui/images/animation/AnimHuge20.gif | Bin 0 -> 1475 bytes .../ui/images/animation/AnimHuge21.gif | Bin 0 -> 1530 bytes .../ui/images/animation/AnimHuge22.gif | Bin 0 -> 1530 bytes .../ui/images/animation/AnimHuge23.gif | Bin 0 -> 1530 bytes .../ui/images/animation/AnimHuge24.gif | Bin 0 -> 1530 bytes .../ui/images/animation/AnimHuge25.gif | Bin 0 -> 1475 bytes .../ui/images/animation/AnimHuge26.gif | Bin 0 -> 1475 bytes .../ui/images/animation/AnimHuge27.gif | Bin 0 -> 1428 bytes .../ui/images/animation/AnimHuge28.gif | Bin 0 -> 1428 bytes .../ui/images/animation/AnimHuge29.gif | Bin 0 -> 1422 bytes .../ui/images/animation/AnimHuge30.gif | Bin 0 -> 1422 bytes .../ui/images/animation/AnimHuge31.gif | Bin 0 -> 1404 bytes .../ui/images/animation/AnimHuge32.gif | Bin 0 -> 1404 bytes .../ui/images/animation/AnimHuge33.gif | Bin 0 -> 1385 bytes .../ui/images/animation/AnimHuge34.gif | Bin 0 -> 1385 bytes .../ui/images/animation/AnimHuge35.gif | Bin 0 -> 1384 bytes .../ui/images/animation/AnimHuge36.gif | Bin 0 -> 1384 bytes .../ui/images/animation/AnimHuge37.gif | Bin 0 -> 1368 bytes .../ui/images/animation/AnimHuge38.gif | Bin 0 -> 1368 bytes .../ui/images/animation/AnimHuge39.gif | Bin 0 -> 1364 bytes .../ui/images/animation/AnimHuge40.gif | Bin 0 -> 1352 bytes mozilla/grendel/ui/images/animation/README | 16 + mozilla/grendel/ui/images/biffNew.gif | Bin 0 -> 160 bytes mozilla/grendel/ui/images/biffNone.gif | Bin 0 -> 140 bytes mozilla/grendel/ui/images/biffUnknown.gif | Bin 0 -> 160 bytes mozilla/grendel/ui/images/connected-small.gif | Bin 0 -> 112 bytes mozilla/grendel/ui/images/deleted.gif | Bin 0 -> 118 bytes mozilla/grendel/ui/images/flagged.gif | Bin 0 -> 96 bytes mozilla/grendel/ui/images/folder-small.gif | Bin 0 -> 116 bytes mozilla/grendel/ui/images/inbox-small.gif | Bin 0 -> 172 bytes mozilla/grendel/ui/images/msg-small.gif | Bin 0 -> 112 bytes mozilla/grendel/ui/images/msgRead-small.gif | Bin 0 -> 132 bytes mozilla/grendel/ui/images/newsgroup-small.gif | Bin 0 -> 168 bytes mozilla/grendel/ui/images/read.gif | Bin 0 -> 50 bytes mozilla/grendel/ui/images/splitLeft.gif | Bin 0 -> 71 bytes mozilla/grendel/ui/images/splitRight.gif | Bin 0 -> 70 bytes mozilla/grendel/ui/images/splitTop.gif | Bin 0 -> 67 bytes mozilla/grendel/ui/images/stacked.gif | Bin 0 -> 61 bytes .../grendel/ui/images/storeLocal-small.gif | Bin 0 -> 130 bytes .../grendel/ui/images/storeRemote-small.gif | Bin 0 -> 128 bytes mozilla/grendel/ui/images/toolbar/README | 16 + .../images/toolbar/markAllRead-disabled.gif | Bin 0 -> 199 bytes .../ui/images/toolbar/markAllRead-pressed.gif | Bin 0 -> 216 bytes .../images/toolbar/markAllRead-rollover.gif | Bin 0 -> 216 bytes .../grendel/ui/images/toolbar/markAllRead.gif | Bin 0 -> 216 bytes .../ui/images/toolbar/msgDelete-disabled.gif | Bin 0 -> 197 bytes .../ui/images/toolbar/msgDelete-pressed.gif | Bin 0 -> 223 bytes .../ui/images/toolbar/msgDelete-rollover.gif | Bin 0 -> 230 bytes .../grendel/ui/images/toolbar/msgDelete.gif | Bin 0 -> 224 bytes .../ui/images/toolbar/msgForward-disabled.gif | Bin 0 -> 212 bytes .../ui/images/toolbar/msgForward-pressed.gif | Bin 0 -> 220 bytes .../ui/images/toolbar/msgForward-rollover.gif | Bin 0 -> 247 bytes .../grendel/ui/images/toolbar/msgForward.gif | Bin 0 -> 244 bytes .../ui/images/toolbar/msgGetNew-disabled.gif | Bin 0 -> 218 bytes .../ui/images/toolbar/msgGetNew-pressed.gif | Bin 0 -> 242 bytes .../ui/images/toolbar/msgGetNew-rollover.gif | Bin 0 -> 269 bytes .../grendel/ui/images/toolbar/msgGetNew.gif | Bin 0 -> 265 bytes .../ui/images/toolbar/msgNew-disabled.gif | Bin 0 -> 204 bytes .../ui/images/toolbar/msgNew-pressed.gif | Bin 0 -> 247 bytes .../ui/images/toolbar/msgNew-rollover.gif | Bin 0 -> 322 bytes mozilla/grendel/ui/images/toolbar/msgNew.gif | Bin 0 -> 253 bytes .../ui/images/toolbar/msgReply-disabled.gif | Bin 0 -> 210 bytes .../ui/images/toolbar/msgReply-pressed.gif | Bin 0 -> 224 bytes .../ui/images/toolbar/msgReply-rollover.gif | Bin 0 -> 253 bytes .../grendel/ui/images/toolbar/msgReply.gif | Bin 0 -> 246 bytes .../ui/images/toolbar/print-disabled.gif | Bin 0 -> 202 bytes .../ui/images/toolbar/print-pressed.gif | Bin 0 -> 205 bytes .../ui/images/toolbar/print-rollover.gif | Bin 0 -> 218 bytes mozilla/grendel/ui/images/toolbar/print.gif | Bin 0 -> 213 bytes .../ui/images/toolbar/stop-disabled.gif | Bin 0 -> 216 bytes .../ui/images/toolbar/stop-pressed.gif | Bin 0 -> 254 bytes .../ui/images/toolbar/stop-rollover.gif | Bin 0 -> 269 bytes mozilla/grendel/ui/images/toolbar/stop.gif | Bin 0 -> 262 bytes mozilla/grendel/ui/images/unflagged.gif | Bin 0 -> 50 bytes mozilla/grendel/ui/images/unread.gif | Bin 0 -> 79 bytes mozilla/grendel/ui/menus.xml | 145 ++ mozilla/grendel/util/Makefile | 23 + mozilla/grendel/util/MessageIDGenerator.java | 99 + mozilla/grendel/view/DummyThreadable.java | 34 + mozilla/grendel/view/FolderView.java | 32 + mozilla/grendel/view/FolderViewBase.java | 582 +++++ mozilla/grendel/view/FolderViewFactory.java | 34 + mozilla/grendel/view/ISortable.java | 40 + mozilla/grendel/view/IThreadable.java | 107 + mozilla/grendel/view/Makefile | 41 + mozilla/grendel/view/MessageSetView.java | 79 + .../grendel/view/MessageSetViewObserver.java | 44 + mozilla/grendel/view/Sorter.java | 101 + mozilla/grendel/view/TestThreader.java | 511 +++++ mozilla/grendel/view/Threader.java | 587 +++++ mozilla/grendel/view/ViewedFolder.java | 96 + mozilla/grendel/view/ViewedFolderBase.java | 410 ++++ mozilla/grendel/view/ViewedMessage.java | 59 + mozilla/grendel/view/ViewedMessageBase.java | 236 ++ mozilla/grendel/view/ViewedStore.java | 114 + mozilla/grendel/view/ViewedStoreBase.java | 360 +++ mozilla/grendel/view/ViewedStoreEvent.java | 41 + mozilla/grendel/view/ViewedStoreListener.java | 31 + mozilla/grendel/widgets/Animation.java | 128 ++ mozilla/grendel/widgets/CellEditor.java | 29 + mozilla/grendel/widgets/CellRenderer.java | 29 + mozilla/grendel/widgets/Column.java | 328 +++ .../grendel/widgets/ColumnChangeListener.java | 30 + mozilla/grendel/widgets/ColumnHeader.java | 432 ++++ mozilla/grendel/widgets/ColumnModel.java | 157 ++ mozilla/grendel/widgets/ColumnModelEvent.java | 44 + mozilla/grendel/widgets/ColumnModelImp.java | 285 +++ .../grendel/widgets/ColumnModelListener.java | 36 + .../grendel/widgets/DefaultCellRenderer.java | 118 + .../widgets/DefaultHeaderRenderer.java | 159 ++ mozilla/grendel/widgets/HeaderRenderer.java | 36 + mozilla/grendel/widgets/Makefile | 52 + .../widgets/MultiSelectionManager.java | 300 +++ mozilla/grendel/widgets/SelectionEvent.java | 47 + .../grendel/widgets/SelectionListener.java | 34 + mozilla/grendel/widgets/SelectionManager.java | 127 ++ .../widgets/SingleSelectionManager.java | 243 +++ mozilla/grendel/widgets/Splitter.java | 514 +++++ mozilla/grendel/widgets/StatusEvent.java | 67 + mozilla/grendel/widgets/TextCellEditor.java | 153 ++ mozilla/grendel/widgets/ToggleCellEditor.java | 119 + .../grendel/widgets/ToggleCellRenderer.java | 52 + mozilla/grendel/widgets/TreePath.java | 106 + mozilla/grendel/widgets/TreeTable.java | 1942 +++++++++++++++++ .../grendel/widgets/TreeTableDataModel.java | 205 ++ .../widgets/TreeTableModelBroadcaster.java | 85 + .../grendel/widgets/TreeTableModelEvent.java | 72 + .../widgets/TreeTableModelListener.java | 32 + mozilla/grendel/widgets/Util.java | 298 +++ mozilla/grendel/widgets/images/README | 16 + mozilla/grendel/widgets/images/minus.gif | Bin 0 -> 122 bytes mozilla/grendel/widgets/images/plus.gif | Bin 0 -> 123 bytes 554 files changed, 67063 insertions(+) create mode 100644 mozilla/grendel/Main.java create mode 100644 mozilla/grendel/Makefile create mode 100644 mozilla/grendel/README create mode 100644 mozilla/grendel/SelfTest.java create mode 100644 mozilla/grendel/TestFolderViewer.java create mode 100644 mozilla/grendel/addressbook/AddressBook.java create mode 100644 mozilla/grendel/addressbook/Makefile create mode 100644 mozilla/grendel/addressbook/Menus.properties create mode 100644 mozilla/grendel/addressbook/NewCardDialog.java create mode 100644 mozilla/grendel/addressbook/README create mode 100644 mozilla/grendel/addressbook/addresscard/ACS_Personal.java create mode 100644 mozilla/grendel/addressbook/addresscard/AC_Attribute.java create mode 100644 mozilla/grendel/addressbook/addresscard/AC_Exception.java create mode 100644 mozilla/grendel/addressbook/addresscard/AC_ID.java create mode 100644 mozilla/grendel/addressbook/addresscard/AC_IDSet.java create mode 100644 mozilla/grendel/addressbook/addresscard/IAttribute.java create mode 100644 mozilla/grendel/addressbook/addresscard/IAttributeSet.java create mode 100644 mozilla/grendel/addressbook/addresscard/ICard.java create mode 100644 mozilla/grendel/addressbook/addresscard/ICardSet.java create mode 100644 mozilla/grendel/addressbook/addresscard/ICardSource.java create mode 100644 mozilla/grendel/addressbook/addresscard/IQuerySet.java create mode 100644 mozilla/grendel/addressbook/addresscard/IQueryString.java create mode 100644 mozilla/grendel/addressbook/addresscard/ITerm.java create mode 100644 mozilla/grendel/addressbook/addresscard/LDAP_Attribute.java create mode 100644 mozilla/grendel/addressbook/addresscard/LDAP_AttributeSet.java create mode 100644 mozilla/grendel/addressbook/addresscard/LDAP_Card.java create mode 100644 mozilla/grendel/addressbook/addresscard/LDAP_CardSet.java create mode 100644 mozilla/grendel/addressbook/addresscard/LDAP_Server.java create mode 100644 mozilla/grendel/addressbook/addresscard/Makefile create mode 100644 mozilla/grendel/addressbook/addresscard/SelfTest.java create mode 100644 mozilla/grendel/addressbook/addresscard/TermAnd.java create mode 100644 mozilla/grendel/addressbook/addresscard/TermEqual.java create mode 100644 mozilla/grendel/addressbook/addresscard/TermNotEqual.java create mode 100644 mozilla/grendel/addressbook/addresscard/TermOr.java create mode 100644 mozilla/grendel/calypso/Makefile create mode 100644 mozilla/grendel/calypso/README create mode 100644 mozilla/grendel/calypso/util/Abacus.java create mode 100644 mozilla/grendel/calypso/util/ArrayEnumeration.java create mode 100644 mozilla/grendel/calypso/util/Assert.java create mode 100644 mozilla/grendel/calypso/util/AssertionError.java create mode 100644 mozilla/grendel/calypso/util/AssertionException.java create mode 100644 mozilla/grendel/calypso/util/Atom.java create mode 100644 mozilla/grendel/calypso/util/AtomHashtable.java create mode 100644 mozilla/grendel/calypso/util/AttributeValuePair.java create mode 100644 mozilla/grendel/calypso/util/ByteBuf.java create mode 100644 mozilla/grendel/calypso/util/ByteLineBuffer.java create mode 100644 mozilla/grendel/calypso/util/ByteToCharConverterEnumeration.java create mode 100644 mozilla/grendel/calypso/util/CacheOutputStream.java create mode 100644 mozilla/grendel/calypso/util/CharArray.java create mode 100644 mozilla/grendel/calypso/util/CharArrayIterator.java create mode 100644 mozilla/grendel/calypso/util/CharToByteConverterEnumeration.java create mode 100644 mozilla/grendel/calypso/util/ClasspathEntryEnumeration.java create mode 100644 mozilla/grendel/calypso/util/Comparer.java create mode 100644 mozilla/grendel/calypso/util/ConfigUtils.java create mode 100644 mozilla/grendel/calypso/util/Counter.java create mode 100644 mozilla/grendel/calypso/util/DataExtension.java create mode 100644 mozilla/grendel/calypso/util/DefaultPreferences.java create mode 100644 mozilla/grendel/calypso/util/EnumerationEnumerator.java create mode 100644 mozilla/grendel/calypso/util/HashtableBase.java create mode 100644 mozilla/grendel/calypso/util/HashtableLite.java create mode 100644 mozilla/grendel/calypso/util/HashtableRecycler.java create mode 100644 mozilla/grendel/calypso/util/IDMap.java create mode 100644 mozilla/grendel/calypso/util/LineBufferingInputStream.java create mode 100644 mozilla/grendel/calypso/util/ListenerList.java create mode 100644 mozilla/grendel/calypso/util/ListenerListState.java create mode 100644 mozilla/grendel/calypso/util/Makefile create mode 100644 mozilla/grendel/calypso/util/MemoryManager.java create mode 100644 mozilla/grendel/calypso/util/MemoryMonitor.java create mode 100644 mozilla/grendel/calypso/util/MemoryPressure.java create mode 100644 mozilla/grendel/calypso/util/NetworkDate.java create mode 100644 mozilla/grendel/calypso/util/NullEnumeration.java create mode 100644 mozilla/grendel/calypso/util/NullJavaEnumeration.java create mode 100644 mozilla/grendel/calypso/util/Preferences.java create mode 100644 mozilla/grendel/calypso/util/PreferencesBase.java create mode 100644 mozilla/grendel/calypso/util/PreferencesFactory.java create mode 100644 mozilla/grendel/calypso/util/PrefetchEnumeration.java create mode 100644 mozilla/grendel/calypso/util/QSort.java create mode 100644 mozilla/grendel/calypso/util/RWLock.java create mode 100644 mozilla/grendel/calypso/util/RandomAccessFileWithByteLines.java create mode 100644 mozilla/grendel/calypso/util/Recycler.java create mode 100644 mozilla/grendel/calypso/util/SelfTest.java create mode 100644 mozilla/grendel/calypso/util/SelfTestAtom.java create mode 100644 mozilla/grendel/calypso/util/SelfTestException.java create mode 100644 mozilla/grendel/calypso/util/SelfTestIDMap.java create mode 100644 mozilla/grendel/calypso/util/SelfTestRWLock.java create mode 100644 mozilla/grendel/calypso/util/SignedInteger.java create mode 100644 mozilla/grendel/calypso/util/SingleEnumeration.java create mode 100644 mozilla/grendel/calypso/util/StringBuf.java create mode 100644 mozilla/grendel/calypso/util/StringBufRecycler.java create mode 100644 mozilla/grendel/calypso/util/StringUtils.java create mode 100644 mozilla/grendel/calypso/util/TempFile.java create mode 100644 mozilla/grendel/calypso/util/TimeBomb.java create mode 100644 mozilla/grendel/calypso/util/URLClassLoader.java create mode 100644 mozilla/grendel/calypso/util/Vec.java create mode 100644 mozilla/grendel/calypso/util/VectorRecycler.java create mode 100644 mozilla/grendel/calypso/util/WeakLink.java create mode 100644 mozilla/grendel/calypso/util/WeakLinkArray.java create mode 100644 mozilla/grendel/calypso/util/WeakLinkArrayEnumeration.java create mode 100644 mozilla/grendel/composition/AddressBar.java create mode 100644 mozilla/grendel/composition/AddressDialog.java create mode 100644 mozilla/grendel/composition/AddressList.java create mode 100644 mozilla/grendel/composition/AddressListBeanInfo.java create mode 100644 mozilla/grendel/composition/Addressee.java create mode 100644 mozilla/grendel/composition/AttachmentsList.java create mode 100644 mozilla/grendel/composition/AttachmentsListBeanInfo.java create mode 100644 mozilla/grendel/composition/Composition.java create mode 100644 mozilla/grendel/composition/CompositionPanel.java create mode 100644 mozilla/grendel/composition/CompositionPanelListener.java create mode 100644 mozilla/grendel/composition/CompositionTest.java create mode 100644 mozilla/grendel/composition/Makefile create mode 100644 mozilla/grendel/composition/MenuLabels.properties create mode 100644 mozilla/grendel/composition/NSTabbedPane.java create mode 100644 mozilla/grendel/composition/OptionsPanel.java create mode 100644 mozilla/grendel/composition/README create mode 100644 mozilla/grendel/composition/TestDataSource2.java create mode 100644 mozilla/grendel/composition/contenthandlers create mode 100644 mozilla/grendel/composition/images/AttachmentsListIcon16.gif create mode 100644 mozilla/grendel/composition/images/AttachmentsListIcon32.gif create mode 100644 mozilla/grendel/composition/images/README create mode 100644 mozilla/grendel/composition/images/address-disabled.gif create mode 100644 mozilla/grendel/composition/images/address-pressed.gif create mode 100644 mozilla/grendel/composition/images/address-rollover.gif create mode 100644 mozilla/grendel/composition/images/address.gif create mode 100644 mozilla/grendel/composition/images/attach-disabled.gif create mode 100644 mozilla/grendel/composition/images/attach-pressed.gif create mode 100644 mozilla/grendel/composition/images/attach-rollover.gif create mode 100644 mozilla/grendel/composition/images/attach.gif create mode 100644 mozilla/grendel/composition/images/card.gif create mode 100644 mozilla/grendel/composition/images/logo.gif create mode 100644 mozilla/grendel/composition/images/quote-disabled.gif create mode 100644 mozilla/grendel/composition/images/quote-pressed.gif create mode 100644 mozilla/grendel/composition/images/quote-rollover.gif create mode 100644 mozilla/grendel/composition/images/quote.gif create mode 100644 mozilla/grendel/composition/images/save-disabled.gif create mode 100644 mozilla/grendel/composition/images/save-pressed.gif create mode 100644 mozilla/grendel/composition/images/save-rollover.gif create mode 100644 mozilla/grendel/composition/images/save.gif create mode 100644 mozilla/grendel/composition/images/security-disabled.gif create mode 100644 mozilla/grendel/composition/images/security-pressed.gif create mode 100644 mozilla/grendel/composition/images/security-rollover.gif create mode 100644 mozilla/grendel/composition/images/security.gif create mode 100644 mozilla/grendel/composition/images/send-disabled.gif create mode 100644 mozilla/grendel/composition/images/send-pressed.gif create mode 100644 mozilla/grendel/composition/images/send-rollover.gif create mode 100644 mozilla/grendel/composition/images/send.gif create mode 100644 mozilla/grendel/composition/images/small_address.gif create mode 100644 mozilla/grendel/composition/images/small_attachments.gif create mode 100644 mozilla/grendel/composition/images/small_otpions.gif create mode 100644 mozilla/grendel/composition/images/spelling-disabled.gif create mode 100644 mozilla/grendel/composition/images/spelling-pressed.gif create mode 100644 mozilla/grendel/composition/images/spelling-rollover.gif create mode 100644 mozilla/grendel/composition/images/spelling.gif create mode 100644 mozilla/grendel/composition/images/stop-disabled.gif create mode 100644 mozilla/grendel/composition/images/stop-pressed.gif create mode 100644 mozilla/grendel/composition/images/stop-rollover.gif create mode 100644 mozilla/grendel/composition/images/stop.gif create mode 100644 mozilla/grendel/composition/menus.xml create mode 100644 mozilla/grendel/dnd/DnDConstants.java create mode 100644 mozilla/grendel/dnd/DragSource.java create mode 100644 mozilla/grendel/dnd/DragSourceContext.java create mode 100644 mozilla/grendel/dnd/DragSourceDragEvent.java create mode 100644 mozilla/grendel/dnd/DragSourceDropEvent.java create mode 100644 mozilla/grendel/dnd/DragSourceListener.java create mode 100644 mozilla/grendel/dnd/DropTarget.java create mode 100644 mozilla/grendel/dnd/DropTargetComponent.java create mode 100644 mozilla/grendel/dnd/DropTargetContext.java create mode 100644 mozilla/grendel/dnd/DropTargetDragEvent.java create mode 100644 mozilla/grendel/dnd/DropTargetDropEvent.java create mode 100644 mozilla/grendel/dnd/DropTargetEvent.java create mode 100644 mozilla/grendel/dnd/DropTargetListener.java create mode 100644 mozilla/grendel/dnd/InvalidDnDOperationException.java create mode 100644 mozilla/grendel/dnd/Makefile create mode 100644 mozilla/grendel/filters/DeleteFilterActionFactory.java create mode 100644 mozilla/grendel/filters/FilterBase.java create mode 100644 mozilla/grendel/filters/FilterFactory.java create mode 100644 mozilla/grendel/filters/FilterMaster.java create mode 100644 mozilla/grendel/filters/FilterRulesParser.java create mode 100644 mozilla/grendel/filters/FilterSyntaxException.java create mode 100644 mozilla/grendel/filters/IFilter.java create mode 100644 mozilla/grendel/filters/IFilterAction.java create mode 100644 mozilla/grendel/filters/IFilterActionFactory.java create mode 100644 mozilla/grendel/filters/IFilterTermFactory.java create mode 100644 mozilla/grendel/filters/Makefile create mode 100644 mozilla/grendel/filters/MoveFilterActionFactory.java create mode 100644 mozilla/grendel/filters/README create mode 100644 mozilla/grendel/filters/SubjectTermFactory.java create mode 100644 mozilla/grendel/integrator/CompositionShell.java create mode 100644 mozilla/grendel/integrator/CompositionView.java create mode 100644 mozilla/grendel/integrator/FolderCtx.java create mode 100644 mozilla/grendel/integrator/FolderView.java create mode 100644 mozilla/grendel/integrator/Makefile create mode 100644 mozilla/grendel/integrator/MenuLabels.properties create mode 100644 mozilla/grendel/integrator/SessionView.java create mode 100644 mozilla/grendel/integrator/Shell.java create mode 100644 mozilla/grendel/integrator/StoreCtx.java create mode 100644 mozilla/grendel/integrator/menus.xml create mode 100644 mozilla/grendel/mime/HeaderCharsetDecoder.java create mode 100644 mozilla/grendel/mime/IMimeObject.java create mode 100644 mozilla/grendel/mime/IMimeOperator.java create mode 100644 mozilla/grendel/mime/IMimeParser.java create mode 100644 mozilla/grendel/mime/Makefile create mode 100644 mozilla/grendel/mime/README create mode 100644 mozilla/grendel/mime/TestMime.java create mode 100644 mozilla/grendel/mime/encoder/Makefile create mode 100644 mozilla/grendel/mime/encoder/MimeBase64Decoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeBase64Encoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeEncoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeQuotedPrintableDecoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeQuotedPrintableEncoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeUUDecoder.java create mode 100644 mozilla/grendel/mime/encoder/MimeUUEncoder.java create mode 100644 mozilla/grendel/mime/encoder/README create mode 100644 mozilla/grendel/mime/extractor/Makefile create mode 100644 mozilla/grendel/mime/extractor/MimeExtractorOperatorFactory.java create mode 100644 mozilla/grendel/mime/extractor/README create mode 100644 mozilla/grendel/mime/html/BriefHeaderFormatter.java create mode 100644 mozilla/grendel/mime/html/FullHeaderFormatter.java create mode 100644 mozilla/grendel/mime/html/HeaderFormatter.java create mode 100644 mozilla/grendel/mime/html/Makefile create mode 100644 mozilla/grendel/mime/html/MimeContainerOperator.java create mode 100644 mozilla/grendel/mime/html/MimeExternalBodyOperator.java create mode 100644 mozilla/grendel/mime/html/MimeExternalObjectOperator.java create mode 100644 mozilla/grendel/mime/html/MimeHTMLOperator.java create mode 100644 mozilla/grendel/mime/html/MimeHTMLOperatorFactory.java create mode 100644 mozilla/grendel/mime/html/MimeLeafOperator.java create mode 100644 mozilla/grendel/mime/html/MimeMessageOperator.java create mode 100644 mozilla/grendel/mime/html/MimeMultipartAlternativeOperator.java create mode 100644 mozilla/grendel/mime/html/MimeMultipartAppleDoubleOperator.java create mode 100644 mozilla/grendel/mime/html/MimeMultipartRelatedOperator.java create mode 100644 mozilla/grendel/mime/html/MimeMultipartSignedOperator.java create mode 100644 mozilla/grendel/mime/html/MimeTextEnrichedOperator.java create mode 100644 mozilla/grendel/mime/html/MimeTextHTMLOperator.java create mode 100644 mozilla/grendel/mime/html/MimeTextOperator.java create mode 100644 mozilla/grendel/mime/html/MimeTextRichtextOperator.java create mode 100644 mozilla/grendel/mime/html/MimeTextVCardOperator.java create mode 100644 mozilla/grendel/mime/html/NormalHeaderFormatter.java create mode 100644 mozilla/grendel/mime/html/README create mode 100644 mozilla/grendel/mime/html/TextHTMLConverter.java create mode 100644 mozilla/grendel/mime/parser/Makefile create mode 100644 mozilla/grendel/mime/parser/MimeContainer.java create mode 100644 mozilla/grendel/mime/parser/MimeDwimText.java create mode 100644 mozilla/grendel/mime/parser/MimeLeaf.java create mode 100644 mozilla/grendel/mime/parser/MimeMessageExternalBody.java create mode 100644 mozilla/grendel/mime/parser/MimeMessageRFC822.java create mode 100644 mozilla/grendel/mime/parser/MimeMultipart.java create mode 100644 mozilla/grendel/mime/parser/MimeMultipartDigest.java create mode 100644 mozilla/grendel/mime/parser/MimeObject.java create mode 100644 mozilla/grendel/mime/parser/MimeParserFactory.java create mode 100644 mozilla/grendel/mime/parser/MimeXSunAttachment.java create mode 100644 mozilla/grendel/mime/parser/README create mode 100644 mozilla/grendel/prefs/MailServerPrefs.java create mode 100644 mozilla/grendel/prefs/MailServerPrefsEditor.java create mode 100644 mozilla/grendel/prefs/Makefile create mode 100644 mozilla/grendel/prefs/PrefDialogs.xml create mode 100644 mozilla/grendel/prefs/PrefLabels.properties create mode 100644 mozilla/grendel/prefs/Prefs.java create mode 100644 mozilla/grendel/prefs/PrefsBeanInfo.java create mode 100644 mozilla/grendel/prefs/UIPrefs.java create mode 100644 mozilla/grendel/prefs/UIPrefsEditor.java create mode 100644 mozilla/grendel/prefs/UserPrefs.java create mode 100644 mozilla/grendel/prefs/UserPrefsEditor.java create mode 100644 mozilla/grendel/rules.mk create mode 100644 mozilla/grendel/search/AndPanel.java create mode 100644 mozilla/grendel/search/AttributeLayout.java create mode 100644 mozilla/grendel/search/AttributePanel.java create mode 100644 mozilla/grendel/search/ISearchAttribute.java create mode 100644 mozilla/grendel/search/ISearchResults.java create mode 100644 mozilla/grendel/search/ISearchable.java create mode 100644 mozilla/grendel/search/MailSearch.java create mode 100644 mozilla/grendel/search/Makefile create mode 100644 mozilla/grendel/search/NaryTermPanel.java create mode 100644 mozilla/grendel/search/NoopPanel.java create mode 100644 mozilla/grendel/search/OperatorLayout.java create mode 100644 mozilla/grendel/search/OrPanel.java create mode 100644 mozilla/grendel/search/ResultsFrame.java create mode 100644 mozilla/grendel/search/SearchFrame.java create mode 100644 mozilla/grendel/search/SearchPanel.java create mode 100644 mozilla/grendel/search/TermPanel.java create mode 100644 mozilla/grendel/storage/BerkeleyFolder.java create mode 100644 mozilla/grendel/storage/BerkeleyMessage.java create mode 100644 mozilla/grendel/storage/BerkeleyStore.java create mode 100644 mozilla/grendel/storage/ByteString.java create mode 100644 mozilla/grendel/storage/ByteStringTable.java create mode 100644 mozilla/grendel/storage/Constants.java create mode 100644 mozilla/grendel/storage/DotTerminatedInputStream.java create mode 100644 mozilla/grendel/storage/FilterFolder.java create mode 100644 mozilla/grendel/storage/FilterFolderFactory.java create mode 100644 mozilla/grendel/storage/FolderBase.java create mode 100644 mozilla/grendel/storage/FolderExtra.java create mode 100644 mozilla/grendel/storage/FolderExtraFactory.java create mode 100644 mozilla/grendel/storage/MailDrop.java create mode 100644 mozilla/grendel/storage/MailDropException.java create mode 100644 mozilla/grendel/storage/MailSummaryFile.java create mode 100644 mozilla/grendel/storage/MailSummaryFileCheddar.java create mode 100644 mozilla/grendel/storage/MailSummaryFileFactory.java create mode 100644 mozilla/grendel/storage/MailSummaryFileGrendel.java create mode 100644 mozilla/grendel/storage/Makefile create mode 100644 mozilla/grendel/storage/MasterBase.java create mode 100644 mozilla/grendel/storage/MessageBase.java create mode 100644 mozilla/grendel/storage/MessageExtra.java create mode 100644 mozilla/grendel/storage/MessageExtraFactory.java create mode 100644 mozilla/grendel/storage/MessageExtraWrapper.java create mode 100644 mozilla/grendel/storage/MessageID.java create mode 100644 mozilla/grendel/storage/MessageIDTable.java create mode 100644 mozilla/grendel/storage/MessageReadOnly.java create mode 100644 mozilla/grendel/storage/NNTPConnection.java create mode 100644 mozilla/grendel/storage/NNTPException.java create mode 100644 mozilla/grendel/storage/NewsFolder.java create mode 100644 mozilla/grendel/storage/NewsFolderRoot.java create mode 100644 mozilla/grendel/storage/NewsMessage.java create mode 100644 mozilla/grendel/storage/NewsRC.java create mode 100644 mozilla/grendel/storage/NewsRCLine.java create mode 100644 mozilla/grendel/storage/NewsSet.java create mode 100644 mozilla/grendel/storage/NewsStore.java create mode 100644 mozilla/grendel/storage/Obarray.java create mode 100644 mozilla/grendel/storage/PackedIntArray.java create mode 100644 mozilla/grendel/storage/ParseBerkeleyFolder.java create mode 100644 mozilla/grendel/storage/ParseBerkeleyFolderAndExpunge.java create mode 100644 mozilla/grendel/storage/ParseBerkeleyFolderWithSummary.java create mode 100644 mozilla/grendel/storage/PartialFileInputStream.java create mode 100644 mozilla/grendel/storage/PopFolder.java create mode 100644 mozilla/grendel/storage/PopMailDrop.java create mode 100644 mozilla/grendel/storage/PopMessage.java create mode 100644 mozilla/grendel/storage/PopStore.java create mode 100644 mozilla/grendel/storage/README create mode 100644 mozilla/grendel/storage/SearchResultsFolderFactory.java create mode 100644 mozilla/grendel/storage/SelfTest.java create mode 100644 mozilla/grendel/storage/TestBerkeley.java create mode 100644 mozilla/grendel/storage/UnixDotLock.java create mode 100644 mozilla/grendel/storage/addressparser/AddressCorrector.java create mode 100644 mozilla/grendel/storage/addressparser/AddressToken.java create mode 100644 mozilla/grendel/storage/addressparser/GroupToken.java create mode 100644 mozilla/grendel/storage/addressparser/MailAddressToken.java create mode 100644 mozilla/grendel/storage/addressparser/MailboxToken.java create mode 100644 mozilla/grendel/storage/addressparser/Makefile create mode 100644 mozilla/grendel/storage/addressparser/P1.java create mode 100644 mozilla/grendel/storage/addressparser/P2.java create mode 100644 mozilla/grendel/storage/addressparser/README create mode 100644 mozilla/grendel/storage/addressparser/RFC822AddressList.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822AddressParser.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822Mailbox.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822MailboxList.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822ParserException.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822Token.java create mode 100644 mozilla/grendel/storage/addressparser/RFC822Tokenizer.java create mode 100644 mozilla/grendel/storage/addressparser/RouteAddrNoPhraseException.java create mode 100644 mozilla/grendel/storage/folderstats.el create mode 100644 mozilla/grendel/storage/intertwingle/BGDB.java create mode 100644 mozilla/grendel/storage/intertwingle/BaseDB.java create mode 100644 mozilla/grendel/storage/intertwingle/DB.java create mode 100644 mozilla/grendel/storage/intertwingle/Dump.java create mode 100644 mozilla/grendel/storage/intertwingle/HackDB.java create mode 100644 mozilla/grendel/storage/intertwingle/Makefile create mode 100644 mozilla/grendel/storage/intertwingle/PhoneTest.java create mode 100644 mozilla/grendel/storage/intertwingle/README create mode 100644 mozilla/grendel/storage/intertwingle/SelfTest.java create mode 100644 mozilla/grendel/storage/intertwingle/SimpleDB.java create mode 100644 mozilla/grendel/storage/intertwingle/Twingle.java create mode 100644 mozilla/grendel/storage/selftestdata/Inbox create mode 100644 mozilla/grendel/ui/ActionFactory.java create mode 100644 mozilla/grendel/ui/BiffIcon.java create mode 100644 mozilla/grendel/ui/BiffThread.java create mode 100644 mozilla/grendel/ui/DialogAuthenticator.java create mode 100644 mozilla/grendel/ui/EditHostDialog.java create mode 100644 mozilla/grendel/ui/FolderCombo.java create mode 100644 mozilla/grendel/ui/FolderFrame.java create mode 100644 mozilla/grendel/ui/FolderListTransferable.java create mode 100644 mozilla/grendel/ui/FolderPanel.java create mode 100644 mozilla/grendel/ui/FolderPanelListener.java create mode 100644 mozilla/grendel/ui/GeneralDialog.java create mode 100644 mozilla/grendel/ui/GeneralFrame.java create mode 100644 mozilla/grendel/ui/GeneralPanel.java create mode 100644 mozilla/grendel/ui/Labels.properties create mode 100644 mozilla/grendel/ui/Makefile create mode 100644 mozilla/grendel/ui/MasterPanel.java create mode 100644 mozilla/grendel/ui/MasterPanelListener.java create mode 100644 mozilla/grendel/ui/MenuLabels.properties create mode 100644 mozilla/grendel/ui/Menus.properties create mode 100644 mozilla/grendel/ui/MessageCellRenderer.java create mode 100644 mozilla/grendel/ui/MessageDisplayManager.java create mode 100644 mozilla/grendel/ui/MessageFrame.java create mode 100644 mozilla/grendel/ui/MessageHeader.java create mode 100644 mozilla/grendel/ui/MessageListTransferable.java create mode 100644 mozilla/grendel/ui/MessageModel.java create mode 100644 mozilla/grendel/ui/MessagePanel.java create mode 100644 mozilla/grendel/ui/MessagePanelListener.java create mode 100644 mozilla/grendel/ui/MultiMessageDisplayManager.java create mode 100644 mozilla/grendel/ui/NewFolderDialog.java create mode 100644 mozilla/grendel/ui/PrefsDialog.java create mode 100644 mozilla/grendel/ui/ProgressFactory.java create mode 100644 mozilla/grendel/ui/ProgressFrame.java create mode 100644 mozilla/grendel/ui/README create mode 100644 mozilla/grendel/ui/SimpleAuthenticator.java create mode 100644 mozilla/grendel/ui/SimpleMessageHeader.java create mode 100644 mozilla/grendel/ui/StoreFactory.java create mode 100644 mozilla/grendel/ui/UIFactory.java create mode 100644 mozilla/grendel/ui/UnifiedMessageDisplayManager.java create mode 100644 mozilla/grendel/ui/Util.java create mode 100644 mozilla/grendel/ui/dialogs.xml create mode 100644 mozilla/grendel/ui/images/GrendelIcon16.gif create mode 100644 mozilla/grendel/ui/images/GrendelIcon32.gif create mode 100644 mozilla/grendel/ui/images/README create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge00.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge01.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge02.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge03.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge04.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge05.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge06.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge07.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge08.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge09.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge10.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge11.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge12.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge13.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge14.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge15.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge16.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge17.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge18.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge19.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge20.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge21.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge22.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge23.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge24.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge25.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge26.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge27.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge28.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge29.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge30.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge31.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge32.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge33.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge34.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge35.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge36.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge37.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge38.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge39.gif create mode 100644 mozilla/grendel/ui/images/animation/AnimHuge40.gif create mode 100644 mozilla/grendel/ui/images/animation/README create mode 100644 mozilla/grendel/ui/images/biffNew.gif create mode 100644 mozilla/grendel/ui/images/biffNone.gif create mode 100644 mozilla/grendel/ui/images/biffUnknown.gif create mode 100644 mozilla/grendel/ui/images/connected-small.gif create mode 100644 mozilla/grendel/ui/images/deleted.gif create mode 100644 mozilla/grendel/ui/images/flagged.gif create mode 100644 mozilla/grendel/ui/images/folder-small.gif create mode 100644 mozilla/grendel/ui/images/inbox-small.gif create mode 100644 mozilla/grendel/ui/images/msg-small.gif create mode 100644 mozilla/grendel/ui/images/msgRead-small.gif create mode 100644 mozilla/grendel/ui/images/newsgroup-small.gif create mode 100644 mozilla/grendel/ui/images/read.gif create mode 100644 mozilla/grendel/ui/images/splitLeft.gif create mode 100644 mozilla/grendel/ui/images/splitRight.gif create mode 100644 mozilla/grendel/ui/images/splitTop.gif create mode 100644 mozilla/grendel/ui/images/stacked.gif create mode 100644 mozilla/grendel/ui/images/storeLocal-small.gif create mode 100644 mozilla/grendel/ui/images/storeRemote-small.gif create mode 100644 mozilla/grendel/ui/images/toolbar/README create mode 100644 mozilla/grendel/ui/images/toolbar/markAllRead-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/markAllRead-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/markAllRead-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/markAllRead.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgDelete-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgDelete-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgDelete-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgDelete.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgForward-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgForward-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgForward-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgForward.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgGetNew-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgGetNew-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgGetNew-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgGetNew.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgNew-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgNew-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgNew-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgNew.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgReply-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgReply-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgReply-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/msgReply.gif create mode 100644 mozilla/grendel/ui/images/toolbar/print-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/print-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/print-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/print.gif create mode 100644 mozilla/grendel/ui/images/toolbar/stop-disabled.gif create mode 100644 mozilla/grendel/ui/images/toolbar/stop-pressed.gif create mode 100644 mozilla/grendel/ui/images/toolbar/stop-rollover.gif create mode 100644 mozilla/grendel/ui/images/toolbar/stop.gif create mode 100644 mozilla/grendel/ui/images/unflagged.gif create mode 100644 mozilla/grendel/ui/images/unread.gif create mode 100644 mozilla/grendel/ui/menus.xml create mode 100644 mozilla/grendel/util/Makefile create mode 100644 mozilla/grendel/util/MessageIDGenerator.java create mode 100644 mozilla/grendel/view/DummyThreadable.java create mode 100644 mozilla/grendel/view/FolderView.java create mode 100644 mozilla/grendel/view/FolderViewBase.java create mode 100644 mozilla/grendel/view/FolderViewFactory.java create mode 100644 mozilla/grendel/view/ISortable.java create mode 100644 mozilla/grendel/view/IThreadable.java create mode 100644 mozilla/grendel/view/Makefile create mode 100644 mozilla/grendel/view/MessageSetView.java create mode 100644 mozilla/grendel/view/MessageSetViewObserver.java create mode 100644 mozilla/grendel/view/Sorter.java create mode 100644 mozilla/grendel/view/TestThreader.java create mode 100644 mozilla/grendel/view/Threader.java create mode 100644 mozilla/grendel/view/ViewedFolder.java create mode 100644 mozilla/grendel/view/ViewedFolderBase.java create mode 100644 mozilla/grendel/view/ViewedMessage.java create mode 100644 mozilla/grendel/view/ViewedMessageBase.java create mode 100644 mozilla/grendel/view/ViewedStore.java create mode 100644 mozilla/grendel/view/ViewedStoreBase.java create mode 100644 mozilla/grendel/view/ViewedStoreEvent.java create mode 100644 mozilla/grendel/view/ViewedStoreListener.java create mode 100644 mozilla/grendel/widgets/Animation.java create mode 100644 mozilla/grendel/widgets/CellEditor.java create mode 100644 mozilla/grendel/widgets/CellRenderer.java create mode 100644 mozilla/grendel/widgets/Column.java create mode 100644 mozilla/grendel/widgets/ColumnChangeListener.java create mode 100644 mozilla/grendel/widgets/ColumnHeader.java create mode 100644 mozilla/grendel/widgets/ColumnModel.java create mode 100644 mozilla/grendel/widgets/ColumnModelEvent.java create mode 100644 mozilla/grendel/widgets/ColumnModelImp.java create mode 100644 mozilla/grendel/widgets/ColumnModelListener.java create mode 100644 mozilla/grendel/widgets/DefaultCellRenderer.java create mode 100644 mozilla/grendel/widgets/DefaultHeaderRenderer.java create mode 100644 mozilla/grendel/widgets/HeaderRenderer.java create mode 100644 mozilla/grendel/widgets/Makefile create mode 100644 mozilla/grendel/widgets/MultiSelectionManager.java create mode 100644 mozilla/grendel/widgets/SelectionEvent.java create mode 100644 mozilla/grendel/widgets/SelectionListener.java create mode 100644 mozilla/grendel/widgets/SelectionManager.java create mode 100644 mozilla/grendel/widgets/SingleSelectionManager.java create mode 100644 mozilla/grendel/widgets/Splitter.java create mode 100644 mozilla/grendel/widgets/StatusEvent.java create mode 100644 mozilla/grendel/widgets/TextCellEditor.java create mode 100644 mozilla/grendel/widgets/ToggleCellEditor.java create mode 100644 mozilla/grendel/widgets/ToggleCellRenderer.java create mode 100644 mozilla/grendel/widgets/TreePath.java create mode 100644 mozilla/grendel/widgets/TreeTable.java create mode 100644 mozilla/grendel/widgets/TreeTableDataModel.java create mode 100644 mozilla/grendel/widgets/TreeTableModelBroadcaster.java create mode 100644 mozilla/grendel/widgets/TreeTableModelEvent.java create mode 100644 mozilla/grendel/widgets/TreeTableModelListener.java create mode 100644 mozilla/grendel/widgets/Util.java create mode 100644 mozilla/grendel/widgets/images/README create mode 100644 mozilla/grendel/widgets/images/minus.gif create mode 100644 mozilla/grendel/widgets/images/plus.gif diff --git a/mozilla/grendel/Main.java b/mozilla/grendel/Main.java new file mode 100644 index 00000000000..7c70b12043f --- /dev/null +++ b/mozilla/grendel/Main.java @@ -0,0 +1,49 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import grendel.ui.MessageDisplayManager; +import grendel.ui.MultiMessageDisplayManager; +import grendel.ui.UnifiedMessageDisplayManager; + +/** + * This launches the Grendel GUI. + */ + +public class Main { + static MessageDisplayManager fManager; + + public static void main(String argv[]) { + Preferences prefs = PreferencesFactory.Get(); + String pref = prefs.getString("mail.layout", "multi_pane"); + + if (pref.equals("multi_pane")) { + fManager = new UnifiedMessageDisplayManager(); + } else { + fManager = new MultiMessageDisplayManager(); + } + MessageDisplayManager.SetDefaultManager(fManager); + fManager.displayMaster(); + } +} + diff --git a/mozilla/grendel/Makefile b/mozilla/grendel/Makefile new file mode 100644 index 00000000000..11006fcc08a --- /dev/null +++ b/mozilla/grendel/Makefile @@ -0,0 +1,55 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SUBDIRS= \ + addressbook \ + calypso \ + composition \ + dnd \ + filters \ + integrator \ + mime \ + prefs \ + search \ + storage \ + ui \ + util \ + view \ + widgets \ + $(NULL) + +SRCS= \ + Main.java \ + TestFolderViewer.java \ + $(NULL) + +# SelfTest.java \ + + +include rules.mk + +TARFILE=/tmp/grendel.tar.gz +tar:: + @echo writing $(TARFILE)... ; \ + dir=`pwd | sed 's@^.*/\([^/]*\)$$@\1@'` ; \ + cd .. ; tar -vcf - $$dir \ + --exclude '*.class' \ + --exclude '*.bak' \ + --exclude '*~' \ + --exclude 'core' \ + | gzip -v9 > $(TARFILE) diff --git a/mozilla/grendel/README b/mozilla/grendel/README new file mode 100644 index 00000000000..fc30f4e1800 --- /dev/null +++ b/mozilla/grendel/README @@ -0,0 +1,5 @@ +This is Grendel -- a Java mail/news client. + +It is incomplete, and currently has no active maintainer. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/SelfTest.java b/mozilla/grendel/SelfTest.java new file mode 100644 index 00000000000..dfe4573b323 --- /dev/null +++ b/mozilla/grendel/SelfTest.java @@ -0,0 +1,245 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 3 Dec 1997. + */ + +package grendel; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.PrintWriter; + +import java.text.DateFormat; +import java.util.Date; +import java.util.Properties; + +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; + +import selftest.SelfTestRoot; + +/** + * This is the base grendel SelfTest. Running this classes main() will + * execute the main() of all the SelfTest classes within the Grendel + * project. + * + *

+ * + * This class also contains useful stuff for all of the Grendel SelfTest + * classes to use. It is expected that all of those classes will inherit + * from this one. + */ + +public class SelfTest extends SelfTestRoot { + /** The Properties instance that all the javamail stuff will use. We try not + to use calypso.util.Preferences during SelfTest, because there's no real + way to control the values to be found there. Typically, your SelfTest + code will stuff values into this Properties database so that the Store + you're using will pull those values out. */ + static protected Properties props; + + /** A stupid authenticator that we use to stuff in name/password info into + our tests. */ + static private StupidAuthenticator authenticator; + + /** The javax.mail.Session object. This is created for you by startTests. */ + static protected Session session; + + /** The directory where you can store temporary stuff. If you want to use + this, be sure to call makePlayDir() at the beginning of your test; that + will ensure that the directory exists and is empty. */ + static protected File playdir = new File("selftestdir"); + + + /** Run all the grendel selftests. Every package within grendel ought to + add itself here. */ + public static void main(String args[]) { + grendel.storage.SelfTest.main(args); + } + + + /** Initialize things. If a subclass overrides this method, it must call + this superclass's method (via super.startTests()) + The framework requires args to be passed down to SelfTestRoot.startTests.*/ + public void startTests(String args[]) { + super.startTests(args); + + // ###HACKHACKHACK Remove me when javamail fixes their stuff. + java.io.File mailcapfile = new java.io.File("mailcap"); + if (!mailcapfile.exists()) { + try { + (new java.io.RandomAccessFile(mailcapfile, "rw")).close(); + writeMessage(null, "setup", "*** Created empty mailcap file in current"); + writeMessage(null, "setup", "*** directory (to work around buggy javamail"); + writeMessage(null, "setup", "*** software from JavaSoft)."); + } catch (java.io.IOException e) { + writeMessage(null, "setup", "*** Couldn't create empty mailcap file: " + e); + writeMessage(null, "setup", "*** Immanent crash is likely due to buggy"); + writeMessage(null, "setup", "*** javamail software from JavaSoft."); + } + } + + + if (session == null) { + props = new Properties(); + authenticator = new StupidAuthenticator(); + session = Session.getDefaultInstance(props, authenticator); + } + } + + + /** Clean up at the end. If a subclass overrides this method, it must call + this superclass's method (via super.endTests()) */ + + public void endTests() { + cleanPlayDirectory(); + super.endTests(); + } + + /** Stuff in the name and password we want to be used for the next test. */ + public void setUserAndPassword(String user, String password) { + authenticator.set(user, password); + } + + static private boolean firsttime = true; + + /** Creates an empty directory for your test to put stuff into. If a + previously running test made one, it gets blown away. +

+ The very first time this is called, we make sure that the directory doesn't + already exist. We want to make sure not to blow away something that was + already sitting on disk that doesn't belong to us. */ + public void makePlayDir() { + if (firsttime) { + if (playdir.exists()) { + throw new + Error("A directory or file named selftestdir already exists. " + + "It must be moved or deleted before SelfTest can be run."); + } + } + firsttime = false; + cleanPlayDirectory(); + if (!playdir.mkdirs()) { + throw new + Error("Couldn't create a directory named selftestdir, so " + + "SelfTest can't be run."); + } + } + + /** Recursively cleans out the contents of the given directory. Potentially + very dangerous! */ + public void cleanDirectory(File dir) { + String [] list = dir.list(); + for (int i=0 ; i 0) { + out.write(copybuf, 0, length); + } + in.close(); + out.close(); + } catch (IOException e) { + throw new Error("IOException " + e + " while installing file " + filename); + } + } + + + /** Given a long that represents a time, returns a String representation of + it suitable for putting in a log message. */ + public String prettyTime(long time) { + return + DateFormat.getDateTimeInstance(DateFormat.FULL, + DateFormat.FULL).format(new Date(time)) + + " (" + time + ")"; + } + + + /** Report a bug that we already know about and that has an outstanding bug + report sitting in the bug database. */ + + public void writeKnownBug(Object o, String methodName, int bugnum, + String message) + { + // Should this be a warning or fatal? I dunno! + writeWarning(o, methodName, "Known bug " + bugnum + ": " + message); + } +} + + +class StupidAuthenticator extends Authenticator { + String user; + String password; + + StupidAuthenticator() {} + + void set(String u, String p) { + user = u; + password = p; + } + + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(user, password); + } +} diff --git a/mozilla/grendel/TestFolderViewer.java b/mozilla/grendel/TestFolderViewer.java new file mode 100644 index 00000000000..41717b59d94 --- /dev/null +++ b/mozilla/grendel/TestFolderViewer.java @@ -0,0 +1,279 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 7-Sep-98. +*/ + +package grendel; + +import java.io.File; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.io.InputStream; +import java.io.DataInputStream; +import java.io.InputStreamReader; + +import java.util.Properties; +import java.util.Hashtable; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Folder; +import javax.mail.Session; + +import java.util.Vector; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Date; + +import calypso.util.ByteBuf; +import calypso.util.ArrayEnumeration; +import calypso.util.QSort; +import calypso.util.Comparer; +import calypso.util.NetworkDate; + +import grendel.storage.BerkeleyStore; +import grendel.storage.MessageExtra; + +import grendel.view.FolderView; +import grendel.view.FolderViewFactory; +import grendel.view.MessageSetView; +import grendel.view.ViewedMessage; + + +class TestFolderViewer { + + public static void main(String args[]) + throws IOException, MessagingException { + if (args.length != 1) { + System.out.println("usage: TestFolderViewer \n"); + return; + } + + // First, split the directory and file name, and store the directory + // in the "mail.directory" property (since JavaMail expects that.) + // + File pathname = new File(new File(args[0]).getAbsolutePath()); + String dir = pathname.getParent(); + String name = pathname.getName(); + + Properties props = new Properties(); + props.put("mail.directory", dir); + + // Make the global "session" and "store" object. + // + Session session = Session.getDefaultInstance(props, null); + BerkeleyStore store = new BerkeleyStore(session); + + // Parse the folder. This will use a .summary file if one exists and is + // up to date; otherwise, it will parse the whole folder. The `list' + // array will hold Message (really, BerkeleyMessage) objects representing + // each of the messages in the folder. Message objects are lightweight: + // really they are just info like sender/subject/offset-in-file, etc. + // + System.out.println("Folder " + new File(dir, name)); + Folder folder = store.getDefaultFolder().getFolder(name); + Message list[] = folder.getMessages(); + System.out.println("Found " + list.length + " messages."); + + // Print out a summary of the contents of the folder. + threadAndPrint(folder, MessageSetView.DATE, true); + + // Interact with the user. + mainLoop(folder); + } + + + // Gag -- for some reason our Message objects don't have getMessageNumber() + // set in them (see FolderBase.noticeInitialMessage()) so until this is + // fixed, let's kludge around it by keeping track of the index of the + // message in its folder externally, in this hash table. This is totally + // the wrong thing, but for now, it's expedient. + // + static Hashtable msgnum_kludge; + + static void threadAndPrint(Folder folder, int sort_type, boolean thread_p) { + FolderView view = FolderViewFactory.Make(folder); + int order[] = { sort_type, MessageSetView.NUMBER }; + + // See "gag" comment above. + // Populate the msgnum_kludge with message -> folder-index numbers. + msgnum_kludge = new Hashtable(); + try { + for (int i = 0; i < folder.getMessageCount(); i++) + msgnum_kludge.put(folder.getMessage(i+1), new Integer(i+1)); + } catch (MessagingException e) { + System.out.println("Error: " + e); + } + + System.out.println("Sorting by " + + (sort_type == MessageSetView.NUMBER ? "number" : + sort_type == MessageSetView.DATE ? "date" : + sort_type == MessageSetView.SUBJECT ? "subject" : + sort_type == MessageSetView.AUTHOR ? "author" : + sort_type == MessageSetView.READ ? "read" : + sort_type == MessageSetView.FLAGGED ? "flagged" : + sort_type == MessageSetView.SIZE ? "size" : + sort_type == MessageSetView.DELETED ? "deleted" :"?") + + "."); + + System.out.println(thread_p ? "Threading." : "Not threading."); + + // Tell the FolderView how to sort/thread, and then do it. + // + view.setSortOrder(order); + view.setIsThreaded(thread_p); + view.reThread(); + + // Now show the result. + printThread(view.getMessageRoot(), 0); + } + + + static void printThread(ViewedMessage vm, int depth) { + Message msg = (Message) vm.getMessage(); + String str = ""; + + for (int i = 0; i < depth; i++) + str += " "; + + if (msg == null) { + // A ViewedMessage with no Message inside is a dummy container, holding + // a thread together (for example, when the parent message of two + // siblings is not present in the folder (expired or deleted.)) + // + str += " [dummy]"; + + } else { + + // Construct a string describing this message. + // + String a = "", s = "", n = ""; + Date d = null; + + try { + a = ((MessageExtra) msg).getAuthor(); + } catch (MessagingException e) { + } + + try { + s = msg.getSubject(); + } catch (MessagingException e) { + } + + // See "gag" comment, above. + int ni = ((Integer) msgnum_kludge.get(msg)).intValue() - 1; +// int ni = msg.getMessageNumber(); + + n = "" + ni; + if (ni < 10) n += " "; + else if (ni < 100) n += " "; + else if (ni < 1000) n += " "; + else if (ni < 10000) n += " "; + + str = n + str; + + int L = str.length(); + + if (a.length() > 23-L) a = a.substring(0, 23-L); + if (s.length() > 23) s = s.substring(0, 23); + + str += a; + for (int i = L+a.length(); i < 25; i++) + str += " "; + + str += s; + for (int i = s.length(); i < 25; i++) + str += " "; + + try { + d = msg.getSentDate(); + } catch (MessagingException e) { + } + if (d != null && d.getTime() != 0) + str += d; + else + str += "date unknown"; + } + + // Print the string describing this message. + System.out.println(str); + + // If this message has children, print them now (indented.) + // After printing this message's children/grandchildren, + // move on and print the next message in the list. (Note + // that we're walking the list by recursing, but that's + // probably ok, as its tail-recursion.) + // + ViewedMessage next = vm.getNext(); + ViewedMessage kid = vm.getChild(); + if (kid != null) printThread(kid, depth+1); + if (next != null) printThread(next, depth); + } + + + static void mainLoop(Folder f) throws IOException { + + DataInputStream in = new DataInputStream(System.in); + + while (true) { + + // Read a line from the user; parse an integer from it; and dump + // the selected message to stdout. Then repeat. + // + System.out.print("\nDisplay which message: "); + String s = in.readLine(); + try { + int n = Integer.parseInt(s, 10); + System.out.println("Displaying message " + n + "."); + + try { + Message m = f.getMessage(n+1); + + try { + InputStream stream = + ((MessageExtra)m).getInputStreamWithHeaders(); +// if (makeRealHTML) { +// stream = new MakeItHTML(stream).getHTMLInputStream(); +// } + InputStreamReader reader = new InputStreamReader(stream); + char buff[] = new char[4096]; + int count; + System.out.println("\n-----------"); + while ((count = reader.read(buff, 0, 4096)) != -1) { + System.out.println(new String(buff, 0, count)); + } + System.out.println("\n-----------"); + } catch (MessagingException e) { + System.out.println("Error: " + e); + } catch (IOException e) { + System.out.println("Error: " + e); + } + + } catch (MessagingException e) { + System.out.println("Error: " + e); + } + + } catch (NumberFormatException e) { + System.out.println("That's not a number."); + } + } + } + + +} diff --git a/mozilla/grendel/addressbook/AddressBook.java b/mozilla/grendel/addressbook/AddressBook.java new file mode 100644 index 00000000000..46246ef6b89 --- /dev/null +++ b/mozilla/grendel/addressbook/AddressBook.java @@ -0,0 +1,692 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.addressbook; + +import grendel.addressbook.AddressCard.*; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; + +import com.sun.java.swing.*; +import com.sun.java.swing.text.*; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.table.*; +//import com.sun.java.swing.table.DefaultTableModel; +import com.sun.java.swing.event.TableModelEvent; +import com.sun.java.swing.border.EmptyBorder; + +import netscape.ldap.*; +import netscape.orion.toolbars.*; +import netscape.orion.menus.NsMenuManager; + +/** + * + * @author Lester Schueler + */ +public class AddressBook extends JFrame { + private Hashtable mCommands; + private Hashtable mMenuItems; + + private JMenuBar mMenubar; + private NSToolbar mTtoolbar; +// private Component mStatusbar; + private JTable mTable; + private JButton mSearchButton; + protected DataSourceList mDataSourceList; + protected JComboBox mSearchSource; + protected JTextField mSearchField; + + public static void main(String[] args) { + AddressBook AddressBookFrame = new AddressBook(); + AddressBookFrame.addWindowListener(new AppCloser()); + AddressBookFrame.show(); + } + + protected static final class AppCloser extends WindowAdapter { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + } + + //*************************** + class DataSource { + private String mReadableName; + private String mDomainName; + private int mPort; + + DataSource (String aReadableName, String aDomainName) { + this (aReadableName, aDomainName, 389); + } + + DataSource (String aReadableName, String aDomainName, int aPort) { + mReadableName = aReadableName;; + mDomainName = aDomainName; + mPort = aPort; + } + + public String getReadableName () { return mReadableName; } + public String getDomainName () { return mDomainName; } + public int getPort () { return mPort; } + } + + //*************************** + class DataSourceList { + private Vector mDataSources; + + DataSourceList () { + mDataSources = new Vector (); + } + + public Enumeration getEnumeration () { return mDataSources.elements(); } + public void addEntry (DataSource aDataSource) { mDataSources.addElement (aDataSource); } + + public DataSource find (String aReadableName) { + for (Enumeration e = mDataSources.elements() ; e.hasMoreElements() ;) { + DataSource ds = (DataSource) e.nextElement(); + + if (ds.getReadableName ().equalsIgnoreCase(aReadableName)) + return ds; + } + + return null; + } + } + + /** + * + */ + public AddressBook() { + super("Address Book"); + + setBackground(Color.lightGray); + //setBorderStyle(JPanel.ETCHED); + setLayout(new BorderLayout()); + +// addWindowListener(new FrameHider()); + + //create menubar (top) + //merge both the editors commands with this applications commands. + mMenubar = NsMenuManager.createMenuBar("grendel.addressbook.Menus", "grendel.addressbook.MenuLabels", "mainMenubar", defaultActions); + + //collapsble panels holds toolbar. + CollapsibleToolbarPanel collapsePanel = new CollapsibleToolbarPanel(this); + collapsePanel.setBorder (new EmptyBorder(5,5,5,5)); + + //toolbar buttons + mTtoolbar = createToolbar(); + + //collapsible item + collapsePanel.add(mTtoolbar); + + //create status bar (bottom) +// mStatusbar = createStatusbar(); + + JPanel panel1 = new JPanel(); + panel1.setLayout(new BorderLayout()); + panel1.add(collapsePanel, BorderLayout.NORTH); + + //hack togetther the data sources. + mDataSourceList = new DataSourceList (); + mDataSourceList.addEntry (new DataSource ("Four11 Directory", "ldap.four11.com")); + mDataSourceList.addEntry (new DataSource ("InfoSpace Directory", "ldap.infospace.com")); + mDataSourceList.addEntry (new DataSource ("WhoWhere Directory", "ldap.whowhere.com")); + + //Create address panel + AddressPanel addressPanel = new AddressPanel (mDataSourceList); + panel1.add(addressPanel, BorderLayout.CENTER); + + add(mMenubar, BorderLayout.NORTH); + add(panel1, BorderLayout.CENTER); + + setSize (600, 400); + } + + /** + * Hide this frame. + */ + protected void hideThisFrame () { + setVisible(false); + } + + /** + * Handles windowClosing for window listener. + */ + protected class FrameHider extends WindowAdapter { + public void windowClosing(WindowEvent e) { + hideThisFrame(); + } + } + + /** + * Find the hosting frame, for the file-chooser dialog. + */ + protected Frame getParentFrame() { +// for (Container p = getParent(); p != null; p = p.getParent()) { +// if (p instanceof Frame) { +// return (Frame) p; +// } +// } + return this; + } + + //********************** + //********************** + // menus + private JMenuBar menubar; + + //"File" actions + public static final String newCardTag ="newCard"; + public static final String newListTag ="newList"; + public static final String importTag ="import"; + public static final String saveAsTag ="saveAs"; + public static final String callTag ="call"; + public static final String closeWindowTag ="closeWindow"; + + // "file->new" actions + public static final String navigatorWindowTag ="navigatorWindow"; + public static final String messageTag ="message"; + public static final String blankPageTag ="blankPage"; + public static final String pageFromTemplateTag ="pageFromTemplate"; + public static final String pageFromWizardTag ="pageFromWizard"; + + //"Edit" actions + public static final String undoTag ="undo"; + public static final String redoTag ="redo"; + public static final String deleteTag ="delete"; + public static final String searchDirectoryTag ="searchDirectory"; + public static final String HTMLDomainsTag ="HTMLDomains"; + public static final String cardPropertiesTag ="cardProperties"; + public static final String preferencesTag ="preferences"; + + //"View" actions + public static final String hideMessageToolbarTag ="hideMessageToolbar"; + public static final String byTypeTag ="byType"; + public static final String byNameTag ="byName"; + public static final String byEmailAddressTag ="byEmailAddress"; + public static final String byComapanyTag ="byComapany"; + public static final String byCityTag ="byCity"; + public static final String byNicknameTag ="byNickname"; + public static final String sortAscendingTag ="sortAscending"; + public static final String sortDescendingTag ="sortDescending"; + public static final String myAddressBookCardTag ="myAddressBookCard"; + + // --- action implementations ----------------------------------- + private Action[] defaultActions = { + //"File" actions + new NewCard(), +// new NewList(), +// new Import(), + new SaveAs(), +// new Call(), + new CloseWindow() + + // "file->new" actions +// new NavigatorWindow(), +// new Message(), +// new BlankPage(), +// new PageFromTemplate(), +// new PageFromWizard(), + + //"Edit" actions +// new Undo(), +// new Redo(), +// new Delete(), +// new SearchDirectory(), +// new HTMLDomains(), +// new CardProperties(), +// new Preferences(), + + //"View" actions +// new HideMessageToolbar(), +// new ByType(), +// new ByName(), +// new ByEmailAddress(), +// new ByCompany(), +// new ByCity(), +// new ByNickname(), +// new SortAscending(), +// new SortDescending(), +// new MyAddressBookCard(), +// new WrapLongLines() + }; + + //----------------------- + //"File" actions + //----------------------- + /** + */ + class NewCard extends AbstractAction { + NewCard() { + super(newCardTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + NewCardDialog aDialog = new NewCardDialog(getParentFrame()); + + //display the new card dialog + aDialog.show (); + aDialog.dispose(); + } + } + + class SaveAs extends AbstractAction { + SaveAs() { + super(saveAsTag); + setEnabled(true); + } + public void actionPerformed(ActionEvent ae) { + NewCardDialog aDialog = new NewCardDialog(getParentFrame()); + + //display the new card dialog + aDialog.show (); + aDialog.dispose(); + } + } + + class CloseWindow extends AbstractAction { + CloseWindow() { + super(closeWindowTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + //hide after send. + hideThisFrame(); + } + } + + //----------------------- + // "file->new" actions + //----------------------- + + //----------------------- + //"Edit" actions + //----------------------- + class Undo extends AbstractAction { + Undo() { + super(undoTag); + setEnabled(true); + } + public void actionPerformed(ActionEvent e) {} + } + + //----------------------- + //"View" actions + //----------------------- + class HideMessageToolbar extends AbstractAction { + HideMessageToolbar() { + super(hideMessageToolbarTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) {} + } + + //----------------------- + //----------------------- + class Search extends AbstractAction { + Search() { + super(newListTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + String readableName = (String) mSearchSource.getSelectedItem(); + int index = mSearchSource.getSelectedIndex(); + + DataSource ds = mDataSourceList.find (readableName); + if (null != ds) { + + //get the text to search for. + String textToSearchFor = mSearchField.getText(); + + if (!textToSearchFor.trim().equals ("")) { + DataModel dm = (DataModel) mTable.getModel (); + dm.reloadData (ds.getDomainName(), ds.getPort(), textToSearchFor); + + //repaint the table with results. + mTable.repaint(); + } + } + } + } + + /** + * Create a Toolbar + * @see addToolbarButton + */ + private NSToolbar createToolbar() { + + NSToolbar toolBar = new NSToolbar(); + addToolbarButton(toolBar, null, "images/newcard.gif", "Create a new card"); + addToolbarButton(toolBar, null, "images/newlist.gif", "Create a new list"); + addToolbarButton(toolBar, null, "images/properties.gif", "Edit the selected card"); + addToolbarButton(toolBar, null, "images/newmsg.gif", "New Message (Ctrl+M)"); + addToolbarButton(toolBar, null, "images/directory.gif", "Look up an address"); + addToolbarButton(toolBar, null, "images/call.gif", "Start Netscape Conference"); + addToolbarButton(toolBar, null, "images/delete.gif", "Delete selected cards "); + + return toolBar; + } + + /** + * create a toolbar button + * @param aToolBar The parent toolbar to add this button to. + * @param aActionListener Who you want to be notified when the button is pressed. + * @param aImageName The image name for the button. like "save.gif" + * @param aToolTip The buttons tool tip. like "Save the current file". + * @see createToolbar + */ + public void addToolbarButton(NSToolbar aToolBar, AbstractAction aActionListener, String aImageName, String aToolTip) { + NSButton b = new NSButton(); + + b.setHorizontalTextPosition(JButton.CENTER); + b.setVerticalTextPosition(JButton.BOTTOM); + b.setToolTipText(aToolTip); + +// URL iconUrl = getClass().getResource("images/" + gifName + ".gif"); + b.setIcon(new ImageIcon(getClass().getResource(aImageName))); + +// iconUrl = getClass().getResource("images/" + gifName + "-disabled.gif"); +// button.setDisabledIcon(ImageIcon.createImageIcon(iconUrl)); + +// iconUrl = getClass().getResource("images/" + gifName + "-depressed.gif"); +// button.setPressedIcon(ImageIcon.createImageIcon(iconUrl)); + +// iconUrl = getClass().getResource("images/" + gifName + "-rollover.gif"); +// button.setRolloverIcon(ImageIcon.createImageIcon(iconUrl)); + + +// JButton b = new JButton(new ImageIcon(aImageName)); +// b.setToolTipText(aToolTip); +// b.setPad(new Insets(3,3,3,3)); +// b.addActionListener(aActionListener); + + aToolBar.addItem(b); + } + + private String getFirstEnum (Enumeration enumVals) { + if (enumVals.hasMoreElements()) + return (String) enumVals.nextElement(); + + return null; + } + + public Vector queryLDAP (String aServerName, int aPort, String aSearchString) { + + Vector retVecVec = new Vector(); //return vector of vectors. + +// try { + //open a connection to the LDAP server +System.out.println ("Opening server"); + ICardSource Four11AddressBook = new LDAP_Server (aServerName); + + //create the query + ITerm query = new TermEqual (new AC_Attribute ("sn", aSearchString)); + + String[] attributes = {"sn", "cn", "o", "mail", "city"}; + + //query the LDAP server. +System.out.println ("Send query"); + ICardSet cardSet = Four11AddressBook.getCardSet (query, attributes); + + //Sort the list. +// String[] sortOrder = {"givenname", "surname"}; +// cardSet.sort (sortOrder); + + //hack. I've put the for loop in a try block to catch the exception + //thrown when cardEnum.hasMoreElements() incorrectly returns true. + try { + //enumerate thru the cards. + for (Enumeration cardEnum = cardSet.getEnumeration(); cardEnum.hasMoreElements(); ) { +System.out.println ("got card"); + ICard card = (ICard) cardEnum.nextElement(); //get the addres card + IAttributeSet attrSet = card.getAttributeSet (); //get the attributes for this card + Vector thisRow = new Vector(6); //create a simple vector to hold the attributes values for this card. + + String commonName = ""; + String organization = ""; + String mail = ""; + String phone = ""; + String city = ""; + String nickName = ""; + + // enumerate thru the card attributes. + for (Enumeration attEnum = attrSet.getEnumeration(); attEnum.hasMoreElements(); ) { + IAttribute attr = (IAttribute) attEnum.nextElement(); + String attrName = attr.getName(); + + if (attrName.equals ("cn")) { + commonName = attr.getValue(); + } + + else if (attrName.equals ("o")) { + organization = attr.getValue(); + } + + else if (attrName.equals ("mail")) { + mail = attr.getValue(); + } + } + + //create this row for the table. + thisRow.addElement (commonName); + thisRow.addElement (mail); + thisRow.addElement (organization); + thisRow.addElement (phone); + thisRow.addElement (city); + thisRow.addElement (nickName); + + //add this row to the table + retVecVec.addElement (thisRow); + } + } + catch (Exception e) { + } +// } +// catch( LDAPException e ) { +// System.out.println( "Error: " + e.toString() ); +// } + +System.out.println ("Done."); + return retVecVec; + } + + public class DataModel extends AbstractTableModel { + private Vector mVecVec; + private String[] mColumnNames; + + public DataModel (String[] aNames) { + super(); + mVecVec = new Vector (); + mColumnNames = aNames; + } + + public int getRowCount() { return mVecVec.size(); } + public int getColumnCount () { return mColumnNames.length; } + public String getColumnName(int column) { return mColumnNames[column]; } + public Class getColumnClass(int col) { return String.class; } + public boolean isCellEditable(int row, int col) { return false;} + + public void setValueAt(Object aValue, int row, int column) { + ((Vector)mVecVec.elementAt(row)).setElementAt(aValue, column); + } + + public void reloadData (String aServerName, int aPort, String aSearchString) { + //reload the data from LDAP. + mVecVec = queryLDAP (aServerName, aPort, aSearchString); + } + + public Object getValueAt(int row, int column) { + return (((Vector)mVecVec.elementAt(row)).elementAt(column)); + } + } + + /** + * Create a status bar + * @return + * @see + */ +// protected Component createStatusbar() { +// // need to do something reasonable here +// StatusBar status = new StatusBar(); +// return status; +// } + + //*************************** + /** + * + */ + class StatusBar extends JPanel { + public StatusBar() { + super(); + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + } + + public void paint(Graphics g) { + super.paint(g); + } + } + + //*************************** + /** + */ + class AddressPanel extends JPanel { + + public AddressPanel(DataSourceList aDataSourceList) { + //super(true); + + setBorder (new EmptyBorder(10,10,10,10)); + setLayout (new BorderLayout(10, 5)); + + add(createSearchPane(aDataSourceList), BorderLayout.NORTH); + add(createTable(), BorderLayout.CENTER); + } + + private Box createSearchPane (DataSourceList aDataSourceList) { + //explaination + JLabel explaination = new JLabel ("Type in the name you're looking for:"); + explaination.setAlignmentX((float)0.0); //align left + + //text field + mSearchField = new JTextField (20); + mSearchField.setAlignmentX((float)0.0); //align left + + //box for explain and text field + Box innerBoxPane = new Box (BoxLayout.Y_AXIS); +// innerBoxPane.setAlignmentY((float)0.0); //align to bottom + innerBoxPane.add (explaination); + innerBoxPane.add (mSearchField); + + //drop down combo box + mSearchSource = new JComboBox(); + mSearchSource.setAlignmentY((float)0.0); //align to bottom + for (Enumeration e = aDataSourceList.getEnumeration() ; e.hasMoreElements() ;) { + DataSource ds = (DataSource) e.nextElement(); + mSearchSource.addItem(ds.getReadableName()); + } + + //label + JLabel lbl = new JLabel ("in:"); + lbl.setAlignmentY((float)0.0); //align to bottom + + //search button + mSearchButton = new JButton ("Search"); + mSearchButton.addActionListener(new Search()); + mSearchButton.setAlignmentY((float)0.0); //align to bottom + + Dimension spacer = new Dimension (10, 10); + + //assemble all the pieces together. + Box boxPane = new Box (BoxLayout.X_AXIS); + + boxPane.add (innerBoxPane); //explaination and text field + boxPane.add (Box.createRigidArea(spacer)); //spacer + boxPane.add (lbl); //"in:" label + boxPane.add (Box.createRigidArea(spacer)); //spacer + boxPane.add (mSearchSource); //drop down combo box + boxPane.add (Box.createRigidArea(spacer)); //spacer + boxPane.add (mSearchButton); //search buttton + + return boxPane; + } + + private JScrollPane createTable () { + String[] columnNames = { + "Name", + "Email Address", + "Organization", + "Phone", + "City", + "Nickname"}; + + //create the data model. + DataModel dm = new DataModel (columnNames); + + //create the table. + mTable = new JTable(dm); + +// mTable.setAutoCreateColumnsFromModel(false); + + // Add our columns into the column model +// for (int columnIndex = 0; columnIndex < columnNames.length; columnIndex++){ + // Create a column object for each column of data +// TableColumn newColumn = new TableColumn(columnNames[columnIndex]); + + // Set a tool tip for the column header cell +// TableCellRenderer renderer2 = newColumn.getHeaderRenderer(); +// if (renderer2 instanceof DefaultCellRenderer) +// ((DefaultCellRenderer)renderer2).setToolTipText(columnNames[columnIndex]); + +// newColumn.setWidth(200); +// mTable.addColumn(newColumn); +// } + + //no selection, no grid. + mTable.setColumnSelectionAllowed(false); + mTable.setShowGrid(false); + + // Put the table and header into a scrollPane + JScrollPane scrollpane = JTable.createScrollPaneForTable(mTable); +// JTableHeader tableHeader = mTable.getTableHeader(); + + // create and add the column heading to the scrollpane's + // column header viewport +// JViewport headerViewport = new JViewport(); +// headerViewport.setLayout(new BoxLayout(headerViewport, BoxLayout.X_AXIS)); +// headerViewport.add(tableHeader); +// scrollpane.setColumnHeader(headerViewport); + + // add the table to the viewport +/// JViewport mainViewPort = scrollpane.getViewport(); +// mainViewPort.add(mTable); +// mainViewPort.setBackground (Color.white); + + // speed up resizing repaints by turning off live cell updates +// tableHeader.setUpdateTableInRealTime(false); + + //return the JScrollPane with the table in it. + return scrollpane; + } + } +} diff --git a/mozilla/grendel/addressbook/Makefile b/mozilla/grendel/addressbook/Makefile new file mode 100644 index 00000000000..008a69672e6 --- /dev/null +++ b/mozilla/grendel/addressbook/Makefile @@ -0,0 +1,28 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SUBDIRS= \ + addresscard \ + $(NULL) + +SRCS= \ + AddressBook.java \ + NewCardDialog.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/addressbook/Menus.properties b/mozilla/grendel/addressbook/Menus.properties new file mode 100644 index 00000000000..5fdc123a38c --- /dev/null +++ b/mozilla/grendel/addressbook/Menus.properties @@ -0,0 +1,109 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +# menubar definition for mail/news adress book +# Lester Schueler 97/09/31 + +# main menu definition +mainMenubar=file edit view communicator help + +# "file" Menu definition +file=new newCard newList import - saveAs call - close + + # "file" Menu actions + newCardAction =newCard + newListAction =newList + importAction =import + saveAsAction =saveAs + callAction =call + closeAction =closeWindow + + # "file->new" menu definition + new=navigatorWindow message - blankPage pageFromTemplate pageFromWizard + + # "file->new" Menu actions + navigatorWindowAction =navigatorWindow + messageAction =message + blankPageAction =blankPage + pageFromTemplateAction =pageFromTemplate + pageFromWizardAction =pageFromWizard + +# "edit" Menu definition +edit=undo redo - delete - searchDirectory - HTMLDomains cardProperties preferences + + # "edit" Menu actions + undoAction =undo + redoAction =redo + deleteAction =delete + searchDirectoryAction =searchDirectory + HTMLDomainsAction =HTMLDomains + cardPropertiesAction =cardProperties + preferencesAction =preferences + +# "view" menu definition +view=hideABToolbar - byType byName byEmailAddress byComapany byCity byNickname - sortAscending sortDescending - myAddressBookCard + + # "view" Menu actions + hideABToolbarAction =hideABToolbar + byTypeAction =byType + byNameAction =byName + byEmailAddressAction =byEmailAddress + byComapanyAction =byComapany + byCityAction =byCity + byNicknameAction =byNickname + sortAscendingAction =sortAscending + sortDescendingAction =sortDescending + myAddressBookCardAction =myAddressBookCard + +# "communicator" menu definition +communicator=navigator messenger collabra composer conference calendar IBM netcaster - dockComponentBar messageCenter bookmarks history javaConsolse securityInfo - + + # "communicator" Menu actions + #navigatorAction =navigator + #messengerAction =messenger + #collabraAction =collabra + #composerAction =composer + #conferenceAction =conference + #calendarAction =calendar + #IBMAction =IBM + #NetcasterAction =netcaster + #dockComponentBarAction =dockComponentBar + #messageCenterAction =messageCenter + #bookmarksAction =bookmarks + #historyAction =history + #javaConsolseAction =javaConsolse + #securityInfoAction =securityInfo + + # "communicator->bookmarks" menu definition + bookmarks=bookmark1 bookmark2 bookmark3 + +# "help" menu definition +help=helpContents releaseNotes productInfo - softwareUpdates registerNow memberServices - InternationUsers security netEtiquette - aboutPlugins aboutFontDisplayers aboutCommunicator + + # "help" Menu actions + #helpContentsAction =helpContents + #releaseNotesAction =releaseNotes + #productInfoAction =productInfo + #softwareUpdatesAction =softwareUpdates + #registerNowAction =registerNow + #memberServicesAction =memberServices + #InternationUsersAction =InternationUsers + #securityAction =security + #netEtiquetteAction =netEtiquette + #aboutPluginsAction =aboutPlugins + #aboutFontDisplayersAction =aboutFontDisplayers + #aboutCommunicatorAction =aboutCommunicator diff --git a/mozilla/grendel/addressbook/NewCardDialog.java b/mozilla/grendel/addressbook/NewCardDialog.java new file mode 100644 index 00000000000..56016e99390 --- /dev/null +++ b/mozilla/grendel/addressbook/NewCardDialog.java @@ -0,0 +1,88 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.addressbook; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +import com.sun.java.swing.*; + +class NewCardDialog extends Dialog { + + NewCardDialog(Frame aParent) { + //FIX: Resource + super(aParent, "Card for", true); + +// setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + JComponent namePanel = createNamePanel (); + add (namePanel); + +// JComponent contactPanel = createContactPanel (); +// add (contactPanel); + +// JComponent netConfPanel = createNetConfPanel (); +// add (netConfPanel); + + setResizable(false); + setSize (716, 515); + } + + private JPanel createNamePanel () { + //the outer most panel has groove etched into it. + JPanel pane = new JPanel(false); +// pane.setLayout (new BoxLayout(pane, BoxLayout.Y_AXIS)); + + JTextField mFirstName = makeField ("First Name:", 20); + pane.add (mFirstName); +/* + JTextField mLastName = makeField ("Last Name:", 20); + pane.add (mLastName); + + JTextField mOrganization = makeField ("Organization:", 20); + pane.add (mOrganization); + + JTextField mTitle = makeField ("Title:", 20); + pane.add (mTitle); + + JTextField mEmail = makeField ("Email Address:", 20); + pane.add (mEmail); +*/ + return pane; + } + + private JTextField makeField (String aTitle, int aCol) { +// JPanel box = new JPanel (false); + Box box = new Box (BoxLayout.X_AXIS); + + JLabel title = new JLabel (aTitle); + title.setAlignmentY((float)0.0); //bottom + title.setAlignmentX((float)0.0); //left + box.add (title); + + JTextField textField = new JTextField (aCol); + textField.setAlignmentY((float)0.0); //bottom + textField.setAlignmentX((float)1.0); //right + box.add (textField); + + return textField; + } +} diff --git a/mozilla/grendel/addressbook/README b/mozilla/grendel/addressbook/README new file mode 100644 index 00000000000..26ae0125552 --- /dev/null +++ b/mozilla/grendel/addressbook/README @@ -0,0 +1,6 @@ +This is Grendel -- a Java mail/news client. + +The grendel.addressbook package talks to LDAP, and provides a nice API +to all things dealing with addressbook. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/addressbook/addresscard/ACS_Personal.java b/mozilla/grendel/addressbook/addresscard/ACS_Personal.java new file mode 100644 index 00000000000..06afd9dcb9c --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/ACS_Personal.java @@ -0,0 +1,209 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import grendel.storage.intertwingle.*; +import java.util.Enumeration; +import java.io.File; +import java.io.IOException; +//import java.util.Vector; + +//************************ +//************************ +public class ACS_Personal implements ICardSource, IQuerySet { + private DB fDB; + + public ACS_Personal (String FileName, boolean CreateOnOpen) { + File DBFile = new File(FileName); + try { + fDB = new SimpleDB(DBFile); + } catch (Exception e) {} + } + + /** Get the next card ID for this DB. + */ + private String getNextCardID () { + int nextCardIntID = 0; + String nextCardStrID; + + try { + //initalize the next card ID + nextCardStrID = fDB.findFirst ("Control", "NextCardID", false); + + //if the value wasn't found then assume this is the first time. + if ((null == nextCardStrID) || (nextCardStrID.equals (""))) { + nextCardIntID = 0; + } + + //if the value WAS found then try to extract an integer value from the text. + else { + try { + nextCardIntID = Integer.valueOf(nextCardStrID).intValue(); + } catch (NumberFormatException nfe) { + nextCardIntID = 0; + } + } + } catch (Exception e) { + nextCardIntID = 0; + } + + //create a string representation of the card's ID. + nextCardStrID = String.valueOf(++nextCardIntID); + + try { + //write the new value out to the DB. + fDB.assert ("Control", "NextCardID", nextCardStrID); + } catch (IOException ioe) { + } + + return nextCardStrID; + } + + /** + * closing the source + */ + public void close () { + try { + ((BGDB) fDB).flushChanges(); + } catch (IOException ioe) { + } + } + + /** + * retrieveing address cards + */ + public ICardSet getCardSet (ITerm aQueryTerm, String[] anAttributesArray) { + //get the card ID's that satidsfy the query. + ACIDSet CardIDSet = aQueryTerm.evaluate_ACSP (this); + + //create the address card set that'll be returned. + AddressCardSet retCardSet = new AddressCardSet (); + + for (int i = 0; i < CardIDSet.size(); i++) { + //create an attribute set for the card. + AddressCardAttributeSet attrSet = new AddressCardAttributeSet(); + + //get all attributes for this card. + for (int j = 0; j < anAttributesArray.length; j++) { + String attrName = anAttributesArray[j]; + + //read the attribute from the DB. + try { + String attrValue = fDB.findFirst((String) CardIDSet.elementAt(i), attrName, false); + + //if found then add to the attribute set for this card. + if (null != attrValue) { + attrSet.add (new AddressCardAttribute (attrName, attrValue)); + } + } catch (IOException ioe) {} + } + + //create the new card, and add to the return card set. + retCardSet.add (new AddressCard(this, attrSet)); + } + + //return the card set + return retCardSet; + } + + /** Add a single card to this addressbook. + */ + public void add (AddressCard aCard, boolean OverWrite) { + //give the card a new ID + String thisCardID = getNextCardID (); + aCard.setID (thisCardID); + + //get the set of attributes and enumerate thruough them. + AddressCardAttributeSet attrSet = aCard.getAttributeSet(); + + for (Enumeration enum = attrSet.elements (); enum.hasMoreElements(); ) { + //get the next attribute + AddressCardAttribute attr = (AddressCardAttribute) enum.netxElement (); + + //write the attribute to the DB + try { + fDB.assert (thisCardID, attr.getName(), attr.getValue()); + } catch (IOException ioe) {} + } + } + + /** Add a set of cards to this addressbook. + */ + public void add (AddressCardSet aCardSet, boolean OverWrite) { + for (Enumeration enum = aCardSet.getCardEnumeration (); enum.hasMoreElements() ;) { + AddressCard card = (AddressCard) enum.nextElement(); + addCard (card, OverWrite); + } + } + + //******************************* + //**** Operational functions **** + //******************************* + + /** Search the database for all cards that match this value + * and return a set Adddress Card ID's (ACID's). + */ + public ACIDSet opEqual (AddressCardAttribute ACA) { + ACIDSet retIDSet = new ACIDSet(); + + if (null != ACA) { + //the RDFish DB returns an enumeration of a matching card ID's + for (Enumeration enum = fDB.findAll(ACA.getName(), ACA.getValue (), false); enum.hasMoreElements() ;) { + retIDSet.addElement (enum.nextElement()); + } + } + + return retIDSet; + } + + /** Search the database for all cards that DO NOT match this value + * and return a set Adddress Card ID's (ACID's). + */ + public ACIDSet opNotEqual (AddressCardAttribute ACA) { + ACIDSet retIDSet = new ACIDSet(); + + return retIDSet; + } + +/* +1.Boolean RDF_HasAssertion (RDF_Resource u, RDF_Resource s, void* v, RDF_ValueType type, Boolean tv); +Returns true if there is an arc in the graph whose origin is u, whose label is s, whose target is v and whose truth value is true. + +2.void* RDF_GetSlotValue (RDF_Resource u, RDF_Resource s, RDF_ValueType type, Boolean inversep, Boolean tv); +Returns the target (whose datatype is type) of (any) one of the arcs whose source is u, label is s and truth value is tv. If inversep is true, +then it return the source (whose datatype is type) of (any) one of the arcs whose target is u, label is s and truth value is tv. + +3.RDF_Cursor RDF_GetSlotValues (RDF_Resource u, RDF_Resource s, RDF_ValueType type, Boolean inversep, Boolean tv); +Returns a cursor that can be used to enumerate through the targets (of datatype type) of the arcs whose source is u, label is s and truth +value is tv. If inversep is true, then the cursor enumerates through the sources of the arcs whose label is s, target is u and truth value is tv. +You can store your private data on the pdata field of the cursor. Cursors are cheap. + +4.void* RDF_NextValue(RDF_Cursor c); +Returns the next value from this cursor. Returns NULL if there are no more values. The fields value and type hold the value and datatype +of the value respectively. + +5.RDF_Error RDF_DisposeCursor (RDF_Cursor c); +When you are done with a cursor, call this function. Remember to free your pdata before calling this. +*/ + +} + diff --git a/mozilla/grendel/addressbook/addresscard/AC_Attribute.java b/mozilla/grendel/addressbook/addresscard/AC_Attribute.java new file mode 100644 index 00000000000..883cfce298e --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/AC_Attribute.java @@ -0,0 +1,105 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Vector; +import java.util.Enumeration; + +//************************ +//************************ +public class AC_Attribute implements IAttribute { + private String fName; + private Vector fValues; + + /** Create an attribute with mutliple values. + */ + public AC_Attribute (String aName, String[] aValues) { + fName = aName; + fValues = new Vector(); + + if (null != aValues) { + for (int i = 0; i < aValues.length; i++) { + if (null != aValues[i]) + add(aValues[i]); + } + } + } + + /** Create an attribute with one value. + */ + public AC_Attribute (String aName, String aValue) { + fName = aName; + fValues = new Vector(); + + if (null != aValue) + add(aValue); + } + + /** Create an attribute with no values. + */ + public AC_Attribute (String aName) { + fName = aName; + fValues = new Vector(); + } + + /** Add an value to this attribute + */ + public void add (String aValue) { + fValues.addElement (aValue); + } + + /** Return the name of this attribute. + */ + public String getName() { + return fName; + } + + /** Return the first string value of this attribute. + */ + public String getValue () { + return (String) fValues.elementAt(0); + } + + /** Return the string values of this attribute. + */ + public Enumeration getStringValues () { + return fValues.elements(); + } + + /** Has the attribute new to the parent? + */ + public boolean isNew() { + return false; + } + + /** Has the attribute been deleted since the last update with it's parent. + */ + public boolean isDeleted() { + return false; + } + + /** Has the attribute been modified since the last update with it's parent. + */ + public boolean isModified() { + return false; + } +} diff --git a/mozilla/grendel/addressbook/addresscard/AC_Exception.java b/mozilla/grendel/addressbook/addresscard/AC_Exception.java new file mode 100644 index 00000000000..135a791619d --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/AC_Exception.java @@ -0,0 +1,34 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class AC_Exception extends Exception { + public AC_Exception () { + super (); + } + + public AC_Exception (String anExplaination) { + super (anExplaination); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/AC_ID.java b/mozilla/grendel/addressbook/addresscard/AC_ID.java new file mode 100644 index 00000000000..43a2a07103f --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/AC_ID.java @@ -0,0 +1,29 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class AC_ID { + AC_ID () { + } +} diff --git a/mozilla/grendel/addressbook/addresscard/AC_IDSet.java b/mozilla/grendel/addressbook/addresscard/AC_IDSet.java new file mode 100644 index 00000000000..764d10cdcab --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/AC_IDSet.java @@ -0,0 +1,47 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Vector; + +//************************ +//************************ +public class AC_IDSet extends Vector { + public final static int ASCENDING = 0; + public final static int DESCENDING = 1; + + /** Default constructor + */ + public AC_IDSet () { + } + + /** Copy constructor + */ + public AC_IDSet (AC_IDSet anIDSet) { + } + + public void addSet (AC_IDSet aSet) { + } + + public void sort(int aDirection) { + } +} diff --git a/mozilla/grendel/addressbook/addresscard/IAttribute.java b/mozilla/grendel/addressbook/addresscard/IAttribute.java new file mode 100644 index 00000000000..1ff56de674f --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/IAttribute.java @@ -0,0 +1,46 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public interface IAttribute { + /** Return the name of this attribute. + */ + public String getName(); + + /** Return the first string value of this attribute. + */ + public String getValue (); + + /** Has the attribute new to the parent? + */ + public boolean isNew(); + + /** Has the attribute been deleted since the last update with it's parent. + */ + public boolean isDeleted(); + + /** Has the attribute been modified since the last update with it's parent. + */ + public boolean isModified(); +} diff --git a/mozilla/grendel/addressbook/addresscard/IAttributeSet.java b/mozilla/grendel/addressbook/addresscard/IAttributeSet.java new file mode 100644 index 00000000000..3d0a1b20272 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/IAttributeSet.java @@ -0,0 +1,53 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Enumeration; + +//************************ +//************************ +public interface IAttributeSet { + /** Add an attrbiute to the set. + */ +// public void add(AC_Attribute anAttribute) { + + /** Return the card set enumeration. + */ + public Enumeration getEnumeration(); + + /** Only intended to be accessed by LDAP_AttributeSetEnumeration. + */ +// public boolean hasMoreElements(); + + /** Only intended to be accessed by LDAP_AttributeSetEnumeration. + */ +// public Object nextElement(); + + /** Return the named attribute. + */ + public IAttribute getAttribute(String anAttributeName); + + /** Return the number of attributes in the set. + */ + public int size(); +} + diff --git a/mozilla/grendel/addressbook/addresscard/ICard.java b/mozilla/grendel/addressbook/addresscard/ICard.java new file mode 100644 index 00000000000..57f2ad6a5d8 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/ICard.java @@ -0,0 +1,44 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public interface ICard { + /** Return the parent card source from where this card came from (if any) + */ + public ICardSource getParent(); + + /** Add an attribute to the card. + */ + public void addAttribute (IAttribute anAttribute); + + /** Get a value of the address card. + */ + public IAttribute getAttribute (String anAttributeName); + + /** Get the full attribute set for this card. + */ + public IAttributeSet getAttributeSet (); +} + + diff --git a/mozilla/grendel/addressbook/addresscard/ICardSet.java b/mozilla/grendel/addressbook/addresscard/ICardSet.java new file mode 100644 index 00000000000..6e6a602866b --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/ICardSet.java @@ -0,0 +1,54 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Enumeration; + +//************************ +//************************ +public interface ICardSet { + + /** Add a card to the set. + */ + public void add (ICard aCard); + + /** Remove a card from the set. + * This only removes the card from the set NOT the source of the set. + */ + public void remove (ICard aCard); + + /** Sort the set. + */ + public void sort(String [] anAttributeArray); + + /** Return the card set enumeration. + */ + public Enumeration getEnumeration(); + + /** Only intended to be accessed by LDAP_SetEnumeration. + */ +// public boolean hasMoreElements(); + + /** Only intended to be accessed by LDAP_SetEnumeration. + */ +// public Object nextElement(); +} diff --git a/mozilla/grendel/addressbook/addresscard/ICardSource.java b/mozilla/grendel/addressbook/addresscard/ICardSource.java new file mode 100644 index 00000000000..1632faf9988 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/ICardSource.java @@ -0,0 +1,62 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Enumeration; + +//************************ +//************************ +public interface ICardSource { + /** + * closing the source + */ + public void close (); + + /** + * replication + * Update this address card source + */ +// public void update (ICardSource aSourceOfChangedCards); + + /** + * retrieveing address cards + */ +// public Enumeration getCardEnumeration (Term QueryTerm, String[] AttributesArray); + public ICardSet getCardSet (ITerm QueryTerm, String[] AttributesArray); +// public void getCardSink (Term QueryTerm, String[] AttributesArray, AC_Sink aCardSink); + + /** add a card(s) + */ + public void add (ICard aCard) throws AC_Exception; +// public void add (AC_Set aCardSet) throws AC_Exception; + + /** update a card(s) + */ + public void update (ICard aCard) throws AC_Exception; +// public void update (AC_Set aCardSet) throws AC_Exception; + + /** delete a card(s) + */ + public void delete (ICard aCard) throws AC_Exception; +// public void delete (AC_Set aCardSet) throws AC_Exception; +} + diff --git a/mozilla/grendel/addressbook/addresscard/IQuerySet.java b/mozilla/grendel/addressbook/addresscard/IQuerySet.java new file mode 100644 index 00000000000..f42a0b32354 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/IQuerySet.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +public interface IQuerySet { + public AC_IDSet opEqual (IAttribute ACA); + public AC_IDSet opNotEqual (IAttribute ACA); +// public ACIDSet opGreaterThan (AC_Attribute ACA); +// public ACIDSet opGreaterThanOrEqual (AC_Attribute ACA); +// public ACIDSet opLessThan (AC_Attribute ACA); +// public ACIDSet opLessThanOrEqual (AC_Attribute ACA); +} diff --git a/mozilla/grendel/addressbook/addresscard/IQueryString.java b/mozilla/grendel/addressbook/addresscard/IQueryString.java new file mode 100644 index 00000000000..856b3e2a30f --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/IQueryString.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +public interface IQueryString { + public String opAnd (ITerm aLeftTerm, ITerm aRightTerm); + public String opOr (ITerm aLeftTerm, ITerm aRightTerm); + public String opEqual (IAttribute ACA); + public String opNotEqual (IAttribute ACA); + public String opGTE (IAttribute ACA); + public String opLTE (IAttribute ACA); +} diff --git a/mozilla/grendel/addressbook/addresscard/ITerm.java b/mozilla/grendel/addressbook/addresscard/ITerm.java new file mode 100644 index 00000000000..536d036e35a --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/ITerm.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import grendel.storage.intertwingle.*; + +//************************ +//************************ +public interface ITerm { + public String getExpression(IQueryString iqs); + public AC_IDSet getSet (IQuerySet iqs); +} diff --git a/mozilla/grendel/addressbook/addresscard/LDAP_Attribute.java b/mozilla/grendel/addressbook/addresscard/LDAP_Attribute.java new file mode 100644 index 00000000000..ca157bdf67d --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/LDAP_Attribute.java @@ -0,0 +1,71 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import netscape.ldap.*; +import java.util.Enumeration; + +//************************ +//************************ +public class LDAP_Attribute implements IAttribute { + private LDAPAttribute fMyAttribute; + + protected LDAP_Attribute (LDAPAttribute anAttribute) { + fMyAttribute = anAttribute; + } + + /** Return the name of this attribute. + */ + public String getName() { + return fMyAttribute.getName(); + } + + /** Return the first string value of this attribute. + */ + public String getValue () { + // Get only the first value (handle multiple values later). + Enumeration enumVals = fMyAttribute.getStringValues(); + if ( enumVals.hasMoreElements() ) { + return (String) enumVals.nextElement(); + } + + return null; + } + + /** Has the attribute new to the parent? + */ + public boolean isNew() { + return false; + } + + /** Has the attribute been deleted since the last update with it's parent. + */ + public boolean isDeleted() { + return false; + } + + /** Has the attribute been modified since the last update with it's parent. + */ + public boolean isModified() { + return false; + } +} diff --git a/mozilla/grendel/addressbook/addresscard/LDAP_AttributeSet.java b/mozilla/grendel/addressbook/addresscard/LDAP_AttributeSet.java new file mode 100644 index 00000000000..b5203640a24 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/LDAP_AttributeSet.java @@ -0,0 +1,85 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import netscape.ldap.*; +import java.util.Enumeration; + +//************************ +//************************ +public class LDAP_AttributeSet implements IAttributeSet, Enumeration { + private LDAPAttributeSet fMyAttributeSet; + private Enumeration fAttrEnum; + + /** Create a new emtpy AttributeSet + */ + protected LDAP_AttributeSet (LDAPAttributeSet aSet) { + fMyAttributeSet = aSet; + } + + /** Add an attrbiute to the set. + */ +// public void add(AC_Attribute anAttribute) { +// fSet.put (anAttribute.getName(), anAttribute.getValue()); +// } + + /** Return the named attribute. + */ + public IAttribute getAttribute(String anAttributeName) { + return new LDAP_Attribute (fMyAttributeSet.getAttribute(anAttributeName)); + } + + /** Return the number of attributes in the set. + */ + public int size() { + return fMyAttributeSet.size(); + } + + /** Return the attribute set enumeration. + * We enumerate our own attribute set. + */ + public Enumeration getEnumeration() { + //FIX: Not multi-threaded! + //initiailize the enumeration cursor. + fAttrEnum = fMyAttributeSet.getAttributes(); + return this; + } + + /** Enumeration methods + */ + public boolean hasMoreElements() { + return fAttrEnum.hasMoreElements(); + } + + /** Enumeration methods + */ + public Object nextElement() { + //get the next attribute. + LDAPAttribute attr = (LDAPAttribute) fAttrEnum.nextElement(); + + //cache the element. +// fMyElementCache.add (card); + + //return the LDAPAttribute rapped in a IAttribute. + return new LDAP_Attribute (attr); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/LDAP_Card.java b/mozilla/grendel/addressbook/addresscard/LDAP_Card.java new file mode 100644 index 00000000000..40b5a532996 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/LDAP_Card.java @@ -0,0 +1,76 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import netscape.ldap.*; +import java.util.Enumeration; + +//************************ +//************************ +public class LDAP_Card implements ICard { + private LDAP_Server fParentSource; + private LDAPEntry fMyEntry; + + /** Constructor for LDAP server. + */ + protected LDAP_Card(LDAP_Server aParentSource, LDAPEntry anEntry) { + fParentSource = aParentSource; + fMyEntry = anEntry; + } + + /** Return the parent card source from where this card came from (if any) + */ + public ICardSource getParent() { + return fParentSource; + } + + /** Add an attribute to the card. + * FIX (this updates the server!) + */ + public void addAttribute (IAttribute anAttribute) { + //get the native attribute set. +// LDAPAttributeSet nativeAttrSet = fMyEntry.getAttributeSet(); + + //convert the local attribute into a native LDAP attribute and add. +// nativeAttrSet.add(localAttr.getLDAPAttribute()); + } + + /** Get a value of the address card. + */ + public IAttribute getAttribute (String anAttributeName) { + return new LDAP_Attribute (fMyEntry.getAttribute(anAttributeName)); + } + + /** Get the full attribute set for this card. + */ + public IAttributeSet getAttributeSet () { + return new LDAP_AttributeSet (fMyEntry.getAttributeSet()); + } + + //LDAP specific methods: + + /** Rerturn the DN + */ + public String getDN() { + return fMyEntry.getDN(); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/LDAP_CardSet.java b/mozilla/grendel/addressbook/addresscard/LDAP_CardSet.java new file mode 100644 index 00000000000..3cd5a0e542d --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/LDAP_CardSet.java @@ -0,0 +1,87 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import netscape.ldap.*; +import java.util.Enumeration; + +//************************ +//************************ +public class LDAP_CardSet implements ICardSet, Enumeration { + private LDAP_Server fParentSource; + private LDAPSearchResults fResultSet; + + /** Constructor for LDAP server. + */ + protected LDAP_CardSet (LDAP_Server aParentSource, LDAPSearchResults aResultSet) { + fParentSource = aParentSource; + fResultSet = aResultSet; + } + + /** Add a card to the set. + * This only addes the card to the set NOT the source of the set. + */ + public void add (ICard aCard) { +// fCardList.addElement (aCard); + } + + /** Remove a card from the set. + * This only removes the card from the set NOT the source of the set. + */ + public void remove (ICard aCard) { + } + + /** Sort the set. + */ + public synchronized void sort(String [] sortAttrs) { + boolean[] ascending = {true, true}; + fResultSet.sort(new LDAPCompareAttrNames(sortAttrs, ascending)); + } + + /** Return the card set enumeration. + * We enumerate our own cards. + * FIX: Not multi-threaded! + */ + public Enumeration getEnumeration() { + //initiailize the enumeration cursor. + return this; + } + + /** Enumeration methods + */ + public boolean hasMoreElements() { + return fResultSet.hasMoreElements(); + } + + /** Enumeration methods + */ + public Object nextElement() { + //return the next LDAPentry as an LDAP_Card. + LDAPEntry entry = (LDAPEntry) fResultSet.nextElement(); + + //cache the element. +// fMyElementCache.add (entry); + + //return the LDAPEntry rapped in an ICard interface. + return new LDAP_Card (fParentSource, entry); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/LDAP_Server.java b/mozilla/grendel/addressbook/addresscard/LDAP_Server.java new file mode 100644 index 00000000000..ade8937d509 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/LDAP_Server.java @@ -0,0 +1,275 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import netscape.ldap.*; +import java.util.*; + +//************************ +//************************ +public class LDAP_Server implements ICardSource, IQueryString { + LDAPConnection fConnection; + + public LDAP_Server (String ServerName) { + this (ServerName, 389, "", ""); + } + + public LDAP_Server (String aServerName, int aPort, String aUserName, String aPassword) { + try { + // Connect to server. + fConnection = new LDAPConnection(); + fConnection.connect (aServerName, aPort); + + // authenticate to the directory as nobody. + //fConnection.authenticate( aUserName, aPassword ); + } catch( LDAPException e ) { + System.out.println( "Error: " + e.toString() ); + } + + return; + } + + /** Close the addres card source + */ + public void close () { + } + + private final static String LEFT_PAREN = "("; + private final static String RIGHT_PARAN = ")"; + + private String escape (String aStringToEscape) { + String escapeChars = LEFT_PAREN + RIGHT_PARAN + "*"; + //FIX + return aStringToEscape; + } + + public String opAnd (ITerm aLeftTerm, ITerm aRightTerm) { + return LEFT_PAREN + "&" + aLeftTerm.getExpression (this) + aRightTerm.getExpression (this) + RIGHT_PARAN; + } + + public String opOr (ITerm aLeftTerm, ITerm aRightTerm) { + return LEFT_PAREN + "|" + aLeftTerm.getExpression (this) + aRightTerm.getExpression (this) + RIGHT_PARAN; + } + + public String opEqual (IAttribute ACA) { + return LEFT_PAREN + ACA.getName() + "=" + escape (ACA.getValue ()) + RIGHT_PARAN; + } + + public String opNotEqual (IAttribute ACA) { + return LEFT_PAREN + "!" + opEqual (ACA) + RIGHT_PARAN; + } + + public String opGTE (IAttribute ACA) { + return LEFT_PAREN + ACA.getName() + ">=" + escape (ACA.getValue ()) + RIGHT_PARAN; + } + + public String opLTE (IAttribute ACA) { + return LEFT_PAREN + ACA.getName() + "<=" + escape (ACA.getValue ()) + RIGHT_PARAN; + } + + + /** Retrieveing address cards as a set. + */ + public ICardSet getCardSet (ITerm aQueryTerm, String[] anAttributesArray) { + final String COMMON_NAME_ID = "cn"; + final String ORGANIZATION_ID = "o"; + final String COUNTRY_ID = "c"; + final String SIR_NAME_ID = "sn"; + final String MAIL_ID = "mail"; + + LDAPSearchResults queryResults = null; + + try { + String filter = aQueryTerm.getExpression(this); + String searchBase = COUNTRY_ID + "=" + "US"; + + LDAPSearchConstraints cons = fConnection.getSearchConstraints(); + cons.setBatchSize( 1 ); + + queryResults = fConnection.search( searchBase, + LDAPConnection.SCOPE_ONE, + filter, + anAttributesArray, + false, + cons); + } + catch( LDAPException e ) { + System.out.println( "Error: " + e.toString() ); + } + + return new LDAP_CardSet (this, queryResults); + } + + + /** Add an entry to the server. + */ + public void add (ICard aCard) throws AC_Exception { + //get all attributes for this card as a set + IAttributeSet AC_attrSet = aCard.getAttributeSet (); //From AddressCard + LDAPAttributeSet LDAP_attrSet = new LDAPAttributeSet(); //To LDAP + + //enumerate thru each attribute and translate from CA attributes into LDAP attributes. + for (Enumeration enum = AC_attrSet.getEnumeration(); enum.hasMoreElements() ;) { + + //Get the old + IAttribute AC_attr = (IAttribute) enum.nextElement(); + //Create the new + LDAPAttribute LDAP_attr = new LDAPAttribute(AC_attr.getName(), AC_attr.getValue()); + + //add the attribute to the LDAP set. + LDAP_attrSet.add( LDAP_attr ); + } + + try { + //FIX!!! + //create a new enry for the LDAP server + String dn = "cn=Barbara Jensen,ou=Product Development,o=Ace Industry,c=US"; + LDAPEntry anEntry = new LDAPEntry( dn, LDAP_attrSet ); + + //*** ADD THE ENTRY *** + fConnection.add( anEntry ); + + } catch (LDAPException le) { + //FIX + throw new AC_Exception ("Couldn't add card to server."); + } + } + + /** Delete an entry from the server. + */ + public LDAP_Card checkMyCard (ICard aCard) throws AC_Exception { + //check that the etnry came from an LDAP server. + if (aCard instanceof LDAP_Card) { + LDAP_Card card = (LDAP_Card) aCard; + + //check that the entry can from this LDAP server. + if (this == card.getParent ()) { + + //everthing's fine so return the card. + return card; + } + else { + throw new AC_Exception ("Not from this server."); + } + } + else { + throw new AC_Exception ("Not from an LDAP server."); + } + } + + + /** Delete an entry from the server. + */ + public void delete (ICard aCard) throws AC_Exception { + try { + //make sure the card came from this server. + LDAP_Card card = checkMyCard(aCard); //throws AC_Exception. + + //FIX + //?? What can still LOCALY happen to the card after it's deleted from the LDPA server?? + try { + //*** DELETE THE ENTRY *** + fConnection.delete ( card.getDN() ); + } catch (LDAPException le) { + //FIX + throw new AC_Exception ("Couldn't delete card from server."); + } + } + //BAD card. The card didn't come from this server. + catch (AC_Exception ace) { + //rethrow the exception + throw ace; + } + } + + /** Update an entries attributes on the server. + */ + public void update (ICard aCard) throws AC_Exception { + try { + //make sure the card came from this server. + LDAP_Card card = checkMyCard(aCard); //throws AC_Exception. + + //Ok, the card came from us so lets try to update the server. + //Get the set of attributes that have changed. + IAttributeSet AC_attrSet = card.getAttributeSet (); + + //Create a modifcation set to provide to the LDAP server. + LDAPModificationSet LDAP_modSet = new LDAPModificationSet(); + + //enumerate thru each attribute + for (Enumeration enum = AC_attrSet.getEnumeration() ; enum.hasMoreElements() ;) { + IAttribute AC_attr = (IAttribute) enum.nextElement(); + int LDAP_mod = 0; + boolean attributeModified = false; + + //Has the attribute been added? + if (true == AC_attr.isNew ()) { + LDAP_mod = LDAPModification.ADD; + attributeModified = true; + } + //Has the attribute been deleted? + else if (true == AC_attr.isDeleted ()) { + LDAP_mod = LDAPModification.DELETE; + attributeModified = true; + } + //Has the attribute been changed? + else if (true == AC_attr.isModified ()) { + LDAP_mod = LDAPModification.REPLACE; + attributeModified = true; + } + + //if modified then add to mod set. + if (true == attributeModified) { + LDAPAttribute LDAP_attr = new LDAPAttribute(AC_attr.getName(), AC_attr.getValue()); + LDAP_modSet.add (LDAP_mod, LDAP_attr); + } + } + + //Did we get any modifcations to send to the server? + if (0 < LDAP_modSet.size()) { + //submit the modifcation set to the server + try { + + //*** MODIFY THE ENTRY *** + fConnection.modify (card.getDN(), LDAP_modSet); + + } catch (LDAPException le) { + //FIX + throw new AC_Exception ("Couldn't update server."); + } + } + } + + //BAD card. The card didn't come from this server. + catch (AC_Exception ace) { + //rethrow the exception + throw ace; + } +// //else the card didn't come from this server so throw excpetion +// else { +// //FIX +// //Need to consider weather is ok for a card from one connection to update +// //to another connection of both connections are talking to the same server. +// throw new AC_Exception ("Address Card didn't originate from this connection."); +// } + } +} diff --git a/mozilla/grendel/addressbook/addresscard/Makefile b/mozilla/grendel/addressbook/addresscard/Makefile new file mode 100644 index 00000000000..7ef1fd58612 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/Makefile @@ -0,0 +1,45 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + ACS_Personal.java \ + AC_Attribute.java \ + AC_Exception.java \ + AC_ID.java \ + AC_IDSet.java \ + IAttribute.java \ + IAttributeSet.java \ + ICard.java \ + ICardSet.java \ + ICardSource.java \ + IQuerySet.java \ + IQueryString.java \ + ITerm.java \ + LDAP_Attribute.java \ + LDAP_AttributeSet.java \ + LDAP_Card.java \ + LDAP_CardSet.java \ + LDAP_Server.java \ + SelfTest.java \ + TermAnd.java \ + TermEqual.java \ + TermNotEqual.java \ + TermOr.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/addressbook/addresscard/SelfTest.java b/mozilla/grendel/addressbook/addresscard/SelfTest.java new file mode 100644 index 00000000000..85e4791cea8 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/SelfTest.java @@ -0,0 +1,500 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +import java.util.Enumeration; + +public class SelfTest { + public SelfTest () { + } + + //////////////////////////////////////////////////////// + //////////////////////////////////////////////////////// + //example #0 + //List all cards and their attributes. + public static void main(String argv[]) { + //open the LDAP address card source ldap.infospace.com + ICardSource Four11AddressBook = new LDAP_Server ("ldap.whowhere.com"); + + //define the card query. + ITerm query = new TermEqual (new AC_Attribute ("sn", "Lester")); + + //Define the attributes to return + String[] attributes = {"sn", "cn", "o", "mail", "city"}; + + //query the LDAP server. + ICardSet cardSet = Four11AddressBook.getCardSet (query, attributes); + + //Sort the list. + String[] sortOrder = {"sn", "mail"}; + cardSet.sort (sortOrder); + + //Enumerate thru the cards. + for (Enumeration cardEnum = cardSet.getEnumeration(); cardEnum.hasMoreElements(); ) { + ICard card = (ICard) cardEnum.nextElement(); + + System.out.println ("==== Card ===="); + IAttributeSet attrSet = card.getAttributeSet (); + + //Enumerate thru the attributes. + for (Enumeration attEnum = attrSet.getEnumeration(); attEnum.hasMoreElements(); ) { + IAttribute attr = (IAttribute) attEnum.nextElement(); + + System.out.println ("Attr: " + attr.getName() + " = " + attr.getValue()); + } + } + + //AC_Sources close when they fall out of scope. +// LDAP_Server.close (); + } + +/* + //example #1 + //Tranfering card from LDAP server to local address book using an "AddressSet". + public static void main(String argv[]) { + //open the LDAP address card source + IAC_Source Four11AddressBook = new LDAP_Server ("ldap.whowhere.com"); +// IAC_Source Four11AddressBook = new LDAP_Server ("lpad.four11.com", "lesters", "SecretPassword"); + + //Create the search + FullName + + NickName + FirstName + MiddleName + LastName + Organization + Locality + Region + EmailAddress + Comment + HTMLMail + Title + PostOfficeBox + Address + ZipCode + Country + WorkPhone + HomePhone + FaxPhone + ConferenceAddress + ConferenceServer + DistinguishedName + + TermEqual (AC_Attribute); + TermNotEqual (AC_Attribute); + TermGreaterThan (AC_Attribute); + TermGreaterThanOrEqual (AC_Attribute); + TermLessThan (AC_Attribute); + TermLessThanOrEqual (AC_Attribute); + + // AB.Search.Term query = new TermOr ( new AC_Attribute ("givenname", "Lester"), +// new AC_Attribute ("xmozillanickname", "Les")); + + ITerm query = new TermEqual (new AC_Attribute ("sn", "Lester")); + + //Define the attributes to return +// String[] attributes = {"givenname", "surname", "o", "mail"}; + String[] attributes = {"cn", "o", "mail"}; + String[] sortOrder = {"givenname", "surname"}; + + //query the LDAP server. + AC_Set cardSet = Four11AddressBook.getCardSet (query, attributes); + + //Sort the list. + cardSet.sort (sortOrder); + + //print the list of cards and their attributes. + for (Enumeration cardEnum = cardSet.getCardEnumeration(); cardEnum.hasMoreElements(); ) { + AC card = (AC) cardEnum.nextElement(); + + System.out.println ("==== Card ===="); + AC_AttributeSet attrSet = card.getAttributeSet (); + + for (Enumeration attEnum = attrSet.elements(); attEnum.hasMoreElements(); ) { + AC_Attribute attr = (AC_Attribute) attEnum.nextElement(); + + System.out.println ("Attr: Name = " + attr.getName() + " Value = " + attr.getValue()); + } + } + + //open the local address card source + // (true = create if needed) + ACS_Personal myLocalAddressBook = new ACS_Personal ("MyAddressBook.nab", true); + + //copy over the address cards + // true = overwrite if collision) + myLocalAddressBook.add (cardSet, true); + + //AC_Sources close when they fall out of scope. + myLocalAddressBook.close (); + } + + //example #2 + //Transfer cards from LDAP server to local address book using enumeration. + main () { + //open the LDAP address card source + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + //Create the search + AB.Search.Term query = new TermOr ( new AC_Attribute ("givenname", "Lester"), + new AC_Attribute ("xmozillanickname", "Les")); + + //Define the attributes to return + String[] attributes = {"givenname", "surname", "o", "mail"}; + + //open the local address card source + // (true = create if needed) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + + //query the LDAP server. + for (Enumeration enum = Four11AddressBook.getCardEnumeration (query, attributes); enum.hasMoreElements() ;) { + AC card = (AC_) enum.nextElement(); + + //only copy the netscape people. + if (true == card.getAttribute ("o").getValue().equals ("Netscape")) { + // true = overwrite if collision) + myLocalAddressBook.add (card, true); + } + } + + //AC_Sources close when they fall out of scope. + } + + + //example #3 + //Transfer cards from LDAP server to local address book using an "AddressList" and enumeration. + main () { + //open the LDAP address card source + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + //Create the search + AB.Search.Term query = new OrTerm ( new AC_Attribute ("givenname", "Lester"), + new AC_Attribute ("xmozillanickname", "Les")); + + //Define the attributes to return + String[] attributes = {"givenname", "surname", "o", "mail"}; + String[] sortOrder = {"givenname", "surname"}; + + //query the LDAP server. + AC_Set cardSet = Four11AddressBook.getCardSet (query, attributes); + + //Sort the list. + cardSet.sort (sortOrder); + + //open the local address card source + // (true = create if needed) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + + //Step thru the card list. + for (Enumeration enum = cardSet.getCardEnumeration (); enum.hasMoreElements() ;) { + AC card = (AC) enum.nextElement(); + + //only copy netscape people. + if (true == card.getAttribute ("o").equals ("Netscape")) { + // true = overwrite if collision) + myLocalAddressBook.add (card, true); + } + } + + //AC_Sources close when they fall out of scope. + } + + //example #4 + //Deleting card from the local addres book + main () { + //open the local address card source + // (false = fail if not already created) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", false); + + AB.Search.Term query = new AC_Attribute ("givenname", "Lester"); + + //query the AC_Source + AC_Set cardSet = myLocalAddressBook.getCardSet (query); + + //delete the cards + myLocalAddressBook.delete(cardSet); + + //AC_Sources close when they fall out of scope. + } + + + //example #5 + //Tranfering card from LDAP server to local address book using enumeration and collision detection. + main () { + //open the LDAP address card source + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + //Create the search + AB.Search.Term query = new AC_Attribute ("givenname", "Lester"); + + //open the local address card source + // (true = create if needed) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + + //query the LDAP server. + for (Enumeration enum = Four11AddressBook.getCardEnumeration (query, attributes); enum.hasMoreElements() ;) { + try { + // false = throw exception if collision + myLocalAddressBook.add ((AC_) enum.nextElement(), false); + + // Alternate form (test the set). + // if (false == myLocalAddressBook.willOverwrite ((AC_) enum.nextElement())) { + // myLocalAddressBook.add ((AC_) enum.nextElement(). false); + // } + + } catch (CardOverwriteException coe) { + } + } + + //AC_Sources close when they fall out of scope. + } + + //example #6 + //Find all the attributes of a card source? + main () { + //open the LDAP address card source + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + AC_AttributeSet attrs = Four11AddressBook.getAttributeSet(); + + for (Enumeration enumAttrs = attrs.getAttributes(); ; enumAttrs.hasMoreElements() ;) { + AC_Attribute anAttr = (AC_Attribute) enumAttrs.nextElement(); + + System.out.println( "Attribute Name: " + anAttr.getName() ); + } + + //AC_Sources close when they fall out of scope. + } + + //example #7 + //Update the two address books + main () { + //open the LDAP address card source + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + //open the local address card source + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + + //update the local address book from the LDAp server. + myLocalAddressBook.update (Four11AddressBook); + + //update the LDAP server from the local address book. + Four11AddressBook.update (myLocalAddressBook); + + //AC_Sources close when they fall out of scope. + } + + //example #8 + //Transfer cards from serveral LDAP servers to local address book using an "AddressSet". + main () { + //open the LDAP address card source + AC_SourceSet lpadServers = new AC_SourceSet(); + + //add the LDAP servers to the set. Order is important. + lpadServers.add (new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword")); + lpadServers.add (new LDAPServer ("lpad.infoseek.com", null, null)); + + //get all cards that have a department of "k8040" or "K8050". + Term query = new AC_Attribute ("xdepartment", {"k8040", "k8050"}); + + //Define the attributes to return + String[] attributes = {"givenname", "surname", "o", "mail", "xdepartment"}; + + //query the LDAP servers. + AC_Set cardSet = lpadServers.getCardSet (query, attributes); + + //open the local address card source + // (true = create if needed) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + + //copy over the address cards + // true = overwrite if collision) + myLocalAddressBook.add (cardSet, true); + + //AC_Sources close when they fall out of scope. + } + + + //example #9 + //Add a new card to the local address book then change it then delete it. + main () { + //Define the attributes to return + String[] attributes = {"givenname", "surname", "o", "mail"}; + + //open the local address card source + // (true = create if needed) + AC_Source myLocalAddressBook = new LocalStore ("MyAddressBook.nab", true); + AC newCard = new AC (); + newCard.add (new AC_Attribute ("givenname", "Robert")); + newCard.add (new AC_Attribute ("mail", "robert@ford.com")); + newCard.add (new AC_Attribute ("o", "Ford Motor Company")); + newCard.add (new AC_Attribute ("ou", "Audio")); + newCard.add (new AC_Attribute ("c", "US")); + + //try to store the new card + try { + myLocalAddressBook.addCard (newCard); + } + catch (AC_Exception ace) { + //1) No write access to DB + //2) All required attributes not provided + //3) Network connection down + //4) Card already in DB + } + + //try to change the card + try { + newCard.updateAttribute ("ou", "Electronics Division"); + myLocalAddressBook.updateCard (newCard); + } + catch (AC_Exception ace) { + //1) No write access to DB or this card. + //2) All required attributes not provided + //3) Network connection down + } + + //try to delete an attribute from the card + try { + newCard.deleteAttribute ("ou"); + myLocalAddressBook.updateCard (newCard); + } + catch (AC_Exception ace) { + //1) No write access to DB or this card + //2) Network connection down + //3) Card NOT in DB + } + + //try to delete the card + try { + myLocalAddressBook.deleteCard (newCard); + } + catch (AC_Exception ace) { + //1) No write access to DB or this card + //2) Network connection down + //3) Card NOT in DB + } + + //AC_Sources close when they fall out of scope. + } + + + + //example # + //List all cards and their attributes. + public static void main(String argv[]) { + //open the LDAP address card source + IAC_Source Four11AddressBook = new LDAP_Server ("ldap.whowhere.com"); + + + + //define the card query. + ITerm query = new TermEqual (new AC_Scope ("ObjectType", "Room")); + + + AB.Search.Term query = new AC_Attribute ("objectclass", "resource"); + AB.Search.Term query = new AC_Attribute ("objectclass", "person"); + + + //All object of type room on the second floor. + ITerm query = new AC_Scope ("Room", new TermEqual (new AC_Attribute ("Floor", "2"))); + + + + //Define the attributes to return + String[] attributes = {"cn", "o", "mail"}; + String[] sortOrder = {"givenname", "surname"}; + + //query the LDAP server. + AC_Set cardSet = Four11AddressBook.getCardSet (query, attributes); + + //Sort the list. + cardSet.sort (sortOrder); + + //Enumerate thru the cards. + for (Enumeration cardEnum = cardSet.getCardEnumeration(); cardEnum.hasMoreElements(); ) { + AC card = (AC) cardEnum.nextElement(); + + System.out.println ("==== Card ===="); + AC_AttributeSet attrSet = card.getAttributeSet (); + + //Enumerate thru the attributes. + for (Enumeration attEnum = attrSet.elements(); attEnum.hasMoreElements(); ) { + AC_Attribute attr = (AC_Attribute) attEnum.nextElement(); + + System.out.println ("Attr: Name = " + attr.getName() + " Value = " + attr.getValue()); + } + } + + //AC_Sources close when they fall out of scope. +// LDAP_Server.close (); + } + + + + + public static void main() { + try { + AC_Source Four11AddressBook = new LDAPServer ("lpad.four11.com", "lesters", "SecretPassword"); + + // search for all entries with surname of Jensen + String MY_FILTER = "sn=lester"; +// String MY_SEARCHBASE = "o=Ace Industry, c=US"; + String MY_SEARCHBASE = "c=US"; + + LDAPSearchConstraints cons = ld.getSearchConstraints(); + cons.setBatchSize( 1 ); + AC_Set cardSet = ld.search( MY_SEARCHBASE, + MY_FILTER, + null, + false, + cons ); + + // Loop on results until finished + while ( cardSet.hasMoreElements() ) { + + // Next entry + AC card = cardSet.nextElement(); + + // Get the attributes of the entry + AC_AttributeSet attrs = card.getAttributeSet(); + Enumeration enumAttrs = attrs.getAttributes(); + + // Loop on attributes + while ( enumAttrs.hasMoreElements() ) { + AC_Attribute anAttr = (AC_Attribute) enumAttrs.nextElement(); + String attrName = anAttr.getName(); + System.out.println( "\t\t" + attrName ); + + // Loop on values for this attribute + Enumeration enumVals = anAttr.getStringValues(); + while ( enumVals.hasMoreElements() ) { + String aVal = ( String )enumVals.nextElement(); + System.out.println( "\t\t\t" + aVal ); + } + } + } + } + catch( Exception e ) { + System.out.prin + */ +} diff --git a/mozilla/grendel/addressbook/addresscard/TermAnd.java b/mozilla/grendel/addressbook/addresscard/TermAnd.java new file mode 100644 index 00000000000..4e5240ff72e --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/TermAnd.java @@ -0,0 +1,57 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class TermAnd implements ITerm { + ITerm fLeftTerm; + ITerm fRightTerm; + + public TermAnd (ITerm aLeftTerm, ITerm aRightTerm) { + fLeftTerm = aLeftTerm; + fRightTerm = aRightTerm; + } + + public String getExpression(IQueryString iqs) { + return iqs.opAnd (fLeftTerm, fRightTerm); + } + + /** Call the Address Card Source to AND the left and right sets. + */ + public AC_IDSet getSet (IQuerySet iqs) { + AC_IDSet leftCardIDs = fLeftTerm.getSet (iqs); + AC_IDSet rightCardIDs = fRightTerm.getSet (iqs); + + //create the merge cardID list for return. + AC_IDSet mergedIDs = new AC_IDSet (); + + //keep only card ID's that are in both left and right terms. + for (int i = 0; i < leftCardIDs.size(); i++) { + AC_ID id = (AC_ID) leftCardIDs.elementAt (i); + if (rightCardIDs.contains (id)) + mergedIDs.addElement (id); + } + + return mergedIDs; + } +} diff --git a/mozilla/grendel/addressbook/addresscard/TermEqual.java b/mozilla/grendel/addressbook/addresscard/TermEqual.java new file mode 100644 index 00000000000..05e2005cc1c --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/TermEqual.java @@ -0,0 +1,42 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class TermEqual implements ITerm { + private IAttribute fAttribute; + + public TermEqual (IAttribute anAttribute) { + fAttribute = anAttribute; + } + + public String getExpression(IQueryString iqs) { + return iqs.opEqual (fAttribute); + } + + /** Call the Address Card Source to find a set that's equal. + */ + public AC_IDSet getSet (IQuerySet iqs) { + return iqs.opEqual (fAttribute); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/TermNotEqual.java b/mozilla/grendel/addressbook/addresscard/TermNotEqual.java new file mode 100644 index 00000000000..f3d274295b7 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/TermNotEqual.java @@ -0,0 +1,46 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class TermNotEqual implements ITerm { + private IAttribute fAttribute; + + /** + */ + public TermNotEqual (IAttribute anAttribute) { + fAttribute = anAttribute; + } + + /** + */ + public String getExpression(IQueryString iqs) { + return iqs.opNotEqual (fAttribute); + } + + /** + */ + public AC_IDSet getSet (IQuerySet iqs) { + return iqs.opNotEqual (fAttribute); + } +} diff --git a/mozilla/grendel/addressbook/addresscard/TermOr.java b/mozilla/grendel/addressbook/addresscard/TermOr.java new file mode 100644 index 00000000000..f7fdfcb8ef3 --- /dev/null +++ b/mozilla/grendel/addressbook/addresscard/TermOr.java @@ -0,0 +1,74 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Lester Schueler , 14 Nov 1997. + */ + +package grendel.addressbook.AddressCard; + +//************************ +//************************ +public class TermOr implements ITerm { + ITerm fLeftTerm; + ITerm fRightTerm; + + public TermOr (ITerm aLeftTerm, ITerm aRightTerm) { + fLeftTerm = aLeftTerm; + fRightTerm = aRightTerm; + } + + public String getExpression(IQueryString iqs) { + return iqs.opOr (fLeftTerm, fRightTerm); + } + + /** Logically or the left and right sets. + */ + public AC_IDSet getSet (IQuerySet iqs) { + AC_IDSet leftCardIDs = fLeftTerm.getSet (iqs); + AC_IDSet rightCardIDs = fRightTerm.getSet (iqs); + + //merge and sort the left and right lists. + AC_IDSet mergedIDs = new AC_IDSet (leftCardIDs); + mergedIDs.addSet (rightCardIDs); + mergedIDs.sort(AC_IDSet.ASCENDING); + + //Don't process any more if the merged set is emtpy. + if (0 == mergedIDs.size()) + return mergedIDs; //return an empty merged set. + + //prime the pump. + String lastID = (String) mergedIDs.elementAt(0); + + //remove any duplicates from the SORTED list. + for (int i = 0; i < mergedIDs.size(); ) { + String currentID = (String) mergedIDs.elementAt(i); + + //if the last equals this (this list is sorted) then remove this duplicate. + if (lastID.equals (currentID)) { + mergedIDs.removeElementAt (i); + } + else { + lastID = currentID; + i++; //advance to the next cardID + } + } + + return mergedIDs; + } +} + diff --git a/mozilla/grendel/calypso/Makefile b/mozilla/grendel/calypso/Makefile new file mode 100644 index 00000000000..c93190c2518 --- /dev/null +++ b/mozilla/grendel/calypso/Makefile @@ -0,0 +1,23 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SUBDIRS= \ + util \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/calypso/README b/mozilla/grendel/calypso/README new file mode 100644 index 00000000000..70f92c09567 --- /dev/null +++ b/mozilla/grendel/calypso/README @@ -0,0 +1,7 @@ +This is Grendel -- a Java mail/news client. + +Originally the calypso package was a general utility package for all of +the Xena project; what is present here are just the parts of Calypso +that are used by Grendel. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/calypso/util/Abacus.java b/mozilla/grendel/calypso/util/Abacus.java new file mode 100644 index 00000000000..83cf540a10b --- /dev/null +++ b/mozilla/grendel/calypso/util/Abacus.java @@ -0,0 +1,289 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** A class for counting in a variety of bases. + */ + +public class Abacus +{ + +/************************************************ + * Formatting Strings + ************************************************/ + + public final static String ones[]= + {"zero","one ","two ","three ","four ","five ", + "six ","seven ","eight ","nine ","ten "}; + + public final static String teens[]= + {"ten ","eleven ","twelve ","thirteen ","fourteen ","fifteen ", + "sixteen ","seventeen ","eighteen ","nineteen "}; + + public final static String tens[]= + {"","ten ","twenty ","thirty ","forty ","fifty ", + "sixty ","seventy ","eighty ","ninety ","hundred "}; + + public final static String bases[] = + {"","hundred ","thousand ","million ","billion ","trillion ","quadrillion "}; + + protected final static String gAlphaChars = "abcdefghijklmnopqrstuvwxyz"; + protected final static String gRomanCharsA = "ixcm"; + protected final static String gRomanCharsB = "vld?"; + protected final static String gHexChars = "0123456789abcdef"; + protected final static String gBinaryChars = "01"; + +/************************************************ + * Data members... + ************************************************/ + + //notice that we don't have any? + + /** Formats the value as an alpha string + * + * + * @return + * @author gess 05-05-97 12:22pm + * @notes + */ + public static String getAlphaString(int aValue) + { + return getSeriesString(Math.abs(aValue)-1,gAlphaChars,-1,gAlphaChars.length()); + } + + /** + * Convert the given integer value into a roman numeral string + * + * @param aValue - int to be converted to roman + * @return new string + * @author gess 05-05-97 12:53pm + * @notes Those pesky romans used dashed above a value to multiply + * that number by 1000. This has the unfortunate side effect + * that without special font treatment, we cant represent + * numbers in roman above 3999. + * THIS METHOD HANDLES VALUES IN THE RANGE [1..3999]. + * (any other value is converted to simple numeric format.) + */ + public static String getRomanString(int aValue) + { + StringBuffer addOn = new StringBuffer(); + StringBuffer result = new StringBuffer(); + StringBuffer decStr = new StringBuffer(); + + decStr.append(aValue); + + int len=decStr.length(); + int romanPos=len; + int n,digitPos; + boolean negative=(aValue<0); + + aValue=Math.abs(aValue); + for(digitPos=0;digitPos1) result.append(bases[expn]); + } + expn--; + root/=1000; + } + if (0==result.length()) + result.append("zero"); + return result.toString(); + } + + /** + * Convert the given integer value into a series string. These are any + * arbitrary but repeating pattern of characters. + * + * @param aValue - int to be converted to series string + * @return new string + * @author gess 05-05-97 12:53pm + * @notes this method gets used for binary and hex conversion + */ + public static String getSeriesString(int aValue,String aCharSet,int anOffset,int aBase) + { + int ndex=0; + int root=1; + int next=aBase; + int expn=1; + StringBuffer result=new StringBuffer(); + + aValue=Math.abs(aValue); // must be positive here... + while(next<=aValue) // scale up in baseN; exceed current value. + { + root=next; + next*=aBase; + expn++; + } + + while(0!=(expn--)) + { + ndex = ((root<=aValue) && (0!=root)) ? (aValue/root): 0; + aValue%=root; + if(root>1) + result.append(aCharSet.charAt(ndex+(1*anOffset))); + else result.append(aCharSet.charAt(ndex)); + root/=aBase; + }; + return result.toString(); + } + + public static void PadPrint(int aValue,int width) + { + StringBuffer temp=new StringBuffer(); + temp.append(aValue); + PadPrint(temp.toString(),width); + } + + public static void PadPrint(String aString, int aWidth) + { + System.out.print(aString); + int padCount=aWidth-aString.length(); + if(padCount>0) + for(int i=0;i 0) { + try { + throw new RuntimeException("Atom.ForObject: deprecated method called! Use Atom.Find instead. Object:" + aObject.toString()); + } + catch(RuntimeException e){ + e.printStackTrace(); + } + gNoisyUsageCount--; + } + return gAtoms.find(aObject.toString()); + } + } + + /** + * Find the atom for the given argument string. If the atom doesn't + * already exist then it is created. + */ + static public Atom Find(String aString) { + synchronized (gAtoms) { + return gAtoms.find(aString); + } + } + + /** + * Find the atom for the given argument string. If the atom doesn't + * already exist then it is created. + */ + static public Atom Find(StringBuf aString) { + synchronized (gAtoms) { + return gAtoms.find(aString); + } + } + + private static int gNextAtomID; + static public Atom UniqueAtom(String aPrefix) { + synchronized (gAtoms) { + StringBuf sbuf = StringBuf.Alloc(); + Atom atom = null; + for (;;) { + int id = ++gNextAtomID; + sbuf.setLength(0); + sbuf.append(aPrefix); + sbuf.append(id); + if (!gAtoms.contains(sbuf)) { + atom = gAtoms.find(sbuf); + break; + } + // Hmmm. Somebody else recorded an atom with this name. Try + // again with the next id + } + StringBuf.Recycle(sbuf); + return atom; + } + } + + //---------------------------------------------------------------------- + + // key's are String's/StringBuf's + // value's are WeakLink's which hold a weak reference to Atom's + + static final class AtomTable extends HashtableBase { + Atom find(String aKey) { + return find(aKey, HashCodeFor(aKey)); + } + + Atom find(StringBuf aKey) { + return find(aKey, HashCodeFor(aKey)); + } + + private Atom find(Object aKey, int aHash) { + if (hashCodes == null) { + // create tables + grow(); + } + int index = tableIndexFor(aKey, aHash); + Atom atom = null; + WeakLink link = (WeakLink) elements[index]; + if (link == null) { + if (hashCodes[index] == EMPTY) { + // If the total number of occupied slots (either with a real + // element or a removed marker) gets too big, grow the table. + if (totalCount >= capacity) { + grow(); + return find(aKey, aHash); + } + totalCount++; + } + count++; + + hashCodes[index] = aHash; + keys[index] = aKey.toString(); + elements[index] = link = new WeakLink(); + } else { + atom = (Atom) link.get(); + } + + // Create atom if necessary + if (atom == null) { + atom = new Atom(aKey.toString(), aHash); + link.setThing(atom); + } + return atom; + } + + // XXX replace with a faster better function + static final int HashCodeFor(String aString) { + int h = 0; + int len = aString.length(); + if (len < 16) { + for (int i = 0; i < len; i++) { + h = (h * 37) + aString.charAt(i); + } + } else { + // only sample some characters + int skip = len / 8; + for (int i = len, off = 0 ; i > 0; i -= skip, off += skip) { + h = (h * 39) + aString.charAt(off); + } + } + return h; + } + + static final int HashCodeFor(StringBuf aString) { + int h = 0; + int len = aString.length(); + if (len < 16) { + for (int i = 0; i < len; i++) { + h = (h * 37) + aString.charAt(i); + } + } else { + // only sample some characters + int skip = len / 8; + for (int i = len, off = 0 ; i > 0; i -= skip, off += skip) { + h = (h * 39) + aString.charAt(off); + } + } + return h; + } + + boolean contains(StringBuf aKey) { + if (hashCodes == null) { + return false; + } + int index = tableIndexFor(aKey, HashCodeFor(aKey)); + WeakLink link = (WeakLink) elements[index]; + if (link != null) { + return link.get() != null; + } + return false; + } + + void remove(String aKey, int aHashCode) { + int index = tableIndexFor(aKey, aHashCode); + count--; + hashCodes[index] = REMOVED; + keys[index] = null; + elements[index] = null; + } + } +} diff --git a/mozilla/grendel/calypso/util/AtomHashtable.java b/mozilla/grendel/calypso/util/AtomHashtable.java new file mode 100644 index 00000000000..45b8a146cfe --- /dev/null +++ b/mozilla/grendel/calypso/util/AtomHashtable.java @@ -0,0 +1,193 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * Special hashtable that uses Atoms as keys. This extends HashtableBase to + * expose a public Atom based api + * This hastable uses identity comparisons on keys + * + * @author psl 10-15-97 1:22pm + * @version $Revision: 1.1 $ + * @see + */ +public class AtomHashtable extends HashtableBase +{ + /** Constructs an empty Hashtable. The Hashtable will grow on demand + * as more elements are added. + */ + public AtomHashtable () + { + super (); + } + + /** Constructs a Hashtable capable of holding at least + * initialCapacity elements before needing to grow. + */ + public AtomHashtable (int aInitialCapacity) + { + super (aInitialCapacity); + } + + /** Returns an Object array containing the Hashtable's keys. + */ + public Atom[] keysArray () + { + Atom[] result = new Atom[count]; + getKeysArray (result); + return result; + } + + /** Returns an Object array containing the Hashtable's elements. + */ + public Object[] elementsArray () + { + Object[] result = new Object[count]; + getElementsArray (result); + return result; + } + + /** Returns true if the Hashtable contains the element. This method + * is slow -- O(n) -- because it must scan the table searching for the + * element. + */ + public boolean contains (Object aElement) + { + return containsElement (aElement); + } + + /** Returns true if the Hashtable contains the key key. + */ + public boolean containsKey (Atom aKey) + { + return (get (aKey) != null); + } + + /** Returns the element associated with the key. This method returns + * null if the Hashtable does not contain key. Hashtable + * hashes and compares key using hashCode() and + * equals(). + */ + public Object get (Atom aKey) + { + // We need to short-circuit here since the data arrays may not have + // been allocated yet. + + if (count == 0) + return null; + + return elements[tableIndexFor (aKey, hash (aKey))]; + } + + /** Removes key and the element associated with it from the + * Hashtable. Returns the element associated with key, or + * null if key was not present. + */ + public Object remove (Atom aKey) + { + return removeKey (aKey); + } + + /** Places the key/element pair in the Hashtable. Neither + * key nor element may be null. Returns the old + * element associated with key, or null if the key + * was not present. + */ + public Object put (Atom aKey, Object aElement) + { + return super.put (aKey, aElement); + } + + int hash (Object aKey) + { + return ((Atom)aKey).fHashCode; + } + + int hash (Atom aKey) + { + return aKey.fHashCode; + } + + /** Primitive method used internally to find slots in the + * table. If the key is present in the table, this method will return the + * index + * under which it is stored. If the key is not present, then this + * method will return the index under which it can be put. The caller + * must look at the hashCode at that index to differentiate between + * the two possibilities. + */ + int tableIndexFor (Object aKey, int aHash) + { + int product, testHash, index, step, removedIndex, probeCount; + + product = aHash * A; + index = product >>> shift; + + // Probe the first slot in the table. We keep track of the first + // index where we found a REMOVED marker so we can return that index + // as the first available slot if the key is not already in the table. + + testHash = hashCodes[index]; + + if (testHash == aHash) { + if (aKey == keys[index]) + return index; + removedIndex = -1; + } else if (testHash == EMPTY) + return index; + else if (testHash == REMOVED) + removedIndex = index; + else + removedIndex = -1; + + // Our first probe has failed, so now we need to start looking + // elsewhere in the table. + + step = ((product >>> (2 * shift - 32)) & indexMask) | 1; + probeCount = 1; + + do + { + probeCount++; + index = (index + step) & indexMask; + + testHash = hashCodes[index]; + + if (testHash == aHash) { + if (aKey == keys[index]) + return index; + } else if (testHash == EMPTY) { + if (removedIndex < 0) + return index; + else + return removedIndex; + } else if (testHash == REMOVED && removedIndex == -1) + removedIndex = index; + + } while (probeCount <= totalCount); + + // Something very bad has happened. + + throw new Error("Hashtable overflow"); + } +} + diff --git a/mozilla/grendel/calypso/util/AttributeValuePair.java b/mozilla/grendel/calypso/util/AttributeValuePair.java new file mode 100644 index 00000000000..24146c4f30a --- /dev/null +++ b/mozilla/grendel/calypso/util/AttributeValuePair.java @@ -0,0 +1,109 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * Simple attribute/value pair class + * + * XXX add in a recylcer + * XXX add in a method to reconsitute one from an InputStream/String? + */ +public class AttributeValuePair { + protected Atom fName; + protected String fValue; + + public AttributeValuePair(String aName, String aValue) { + this(Atom.Find(aName), aValue); + } + + public AttributeValuePair(Atom aName, String aValue) { + fName = aName; + fValue = aValue; + } + + public Atom getName() { + return fName; + } + + public String getValue() { + return fValue; + } + + public boolean equals(Object aObject) { + if ((null != aObject) && (aObject instanceof AttributeValuePair)) { + AttributeValuePair other = (AttributeValuePair) aObject; + if (fName.equals(other.fName)) { + if (fValue != null) { + return fValue.equals(other.fValue); + } + if (other.fValue == null) { + return true; + } + } + } + return false; + } + + public int hashCode() { + int h = fName.hashCode(); + if (null != fValue) { + h = (h << 5) + fValue.hashCode(); + } + return h; + } + + public String toString() { + StringBuf sbuf = StringBuf.Alloc(); + sbuf.append(fName); + if (fValue != null) { + sbuf.append("="); + sbuf.append(quoteValue()); + } + String rv = sbuf.toString(); + StringBuf.Recycle(sbuf); + return rv; + } + + /** + * Quote the value portion of an attribute,value pair making it + * suitable for reading in later. We assume "C" style quoting + * by backslashing quotes embedded in the value. + */ + protected String quoteValue() { + if (fValue == null) { + return "\"\""; + } + StringBuf buf = StringBuf.Alloc(); + buf.append('"'); + int n = fValue.length(); + for (int i = 0; i < n; i++) { + char ch = fValue.charAt(i); + if (ch == '"') { + buf.append("\""); + } else { + buf.append(ch); + } + } + buf.append('"'); + String rv = buf.toString(); + StringBuf.Recycle(buf); + return rv; + } +} diff --git a/mozilla/grendel/calypso/util/ByteBuf.java b/mozilla/grendel/calypso/util/ByteBuf.java new file mode 100644 index 00000000000..452a1e04237 --- /dev/null +++ b/mozilla/grendel/calypso/util/ByteBuf.java @@ -0,0 +1,918 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.RandomAccessFile; + +/** + * This class is similar to java/lang/StringBuffer with the following changes: + *

+ */ +public final class ByteBuf { + private byte value[]; + private int count; + +// static ByteBufRecycler gRecycler; + + /** + * Constructs an empty String buffer, reusing one from the + * recycler. + */ + static synchronized public ByteBuf Alloc() { + return new ByteBuf(); +// if (gRecycler == null) { +// gRecycler = new ByteBufRecycler(); +// } +// return gRecycler.alloc(); + } + + /** + * Release a ByteBuf to the recycler. It is up to the caller + * to ensure that buffer is no longer being used otherwise + * unpredicatable program behavior will result. + */ + static synchronized public void Recycle(ByteBuf aBuf) { +// if (gRecycler == null) { +// gRecycler = new ByteBufRecycler(); +// } +// gRecycler.recycle(aBuf); + } + + /** + * Empty the recycler discarding any cached ByteBuf objects + */ + static synchronized public void EmptyRecycler() { +// if (null != gRecycler) { +// gRecycler = null; +// } + } + + static void classFinalize() throws Throwable { + // Poof! Now we are unloadable even though we have a static + // variable. + } + + /** + * Constructs an empty String buffer. + */ + public ByteBuf() { + this(16); + } + + /** + * Constructs an empty byte buffer with the specified initial length. + * @param length the initial length + */ + public ByteBuf(int length) { + value = new byte[length]; + } + + /** + * Constructs a byte buffer with the specified initial value. + * @param str the initial value of the buffer + */ + public ByteBuf(String str) { + this(str.length() + 16); + append(str); + } + + public ByteBuf(byte bytes[], int offset, int length) { + value = new byte[length]; + System.arraycopy(bytes, offset, value, 0, length); + count = length; + } + + /** + * Returns the length (character count) of the buffer. + */ + public int length() { + return count; + } + + /** + * Returns the current capacity of the String buffer. The capacity + * is the amount of storage available for newly inserted + * characters; beyond which an allocation will occur. + */ + public int capacity() { + return value.length; + } + + /** + * Ensures that the capacity of the buffer is at least equal to the + * specified minimum. + * @param minimumCapacity the minimum desired capacity + */ + public void ensureCapacity(int minimumCapacity) { + int maxCapacity = value.length; + + if (minimumCapacity > maxCapacity) { + int newCapacity = (maxCapacity + 1) * 2; + if (minimumCapacity > newCapacity) { + newCapacity = minimumCapacity; + } + + byte newValue[] = new byte[newCapacity]; + System.arraycopy(value, 0, newValue, 0, count); + value = newValue; + } + } + + /** + * Sets the length of the String. If the length is reduced, characters + * are lost. If the length is extended, the values of the new characters + * are set to 0. + * @param newLength the new length of the buffer + * @exception StringIndexOutOfBoundsException If the length is invalid. + */ + public void setLength(int newLength) { + if (newLength < 0) { + throw new StringIndexOutOfBoundsException(newLength); + } + if (count < newLength) { + ensureCapacity(newLength); + for (; count < newLength; count++) { + value[count] = '\0'; + } + } + count = newLength; + } + + /** + * Returns the byte at the specified index. An index ranges + * from 0..length()-1. + * @param index the index of the desired character + * @exception StringIndexOutOfBoundsException If the index is invalid. + */ + public byte byteAt(int index) { + if ((index < 0) || (index >= count)) { + throw new StringIndexOutOfBoundsException(index); + } + return value[index]; + } + + /** + * Copies the characters of the specified substring (determined by + * srcBegin and srcEnd) into the character array, starting at the + * array's dstBegin location. Both srcBegin and srcEnd must be legal + * indexes into the buffer. + * @param srcBegin begin copy at this offset in the String + * @param srcEnd stop copying at this offset in the String + * @param dst the array to copy the data into + * @param dstBegin offset into dst + * @exception StringIndexOutOfBoundsException If there is an invalid index into the buffer. + */ + public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) { + if ((srcBegin < 0) || (srcBegin >= count)) { + throw new StringIndexOutOfBoundsException(srcBegin); + } + if ((srcEnd < 0) || (srcEnd > count)) { + throw new StringIndexOutOfBoundsException(srcEnd); + } + if (srcBegin < srcEnd) { + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); + } + } + + /** + * Changes the byte at the specified index to be ch. + * @param index the index of the character + * @param ch the new character + * @exception StringIndexOutOfBoundsException If the index is invalid. + */ + public void setByteAt(int index, byte b) { + if ((index < 0) || (index >= count)) { + throw new StringIndexOutOfBoundsException(index); + } + value[index] = b; + } + + /** + * Appends an object to the end of this buffer. + * @param obj the object to be appended + * @return the ByteBuf itself, NOT a new one. + */ + public ByteBuf append(Object obj) { + return append(String.valueOf(obj)); + } + + /** + * Appends a String to the end of this buffer. This just appends one byte + * per char; it strips off the upper 8 bits. If you want the localized + * method of converting chars to bytes, use append(String.getBytes()). + * @param str the String to be appended + * @return the ByteBuf itself, NOT a new one. + */ + public ByteBuf append(String str) { + if (str == null) { + str = String.valueOf(str); + } + + int len = str.length(); + ensureCapacity(count + len); + for (int i=0 ; i count)) { + throw new StringIndexOutOfBoundsException(); + } + int len = str.length(); + ensureCapacity(count + len); + System.arraycopy(value, offset, value, offset + len, count - offset); + for (int i=0 ; i count)) { + throw new StringIndexOutOfBoundsException(); + } + int len = str.length; + ensureCapacity(count + len); + System.arraycopy(value, offset, value, offset + len, count - offset); + System.arraycopy(str, 0, value, offset, len); + count += len; + return this; + } + + /** + * Inserts a boolean into the String buffer. + * @param offset the offset at which to insert + * @param b the boolean to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public ByteBuf insert(int offset, boolean b) { + return insert(offset, String.valueOf(b)); + } + + /** + * Inserts a byte into the String buffer. + * @param offset the offset at which to insert + * @param ch the character to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset invalid. + */ + public ByteBuf insert(int offset, byte b) { + ensureCapacity(count + 1); + System.arraycopy(value, offset, value, offset + 1, count - offset); + value[offset] = b; + count += 1; + return this; + } + + /** + * Inserts an integer into the String buffer. + * @param offset the offset at which to insert + * @param i the integer to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public ByteBuf insert(int offset, int i) { + return insert(offset, String.valueOf(i)); + } + + /** + * Inserts a long into the String buffer. + * @param offset the offset at which to insert + * @param l the long to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public ByteBuf insert(int offset, long l) { + return insert(offset, String.valueOf(l)); + } + + /** + * Inserts a float into the String buffer. + * @param offset the offset at which to insert + * @param f the float to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public ByteBuf insert(int offset, float f) { + return insert(offset, String.valueOf(f)); + } + + /** + * Inserts a double into the String buffer. + * @param offset the offset at which to insert + * @param d the double to insert + * @return the ByteBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public ByteBuf insert(int offset, double d) { + return insert(offset, String.valueOf(d)); + } + + /** + * Reverse the order of the characters in the String buffer. + */ + public ByteBuf reverse() { + int n = count - 1; + for (int j = (n-1) >> 1; j >= 0; --j) { + byte temp = value[j]; + value[j] = value[n - j]; + value[n - j] = temp; + } + return this; + } + + + /** + * Converts to a String representing the data in the buffer. + */ + public String toString() { + return new String(value, 0, count); + } + + public byte[] toBytes() { + return value; + } + + + /** + * Compares this ByteBuf to another ByteBuf. Returns true if the + * other ByteBuf is equal to this ByteBuf; that is, has the same length + * and the same characters in the same sequence. Upper case + * characters are folded to lower case before they are compared. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equalsIgnoreCase(ByteBuf anotherString) { + if ((anotherString != null) && (anotherString.count == count)) { + for (int i = 0; i < count; i++) { + byte c1 = value[i]; + byte c2 = anotherString.value[i]; + if (c1 != c2) { + // If characters don't match but case may be ignored, + // try converting both characters to uppercase. + // If the results match, then the comparison scan should + // continue. + char u1 = Character.toUpperCase((char) c1); + char u2 = Character.toUpperCase((char) c2); + if (u1 == u2) + continue; + + // Unfortunately, conversion to uppercase does not work properly + // for the Georgian alphabet, which has strange rules about case + // conversion. So we need to make one last check before + // exiting. + if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) + continue; + return false; + } + } + return true; + } + return false; + } + + public boolean equals(Object aObject) { + if (aObject != null) { + if (aObject instanceof ByteBuf) { + return equals((ByteBuf) aObject); + } else if (aObject instanceof String) { + return equals((String) aObject); + } + } + return false; + } + + /** + * Compares this ByteBuf to another ByteBuf. Returns true if the + * other ByteBuf is equal to this ByteBuf; that is, has the same length + * and the same characters in the same sequence. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equals(ByteBuf anotherString) { + if ((anotherString != null) && (anotherString.count == count)) { + for (int i = 0; i < count; i++) { + byte c1 = value[i]; + byte c2 = anotherString.value[i]; + if (c1 != c2) { + return false; + } + } + return true; + } + return false; + } + + /** + * Compares this ByteBuf to another String. Returns true if the + * other String is equal to this ByteBuf; that is, has the same length + * and the same characters in the same sequence. (No localization is done; + * if the string doesn't contain 8-bit chars, it won't be equal to this + * ByteBuf.) + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equals(String anotherString) { + if ((anotherString != null) && (anotherString.length() == count)) { + for (int i = 0; i < count; i++) { + byte c1 = value[i]; + byte c2 = (byte) anotherString.charAt(i); + if (c1 != c2) { + return false; + } + } + return true; + } + return false; + } + + /** + * Tests if two byte regions are equal. + *

+ * If toffset or ooffset is negative, or + * if toffset+length is greater than the + * length of this ByteBuf, or if + * ooffset+length is greater than the + * length of the argument, then this method returns + * false. + * + * @param toffset the starting offset of the subregion in this ByteBuf. + * @param other the other bytes. + * @param ooffset the starting offset of the subregion in the argument. + * @param len the number of bytes to compare. + * @return true if the specified subregion of this ByteBuf + * exactly matches the specified subregion of the argument; + * false otherwise. + */ + public boolean regionMatches(int toffset, byte other[], + int ooffset, int len) { + return regionMatches(false, toffset, other, ooffset, len); + } + + + /** + * Tests if two byte regions are equal. + *

+ * If toffset or ooffset is negative, or + * if toffset+length is greater than the + * length of this ByteBuf, or if + * ooffset+length is greater than the + * length of the argument, then this method returns + * false. + * + * @param ignoreCase if true, ignore case when comparing + * bytes (treating them as characters). + * @param toffset the starting offset of the subregion in this + * ByteBuf. + * @param other the other bytes. + * @param ooffset the starting offset of the subregion in the + * argument. + * @param len the number of bytes to compare. + * @return true if the specified subregion of this ByteBuf + * matches the specified subregion of the argument; + * false otherwise. Whether the matching is exact + * or case insensitive depends on the ignoreCase + * argument. + */ + public boolean regionMatches(boolean ignoreCase, + int toffset, + byte other[], + int ooffset, + int len) { + /* Lifted (and changed from char to byte) from java.lang.String. */ + byte ta[] = value; + int to = toffset; + int tlim = count; + byte pa[] = other; + int po = ooffset; + // Note: toffset, ooffset, or len might be near -1>>>1. + if ((ooffset < 0) || (toffset < 0) || (toffset > count - len) || + (ooffset > other.length - len)) { + return false; + } + while (len-- > 0) { + byte c1 = ta[to++]; + byte c2 = pa[po++]; + if (c1 == c2) + continue; + if (ignoreCase) { + // If characters don't match but case may be ignored, + // try converting both characters to uppercase. + // If the results match, then the comparison scan should + // continue. + char u1 = Character.toUpperCase((char) c1); + char u2 = Character.toUpperCase((char) c2); + if (u1 == u2) + continue; + // Unfortunately, conversion to uppercase does not work properly + // for the Georgian alphabet, which has strange rules about case + // conversion. So we need to make one last check before + // exiting. + if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) + continue; + } + return false; + } + return true; + } + + + public boolean regionMatches(int toffset, ByteBuf other, + int ooffset, int len) { + return regionMatches(false, toffset, other.value, ooffset, len); + } + + public boolean regionMatches(boolean ignoreCase, + int toffset, ByteBuf other, + int ooffset, int len) { + return regionMatches(ignoreCase, toffset, other.value, ooffset, len); + } + + + /** + * Tests if two byte regions are equal. + *

+ * If toffset or ooffset is negative, or + * if toffset+length is greater than the + * length of this ByteBuf, or if + * ooffset+length is greater than the + * length of the argument, then this method returns + * false. + * + * @param toffset the starting offset of the subregion in this ByteBuf. + * @param other the other String. + * @param ooffset the starting offset of the subregion in the argument. + * @param len the number of bytes/characters to compare. + * @return true if the specified subregion of this ByteBuf + * exactly matches the specified subregion of the String argument; + * false otherwise. + */ + public boolean regionMatches(int toffset, String other, + int ooffset, int len) { + return regionMatches(false, toffset, other, ooffset, len); + } + + + /** + * Tests if two byte regions are equal. + *

+ * If toffset or ooffset is negative, or + * if toffset+length is greater than the + * length of this ByteBuf, or if + * ooffset+length is greater than the + * length of the argument, then this method returns + * false. + * + * @param ignoreCase if true, ignore case when comparing + * bytes (treating them as characters). + * @param toffset the starting offset of the subregion in this + * ByteBuf. + * @param other the other String. + * @param ooffset the starting offset of the subregion in the + * String argument. + * @param len the number of bytes to compare. + * @return true if the specified subregion of this ByteBuf + * matches the specified subregion of the String argument; + * false otherwise. Whether the matching is exact + * or case insensitive depends on the ignoreCase + * argument. + */ + public boolean regionMatches(boolean ignoreCase, + int toffset, + String other, + int ooffset, + int len) { + /* Lifted (and changed from char to byte) from java.lang.String. */ + byte ta[] = value; + int to = toffset; + int tlim = count; + int po = ooffset; + // Note: toffset, ooffset, or len might be near -1>>>1. + if ((ooffset < 0) || (toffset < 0) || (toffset > count - len) || + (ooffset > other.length() - len)) { + return false; + } + while (len-- > 0) { + byte c1 = ta[to++]; + byte c2 = (byte) other.charAt(po++); + if (c1 == c2) + continue; + if (ignoreCase) { + // If characters don't match but case may be ignored, + // try converting both characters to uppercase. + // If the results match, then the comparison scan should + // continue. + char u1 = Character.toUpperCase((char) c1); + char u2 = Character.toUpperCase((char) c2); + if (u1 == u2) + continue; + // Unfortunately, conversion to uppercase does not work properly + // for the Georgian alphabet, which has strange rules about case + // conversion. So we need to make one last check before + // exiting. + if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) + continue; + } + return false; + } + return true; + } + + + public int indexOf(int ch) { + return indexOf(ch, 0); + } + + public int indexOf(int ch, int fromIndex) { + int max = count; + byte v[] = value; + + if (fromIndex < 0) { + fromIndex = 0; + } else if (fromIndex >= count) { + // Note: fromIndex might be near -1>>>1. + return -1; + } + for (int i = fromIndex ; i < max ; i++) { + if (v[i] == ch) { + return i; + } + } + return -1; + } + + public void remove(int fromIndex) { + remove(fromIndex, count); + } + + /** + * Remove characters from the ByteBuf starting at fromIndex and up + * to but not including toIndex. If toIndex is beyond the length of + * the ByteBuf then it is automatically clamped to the end of the + * ByteBuf. If fromIndex is out of range a StringIndexOutOfBoundsException + * is thrown. + */ + public void remove(int fromIndex, int toIndex) { + if ((fromIndex >= toIndex) || (fromIndex >= count)) { + throw new StringIndexOutOfBoundsException(fromIndex); + } + if (toIndex > count) toIndex = count; + if (toIndex == count) { + count = fromIndex; + return; + } + System.arraycopy(value, toIndex, value, fromIndex, count - toIndex); + count -= toIndex - fromIndex; + } + + public int toInteger() throws NumberFormatException { + int result = 0; + int sign = 1; + int i = 0; + if (count == 0) { + throw new NumberFormatException(); + } + while (i < count - 1 && (value[i] <= ' ')) { + i++; + } + if (value[i] == '-' && i < count - 1) { + sign = -1; + i++; + } + do { + byte b = value[i]; + if (b >= '0' || b <= '9') { + result = (result * 10) + (b - '0'); + } else { + throw new NumberFormatException(); + } + i++; + } while (i < count && value[i] > ' '); + while (i < count && value[i] < ' ') { + i++; + } + if (i < count) { + throw new NumberFormatException(); + } + return result * sign; + } + + public ByteBuf trim() { + int i=0; + while (i < count && value[i] <= ' ') i++; + if (i > 0) { + count -= i; + System.arraycopy(value, i, value, 0, count); + } + while (count > 0 && value[count-1] <= ' ') count--; + return this; + } + + + /** Write to the given output stream a detailed description of each + byte in this buffer. */ + public void fullDump(PrintStream out) { + fullDump(out, 0, count); + } + + /** Write to the given output stream a detailed description of the given + bytes in this buffer. */ + public void fullDump(PrintStream out, int start, int end) { + for (int i=start ; i 0) count += i; + return i; + } + + /** Invokes RandomAccessFile.read(), appending the bytes to this Bytebuf. + (A RandomAccessFile is not an InputStream, because Java is a crock.) + @return the number of bytes read, or -1 if eof. + */ + public int read(RandomAccessFile file, int max_bytes) + throws IOException { + ensureCapacity(count + max_bytes); + int i = file.read(value, count, max_bytes); + if (i > 0) count += i; + return i; + } + + /** Writes the contents to the given output stream. */ + public void write(OutputStream out) throws IOException { + out.write(value, 0, count); + } + + /** Writes the contents to the given RandomAccessFile. */ + public void write(RandomAccessFile out) throws IOException { + out.write(value, 0, count); + } + + /** Creates a new InputStream whose content is this ByteBuf. Note that + changing the ByteBuf can affect the stream; the data is not + copied. */ + public InputStream makeInputStream() { + return new ByteArrayInputStream(value, 0, count); + } +} diff --git a/mozilla/grendel/calypso/util/ByteLineBuffer.java b/mozilla/grendel/calypso/util/ByteLineBuffer.java new file mode 100644 index 00000000000..15aed56770e --- /dev/null +++ b/mozilla/grendel/calypso/util/ByteLineBuffer.java @@ -0,0 +1,346 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * This class is stores data in a private ByteBuf appending new + * data as needed, and returning line separated data. + */ + +public class ByteLineBuffer { + + private static final boolean DEBUG = false; + + private boolean buffer_full; + private ByteBuf buffer; + private ByteBuf outputeol; + private ByteBuf inputeol; + + /** + * Constructs an empty ByteLineBuffer. + */ + + public ByteLineBuffer() { + buffer = new ByteBuf(); + buffer_full = false; + + if (DEBUG) { + System.err.println("BLB " + hashCode() + ": created"); + } + } + + /** + * Appends data to the end of the internal ByteBuf. + * @param buf data to append to internal buffer + */ + public void pushBytes(ByteBuf buf) { + if (DEBUG) { + System.err.print("BLB " + hashCode() + ": pushBytes(\""); + byte b[] = buf.toBytes(); + for (int i = 0; i < buf.length(); i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '"' ? "\\\"" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\")\n"); + } + buffer.append(buf); + } + + /** + * indicates that the last piece of data is now present. + */ + + public void pushEOF() { + if (DEBUG) { + System.err.println("BLB " + hashCode() + ": pushEOF()"); + } + buffer_full = true; + } + + /** Sets the EOL characters to look for in the incoming stream. Setting + * this to be null will cause it to look for any of , , or . + * Note that null (the default) could cause up to one extra to be held in + * the buffer (in the case where the last byte read from the underlying + * stream was , and no following byte (or EOF) has yet been read.) + */ + public void setInputEOL(ByteBuf buf) { + Assert.Assertion(buf == null || buf.length() > 0); + inputeol = buf; + } + + /** + * End of line characters & or any combination + * will be replaced by this string if it is present. Setting + * this to a zero length string will cause them to be stripped. + * Setting this to null will cause the EOL characters to be passed + * through unchanged (the default). + * @param buf ByteBuf representing EOL replacement string + */ + + public void setOutputEOL(ByteBuf buf) { + outputeol = buf; + } + + /** + * Returns a ByteBuf with a line of data which was separated by + * or any combination. Holds the last line in the ByteBuf + * until pushEOF() is called. If a ByteBuf containing data is sent in, + * the data is removed. + * @param buf single line of data in a ByteBuf + * @return true if a line was retrieved, false if not + */ + + public boolean pullLine(ByteBuf buf) { + int position; + int buffer_length; + byte value; + // Remove any data that already exists in the buffer sent in + buf.setLength(0); + + if (buffer_full && buffer == null) + return false; + + buffer_length = buffer.length(); + + if (inputeol != null) { + int last = buffer_length - inputeol.length() + 1; + byte first = inputeol.byteAt(0); + byte[] peekbuf = buffer.toBytes(); + for (position = 0 ; position < last ; position++) { + if (peekbuf[position] == first) { + boolean match = true; + for (int j=inputeol.length() - 1 ; j>=1 ; j--) { + if (peekbuf[position + j] != inputeol.byteAt(j)) { + match = false; + break; + } + } + if (match) { + buf.setLength(0); + if (outputeol != null) { + buf.append(peekbuf, 0, position); + buf.append(outputeol); + } else { + buf.append(peekbuf, 0, + position + inputeol.length()); + } + buffer.remove(0, position + inputeol.length()); + + if (DEBUG) { + System.err.print("BLB " + hashCode() + + ": EOL matched; returning: \""); + byte b[] = buf.toBytes(); + for (int i = 0; i < buf.length(); i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '"' ? "\\\"" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\"\n"); + } + + return true; + } + } + } + } else { + // process the entire buffer, or until a newline character is hit + for (position = 0; position < buffer_length; position++) { + value = buffer.byteAt(position); + if (value == '\n' || value == '\r') { + + boolean ambiguous = true; + + position++; + // got a newline, pass it + if (value == '\r' && position < buffer_length) { + + if (DEBUG) { + System.err.println("BLB " + hashCode() + + ": got \\r not at end of buffer"); + } + + // if we have seen \r, and we know what character + // lies after it, then there is no ambiguity: + // the linebreak is "\r" or "\r\n", and we can + // tell which it is. + // + ambiguous = false; + + // if we are not replacing the EOL chars, return the + // existing ones + if (outputeol == null) + buf.append(value); + value = buffer.byteAt(position); + if (value == '\n') { + position++; + if (outputeol == null) + buf.append(value); + } + } else { + + if (DEBUG) { + if (value == '\r') + System.err.println("BLB " + hashCode() + + ": got \\r at end of buffer"); + else + System.err.println("BLB " + hashCode() + + ": got \\n without \\r"); + } + + // If we have seen \n, then the linebreak is + // unambiguously "\n". If we have seen \r, but we + // don't know what character follows it (because + // it happened to be at the end of the buffer) then + // the linebreak is ambiguous ("\r" or "\r\n"). + // + ambiguous = (value == '\r'); + + if (outputeol == null) { + buf.append(value); + } + } + + // if this is the last line in the buffer, and pushEOF() + // hasn't been called, and the linebreak was ambiguous, + // then wait for more data or a call to pushEOF() before + // returning this line. + // + if (position == buffer_length && + !buffer_full && + ambiguous) { + + if (DEBUG) { + System.err.println("BLB " + hashCode() + + ": waiting for next line"); + } + + return false; + } + // remove the string from the internal ByteBuf + buffer.remove(0,position); + // if we have a replacement EOL string, use it + if (outputeol != null) + buf.append(outputeol); + + if (DEBUG) { + System.err.print("BLB " + hashCode() + + ": returning line: \""); + byte b[] = buf.toBytes(); + for (int i = 0; i < buf.length(); i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '"' ? "\\\"" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\"\n"); + } + + return true; + } + else + // append bytes to output ByteBuf + buf.append(value); + } + } + // process full buffer + if (buffer_full && buffer_length > 0) + { + if (outputeol != null) + buf.append(outputeol); + buffer.remove(0,position); + buffer.Recycle(buffer); + buffer = null; + + if (DEBUG) { + System.err.print("BLB " + hashCode() + + ": at EOF; returning line: \""); + byte b[] = buf.toBytes(); + for (int i = 0; i < buf.length(); i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '"' ? "\\\"" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\"\n"); + } + + return true; + } + + // didn't get a string this time around. + buf.setLength(0); + + if (DEBUG) { + System.err.println("BLB " + hashCode() + + ": don't have a line yet"); + } + + return false; + } + +// public static void main(String args[]) { +// +// ByteLineBuffer blb = new ByteLineBuffer(); +// ByteBuf bb = new ByteBuf(); +// ByteBuf line_bytes = new ByteBuf(100); +// +// bb.setLength(0); +// bb.append("from: jwz"); +// System.err.println("\npushed \"" + bb + "\""); blb.pushBytes(bb); +// while(blb.pullLine(line_bytes)) { +// System.err.println(" pulled \"" + line_bytes + "\""); +// } +// +// bb.setLength(0); +// bb.append("\r\n"); +// System.err.println("\npushed \"" + bb + "\""); blb.pushBytes(bb); +// while(blb.pullLine(line_bytes)) { +// System.err.println(" pulled \"" + line_bytes + "\""); +// } +// +// bb.setLength(0); +// bb.append("content-type: text/plain"); +// System.err.println("\npushed \"" + bb + "\""); blb.pushBytes(bb); +// while(blb.pullLine(line_bytes)) { +// System.err.println(" pulled \"" + line_bytes + "\""); +// } +// +// bb.setLength(0); +// bb.append("\r\n"); +// System.err.println("\npushed \"" + bb + "\""); blb.pushBytes(bb); +// while(blb.pullLine(line_bytes)) { +// System.err.println(" pulled \"" + line_bytes + "\""); +// } +// +// bb.setLength(0); +// System.err.println("\npushed EOF"); blb.pushEOF(); +// while(blb.pullLine(line_bytes)) { +// System.err.println(" pulled \"" + line_bytes + "\""); +// } +// } + +} diff --git a/mozilla/grendel/calypso/util/ByteToCharConverterEnumeration.java b/mozilla/grendel/calypso/util/ByteToCharConverterEnumeration.java new file mode 100644 index 00000000000..a5e0eeb1a03 --- /dev/null +++ b/mozilla/grendel/calypso/util/ByteToCharConverterEnumeration.java @@ -0,0 +1,85 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Frank Tang + */ + +package calypso.util; + +import java.util.Enumeration; +import sun.io.ByteToCharConverter; +import java.lang.String; + +/* + * ByteToCharConverterEnumeration return a Enumeration of String which + * represent ByteToCharConverter available in the classpath + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ +public class ByteToCharConverterEnumeration extends PrefetchEnumeration { + Enumeration fDelegate; +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public ByteToCharConverterEnumeration() + { + fDelegate = new ClasspathEntryEnumeration("sun.io", "ByteToChar", ".class"); + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + protected boolean verify(Object obj) + { + try { + ByteToCharConverter bc = ByteToCharConverter.getConverter((String)obj); + return true; + } catch (Exception e) + { + return false; + } + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + protected Object fetch() + { + for(; fDelegate.hasMoreElements(); ) + { + Object candidcate = fDelegate.nextElement(); + if(verify(candidcate)) + { + return candidcate; + } + } + return null; + } +} diff --git a/mozilla/grendel/calypso/util/CacheOutputStream.java b/mozilla/grendel/calypso/util/CacheOutputStream.java new file mode 100644 index 00000000000..b72f14fe255 --- /dev/null +++ b/mozilla/grendel/calypso/util/CacheOutputStream.java @@ -0,0 +1,409 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import calypso.util.TempFile; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.lang.OutOfMemoryError; + +/** This class provides buffering for arbitrarily large streams of data. + It is an OutputStream, so you just write the data you want to + cache to it. Then, when you've written all of your data (and called + close()) you can get the data back by calling + makeInputStream(), which returns an InputStream. + +

If you decide you don't want the data at all, you should call + discardBuffer() as early as possible, to avoid having a temporary + file stay around longer than necessary. It's not necessary to call + discardBuffer() if you have called makeInputStream(); + it is called automatically when that stream is closed. However, it is + ok to call discardBuffer() than once. + +

So, one conservative way of using this would be: +

    + CacheOutputStream output = new CacheOutputStream();
    + try {
    +   ... write bytes to `output' ...
    +   output.close();
    +
    +   InputStream input = output.makeInputStream();
    +   try {
    +     ... read bytes from `input' ... +
    +   } finally {
    +     input.close();
    +   }
    +
    + } finally {
    +   output.discardBuffer();
    + }

+ +

Implementation:
+

The bytes written to the stream are buffered in memory, until + a size threshold is reached, or a memory-allocation error occurs. + At that point, a temporary file is created, and buffering continues + there. Therefore, files are not used for small objects; this ensures + that buffering of small objects is fast, but buffering of arbitrarily + large objects is possible. +

+ */ + +public class CacheOutputStream extends OutputStream { + + protected byte mem_cache[] = null; + protected int mem_cache_fp = 0; // fill pointer + protected int mem_cache_max_size = 0; + protected double mem_cache_increment = 1.3; // expansion factor + + protected Object bs_handle = null; // backing store + protected OutputStream bs_stream = null; + + + /** Creates a CacheOutputStream with default buffer sizes. */ + public CacheOutputStream() { + this(64 * 1024); + } + + /** Creates a CacheOutputStream with the given maximum memory usage. + If an attempt is made to buffer more than the specified number of + bytes, then the memory buffer will be traded for a disk buffer. + */ + public CacheOutputStream(int max_resident_size) { + this(0, max_resident_size); + } + + /** Creates a CacheOutputStream with the given initial memory buffer size, + and given maximum memory usage. + If an attempt is made to buffer more than the specified number of + bytes, then the memory buffer will be traded for a disk buffer. + */ + public CacheOutputStream(int probable_size, int max_resident_size) { + if (probable_size > max_resident_size) + probable_size = 0; + if (probable_size < 1024) + probable_size = 1024; + mem_cache = new byte[probable_size]; + mem_cache_max_size = max_resident_size; + } + + public void write(byte bytes[]) throws IOException { + write (bytes, 0, bytes.length); + } + + public void write(byte bytes[], int start, int length) throws IOException { + if (mem_cache != null) + increaseCapacity(length); + if (mem_cache != null) { + System.arraycopy(bytes, start, mem_cache, mem_cache_fp, length); + mem_cache_fp += length; + } else { + bs_stream.write(bytes, start, length); + } + } + + public void write(int b) throws IOException { + if (mem_cache != null) + increaseCapacity(1); + if (mem_cache != null) { + mem_cache[mem_cache_fp++] = (byte) b; + } else { + bs_stream.write(b); + } + } + + /** Indicate (only) that no more data will be written in to the buffer. + After calling close(), one must eventually call either + makeInputStream() or discardBuffer(). + */ + public void close() throws IOException { + if (bs_stream != null) { + bs_stream.close(); + bs_stream = null; + } + } + + public void flush() throws IOException { + if (bs_stream != null) + bs_stream.flush(); + } + + + /** Ensure that there is room to write an additonal `count' bytes. + If this would exceed the memory cache size, set up the disk cache. + */ + protected void increaseCapacity(int count) throws IOException { + if (mem_cache == null) // already going to backing store... + return; + + int new_fp = mem_cache_fp + count; + if (new_fp < mem_cache.length) // it still fits... + return; + if (new_fp < mem_cache_max_size) { // time to grow the array... + growMemCache(count); + } else { // time to switch to backing store... + switchToBackingStore(); + } + } + + /** Assuming the memory cache is in use, expand the mem_cache to be + able to hold an additional `count' bytes if necessary. */ + protected void growMemCache(int count) throws IOException { + int new_fp = mem_cache_fp + count; + int new_size = (int) (mem_cache.length * mem_cache_increment); + if (new_size < mem_cache_fp + 1024) + new_size = mem_cache_fp + 1024; + if (new_size < new_fp) + new_size = new_fp; + + try { + byte new_mem_cache[] = new byte[new_size]; + System.arraycopy(mem_cache, 0, new_mem_cache, 0, mem_cache_fp); + mem_cache = new_mem_cache; + } catch (OutOfMemoryError e) { + // Couldn't allocate a new, bigger vector! + // That's fine, we'll just switch to backing-store mode. + switchToBackingStore(); + } + } + + /** Call this when you're done with this object. + It's important that you call this if you're not planning on calling + makeInputStream(); if you don't, a temp file could stay around longer + than necessary. + +

You must not use this CacheOutputStream object after having called + discardBuffer(). + +

If you call makeInputStream(), you must not call discardBuffer() + before you are finished with the returned stream. + */ + public synchronized void discardBuffer() { + mem_cache = null; + mem_cache_fp = 0; + + if (bs_stream != null) { + try { + bs_stream.close(); + } catch (IOException e) { + // just ignore it... + } + bs_stream = null; + } + + discardBackingStore(); + } + + /** Returns an InputStream that returns the data previously written + into this object. This may only be called once, and only after + close() has been called. It is also important that the + returned InputStream be closed when the caller is done with it; + failure to do so could cause a temp file to stay around longer + than necessary. + */ + public InputStream makeInputStream() + throws IOException, FileNotFoundException { + close(); + if (mem_cache != null) { + byte[] v = mem_cache; + int fp = mem_cache_fp; + mem_cache = null; + mem_cache_fp = 0; + return new ByteArrayInputStream(v, 0, fp); + } else if (bs_handle != null) { + return createBackingStoreInputStream(); + } else { + return null; + } + } + + /** Calls discardBuffer(), in case this object was dropped without being + properly shut down. */ + protected void finalize() { + discardBuffer(); + } + + + /** Assuming the memory cache is in use, switch to using the disk cache. */ + protected void switchToBackingStore() throws IOException { + TempFile f = TempFile.TempName(""); + OutputStream s = f.create(); + if (mem_cache_fp > 0) + s.write(mem_cache, 0, mem_cache_fp); + + mem_cache = null; + mem_cache_fp = 0; + + bs_handle = f; + bs_stream = s; + } + + /** Assuming backing-store is in use, delete the underlying temp file. */ + protected void discardBackingStore() { + if (bs_handle != null) { + TempFile f = (TempFile) bs_handle; + f.delete(); + bs_handle = null; + } + } + + /** Assuming backing-store is in use, creates an InputStream that reads + from the underlying TempFile, and will call this.discardBuffer() + when that InputStream reaches EOF. */ + protected InputStream createBackingStoreInputStream() + throws FileNotFoundException { + TempFile f = (TempFile) bs_handle; + InputStream s = new BufferedInputStream(new FileInputStream(f.getName())); + return new CacheInputStream(this, s); + } + + + +/* + public static final void main(String a[]) + throws FileNotFoundException, IOException { + String s = "0123456789"; + String ss = ""; + while (s.length() < 1000) + s = s + s; + System.err.println("s len " + s.length()); + + CacheOutputStream output = new CacheOutputStream(0); + try { + + for (int ii = 0; ii < (80 * 1024); ii += s.length()) { + System.err.println("writing " + s.length()); + output.write(s.getBytes()); + ss += s; + } + + output.close(); + + InputStream input = output.makeInputStream(); + try { + byte b[] = new byte[4000]; + int i; + String s2 = ""; + do { + i = input.read(b); + if (i >= 0) { + System.err.println("read " + i); + s2 = s2 + new String(b, 0, i); + } + } while (i >= 0); + + if (ss.equals(s2)) + System.err.println("read correct bytes"); + else + throw new Error("read wrong bytes"); + + } finally { + input.close(); + } + + } finally { + output.discardBuffer(); + } + } + */ + +} + + +/** This is the private class that is used for reading data out of the + file underlying a CacheOutputStream. This is used only when a file is + being used for cacheing, not when the memory buffer is being used. + Mainly this just passes the various InputStream methods along to + the underlying file stream; but it adds one other thing, which is + that as soon as the underlying file stream hits eof, the underlying + file is deleted (by calling CacheOutputStream.discardBuffer().) + */ +class CacheInputStream extends InputStream { + + protected CacheOutputStream stream_cache; + protected InputStream stream; + + CacheInputStream(CacheOutputStream sc, InputStream s) { + stream_cache = sc; + stream = s; + } + + public int available() throws IOException { + return stream.available(); + } + + public void close() throws IOException { + stream.close(); + stream = null; + discardBackingStore(); + } + + public void mark(int i) { + stream.mark(i); + } + + public boolean markSupported() { + return stream.markSupported(); + } + + public int read() throws IOException { + int result = stream.read(); + if (result < 0) discardBackingStore(); + return result; + } + + public int read(byte b[]) throws IOException { + int result = stream.read(b); + if (result < 0) discardBackingStore(); + return result; + } + + public int read(byte b[], int start, int length) throws IOException { + int result = stream.read(b, start, length); + if (result < 0) discardBackingStore(); + return result; + } + + public void reset() throws IOException { + stream.reset(); + } + + public long skip(long i) throws IOException { + return stream.skip(i); + } + + protected synchronized void discardBackingStore() { + if (stream_cache != null) { + stream_cache.discardBuffer(); + stream_cache = null; + } + } + + protected void finalize() { + discardBackingStore(); + } +} diff --git a/mozilla/grendel/calypso/util/CharArray.java b/mozilla/grendel/calypso/util/CharArray.java new file mode 100644 index 00000000000..5ee66d119a2 --- /dev/null +++ b/mozilla/grendel/calypso/util/CharArray.java @@ -0,0 +1,41 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +public class CharArray +{ + public static int indexOf(char[] str, int ch, int fromIndex) + { + int len = str.length; + for (int i = fromIndex; i < len; i++) + { + if (str[i] == ch) + { + return i; + } + } + return -1; + } + + public static int indexOf(char[] str, int ch) + { + return indexOf(str, ch, 0); + } +} diff --git a/mozilla/grendel/calypso/util/CharArrayIterator.java b/mozilla/grendel/calypso/util/CharArrayIterator.java new file mode 100644 index 00000000000..b79d99f8794 --- /dev/null +++ b/mozilla/grendel/calypso/util/CharArrayIterator.java @@ -0,0 +1,123 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.text.*; + +public final class CharArrayIterator implements CharacterIterator +{ + private char[] fText; + private int fStart; + private int fEnd; + private int fPosition; + + public CharArrayIterator(char[] aText, int aStartOffset, int aLength) + { + int endOffset = aStartOffset + aLength; + + if (aStartOffset < 0 || aLength < 0 || + (aStartOffset + aLength) > aText.length) + throw new IllegalArgumentException("parameter out of range"); + + fText = aText; + fStart = aStartOffset; + fEnd = endOffset; + fPosition = 0; + } + + public char first() + { + fPosition = fStart; + return fText[fPosition]; + } + + public char last() + { + fPosition = fEnd - 1; + return fText[fPosition]; + } + + public char current() + { + if (fPosition < fStart || fPosition >= fEnd) + return CharacterIterator.DONE; + + return fText[fPosition]; + } + + public char next() + { + fPosition += 1; + + if (fPosition >= fEnd) + return CharacterIterator.DONE; + + return fText[fPosition]; + } + + public char previous() + { + fPosition -= 1; + + if (fPosition < fStart) + return CharacterIterator.DONE; + + return fText[fPosition]; + } + + public char setIndex(int aPosition) + { + if (aPosition < fStart || aPosition >= fEnd) + throw new IllegalArgumentException("aPosition out of range"); + + fPosition = aPosition; + return fText[fPosition]; + } + + public int getBeginIndex() + { + return fStart; + } + + public int getEndIndex() + { + return fEnd; + } + + public int getIndex() + { + return fPosition; + } + + public Object clone() + { + try + { + CharArrayIterator other + = (CharArrayIterator) super.clone(); + + return other; + } + catch (CloneNotSupportedException e) + { + throw new InternalError(); + } + } +} diff --git a/mozilla/grendel/calypso/util/CharToByteConverterEnumeration.java b/mozilla/grendel/calypso/util/CharToByteConverterEnumeration.java new file mode 100644 index 00000000000..d7a64b5fd7a --- /dev/null +++ b/mozilla/grendel/calypso/util/CharToByteConverterEnumeration.java @@ -0,0 +1,85 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Frank Tang + */ + +package calypso.util; +import java.util.Enumeration; +import sun.io.CharToByteConverter; +/* + * CharToByteConverterEnumeration return a Enumeration of String + * which represent available CharToByteConverter in the class path + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + +public class CharToByteConverterEnumeration extends PrefetchEnumeration { + Enumeration fDelegate; +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public CharToByteConverterEnumeration() + { + fDelegate = new ClasspathEntryEnumeration("sun.io", "CharToByte", ".class"); + } + +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + protected boolean verify(Object obj) + { + try { + CharToByteConverter bc = CharToByteConverter.getConverter((String)obj); + return true; + } catch (Exception e) + { + return false; + } + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + protected Object fetch() + { + for(; fDelegate.hasMoreElements(); ) + { + Object candidcate = fDelegate.nextElement(); + if(verify(candidcate)) + { + return candidcate; + } + } + return null; + } +} diff --git a/mozilla/grendel/calypso/util/ClasspathEntryEnumeration.java b/mozilla/grendel/calypso/util/ClasspathEntryEnumeration.java new file mode 100644 index 00000000000..e5754971ffb --- /dev/null +++ b/mozilla/grendel/calypso/util/ClasspathEntryEnumeration.java @@ -0,0 +1,234 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Frank Tang + */ + +package calypso.util; +import java.util.Enumeration; +import java.io.File; +import java.lang.String; +import java.util.zip.ZipFile; +import java.util.zip.ZipEntry; +import java.util.StringTokenizer; +import java.util.Hashtable; + +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + +abstract class FilterEnumeration extends PrefetchEnumeration { + private Enumeration fDelegate; + public FilterEnumeration( Enumeration en) + { + fDelegate = en; + } + abstract protected boolean verify(Object obj); + protected Object fetch() + { + if(fDelegate.hasMoreElements()) { + Object candidcate; + for( candidcate = fDelegate.nextElement(); + fDelegate.hasMoreElements(); + candidcate = fDelegate.nextElement()) + { + if(this.verify(candidcate)) + return candidcate; + } + } + return null; + } +} + +class ZipFileEnumeration implements Enumeration { + class StringStartEndFilterEnumeration extends FilterEnumeration { + private String fStart; + private String fEnd; + public StringStartEndFilterEnumeration( + Enumeration en, + String start, + String end + ) + { + super(en); + fStart = start; + fEnd = end; + } + protected boolean verify(Object obj) + { + String str = (String)((ZipEntry) obj).getName(); + return str.startsWith(fStart) && str.endsWith(fEnd); + } + } + private Enumeration fDelegate; + private String fPrefix; + private String fPostfix; + + public ZipFileEnumeration ( + File zip ,String pkg, String prefix, String postfix) + { + fPrefix = pkg.replace('.', '/') + '/' + prefix; + fPostfix = postfix; + try { + ZipFile zf = new ZipFile(zip); + fDelegate = new + StringStartEndFilterEnumeration( + zf.entries(), + fPrefix, + fPostfix + ); + } + catch (Exception e) + { + System.out.println(e); // For Debug only + fDelegate = null; + } + } + public boolean hasMoreElements() + { + return ((fDelegate != null) && + fDelegate.hasMoreElements()); + } + public Object nextElement() + { + if(fDelegate == null) + return null; + String current = (String)((ZipEntry) fDelegate.nextElement()).getName(); + return current.substring( + fPrefix.length(), + (current.length() - fPostfix.length())); + } +} +class DirectoryEnumeration extends PrefetchEnumeration { + private String fList[] = null; + private String fPrefix; + private String fPostfix; + private int fIdx = 0; + + public DirectoryEnumeration ( + File dir ,String pkg, String prefix, String postfix) + { + fPrefix = prefix; + fPostfix = postfix; + File realdir = new File(dir, pkg.replace('.',File.separatorChar)); + if(realdir.exists() && realdir.isDirectory() && realdir.canRead()) + { + fList = realdir.list(); + fIdx = -1; + } + } + protected Object fetch() + { + if(fList != null) + { + for(fIdx++ ; fIdx < fList.length; fIdx++) + { + if( fList[fIdx].startsWith( fPrefix ) && + fList[fIdx].endsWith( fPostfix ) ) + { + return fList[fIdx].substring( + fPrefix.length(), + (fList[fIdx].length() - fPostfix.length())); + } + } + } + return null; + } +} + + + + +public class ClasspathEntryEnumeration extends PrefetchEnumeration { + + private String fPkg; + private String fPrefix; + private String fPostfix; + private Enumeration fEntryEnumeration; + private Enumeration fPathEnumeration; + private Hashtable cache; + +/* + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public ClasspathEntryEnumeration( String pkg, String prefix, String postfix) + { + fPrefix = prefix; + fPostfix = postfix; + fPkg = pkg; + fFetch = null; + cache = new Hashtable(20); + + fPathEnumeration = new StringTokenizer( + System.getProperty("java.class.path"), + File.pathSeparator); + fEntryEnumeration = null; + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + protected Object fetch() + { + + if((fEntryEnumeration != null) && + fEntryEnumeration.hasMoreElements()) + { + Object candidcate = fEntryEnumeration.nextElement(); + + if(cache.get(candidcate) != null) + return fetch(); // have been report once, try next + cache.put(candidcate, candidcate); + return candidcate; + + } + if(fPathEnumeration.hasMoreElements()) + { + String newPathEntry = (String) fPathEnumeration.nextElement(); + File newPathBase = new File(newPathEntry); + if(newPathBase.exists() && newPathBase.canRead()) + { + if(newPathBase.isDirectory()) + { + fEntryEnumeration = + new DirectoryEnumeration ( + newPathBase, fPkg, fPrefix, fPostfix); + } + else + { + fEntryEnumeration = + new ZipFileEnumeration ( + newPathBase, fPkg, fPrefix, fPostfix); + } + } + return fetch(); + } + return null; + } +} + diff --git a/mozilla/grendel/calypso/util/Comparer.java b/mozilla/grendel/calypso/util/Comparer.java new file mode 100644 index 00000000000..baf713d1ee3 --- /dev/null +++ b/mozilla/grendel/calypso/util/Comparer.java @@ -0,0 +1,30 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** This interface is the thing used by QSort to compare two objects. + @see QSort + */ +public interface Comparer { + /** Compare two objects for sorting. + Should return negative if A < B, positive if A > B, else 0. + */ + int compare(Object a, Object b); +} diff --git a/mozilla/grendel/calypso/util/ConfigUtils.java b/mozilla/grendel/calypso/util/ConfigUtils.java new file mode 100644 index 00000000000..37bd6465437 --- /dev/null +++ b/mozilla/grendel/calypso/util/ConfigUtils.java @@ -0,0 +1,72 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; +import java.net.*; + +public class ConfigUtils { + + private static String gFileSeparator = System.getProperty("file.separator"); + private static String gUserDir = System.getProperty("user.dir"); + + /** + * This is the standardized place for getting a list of URLs which + * you should search, in order, when trying to locate a resource + * which may be overridden by external files. This is used, at time + * of writing, by the Configuration system, which looks for things + * within packages, but first looks for external files which will + * effectively replace the package resources. + * aBaseClass is necessary if aLocation refers to a resource + * (which it normally will). In this case, we will need a class loader. + * supplied by aBaseClass. + * @param aLocation The resource path for locating the resource in + * its default location, a package. Use a full + * name, like "/netscape/shell/imp/ShellConfig.xml". + * Use a slash for the path separator character. + * @param aBaseClass A base class using the same classloader as + * aLocation. That is, a class from the same .jar file. + * Can be null if aLocation is a simple local system file. + * @return an enumeration of URLs to try, in order + */ + public static final Enumeration fileSearchURLs( + String aLocation, + Class aBaseClass) { + + URL url; + int nameIndex = aLocation.lastIndexOf('/'); + String fileName = nameIndex >= 0 && nameIndex < aLocation.length()-1 ? + aLocation.substring(nameIndex+1) : aLocation; + Vector urls = new Vector(2,2); + + // first, try to find a file with the given name in the user directory. + try { + // can user.dir be off the net (not a file URL) on a Network Computer? + urls.addElement(new URL("file", "", gUserDir + gFileSeparator + fileName)); + } catch (MalformedURLException mue) { + } + url = aBaseClass != null ? aBaseClass.getResource(aLocation) : + ClassLoader.getSystemResource(aLocation); + if (url != null) + urls.addElement(url); + + return urls.elements(); + } +} diff --git a/mozilla/grendel/calypso/util/Counter.java b/mozilla/grendel/calypso/util/Counter.java new file mode 100644 index 00000000000..be35a84528a --- /dev/null +++ b/mozilla/grendel/calypso/util/Counter.java @@ -0,0 +1,47 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * Like Integer except the value is mutable + */ +public class Counter { + protected int fValue; + + public Counter(int i) { + fValue = i; + } + + public int intValue() { + return fValue; + } + + public void increment() { + ++fValue; + } + + public void decrement() { + --fValue; + } + + public void setValue(int aNewValue) { + fValue = aNewValue; + } +} diff --git a/mozilla/grendel/calypso/util/DataExtension.java b/mozilla/grendel/calypso/util/DataExtension.java new file mode 100644 index 00000000000..8cdcf9100c1 --- /dev/null +++ b/mozilla/grendel/calypso/util/DataExtension.java @@ -0,0 +1,102 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +import java.util.Properties; +import java.util.Hashtable; +import java.lang.ClassLoader; +import java.net.URLEncoder; + +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ +public class DataExtension { + private Hashtable cache = null; + private boolean caseInsensitive = false; + private String prefix; + private String postfix; + private String[] urls; + /* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public DataExtension ( String prefix, String postfix, boolean caseInsensitive, String[] fallbackURLs) + { + cache = new Hashtable(20) ; + this.prefix = prefix; + this.postfix = postfix; + this.caseInsensitive = caseInsensitive; + this.urls = fallbackURLs; + + } + + /* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public String getValue (String key, String Name) + { + if(caseInsensitive) + key = URLEncoder.encode(key.toLowerCase()).toLowerCase(); + Hashtable group = null; + group = getGroup(key); + if(group != null) + return (String) group.get(Name); + else + return (String) null; + } + /* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + synchronized private Hashtable getGroup(String key) + { + Object lookup = null; + lookup = cache.get(key); + + if(lookup != null) + return (Hashtable) lookup; + + try { + Properties p = new Properties(); + String name = prefix + key + postfix; + p.load( ClassLoader.getSystemResourceAsStream( name ) ); + cache.put(key, p); + return (Hashtable) p; + } catch(Exception e) { + /* TO DO */ + /* do urls fallback here */ + return (Hashtable) null; + } + + } + +}; diff --git a/mozilla/grendel/calypso/util/DefaultPreferences.java b/mozilla/grendel/calypso/util/DefaultPreferences.java new file mode 100644 index 00000000000..5f3947091a1 --- /dev/null +++ b/mozilla/grendel/calypso/util/DefaultPreferences.java @@ -0,0 +1,56 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.Properties; + +public class DefaultPreferences extends Properties { + + public DefaultPreferences() { + } + + public void pref(String property, String value) { + put(property, value); + } + + public void pref(String property, int value) { + put(property, (String)(""+value)); + } + + public void pref(String property, boolean value) { + put(property, (String)(""+value)); + } + + public void pref(String property, char value) { + put(property, (String)(""+value)); + } + + public void pref(String property, long value) { + put(property, (String)(""+value)); + } + + public void pref(String property, float value) { + put(property, (String)(""+value)); + } + + public void pref(String property, double value) { + put(property, (String)(""+value)); + } +} diff --git a/mozilla/grendel/calypso/util/EnumerationEnumerator.java b/mozilla/grendel/calypso/util/EnumerationEnumerator.java new file mode 100644 index 00000000000..736989e8394 --- /dev/null +++ b/mozilla/grendel/calypso/util/EnumerationEnumerator.java @@ -0,0 +1,63 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +/** + * This takes a bunch of enumerators, and runs through them all, in the order + * given. + */ + +package calypso.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Vector; + +public class EnumerationEnumerator implements Enumeration { + Vector enums; + int cur; + + public EnumerationEnumerator() { + enums = new Vector(); + cur = 0; + } + + public void add(Enumeration e) { + enums.addElement(e); + } + + public boolean hasMoreElements() { + for (int i=cur ; i hashCodes[index] == EMPTY +// 2. Removed -> hashCodes[index] == REMOVED +// 3. Filled -> keys[index] != null + +// You must call Hashtable.hash() to get the real hash code for this table. + +// There needs to be a way to call Object.hashCode() directly when you want +// to do identity hashing. ALERT! + +/** Object subclass that implements a hash table. + * @note 1.0 toString() prints as formatted text + */ +abstract class HashtableBase implements Cloneable +{ + /** For the multiplicative hash, choose the golden ratio: + *

+      *     A = ((sqrt(5) - 1) / 2) * (1 << 32)
+      * 
+ * ala Knuth... + */ + static final int A = 0x9e3779b9; + + /** We use EMPTY and REMOVED as special markers in the table. If some + * poor object returns one of these two values as their hashCode, it + * is wacked to DEFAULT. + */ + static final int EMPTY = 0; + static final int REMOVED = 1; + static final int DEFAULT = 2; + + int count; + int totalCount; + int shift; + int capacity; + int indexMask; + + int hashCodes[]; + Object keys[]; + Object elements[]; + + /** Constructs an empty Hashtable. The Hashtable will grow on demand + * as more elements are added. + */ + HashtableBase () { + super(); + shift = 32 - 3 + 1; + } + + /** Constructs a Hashtable capable of holding at least + * initialCapacity elements before needing to grow. + */ + HashtableBase (int aInitialCapacity) { + this(); + + if (aInitialCapacity < 0) + throw new IllegalArgumentException("aInitialCapacity must be > 0"); + + grow (aInitialCapacity); + } + + /** Creates a shallow copy of the Hashtable. The table itself is cloned, + * but none of the keys or elements are copied. + */ + public Object clone() { + int len; + HashtableBase newTable; + + try { + newTable = (HashtableBase)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError( + "Error in clone(). This shouldn't happen."); + } + + // If there is nothing in the table, then we cloned to make sure the + // class was preserved, but just null everything out except for shift, + // which implies the default initial capacity. + + if (count == 0) { + newTable.shift = 32 - 3 + 1; + newTable.totalCount = 0; + newTable.capacity = 0; + newTable.indexMask = 0; + newTable.hashCodes = null; + newTable.keys = null; + newTable.elements = null; + + return newTable; + } + + len = hashCodes.length; + newTable.hashCodes = new int[len]; + newTable.keys = new Object[len]; + newTable.elements = new Object[len]; + + System.arraycopy(hashCodes, 0, newTable.hashCodes, 0, len); + System.arraycopy(keys, 0, newTable.keys, 0, len); + System.arraycopy(elements, 0, newTable.elements, 0, len); + + return newTable; + } + + /** Returns the number of elements in the Hashtable. + */ + public int count() { + return count; + } + + /** Returns the number of elements in the Hashtable. + */ + public int size() { + return count; + } + + /** Returns true if there are no elements in the Hashtable. + */ + public boolean isEmpty() { + return (count == 0); + } + + /** Returns an Enumeration of the Hashtable's keys. + * @see #elements + */ + public Enumeration keys() { + return new HashtableEnumerator(this, true); + } + + /** Returns an Enumeration of the Hashtable's elements. + * @see #keys + */ + public Enumeration elements() { + return new HashtableEnumerator(this, false); + } + + /** Returns a Vector containing the Hashtable's keys. + */ + public Vector keysVector() { + int i, vectCount; + Object key; + Vector vect; + + if (count == 0) + return new Vector(); + + vect = new Vector(count); + vectCount = 0; + + for (i = 0; i < keys.length && vectCount < count; i++) { + key = keys[i]; + if (key != null) { + vect.addElement(key); + vectCount++; + } + } + + return vect; + } + + /** Returns a Vector containing the Hashtable's elements. + */ + public Vector elementsVector() { + int i, vectCount; + Object element; + Vector vect; + + if (count == 0) + return new Vector(); + + vect = new Vector(count); + vectCount = 0; + + for (i = 0; i < elements.length && vectCount < count; i++) { + element = elements[i]; + if (element != null) { + vect.addElement(element); + vectCount++; + } + } + + return vect; + } + + /** Returns an Object array containing the Hashtable's keys. + */ + + int getKeysArray (Object[] aArray) { + int i, arrayCount; + Object key; + + arrayCount = 0; + + for (i = 0; i < keys.length && arrayCount < count; i++) { + key = keys[i]; + if (key != null) { + aArray[arrayCount++] = key; + } + } + + return arrayCount; + } + + /** Returns an Object array containing the Hashtable's elements. + */ + int getElementsArray (Object[] aArray) { + int i, arrayCount; + Object element; + + arrayCount = 0; + + for (i = 0; i < elements.length && arrayCount < count; i++) { + element = elements[i]; + if (element != null) { + aArray[arrayCount++] = element; + } + } + + return arrayCount; + } + + /** Returns true if the Hashtable contains the element. This method + * is slow -- O(n) -- because it must scan the table searching for the + * element. + */ + boolean containsElement (Object aElement) { + int i; + Object tmp; + + // We need to short-circuit here since the data arrays may not have + // been allocated yet. + + if (count == 0) + return false; + + if (aElement == null) + throw new NullPointerException(); + + if (elements == null) + return false; + + for (i = 0; i < elements.length; i++) { + tmp = elements[i]; + if (tmp != null && aElement.equals (tmp)) + return true; + } + + return false; + } + + + /** Removes key and the element associated with it from the + * Hashtable. Returns the element associated with key, or + * null if key was not present. + */ + Object removeKey (Object aKey) { + int index; + Object oldValue; + + // We need to short-circuit here since the data arrays may not have + // been allocated yet. + + if (count == 0) + return null; + + index = tableIndexFor(aKey, hash(aKey)); + oldValue = elements[index]; + if (oldValue == null) + return null; + + count--; + hashCodes[index] = REMOVED; + keys[index] = null; + elements[index] = null; + + return oldValue; + } + + /** Places the key/element pair in the Hashtable. Neither + * key nor element may be null. Returns the old + * element associated with key, or null if the key + * was not present. + */ + Object put (Object aKey, Object aElement) { + int index, hash; + Object oldValue; + + if (aElement == null) + throw new NullPointerException(); + + // Since we delay allocating the data arrays until we actually need + // them, check to make sure they exist before attempting to put + // something in them. + + if (hashCodes == null) + grow(); + + hash = hash(aKey); + index = tableIndexFor(aKey, hash); + oldValue = elements[index]; + + // If the total number of occupied slots (either with a real element or + // a removed marker) gets too big, grow the table. + + if (oldValue == null) { + if (hashCodes[index] == EMPTY) { + if (totalCount >= capacity) { + grow(); + return put(aKey, aElement); + } + totalCount++; + } + count++; + } + + hashCodes[index] = hash; + keys[index] = aKey; + elements[index] = aElement; + + return oldValue; + } + + /** We preclude the hashCodes EMPTY and REMOVED because we use them to + * indicate empty and previously filled slots in the table. All the + * Hashtable code should go through here and not call hashCode() + * directly on the key. + */ + int hash(Object aKey) { + int hash; + + // On sparc it appears that the last 3 bits of Object.hashCode() are + // insignificant! ALERT! + + hash = aKey.hashCode(); + if (hash == EMPTY || hash == REMOVED) + hash = DEFAULT; + + return hash; + } + + /** Primitive method used internally to find slots in the + * table. If the key is present in the table, this method will return the + * index + * under which it is stored. If the key is not present, then this + * method will return the index under which it can be put. The caller + * must look at the hashCode at that index to differentiate between + * the two possibilities. + */ + int tableIndexFor(Object aKey, int aHash) { + int product, testHash, index, step, removedIndex, probeCount; + + product = aHash * A; + index = product >>> shift; + + // Probe the first slot in the table. We keep track of the first + // index where we found a REMOVED marker so we can return that index + // as the first available slot if the key is not already in the table. + + testHash = hashCodes[index]; + + if (testHash == aHash) { + if (aKey.equals (keys[index])) + return index; + removedIndex = -1; + } else if (testHash == EMPTY) + return index; + else if (testHash == REMOVED) + removedIndex = index; + else + removedIndex = -1; + + // Our first probe has failed, so now we need to start looking + // elsewhere in the table. + + step = ((product >>> (2 * shift - 32)) & indexMask) | 1; + probeCount = 1; + + do { + probeCount++; + index = (index + step) & indexMask; + + testHash = hashCodes[index]; + + if (testHash == aHash) { + if (aKey.equals (keys[index])) + return index; + } else if (testHash == EMPTY) { + if (removedIndex < 0) + return index; + else + return removedIndex; + } else if (testHash == REMOVED && removedIndex == -1) + removedIndex = index; + + } while (probeCount <= totalCount); + + // Something very bad has happened. + + throw new Error ("Hashtable overflow"); + } + + /** Grows the table to accommodate at least capacity number of elements. + */ + private void grow (int aCapacity) { + int tableSize, power; + + // Find the lowest power of 2 size for the table which will allow + // us to insert initialCapacity number of objects before having to + // grow. + + tableSize = (aCapacity * 4) / 3; + + power = 3; + while ((1 << power) < tableSize) + power++; + + // Once shift is set, then grow() will do the right thing when + // called. + + shift = 32 - power + 1; + grow(); + } + + /** Grows the table by a factor of 2 (or creates it if necessary). All + * the REMOVED markers go away and the elements are rehashed into the + * bigger table. + */ + protected void grow() { + int i, index, hash, power, oldHashCodes[]; + Object key, oldKeys[], oldValues[]; + + // The table size needs to be a power of two, and it should double + // when it grows. We grow when we are more than 3/4 full. + + shift--; + power = 32 - shift; + indexMask = (1 << power) - 1; + capacity = (3 * (1 << power)) / 4; + + oldHashCodes = hashCodes; + oldKeys = keys; + oldValues = elements; + + hashCodes = new int[1 << power]; + keys = new Object[1 << power]; + elements = new Object[1 << power]; + + // Reinsert the old elements into the new table if there are any. Be + // sure to reset the counts and increment them as the old entries are + // put back in the table. + + totalCount = 0; + + if (count > 0) { + count = 0; + + for (i = 0; i < oldHashCodes.length; i++) { + key = oldKeys[i]; + + if (key != null) { + hash = oldHashCodes[i]; + index = tableIndexFor(key, hash); + + hashCodes[index] = hash; + keys[index] = key; + elements[index] = oldValues[i]; + + count++; + totalCount++; + } + } + } + } + + /** Removes all keys and elements from the Hashtable. + */ + public void clear() { + int i; + + if (hashCodes == null) + return; + + for (i = 0; i < hashCodes.length; i++) { + hashCodes[i] = EMPTY; + keys[i] = null; + elements[i] = null; + } + + count = 0; + totalCount = 0; + } + + /** Returns a string serialization of the Hashtable using the + * Serializer. + * @see Serializer + */ + public String toString () + { + if ((count == 0) || (keys == null)) + return "{empty}"; + + StringBuf buffer = new StringBuf ("{\n"); + + Object key; + Object element; + for (int index = 0; index < elements.length; index++) + { + key = keys[index]; + if (null != key) + { + element = elements[index]; + buffer.append (" " + key + " = " + element); + } + } + buffer.append ("}"); + + return buffer.toString (); + } + +} + + +class HashtableEnumerator implements Enumeration { + boolean keyEnum; + int index; + int returnedCount; + HashtableBase table; + + HashtableEnumerator(HashtableBase table, boolean keyEnum) { + super(); + this.table = table; + this.keyEnum = keyEnum; + returnedCount = 0; + + if (table.keys != null) + index = table.keys.length; + else + index = 0; + } + + public boolean hasMoreElements() { + if (returnedCount < table.count) + return true; + + return false; + } + + public Object nextElement() { + index--; + + while (index >= 0 && table.elements[index] == null) + index--; + + if (index < 0 || returnedCount >= table.count) + throw new NoSuchElementException(); + + returnedCount++; + + if (keyEnum) + return table.keys[index]; + + return table.elements[index]; + } +} + diff --git a/mozilla/grendel/calypso/util/HashtableLite.java b/mozilla/grendel/calypso/util/HashtableLite.java new file mode 100644 index 00000000000..c5755d8591a --- /dev/null +++ b/mozilla/grendel/calypso/util/HashtableLite.java @@ -0,0 +1,340 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +class ElementEnumeration + implements Enumeration +{ + private int fIndex; + private HashtableLite fTable; + + public ElementEnumeration (HashtableLite theTable) + { + fTable = theTable; + } + + public boolean hasMoreElements () + { + if (fIndex < fTable.fCount) return true; + else return false; + } + + public Object nextElement () + { + return fTable.fItems[(fIndex++*2) + 1]; + } +} + +class KeyEnumeration + implements Enumeration +{ + private int fIndex; + private HashtableLite fTable; + + KeyEnumeration (HashtableLite theTable) + { + fTable = theTable; + } + + public boolean hasMoreElements () + { + if (fIndex < fTable.fCount) return true; + else return false; + } + + public Object nextElement () + { + return fTable.fItems[fIndex++*2]; + } +} + + /** + * A utility class to avoid the pain (and memory bloat) + * of making lots of small hashtables. A real HashTable + * isn't created until the number of elements grows past + * a certain number (defined internally to HashtableLite). + * + * It sure would have been nice if Hashtables were an interface. + * + * @author thom 08-19-97 2:06am + */ + +public class HashtableLite extends Object + implements java.lang.Cloneable +{ + int fCount; + Object fItems[]; // [0] = key, [1] = item, etc + AtomHashtable fRealTable; + + private static final int maxItems = 4; + + public HashtableLite () + { + } + + public void clear() + { + fCount = 0; + fRealTable = null; + fItems = null; + } + + public Object clone() + { + HashtableLite theClone = new HashtableLite(); + + if (fRealTable != null) + { + theClone.fRealTable = (AtomHashtable) fRealTable.clone(); + } + else if (fCount > 0) + { + theClone.fItems = (Object[]) fItems.clone(); + theClone.fCount = fCount; + } + return theClone; + } + + public boolean contains (Object item) + { + if (fCount > 0) + { + for (int i = 0; i < fCount; ++i ) + if (fItems[(i*2) + 1] == item) + return true; + return false; + } + if (fRealTable != null) + return fRealTable.contains (item); + + return false; + } + + public boolean containsKey (Atom key) + { + if (fCount > 0) + { + for (int i = 0; i < fCount; ++i ) + if (fItems[i*2] == key) + return true; + return false; + } + if (fRealTable != null) + return fRealTable.containsKey (key); + + return false; + } + + public int count () + { + if (fRealTable != null) return fRealTable.count(); + return fCount; + } + + public Enumeration elements() + { + if (fCount > 0) + return new ElementEnumeration (this); + + if (fRealTable != null) + return fRealTable.elements(); + + return NullEnumeration.kInstance; + } + + public Object[] elementsArray() + { + if (fRealTable != null) + return fRealTable.elementsArray(); + Assert.NotYetImplemented("Arg"); + return null; + } + + public Vector elementsVector () + { + if (fRealTable != null) + return fRealTable.elementsVector(); + Assert.NotYetImplemented("Arg"); + return null; + } + + /*public void decode (Decoder aDecoder) + { + if (fRealTable != null) + fRealTable.decode(aDecoder); + + } + + public void describeClassInfo (ClassInfo theInfo) + { + if (fRealTable != null) + fRealTable.describeClassInfo(theInfo); + + } + + public void encode (Encoder anEncoder) + { + if (fRealTable != null) + fRealTable.encode (anEncoder); + + } + + public void finishDecoding() + { + if (fRealTable != null) + fRealTable.finishDecoding(); + + }*/ + + public Object get (Atom key) + { + if (fCount > 0) + { + for (int i = 0; i < fCount; ++i ) + if (fItems[i*2] == key) + return fItems[(i*2) + 1]; + return null; + } + else if (fRealTable != null) + return fRealTable.get (key); + + return null; + } + + public boolean isEmpty () + { + if (fCount == 0 && fRealTable == null) + return true; + + return false; + } + + public Object put (Atom key, Object item) + { + if (fRealTable != null) + { + return fRealTable.put (key, item); + } + else + { + if (fItems == null) fItems = new Object[maxItems*2]; + + // is it already in the table? + + for (int i = 0; i < fCount; ++i ) + if (fItems[i*2] == key) + { + Object oldItem = fItems[(i*2) + 1]; + fItems[(i*2) + 1] = item; + return oldItem; + } + + // no, must add new key/item pair + + if (fCount == maxItems) // spill over to real hashtable? + { + fRealTable = new AtomHashtable(); + + for (int i = 0; i < fCount; ++i ) + fRealTable.put (fItems[i*2], fItems[(i*2) + 1]); + + fItems = null; + fCount = 0; + + return fRealTable.put (key, item); + } + + // simple case, add the item + + fItems[fCount*2] = key; + fItems[(fCount*2) + 1] = item; + ++fCount; + } + return null; + } + + public Enumeration keys () + { + if (fRealTable != null) + return fRealTable.keys(); + + if (fCount > 0) return new KeyEnumeration (this); + + return NullEnumeration.kInstance; + } + + public Atom[] keysArray() + { + if (fRealTable != null) + return fRealTable.keysArray(); + Assert.NotYetImplemented("Arg"); + return null; + } + + public Vector keysVector() + { + if (fRealTable != null) + return fRealTable.keysVector(); + Assert.NotYetImplemented("Arg"); + return null; + } + + public Object remove (Atom key) + { + if (fRealTable != null) + return fRealTable.remove(key); + + if (fCount > 0) + for (int i = 0; i < fCount; ++i ) + if (fItems[i*2] == key) + { + Object oldItem = fItems[i*2 + 1]; + + --fCount; + for (int j = i; j < fCount - 1; ++j) + { + fItems[(j*2) + 1] = fItems[((j+1)*2) + 1]; + fItems[ j*2] = fItems[ (j+1)*2]; + } + return oldItem; + } + return null; + } + + public int size() + { + if (fRealTable != null) + return fRealTable.size(); + + return fCount; + } + + public String toString () + { + if (fRealTable != null) + return fRealTable.toString(); + + return "Hi, I'm a hack"; + } +} + + diff --git a/mozilla/grendel/calypso/util/HashtableRecycler.java b/mozilla/grendel/calypso/util/HashtableRecycler.java new file mode 100644 index 00000000000..a843e751452 --- /dev/null +++ b/mozilla/grendel/calypso/util/HashtableRecycler.java @@ -0,0 +1,117 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * Static recycler class for java.util.Hashtable + * + * @author psl 04-23-97 6:48pm + */ +public final class HashtableRecycler +{ + static Recycler gRecycler; + + /** + * Get one + * + * @return + * @exception + * @author psl 04-23-97 6:49pm + */ + public static synchronized Hashtable Alloc () + { + Hashtable result = null; + + if (null != gRecycler) + result = (Hashtable)gRecycler.getRecycledObject (); + if (null == result) + result = new Hashtable (); + return result; + } + + /** + * Get one with an initial size + * since the growth mechanism is private (for now) a recycled + * table won't really grow to capacity + * + * @param aCapacity desired initial capacity + * @return + * @exception + * @author psl 04-23-97 6:49pm + */ + public static synchronized Hashtable Alloc (int aCapacity) + { + Hashtable result = null; + + if (null != gRecycler) + result = (Hashtable)gRecycler.getRecycledObject (); + if (null == result) + result = new Hashtable (aCapacity); + return result; + } + + /** + * Recycle a hashtable + * (it will be cleared by the recycler) + * + * @param aTable table to be recycled + * @return + * @exception + * @author psl 04-23-97 6:50pm + */ + public static synchronized void Recycle (Hashtable aTable) + { + if (null == gRecycler) + gRecycler = new Recycler (); + aTable.clear (); + gRecycler.recycle (aTable); + } + + /** + * Empty the recycler, it's earth day + * + * @param + * @return + * @exception + * @author psl 04-21-97 4:29pm + */ + static synchronized public void EmptyRecycler () + { + if (null != gRecycler) + gRecycler.empty (); + } + + /** + * finalize class for unloading + * + * @param + * @return + * @exception + * @author psl 04-21-97 4:29pm + */ + static void classFinalize () throws Throwable + { + // Poof! Now we are unloadable even though we have a static + // variable. + } +} + diff --git a/mozilla/grendel/calypso/util/IDMap.java b/mozilla/grendel/calypso/util/IDMap.java new file mode 100644 index 00000000000..fcac9129063 --- /dev/null +++ b/mozilla/grendel/calypso/util/IDMap.java @@ -0,0 +1,294 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * An identifier mapper. This provides a 1-1 map between strings and + * integers where the strings are unique within the map. + */ +public class IDMap { + private Object[] fKeys; + private int[] fHashCodes; + private int[] fValues; + private String[] fStrings; + private int fNumStrings; + private int fShift; + private int fIndexMask; + private int fCount; + private int fCapacity; + + public IDMap() { + fShift = 32 - 3 + 1; + } + + /** + * Given a string, find an id for it. If the string is already + * present return the old id. Otherwise allocate a new one. + */ + public synchronized int stringToID(String aStr) { + if (fHashCodes == null) { + grow(); + } + int hash = hashCodeForString(aStr); // hashCodeFor(aStr); + int slot = tableIndexFor(aStr, hash); + if (fHashCodes[slot] == EMPTY) { + // If the total number of occupied slots gets too big, grow the + // table. + if (fCount >= fCapacity) { + grow(); + return stringToID(aStr); + } + fCount++; + fHashCodes[slot] = hash; + fKeys[slot] = aStr; + fValues[slot] = addString(aStr); + } + return fValues[slot]; + } + + /** + * Given a StringBuf, find an id for it. If the string is already + * present return the old id. Otherwise allocate a new one. + */ + public synchronized int stringBufToID(StringBuf aStrBuf) { + if (fHashCodes == null) { + return( stringToID( aStrBuf.toString() ) ); + } + int hash = hashCodeForStringBuf(aStrBuf); + int slot = tableIndexFor(aStrBuf, hash); + if (fHashCodes[slot] == EMPTY) { + // not found, insert it. + return( stringToID( aStrBuf.toString() ) ); + } + return fValues[slot]; + } + + public synchronized String stringBufToString(StringBuf aStrBuf) { + int id = stringBufToID(aStrBuf); + return fStrings[id]; + } + + /** + * If the argument string is already present in the map return the + * original string object. Otherwise, allocate a new id and return + * the argument string. + */ + public synchronized String stringToString(String aStr) { + int id = stringToID(aStr); + return fStrings[id]; + } + + /** + * Given an id, return the string it maps to. + */ + public String idToString(int aID) { + if ((aID < 0) || (aID >= fNumStrings)) { + throw new IllegalArgumentException("invalid id: " + aID); + } + return fStrings[aID]; + } + + /** + * Add string to the end of the internal strings array. Return the + * index in the array. + */ + private int addString(String aString) { + int rv = fNumStrings; + if (fStrings == null) { + fStrings = new String[4]; + } else if (rv == fStrings.length) { + int newLen = rv; + if (newLen < 32) { + // Double the size of small tables + newLen = newLen * 2; + } else { + // Grow by 20% for larger tables + newLen = (int) (newLen * 1.2f); + } + String[] ns = new String[newLen]; + System.arraycopy(fStrings, 0, ns, 0, rv); + fStrings = ns; + } + fStrings[fNumStrings++] = aString; + return rv; + } + + //---------------------------------------------------------------------- + // Hashtable code lifted from the netscape.util package; retargeted + // to use int's as the value's; also removed the "REMOVE" code. + + /** For the multiplicative hash, choose the golden ratio: + *
+   *     A = ((sqrt(5) - 1) / 2) * (1 << 32)
+   * 
+ * ala Knuth... + */ + private static final int A = 0x9e3779b9; + + /** + * We use EMPTY and REMOVED as special markers in the table. If some + * poor object returns one of these two values as their hashCode, it + * is wacked to DEFAULT. + */ + private static final int EMPTY = 0; + private static final int DEFAULT = 2; + + /** + * Provide hashcode for a given key + */ +/* private final int hashCodeFor(Object aKey) { + int hash = aKey.hashCode(); + if (hash == EMPTY) + hash = DEFAULT; + return hash; + } +*/ + // We want StringBuf and String have the same hash code, if + // they contain the same string. Therefor, we need to do our + // own hash code for String. + private final int hashCodeForString(String str ) { + int h = 0; + int len = str.length(); + if (len < 16) { + for (int i = 0; i < len; i++) { + h = (h * 37) + str.charAt(i); + } + } else { + // only sample some characters + int skip = len / 8; + for (int i = len, off = 0 ; i > 0; i -= skip, off += skip) { + h = (h * 39) + str.charAt(off); + } + } + if (h == EMPTY) + h = DEFAULT; + return h; + } + + private final int hashCodeForStringBuf(StringBuf strBuf) { + int h = 0; + int len = strBuf.length(); + if (len < 16) { + for (int i = 0; i < len; i++) { + h = (h * 37) + strBuf.charAt(i); + } + } else { + // only sample some characters + int skip = len / 8; + for (int i = len, off = 0 ; i > 0; i -= skip, off += skip) { + h = (h * 39) + strBuf.charAt(off); + } + } + if (h == EMPTY) + h = DEFAULT; + return h; + } + + private final boolean keysAreEqual(Object aKey1, Object aKey2) { + // It is emportant to use aKey1's equals because aKey1 can be + // a String or StringBuf. + return aKey1.equals (aKey2); + } + + /** + * Primitive method used internally to find slots in the table. If + * the key is present in the table, this method will return the + * index under which it is stored. If the key is not present, then + * this method will return the index under which it can be put. The + * caller must look at the hashCode at that index to differentiate + * between the two possibilities. + */ + private int tableIndexFor(Object aKey, int aHash) { + // Probe the first slot in the table. We keep track of the first + // index where we found a REMOVED marker so we can return that index + // as the first available slot if the key is not already in the table. + int product = aHash * A; + int index = product >>> fShift; + int testHash = fHashCodes[index]; + if (testHash == aHash) { + if (keysAreEqual(aKey, fKeys[index])) + return index; + } else if (testHash == EMPTY) + return index; + + // Our first probe has failed, so now we need to start looking + // elsewhere in the table. + int step = ((product >>> (2 * fShift - 32)) & fIndexMask) | 1; + int probeCount = 1; + do { + probeCount++; + index = (index + step) & fIndexMask; + + testHash = fHashCodes[index]; + + if (testHash == aHash) { + if (keysAreEqual(aKey, fKeys[index])) + return index; + } else if (testHash == EMPTY) { + return index; + } + } while (probeCount <= fCount); + + // Something very bad has happened. + throw new Error ("Hashtable overflow"); + } + + /** + * Grows the table by a factor of 2 (or creates it if necessary). All + * the REMOVED markers go away and the elements are rehashed into the + * bigger table. + */ + private void grow() { + // The table size needs to be a power of two, and it should double + // when it grows. We grow when we are more than 3/4 full. + fShift--; + int power = 32 - fShift; + fIndexMask = (1 << power) - 1; + fCapacity = (3 * (1 << power)) / 4; + + int[] oldHashCodes = fHashCodes; + Object[] oldKeys = fKeys; + int[] oldValues = fValues; + + fHashCodes = new int[1 << power]; + fKeys = new Object[1 << power]; + fValues = new int[1 << power]; + + // Reinsert the old elements into the new table if there are any. Be + // sure to reset the counts and increment them as the old entries are + // put back in the table. + if (fCount > 0) { + fCount = 0; + for (int i = 0; i < oldHashCodes.length; i++) { + Object key = oldKeys[i]; + if (key != null) { + int hash = oldHashCodes[i]; + int index = tableIndexFor(key, hash); + + fHashCodes[index] = hash; + fKeys[index] = key; + fValues[index] = oldValues[i]; + + fCount++; + } + } + } + } +} diff --git a/mozilla/grendel/calypso/util/LineBufferingInputStream.java b/mozilla/grendel/calypso/util/LineBufferingInputStream.java new file mode 100644 index 00000000000..2a81d88ac60 --- /dev/null +++ b/mozilla/grendel/calypso/util/LineBufferingInputStream.java @@ -0,0 +1,185 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +public class LineBufferingInputStream extends FilterInputStream { + + private static final boolean DEBUG = false; + + private static final int CAPACITY = 1024; + + ByteBuf curline = new ByteBuf(); + ByteBuf inbuf = new ByteBuf(CAPACITY); + ByteLineBuffer linebuf = new ByteLineBuffer(); + boolean eof = false; + + public LineBufferingInputStream(InputStream s) { + super(s); + + if (DEBUG) { + System.err.println("LBIS " + hashCode() + ": created for " + s); + } + } + + /** Reads bytes into a portion of an array. This method will block + until some input is available. +

+ The returned data will not be more than one line long: that is, there + will be at most one linebreak (CR, LF, or CRLF) and if it is present, + it will be at the end. +

+ If the next line available to be read is smaller than `length', + then the first `length' bytes of that line will be returned, and + the remainder of the bytes on that line will be available for + subsequent reads. + + @return the total number of bytes read into the buffer, or -1 if + there is no more data because the end of the stream has + been reached. + */ + public int read(byte[] buf, int start, int length) throws IOException { + + if (DEBUG) { + System.err.println("LBIS " + hashCode() + ": read(byte[], " + + start + ", " + length + ")"); + } + + + if (length <= 0) return length; + int curlen = curline.length(); + if (curlen == 0) { + if (eof) return -1; + while (!linebuf.pullLine(curline)) { + inbuf.setLength(CAPACITY); + int inlen = in.read(inbuf.toBytes(), 0, CAPACITY); + + if (inlen <= 0) { + eof = true; + linebuf.pushEOF(); + linebuf.pullLine(curline); // Try for the last gasp... + break; + } + + inbuf.setLength(inlen); + + if (DEBUG) { + byte b[] = inbuf.toBytes(); + System.err.print("LBIS " + hashCode() + ": read " + inlen + + " bytes: "); + for (int i = 0; i < inlen; i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\n"); + } + + linebuf.pushBytes(inbuf); + } + + curlen = curline.length(); + if (curlen == 0) return -1; + } + int result = length; + if (result > curlen) result = curlen; + curline.getBytes(0, result, buf, start); + curlen -= result; + curline.remove(0, result); + + if (DEBUG) { + byte b[] = curline.toBytes(); + System.err.print("LBIS " + hashCode() + ": read line: "); + for (int i = 0; i < curline.length(); i++) + System.err.print((b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1))); + System.err.print("\n"); + } + + return result; + } + + + public int read(byte[] buf) throws IOException { + return read(buf, 0, buf.length); + } + + byte[] singlebyte; + public int read() throws IOException { + if (singlebyte == null) singlebyte = new byte[1]; + if (read(singlebyte, 0, 1) <= 0) return -1; + return singlebyte[0]; + } + + public long skip(long n) { + return 0; // Feh. + } + + public boolean markSupported() { + return false; + } + + public void close() throws IOException { + + if (DEBUG) { + System.err.println("LBIS " + hashCode() + ": closed"); + } + + curline = null; + inbuf = null; + linebuf = null; + eof = true; + super.close(); + } + + /** Sets the EOL characters to look for in the incoming stream. Setting + * this to be null will cause it to look for any of , , or . + * Note that null (the default) could cause up to one extra to be held in + * the buffer (in the case where the last byte read from the underlying + * stream was , and no following byte (or EOF) has yet been read.) + */ + public void setInputEOL(ByteBuf buf) { + linebuf.setInputEOL(buf); + } + + /** + * End of line characters & or any combination + * will be replaced by this string if it is present. Setting + * this to a zero length string will cause them to be stripped. + * Setting this to null will cause the EOL characters to be passed + * through unchanged (the default). + * @param buf ByteBuf representing EOL replacement string + */ + public void setOutputEOL(ByteBuf buf) { + linebuf.setOutputEOL(buf); + } + + +} diff --git a/mozilla/grendel/calypso/util/ListenerList.java b/mozilla/grendel/calypso/util/ListenerList.java new file mode 100644 index 00000000000..036ac29b192 --- /dev/null +++ b/mozilla/grendel/calypso/util/ListenerList.java @@ -0,0 +1,184 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.EventListener; +import java.util.Vector; +import java.util.Enumeration; + + +/** + * ListenersList is a thread-safe, reentrant class which + * contains a list of EventListeners and enforce some + * policy on event dispatching.

+ + * This class guarantees that events delivering is correctly + * accomplished even if listeners are removed from or added to the list during + * event dispatching. Added listeners, though, will not receive the current event.

+ + * Event listeners are returned LIFO.

+ + * User of this class must enclose event dispatching between + * startDelivering()/stopDelivering() calls and pass the state object + * to the nextListener call.

+ *

+ *    ListenerListState state = eventListeners.startDelivering();
+ *
+ *    SomeEventListener el = (SomeEventListener)eventListeners.nextListener(state);
+ *    while (null != el) {
+ *      el.someEvent();
+ *      el = (SomeEventListener)eventListeners.nextListener(state);
+ *    }
+ *
+ *    eventListeners.stopDelivering(state);
+ * 
+ */ +public class ListenerList +{ + private Vector fListeners; // list of listeners + private Vector fEnumerators; // list of enumerators for this list of listeners + + /** + * Construct an array of listeners with the specifed size.

+ * The array size is doubled every time an element is added + * to a full array. + * + * @param aSize the required size + */ + public ListenerList(int aSize) + { + fListeners = new Vector(aSize); + } + + /** + * Construct an array of listeners with the specifed size.

+ * The array size is doubled every time an element is added + * to a full array. + * + */ + public ListenerList() + { + fListeners = new Vector(1); + } + + /** + * Set a new event listener for the class of events + * this ListenersList is mantaining. + * + * @param aListener a listener for the specific set of events + */ + public void addListener(EventListener aListener) + { + fListeners.addElement(aListener); + } + + /** + * Remove a listener from the list of listeners this + * ListenersList is mantaining.

+ * Existing and valid state object are updated to reflect + * this change. + * + * @param aListener a listener for the specific set of events + */ + public void removeListener(EventListener aListener) + { + synchronized(fListeners) { + // find and remove the element + int index = fListeners.indexOf(aListener); + if (index != -1) { + try { + fListeners.removeElementAt(index); + } catch (ArrayIndexOutOfBoundsException e) { + Assert.Assertion(false); // no way!! + } + + // go through the list of live state objects and update the + // index if necessary + if (fEnumerators != null) { + for (int i =0; i < fEnumerators.size(); i++) { + ListenerListState state = (ListenerListState)fEnumerators.elementAt(i); + if (index < state.fIndex) { + state.fIndex--; + } + } + } + + } + } + } + + /** + * A user of this class is starting event delivery.

+ * The state object represents the state of the list for + * that user and has to be passed in every nextListener call. + * + * @param aListener a listener for the specific set of events + * @return ListenerListState the state of the list for the caller + */ + public ListenerListState startDelivering() + { + synchronized(fListeners) { + ListenerListState state = new ListenerListState(fListeners); + if (null == fEnumerators) { + fEnumerators = new Vector(1); + } + fEnumerators.addElement(state); + return state; + } + } + + /** + * A user completed or aborted event delivering.

+ * + * @return aState the state of the list for the caller + */ + public void stopDelivering(ListenerListState aState) + { + synchronized(fListeners) { + fEnumerators.removeElement(aState); + } + } + + /** + * Return the next EventListener in the array.

+ * Listeners are returned with a last-in/first-out (LIFO) policy. + * + * @return aState the state of the list for the caller + */ + public EventListener nextListener(ListenerListState aState) + { + synchronized(fListeners) { + EventListener listener = null; + + if (--aState.fIndex >= 0) { + try { + listener = (EventListener)fListeners.elementAt(aState.fIndex); + } catch (ArrayIndexOutOfBoundsException e) { + Assert.Assertion(false); + aState.fIndex = -1; // invalid state + } + } + + return listener; + } + } + +} + diff --git a/mozilla/grendel/calypso/util/ListenerListState.java b/mozilla/grendel/calypso/util/ListenerListState.java new file mode 100644 index 00000000000..01163705be0 --- /dev/null +++ b/mozilla/grendel/calypso/util/ListenerListState.java @@ -0,0 +1,46 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.Vector; + +/** + * ListenersListState keeps the index into the array of + * listeners for the ListListeners class.

+ * Because a state is created every time event dispatching + * occurs instances of this class act like local references + * into the array and hence are thread-safe.

+ * This class does not expose any behavior and is entirely + * for use by the ListenerList. + */ +public class ListenerListState +{ + int fIndex; + + ListenerListState(Vector aVector) + { + fIndex = aVector.size(); + } + + ListenerListState() + { + fIndex = -1; + } +} diff --git a/mozilla/grendel/calypso/util/Makefile b/mozilla/grendel/calypso/util/Makefile new file mode 100644 index 00000000000..e3d4e2fb65f --- /dev/null +++ b/mozilla/grendel/calypso/util/Makefile @@ -0,0 +1,83 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + Abacus.java \ + ArrayEnumeration.java \ + Assert.java \ + AssertionError.java \ + AssertionException.java \ + Atom.java \ + AtomHashtable.java \ + AttributeValuePair.java \ + ByteBuf.java \ + ByteLineBuffer.java \ + ByteToCharConverterEnumeration.java \ + CacheOutputStream.java \ + CharArray.java \ + CharArrayIterator.java \ + CharToByteConverterEnumeration.java \ + ClasspathEntryEnumeration.java \ + Comparer.java \ + ConfigUtils.java \ + Counter.java \ + DataExtension.java \ + DefaultPreferences.java \ + EnumerationEnumerator.java \ + HashtableBase.java \ + HashtableLite.java \ + HashtableRecycler.java \ + IDMap.java \ + LineBufferingInputStream.java \ + ListenerList.java \ + ListenerListState.java \ + MemoryManager.java \ + MemoryMonitor.java \ + MemoryPressure.java \ + NetworkDate.java \ + NullEnumeration.java \ + NullJavaEnumeration.java \ + Preferences.java \ + PreferencesBase.java \ + PreferencesFactory.java \ + PrefetchEnumeration.java \ + QSort.java \ + RWLock.java \ + RandomAccessFileWithByteLines.java \ + Recycler.java \ + SelfTest.java \ + SelfTestAtom.java \ + SelfTestException.java \ + SelfTestIDMap.java \ + SelfTestRWLock.java \ + SignedInteger.java \ + SingleEnumeration.java \ + StringBuf.java \ + StringBufRecycler.java \ + StringUtils.java \ + TempFile.java \ + TimeBomb.java \ + URLClassLoader.java \ + Vec.java \ + VectorRecycler.java \ + WeakLink.java \ + WeakLinkArray.java \ + WeakLinkArrayEnumeration.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/calypso/util/MemoryManager.java b/mozilla/grendel/calypso/util/MemoryManager.java new file mode 100644 index 00000000000..4613a2bc52e --- /dev/null +++ b/mozilla/grendel/calypso/util/MemoryManager.java @@ -0,0 +1,39 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.Vector; + +public class MemoryManager { + static WeakLinkArray gHandlers; + + static synchronized public void Add(MemoryPressure aHandler) { + if (gHandlers == null) { + gHandlers = new WeakLinkArray(); + } + gHandlers.addElement(aHandler); + } + + static synchronized public void Remove(MemoryPressure aHandler) { + if (gHandlers != null) { + gHandlers.removeElement(aHandler); + } + } +} diff --git a/mozilla/grendel/calypso/util/MemoryMonitor.java b/mozilla/grendel/calypso/util/MemoryMonitor.java new file mode 100644 index 00000000000..8cb42d3e22b --- /dev/null +++ b/mozilla/grendel/calypso/util/MemoryMonitor.java @@ -0,0 +1,111 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.lang.Runtime; + +/** + * A utility class to help track memory usage during development. + * The interface is simple. Just create the monitor, do your business, + * and then invoke the done method.

+ * + * The figure for the ammount of memory allocated should be accurate + * but the number for the ammount of garbage can be off by quite a bit + * if any garbage collection was done between the monitor's creation + * and the call to done.

+ * + * Currently, it just prints some stuff to the console. I'd like to + * extend it so it creates a window and displays it's info in that. + * + * @author Thom FillABomb :-) + */ +public class MemoryMonitor +{ + Runtime fSystem; + + long fFreeAtStart, + fFreeAtEnd, + fGarbageGenerated; + + String fUserString = null; + + public MemoryMonitor (String monitorDescription) + { + fUserString = monitorDescription; + initialize(); + } + + public MemoryMonitor () + { + initialize(); + } + + protected void initialize () + { + fSystem = Runtime.getRuntime(); + beginMonitor(); + } + + public void done () + { + finishMonitor (); + + System.out.println(); + + if (fUserString != null) + System.out.println (fUserString); + + System.out.println (" Starting Free: " + fFreeAtStart); + System.out.println (" Ending Free: " + fFreeAtEnd); + System.out.println (" Memory Delta: " + (fFreeAtStart - fFreeAtEnd)); + System.out.println ("Garbage Generated: " + fGarbageGenerated); + } + + protected void freeUpMemory () + { + // we might want to tickle Kipp's memory Manager here as well... + + fSystem.runFinalization(); + // might want to sleep here + fSystem.gc(); + fSystem.gc(); + } + + void beginMonitor () + { + freeUpMemory(); + fFreeAtStart = fSystem.freeMemory(); + } + + void finishMonitor () + { + long totalUsed, minusGarbage; + + totalUsed = fSystem.freeMemory(); + freeUpMemory(); + minusGarbage = fSystem.freeMemory(); + + // it would also be interesting to purge the recyclers here and + // see how much memory they're occupying + + fGarbageGenerated = minusGarbage - totalUsed; + fFreeAtEnd = minusGarbage; + } +} diff --git a/mozilla/grendel/calypso/util/MemoryPressure.java b/mozilla/grendel/calypso/util/MemoryPressure.java new file mode 100644 index 00000000000..7de9d463ece --- /dev/null +++ b/mozilla/grendel/calypso/util/MemoryPressure.java @@ -0,0 +1,41 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +public interface MemoryPressure { + /** + * This method is called by the garbage collector just before a + * garbage collection is about to begin. + */ + void preGC(long aCurrentHeapSpace, long aMaximumHeapSpace); + + /** + * This method is called by the garbage collector just after a + * collection has finished. + */ + void postGC(long aCurrentHeapSpace, long aMaximumHeapSpace); + + /** + * This method is called by the garbage collector when the + * heap is essentially full and can no longer grow. When this + * occurs all caches of objects should be flushed. + */ + void panic(); +} diff --git a/mozilla/grendel/calypso/util/NetworkDate.java b/mozilla/grendel/calypso/util/NetworkDate.java new file mode 100644 index 00000000000..3e918ff797a --- /dev/null +++ b/mozilla/grendel/calypso/util/NetworkDate.java @@ -0,0 +1,1071 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 3 Aug 1995. + * Ported from C on 19 Aug 1997. + */ + +package calypso.util; + +import java.util.Date; +import java.util.TimeZone; +import calypso.util.ByteBuf; + +/** Parses a date out of a string of bytes. Call it like this: + +

    + ByteBuf bytes = ... ;
    + Date date = NetworkDate.parseDate(bytes); +
+ +

Note that it operates on bytes, not chars, since network + dates are always ASCII. + +

Why would you want to use this instead of + java.text.DateFormat.parse()? Because this algorithm has been + tested in the field against real-world message headers for several + years (the C code hadn't changed substantively since Netscape 2.0.) + (There had been DST-related problems, but the tokenizer/parser was + always sound.) + + @see Date + @see Calendar + @see java.text.DateFormat + */ + +public class NetworkDate { + + private NetworkDate() { } + + static private final int UNKNOWN = 0; // random unknown token + + static private final int SUN = 101; // days of the week + static private final int MON = 102; + static private final int TUE = 103; + static private final int WED = 104; + static private final int THU = 105; + static private final int FRI = 107; + static private final int SAT = 108; + + static private final int JAN = 201; // months + static private final int FEB = 202; + static private final int MAR = 203; + static private final int APR = 204; + static private final int MAY = 205; + static private final int JUN = 206; + static private final int JUL = 207; + static private final int AUG = 208; + static private final int SEP = 209; + static private final int OCT = 210; + static private final int NOV = 211; + static private final int DEC = 212; + + static private final int PST = 301; // a smattering of timezones + static private final int PDT = 302; + static private final int MST = 303; + static private final int MDT = 304; + static private final int CST = 305; + static private final int CDT = 306; + static private final int EST = 307; + static private final int EDT = 308; + static private final int AST = 309; + static private final int NST = 310; + static private final int GMT = 311; + static private final int BST = 312; + static private final int MET = 313; + static private final int EET = 314; + static private final int JST = 315; + + /** This parses a time/date string into a Time object. + If it can't be parsed, -1 is returned. + +

Many formats are handled, including: + +

    +
  • 14 Apr 89 03:20:12 +
  • 14 Apr 89 03:20 GMT +
  • Fri, 17 Mar 89 4:01:33 +
  • Fri, 17 Mar 89 4:01 GMT +
  • Mon Jan 16 16:12 PDT 1989 +
  • Mon Jan 16 16:12 +0130 1989 +
  • 6 May 1992 16:41-JST (Wednesday) +
  • 22-AUG-1993 10:59:12.82 +
  • 22-AUG-1993 10:59pm +
  • 22-AUG-1993 12:59am +
  • 22-AUG-1993 12:59 PM +
  • Friday, August 04, 1995 3:54 PM +
  • 06/21/95 04:24:34 PM +
  • 20/06/95 21:07 +
  • 95-06-08 19:32:48 EDT +
+ +

But note that 6/5/95 is ambiguous, since it's not obvious + which is the day and which is the month. (6/13/95 is not + ambiguous, however.) + + @param buf The bytes to parse. This assumes the input to be + a sequence of 8-bit ASCII characters. High-bit + characters are handled; 16-bit Unicode characters + are not. + + @param default_to_gmt If the input string doesn't contain a description + of the timezone, then this argument determines whether the string is + interpreted relative to the local time zone (false) or to GMT (true). + The correct value to pass in for this argument depends on what standard + specified the time string which you are parsing; for RFC822 dates, this + argument should be true. + + @return Microseconds since Jan 1, 1900, GMT. + You can pass this to new Date(long). + Returns -1 if the string is unparsable + (no other negative value will ever be returned; + dates before the Epoch are not handled.) + */ + + public static long parseLong(ByteBuf buf, boolean default_to_gmt) { + return parseLong(buf.toBytes(), 0, buf.length(), default_to_gmt); + } + + /** The same, but takes a byte array and a region within it, instead + of a ByteBuf. */ + public static long parseLong(byte string[], int start, int end, + boolean default_to_gmt) { + + int dotw = UNKNOWN; + int month = UNKNOWN; + int zone = UNKNOWN; + + int zone_offset = -1; + int date = -1; + int year = -1; + int hour = -1; + int min = -1; + int sec = -1; + + int i = start; + while (i < end) { + + byte c = string[i]; + byte c2 = ((i+1 < end) ? string[i+1] : 0); + byte c3 = ((i+2 < end) ? string[i+2] : 0); + + switch (c) { + case 'a': case 'A': + if (month == UNKNOWN && + (c2 == 'p' || c2 == 'P') && + (c3 == 'r' || c3 == 'R')) + month = APR; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 's') && + (c3 == 't' || c3 == 'T')) + zone = AST; + else if (month == UNKNOWN && + (c2 == 'u' || c2 == 'U') && + (c3 == 'g' || c3 == 'G')) + month = AUG; + break; + case 'b': case 'B': + if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = BST; + break; + case 'c': case 'C': + if (zone == UNKNOWN && + (c2 == 'd' || c2 == 'D') && + (c3 == 't' || c3 == 'T')) + zone = CDT; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = CST; + break; + case 'd': case 'D': + if (month == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 'c' || c3 == 'C')) + month = DEC; + break; + case 'e': case 'E': + if (zone == UNKNOWN && + (c2 == 'd' || c2 == 'D') && + (c3 == 't' || c3 == 'T')) + zone = EDT; + else if (zone == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 't' || c3 == 'T')) + zone = EET; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = EST; + break; + case 'f': case 'F': + if (month == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 'b' || c3 == 'B')) + month = FEB; + else if (dotw == UNKNOWN && + (c2 == 'r' || c2 == 'R') && + (c3 == 'i' || c3 == 'I')) + dotw = FRI; + break; + case 'g': case 'G': + if (zone == UNKNOWN && + (c2 == 'm' || c2 == 'M') && + (c3 == 't' || c3 == 'T')) + zone = GMT; + break; + case 'j': case 'J': + if (month == UNKNOWN && + (c2 == 'a' || c2 == 'A') && + (c3 == 'n' || c3 == 'N')) + month = JAN; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = JST; + else if (month == UNKNOWN && + (c2 == 'u' || c2 == 'U') && + (c3 == 'l' || c3 == 'L')) + month = JUL; + else if (month == UNKNOWN && + (c2 == 'u' || c2 == 'U') && + (c3 == 'n' || c3 == 'N')) + month = JUN; + break; + case 'm': case 'M': + if (month == UNKNOWN && + (c2 == 'a' || c2 == 'A') && + (c3 == 'r' || c3 == 'R')) + month = MAR; + else if (month == UNKNOWN && + (c2 == 'a' || c2 == 'A') && + (c3 == 'y' || c3 == 'Y')) + month = MAY; + else if (zone == UNKNOWN && + (c2 == 'd' || c2 == 'D') && + (c3 == 't' || c3 == 'T')) + zone = MDT; + else if (zone == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 't' || c3 == 'T')) + zone = MET; + else if (dotw == UNKNOWN && + (c2 == 'o' || c2 == 'O') && + (c3 == 'n' || c3 == 'N')) + dotw = MON; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = MST; + break; + case 'n': case 'N': + if (month == UNKNOWN && + (c2 == 'o' || c2 == 'O') && + (c3 == 'v' || c3 == 'V')) + month = NOV; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = NST; + break; + case 'o': case 'O': + if (month == UNKNOWN && + (c2 == 'c' || c2 == 'C') && + (c3 == 't' || c3 == 'T')) + month = OCT; + break; + case 'p': case 'P': + if (zone == UNKNOWN && + (c2 == 'd' || c2 == 'D') && + (c3 == 't' || c3 == 'T')) + zone = PDT; + else if (zone == UNKNOWN && + (c2 == 's' || c2 == 'S') && + (c3 == 't' || c3 == 'T')) + zone = PST; + break; + case 's': case 'S': + if (dotw == UNKNOWN && + (c2 == 'a' || c2 == 'A') && + (c3 == 't' || c3 == 'T')) + dotw = SAT; + else if (month == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 'p' || c3 == 'P')) + month = SEP; + else if (dotw == UNKNOWN && + (c2 == 'u' || c2 == 'U') && + (c3 == 'n' || c3 == 'N')) + dotw = SUN; + break; + case 't': case 'T': + if (dotw == UNKNOWN && + (c2 == 'h' || c2 == 'H') && + (c3 == 'u' || c3 == 'U')) + dotw = THU; + else if (dotw == UNKNOWN && + (c2 == 'u' || c2 == 'U') && + (c3 == 'e' || c3 == 'E')) + dotw = TUE; + break; + case 'u': case 'U': + if (zone == UNKNOWN && + (c2 == 't' || c2 == 'T') && + !(c3 >= 'A' && c3 <= 'Z') && + !(c3 >= 'a' && c3 <= 'z')) + // UT is the same as GMT but UTx is not. + zone = GMT; + break; + case 'w': case 'W': + if (dotw == UNKNOWN && + (c2 == 'e' || c2 == 'E') && + (c3 == 'd' || c3 == 'D')) + dotw = WED; + break; + + + + // parsing timezone offsets + + case '+': case '-': { + + if (zone_offset >= 0) { + // already got one... + i++; + break; + } + + if (zone != UNKNOWN && zone != GMT) { + // GMT+0300 is legal, but PST+0300 is not. + i++; + break; + } + + int sign = ((c == '+') ? 1 : -1); + i++; /* move over sign */ + + int token_start = i; + int token_end; + + for (token_end = token_start; token_end < end; token_end++) { + c = string[token_end]; + if (c < '0' || c > '9') + break; + } + + if ((token_end - token_start) == 4) + // GMT+0000: offset in HHMM + zone_offset = (((((string[token_start]-'0') * 10) + + (string[token_start+1]-'0')) + * 60) + + (((string[token_start+2]-'0') * 10) + + (string[token_start+3]-'0'))); + else if ((token_end - token_start) == 2) + // GMT+00: offset in hours + zone_offset = (((string[token_start]-'0') * 10) + + (string[token_start+1]-'0')) * 60; + else if ((token_end - token_start) == 1) + // GMT+0: offset in hours + zone_offset = (string[token_start]-'0') * 60; + else + /* three digits, or more than 4: unrecognised. */ + break; + + zone_offset *= sign; + zone = GMT; + break; + } + + + // parsing numeric tokens + + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + + int tmp_hour = -1; + int tmp_min = -1; + int tmp_sec = -1; + + int token_start = i; + int token_end; + + // move token_end to the first non-digit, or end of string. + for (token_end = token_start; token_end < end; token_end++) { + c = string[token_end]; + if (c < '0' || c > '9') + break; + } + + if (token_end < end && c == ':') { + // If there's a colon, this is an hour/minute specification. + // If we've already set hour or min, ignore this one. + if (hour > 0 && min > 0) + break; + + // We have seen "[0-9]+:", so this is probably HH:MM[:SS] + + if ((token_end - token_start) == 2) + // two digits then a colon -- it is the hour. + tmp_hour = ((string[token_start]-'0') * 10 + + (string[token_start+1]-'0')); + else if ((token_end - token_start) == 1) + // one digit then a colon -- it is the hour. + tmp_hour = (string[token_start]-'0'); + else + break; + + // Got the hour; move over the colon, and parse the minutes. + + // move token_start just past the colon. + token_start = token_end+1; + if (token_start >= end) + break; + + // move token_end to the first non-digit, or end of string. + for(token_end = token_start; token_end < end; token_end++) { + c = string[token_end]; + if (c < '0' || c > '9') + break; + } + + if ((token_end - token_start) == 2) + // two digits followed by a colon or EOS -- it is the minutes. + tmp_min = ((string[token_start]-'0') * 10 + + (string[token_start+1]-'0')); + else if ((token_end - token_start) == 1) + // one digit followed by a colon or EOS -- it is the minutes. + tmp_min = (string[token_start]-'0'); + else + break; + + // Got the hour and minutes; move over the colon, and try to + // parse the seconds. + + token_start = token_end; + if (token_end < end && string[token_end] == ':') { + // move token_start just past the colon. + token_start++; + + // move token_end to the first non-digit, or end of string. + for(token_end = token_start; token_end < end; token_end++) { + c = string[token_end]; + if (c < '0' || c > '9') + break; + } + + if ((token_end - token_start) == 2) + // two digits followed by EOT -- it is the seconds. + tmp_sec = ((string[token_start]-'0') * 10 + + (string[token_start+1]-'0')); + else if ((token_end - token_start) == 1) + // one digit followed by EOT -- it is the seconds. + tmp_sec = (string[token_start]-'0'); + } + + // If we made it here, we've parsed hour and min, + // and possibly sec, so it worked as a unit. + + // skip over whitespace and see if there's an AM or PM + // directly following the time. + if (token_end < end && tmp_hour <= 12) { + + for(; token_start < end; token_start++) { + c = string[token_start]; + if (c < '0' || c > '9') + break; + } + + while (token_start < end && + string[token_start] <= ' ') + token_start++; + + if (token_start+1 < end) { + if ((string[token_start] == 'p' || + string[token_start] == 'P') && + (string[token_start+1] == 'm' || + string[token_start+1] == 'M')) + // 10:05pm == 22:05, and 12:05pm == 12:05 + tmp_hour = (tmp_hour == 12 ? 12 : tmp_hour + 12); + else if (tmp_hour == 12 && + (string[token_start] == 'a' || + string[token_start] == 'A') && + (string[token_start+1] == 'm' || + string[token_start+1] == 'M')) + // 12:05am == 00:05 + tmp_hour = 0; + } + } + + // if we made it all the way to here, we can keep these values. + // (We might have bugged out after parsing hour and before + // parsing min, and it would be bad to take one and not the + // other.) + hour = tmp_hour; + min = tmp_min; + sec = tmp_sec; + i = token_end-1; + break; + } + + else if (token_end+1 < end && + (c == '/' || c == '-') && + (string[token_end+1] >= '0' && + string[token_end+1] <= '9')) { + // Perhaps this is 6/16/95, 16/6/95, 6-16-95, or 16-6-95 + // or even 95-06-05... + // #### But it doesn't handle 1995-06-22. + // + int n1, n2, n3; + + if (month != UNKNOWN) + // if we saw a month name, this can't be. + break; + + token_start = i; + + // get the first one or two digits + + n1 = (string[token_start] - '0'); + token_start++; + if (token_start+1 < end && + string[token_start] >= '0' && + string[token_start] <= '9') { + n1 = (n1*10) + (string[token_start] - '0'); + token_start++; + } + + // demand that the next char be / or -. + + if (token_start >= end-1 || + (string[token_start] != '/' && + string[token_start] != '-')) + break; + token_start++; + + // demand that the next char be a digit. + + if (string[token_start] < '0' || + string[token_start] > '9') + break; + + // get the second group of one or two digits + + n2 = (string[token_start] - '0'); + token_start++; + if (token_start+1 < end && + string[token_start] >= '0' && + string[token_start] <= '9') { + n2 = (n2*10) + (string[token_start] - '0'); + token_start++; + } + + // demand that the next char be / or -. + + if (token_start >= end-1 || + (string[token_start] != '/' && + string[token_start] != '-')) + break; + token_start++; + + // demand that the next char be a digit. + + if (string[token_start] < '0' || + string[token_start] > '9') + break; + + // get the third group of one, two, or four digits + + n3 = (string[token_start] - '0'); + token_start++; + + if (token_start+1 <= end && + string[token_start] >= '0' && + string[token_start] <= '9') { + // digit two + n3 = (n3*10) + (string[token_start] - '0'); + token_start++; + + // digit three + if (token_start+1 < end && + string[token_start] >= '0' && + string[token_start] <= '9') { + + // if we have digit three but not digit four, give up on it: + // there are no three-digit-numbers in this context. + if (token_start+2 > end || + (string[token_start+1] < '0' && + string[token_start+1] > '9')) + break; + + n3 = (n3*10) + (string[token_start++] - '0'); + n3 = (n3*10) + (string[token_start++] - '0'); + } + } + + // if the digits are followed by an alphanumeric, give up on it. + if (token_start < end && + ((string[token_start] >= '0' && + string[token_start] <= '9') || + (string[token_start] >= 'a' && + string[token_start] <= 'z') || + (string[token_start] >= 'A' && + string[token_start] <= 'Z'))) + break; + + + // At this point, we've parsed three 1-2 digit numbers, with / or - + // between them. Now decide what the hell they are (DD/MM/YY or + // MM/DD/YY or YY/MM/DD.) + + if (n1 > 70) { // must be YY/MM/DD + if (n2 > 12) break; + if (n3 > 31) break; + year = n1; + if (year < 1900) year += 1900; + month = (n2 + JAN - 1); + date = n3; + i = token_start; + break; + } + + if (n3 < 70 || // before epoch - can't represent it. + (n1 > 12 && n2 > 12)) { // illegal + i = token_start; + break; + } + + if (n3 < 1900) n3 += 1900; + + if (n1 > 12) { // must be DD/MM/YY + date = n1; + month = (n2 + JAN - 1); + year = n3; + } else { // assume MM/DD/YY + // #### In the ambiguous case, should we consult the locale to + // find out the local default? + month = (n1 + JAN - 1); + date = n2; + year = n3; + } + + i = token_start; + } + + else if (token_end+1 < end && + (string[token_end] >= 'A' || + string[token_end] <= 'Z') && + (string[token_end+1] >= 'a' && + string[token_end+1] <= 'z')) { + // Digits followed by non-punctuation - what's that? + } + + else if ((token_end - token_start) == 4) { + // four digits in a row must be a year. + if (year < 0) + year = ((string[i]-'0') * 1000 + + (string[i+1]-'0') * 100 + + (string[i+2]-'0') * 10 + + (string[i+3]-'0')); + } + + else if ((token_end - token_start) == 2) { + // two digits in a row might be a date, or a year + + int n = ((string[i]-'0') * 10 + + (string[i+1]-'0')); + // If we don't have a date (day of the month) and we see a number + // less than 32, then assume that is the date. + // + // Otherwise, if we have a date and not a year, assume this is the + // year. If it is less than 70, then assume it refers to the 21st + // century. If it is two digits (>= 70), assume it refers to the + // 20th century. Otherwise, assume it refers to an unambiguous year. + // + // The world will surely end soon. + // + if (date < 0 && n < 32) + date = n; + else if (year < 0) { + if (n < 70) + year = 2000 + n; + else if (n < 100) + year = 1900 + n; + else + year = n; + } + } + + else if ((token_end - token_start) == 1) { + // one digit all alone -- it must be a date. + if (date < 0) + date = string[i]-'0'; + } + + else { + // else, three or more than four digits - what's that? + break; + } + } + } // closes switch + + // Skip to the end of this token, whether we parsed it or not. + // Tokens are delimited by whitespace, or ,;-/ + // But explicitly not :+-. + + for (; i < end; i++) { + c = string[i]; + if (c <= ' ' || c == ',' || c == ';' || c == '-' || c == '+' || + c == '/' || c == '(' || c == ')' || c == '[' || c == ']') + break; + } + + boolean done = false; + while (!done) { + done = true; + // skip over uninteresting chars before the next token. + for (; i < end; i++) { + c = string[i]; + if (! (c <= ' ' || c == ',' || c == ';' || c == '/' || + c == '(' || c == ')' || c == '[' || c == ']')) + break; + } + + // "-" is ignored at the beginning of a token if we have not yet + // parsed a year, or if the character after the dash is not a digit. + if (i < end && + c == '-' && + (year < 0 || + string[i+1] < '0' || + string[i+1] > '9')) { + i++; + done = false; + } + } + } + + // done parsing string -- turn symbolic zones into numeric offsets. + + if (zone != UNKNOWN && zone_offset == -1) { + switch (zone) { + case PST: zone_offset = -8 * 60; break; + case PDT: zone_offset = -7 * 60; break; + case MST: zone_offset = -7 * 60; break; + case MDT: zone_offset = -6 * 60; break; + case CST: zone_offset = -6 * 60; break; + case CDT: zone_offset = -5 * 60; break; + case EST: zone_offset = -5 * 60; break; + case EDT: zone_offset = -4 * 60; break; + case AST: zone_offset = -4 * 60; break; + case NST: zone_offset = -3 * 60 - 30; break; + case GMT: zone_offset = 0 * 60; break; + case BST: zone_offset = 1 * 60; break; + case MET: zone_offset = 1 * 60; break; + case EET: zone_offset = 2 * 60; break; + case JST: zone_offset = 9 * 60; break; + } + } + + // If we didn't find a year, month, or day-of-the-month, we can't + // possibly parse this, so just give up. (In fact, in the Unix C version + // of this code, I'd seen mktime() do something random -- it always + // returned "Tue Feb 5 06:28:16 2036", which is no doubt a + // numerologically significant date... + // + if (month == UNKNOWN || date == -1 || year == -1) + return -1; + + if (zone_offset == -1) { + if (default_to_gmt) + zone_offset = 0; + else + zone_offset = localZoneOffset(); + } + + return UTC((year - 1900), + (month - JAN), + date, + (hour == -1 ? 0 : hour), + (min == -1 ? 0 : min) - zone_offset, + (sec == -1 ? 0 : sec)); + } + + /** The same, but assumes GMT is the default timezone. */ + public static long parseLong(ByteBuf string) { + return parseLong(string, true); + } + + /** Like parseLong(), but returns a new Date object instead. */ + public static Date parseDate(ByteBuf string, boolean default_to_gmt) { + long date = parseLong(string, default_to_gmt); + if (date == -1) return null; + return new Date(date); + } + + /** The same, but assumes GMT is the default timezone. */ + public static Date parseDate(ByteBuf string) { + return parseDate(string, true); + } + + + /** Composes the given date into a number suitable for passing to + new Date(n). This is the number of milliseconds + past the Epoch. + */ + public static long UTC(int year, int month, int date, + int hour, int min, int sec) { + long day = (date + + monthOffset[month] + + ((((year & 3) != 0) + || ((year % 100 == 0) && ((year + 300) % 400 != 0)) + || (month < 2)) + ? -1 : 0) /* convert day-of-month to 0 based range, + * except following February in a leap year, + * in which case we skip the conversion to + * account for the extra day in February */ + + ((year - 70) * 365L) // days per year + + ((year - 69) / 4) // plus leap days + - ((year - 1) / 100) // no leap on century years + + ((year + 299) / 400)); // except %400 years + return (1000 * + ((sec + (60 * (min + (60 * hour)))) + + (60 * 60 * 24) * day)); + } + + static private final short monthOffset[] = { + 0, // 31 January + 31, // 28 February + 59, // 31 March + 90, // 30 April + 120, // 31 May + 151, // 30 June + 181, // 31 July + 212, // 31 August + 243, // 30 September + 273, // 31 October + 304, // 30 November + 334 // 31 December + // 365 + }; + + static private int localZoneOffset() { + TimeZone z = TimeZone.getDefault(); + int off = z.getRawOffset() / (1000 * 60); + if (z.inDaylightTime(new Date())) // Consing!! + off += 60; + return off; + } + + +/* + private static void debug_print(int dotw, int month, int zone, int offset, + int date, int year, int hour, int min, + int sec) { + System.out.println("dotw = " + debug_token_to_string(dotw) + + " (" + dotw + ")"); + System.out.println("date = " + date); + System.out.println("month = " + debug_token_to_string(month) + + " (" + month + ")"); + System.out.println("year = " + year); + System.out.println("hour = " + hour); + System.out.println("min = " + min); + System.out.println("sec = " + sec); + System.out.println("zone = " + debug_token_to_string(zone) + + " (" + zone + ")"); + System.out.println("off = " + offset); + System.out.println(""); + } + + private static String debug_token_to_string(int t) { + switch (t) { + case UNKNOWN: return "UNKNOWN"; case SUN: return "SUN"; + case MON: return "MON"; case TUE: return "TUE"; case WED: return "WED"; + case THU: return "THU"; case FRI: return "FRI"; case SAT: return "SAT"; + case JAN: return "JAN"; case FEB: return "FEB"; case MAR: return "MAR"; + case APR: return "APR"; case MAY: return "MAY"; case JUN: return "JUN"; + case JUL: return "JUL"; case AUG: return "AUG"; case SEP: return "SEP"; + case OCT: return "OCT"; case NOV: return "NOV"; case DEC: return "DEC"; + case PST: return "PST"; case PDT: return "PDT"; case MST: return "MST"; + case MDT: return "MDT"; case CST: return "CST"; case CDT: return "CDT"; + case EST: return "EST"; case EDT: return "EDT"; case AST: return "AST"; + case NST: return "NST"; case GMT: return "GMT"; case BST: return "BST"; + case MET: return "MET"; case EET: return "EET"; case JST: return "JST"; + default: return "???"; + } + } + */ +} + + + /* + We only handle a few time zones, but in case someone is feeling ambitious, + here are a bunch of other timezone names and numbers. + + It's generally frowned upon to put timezone names in date headers instead + of GMT offsets, so the small set of zone names that we already handle is + probably sufficient. + + --------------------------------------------------------------------- + I lifted this list of time zone abbreviations out of a UNIX computers + setup file (specifically, from an AT&T StarServer running UNIX System V + Release 4, in the /usr/lib/local/TZ directory). + + The list is by no means complete or comprehensive, as much of it comes + out of scripts designed to adjust the time on computers when Daylight + Savings Time (DST) rolls around. Also, I would consider it at least a + little suspect. First, because it was compiled by Americans, and you know + how us Americans are with geography :). Second, the data looks to be a + little old, circa 1991 (note the reference to the "Soviet Union"). + + The first column is an approximate name for the time zone described, the + second column gives the time relative to GMT, the third column takes a + stab at listing the country that the time zone is in, and the final + column gives one or more abbreviations that apply to that time zone (note + that abbreviations that end with "DST" or with "S" as the middle letter + indicate Daylight Savings Time is in effect). + + I've also tried to roughly divide the listings into geographical + groupings. + + Hope this helps, + + Raymond McCauley + Texas A&M University + scooter@tamu.edu + + + ================= + + Europe + + Great Britain 0:00 GB-Eire GMT, BST + Western European nations +0:00 W-Eur WET, WET DST + Iceland +0:00 - WET + Middle European nations +1:00 M-Eur MET, MET DST + Poland +1:00 W-Eur MET, MET DST + Eastern European nations +2:00 E-Eur EET, EET DST + Turkey +3:00 Turkey EET, EET DST + Warsaw Pact/Soviet Union +3:00 M-Eur ???? + + North America + + Canada/Newfoundland -3:30 Canada NST, NDT + Canada/Atlantic -4:00 Canada AST, ADT + Canada/Eastern -5:00 Canada EST, EDT + Canada/Central -6:00 Canada CST, CDT + Canada/East-Saskatchewan -6:00 Canada CST + Canada/Mountain -7:00 Canada MST, MDT + Canada/Pacific -8:00 Canada PST, PDT + Canada/Yukon -9:00 Canada YST, YDT + + US/Eastern -5:00 US EST, EDT + US/Central -6:00 US CST, CDT + US/Mountain -7:00 US MST, MDT + US/Pacific -8:00 US PST, PDT + US/Yukon -9:00 US YST, YDT + US/Hawaii -10:00 US HST, PST, PDT, PPET + + Mexico/BajaNorte -8:00 Mexico PST, PDT + Mexico/BajaSur -7:00 Mexico MST + Mexico/General -6:00 Mexico CST + + South America + + Brazil/East -3:00 Brazil EST, EDT + Brazil/West -4:00 Brazil WST, WDT + Brazil/Acre -5:00 Brazil AST, ADT + Brazil/DeNoronha -2:00 Brazil FST, FDT + + Chile/Continental -4:00 Chile CST, CDT + Chile/EasterIsland -6:00 Chile EST, EDT + + + Asia + + People's Repub. of China +8:00 PRC CST, CDT + (Yes, they really have only one time zone.) + + Republic of Korea +9:00 ROK KST, KDT + + Japan +9:00 Japan JST + Singapore +8:00 Singapore SST + Hongkong +8:00 U.K. HKT + ROC +8:00 - CST + + Middle East + + Israel +3:00 Israel IST, IDT + (This was the only listing I found) + + Australia + + Australia/Tasmania +10:00 Oz EST + Australia/Queensland +10:00 Oz EST + Australia/North +9:30 Oz CST + Australia/West +8:00 Oz WST + Australia/South +9:30 Oz CST + + + Hour TZN DZN Zone Example + 0 GMT Greenwich Mean Time GMT0 + 0 UTC Universal Coordinated Time UTC0 + 2 FST FDT Fernando De Noronha Std FST2FDT + 3 BST Brazil Standard Time BST3 + 3 EST EDT Eastern Standard (Brazil) EST3EDT + 3 GST Greenland Standard Time GST3 + 3:30 NST NDT Newfoundland Standard Time NST3:30NDT + 4 AST ADT Atlantic Standard Time AST4ADT + 4 WST WDT Western Standard (Brazil) WST4WDT + 5 EST EDT Eastern Standard Time EST5EDT + 5 CST CDT Chile Standard Time CST5CDT + + Hour TZN DZN Zone Example + 5 AST ADT Acre Standard Time AST5ADT + 5 CST CDT Cuba Standard Time CST5CDT + 6 CST CDT Central Standard Time CST6CDT + 6 EST EDT Easter Island Standard EST6EDT + 7 MST MDT Mountain Standard Time MST7MDT + 8 PST PDT Pacific Standard Time PST8PDT + 9 AKS AKD Alaska Standard Time AKS9AKD + 9 YST YDT Yukon Standard Time YST9YST + 10 HST HDT Hawaii Standard Time HST10HDT + 11 SST Somoa Standard Time SST11 + -12 NZS NZD New Zealand Standard Time NZS-12NZD + -10 GST Guam Standard Time GST-10 + -10 EAS EAD Eastern Australian Standard EAS-10EAD + -9:30 CAS CAD Central Australian Standard CAS-9:30CAD + -9 JST Japan Standard Time JST-9 + -9 KST KDT Korean Standard Time KST-9KDT + -8 CCT China Coast Time CCT-8 + -8 HKT Hong Kong Time HKT-8 + -8 SST Singapore Standard Time SST-8 + -8 WAS WAD Western Australian Standard WAS-8WAD + -7:30 JT Java Standard Time JST-7:30 + -7 NST North Sumatra Time NST-7 + -5:30 IST Indian Standard Time IST-5:30 + -3:30 IST IDT Iran Standard Time IST-3:30IDT + -3 MSK MSD Moscow Winter Time MSK-3MSD + -2 EET Eastern Europe Time EET-2 + -2 IST IDT Israel Standard Time IST-2IDT + -1 MEZ MES Middle European Time MEZ-1MES + -1 SWT SST Swedish Winter Time SWT-1SST + -1 FWT FST French Winter Time FWT-1FST + -1 CET CES Central European Time CET-1CES + -1 WAT West African Time WAT-1 + */ diff --git a/mozilla/grendel/calypso/util/NullEnumeration.java b/mozilla/grendel/calypso/util/NullEnumeration.java new file mode 100644 index 00000000000..03967b29498 --- /dev/null +++ b/mozilla/grendel/calypso/util/NullEnumeration.java @@ -0,0 +1,43 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +/** + * This just returns an enumeration which never has anything in it. + */ + +package calypso.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class NullEnumeration implements Enumeration { + + public static final Enumeration kInstance = new NullEnumeration (); + + protected NullEnumeration () + { + } + + public boolean hasMoreElements() { + return false; + } + public Object nextElement() throws NoSuchElementException { + throw new NoSuchElementException(); + } +} diff --git a/mozilla/grendel/calypso/util/NullJavaEnumeration.java b/mozilla/grendel/calypso/util/NullJavaEnumeration.java new file mode 100644 index 00000000000..ea9d8180441 --- /dev/null +++ b/mozilla/grendel/calypso/util/NullJavaEnumeration.java @@ -0,0 +1,43 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +/** + * This just returns an enumeration which never has anything in it. + */ + +package calypso.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class NullJavaEnumeration implements Enumeration { + + public static final Enumeration kInstance = new NullJavaEnumeration (); + + protected NullJavaEnumeration () + { + } + + public boolean hasMoreElements() { + return false; + } + public Object nextElement() throws NoSuchElementException { + throw new NoSuchElementException(); + } +} diff --git a/mozilla/grendel/calypso/util/Preferences.java b/mozilla/grendel/calypso/util/Preferences.java new file mode 100644 index 00000000000..005425f6099 --- /dev/null +++ b/mozilla/grendel/calypso/util/Preferences.java @@ -0,0 +1,69 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.io.File; +import java.util.Properties; + +/** This is an interface to get things from the Preferences database. + @see PreferencesFactory */ + +public interface Preferences { + /** Add a table of default values. You must add defaults for each + preference you use. */ + void addDefaults(Properties defs); + + /** Given a name of a preference, return its value as a String. If it's not + defined, return the given default. */ + String getString(String prefname, String defaultValue); + + /** Given a name of a preference, return its value as a String. */ + String getString(String prefname); + + /** Given a name of a preference, return its value as an int. If it's not + defined, return the given default. */ + int getInt(String prefname, int defaultValue); + + /** Given a name of a preference, return its value as an int. */ + int getInt(String prefname); + + /** Given a name of a preference, return its value as a boolean. If it's not + defined, return the given default. */ + boolean getBoolean(String prefname, boolean defaultValue); + + /** Given a name of a preference, return its value as a boolean. */ + boolean getBoolean(String prefname); + + /** Given a name of a preference, return its value as an File. If it's not + defined, return the given default. */ + File getFile(String prefname, File defaultValue); + + /** Assign a String value to the given preference. */ + void putString(String prefname, String value); + + /** Assign an int value to the given preference. */ + void putInt(String prefname, int value); + + /** Assign a boolean value to the given preference. */ + void putBoolean(String prefname, boolean value); + + /** Return the preferences as a Properties object */ + Properties getAsProperties(); +}; diff --git a/mozilla/grendel/calypso/util/PreferencesBase.java b/mozilla/grendel/calypso/util/PreferencesBase.java new file mode 100644 index 00000000000..558162a34b6 --- /dev/null +++ b/mozilla/grendel/calypso/util/PreferencesBase.java @@ -0,0 +1,170 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.util.Properties; +import java.util.MissingResourceException; +import java.util.Enumeration; + +class PreferencesBase extends Properties implements Preferences { + + static final String gPrefsPath = System.getProperties().getProperty("user.home"); + static final String gPrefsFile = "xena.pref"; + + PreferencesBase() { + super(); + + File infile = new File(new File(gPrefsPath), gPrefsFile); + InputStream in = null; + try { + in = new FileInputStream(infile); + load(in); + in.close(); + } catch (IOException e) { + } + } + + void writePrefs() { + File outfile = new File(new File(gPrefsPath), gPrefsFile); + OutputStream out = null; + try { + out = new FileOutputStream(outfile); + save(out, "Xena User Preferences. Do not directly modify this file!"); + out.close(); + } catch (IOException e) { + } + } + + public void addDefaults(Properties defs) { + if(defs == null) { + return; + } + + if(defaults == null) { + defaults = defs; + return; + } + + for(Enumeration e = defs.keys(); e.hasMoreElements();) { + Object key = e.nextElement(); + defaults.put(key, defs.get(key)); + } + } + + public String getString(String name, String defaultValue) { + return getProperty(name, defaultValue); + } + + public String getString(String name) + throws MissingResourceException { + String result = getProperty(name); + if(result == null) { + throw new MissingResourceException(name + " is missing!", "", name); + } + return result; + } + + public int getInt(String name, int defaultValue) { + String str = getString(name, null); + int result = defaultValue; + if (str != null) { + try { + result = Integer.parseInt(str); + } catch (NumberFormatException e) { + } + } + return result; + } + + public int getInt(String name) + throws MissingResourceException, NumberFormatException { + String str; + try { + str = getString(name); + } catch(MissingResourceException e) { + throw e; + } + int result; + try { + result = Integer.parseInt(str); + } catch (NumberFormatException e) { + throw e; + } + return result; + } + + public boolean getBoolean(String name, boolean defaultValue) { + String str = getString(name, null); + boolean result = defaultValue; + if (str != null) { + result = Boolean.valueOf(str).booleanValue(); + } + return result; + } + + public boolean getBoolean(String name) + throws MissingResourceException { + String str; + try { + str = getString(name); + } catch(MissingResourceException e) { + throw e; + } + return Boolean.valueOf(str).booleanValue(); + } + + public File getFile(String name, File defaultValue) { + String str = getString(name, null); + File result = defaultValue; + if (str != null) { + result = new File(str); + } + return result; + } + + /** Assign a String value to the given preference. */ + public void putString(String prefName, String value) { + put(prefName, value); + writePrefs(); + } + + /** Assign an int value to the given preference. */ + public void putInt(String prefName, int value) { + put(prefName, (String)(""+value)); + writePrefs(); + } + + /** Assign a boolean value to the given preference. */ + public void putBoolean(String prefName, boolean value) { + put(prefName, (String)(""+value)); + writePrefs(); + } + + /** Return the preferences as a Properties object */ + public Properties getAsProperties() { + return this; + } +} diff --git a/mozilla/grendel/calypso/util/PreferencesFactory.java b/mozilla/grendel/calypso/util/PreferencesFactory.java new file mode 100644 index 00000000000..f938a5972ad --- /dev/null +++ b/mozilla/grendel/calypso/util/PreferencesFactory.java @@ -0,0 +1,39 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** A factory to return the (singleton) preferences object. */ + +public class PreferencesFactory { + static Preferences fPrefs = null; + + /** Return the preferences object. */ + public static Preferences Get() { + if (fPrefs == null) { + synchronized(PreferencesFactory.class) { + // Make sure some other thread didn't already make it. + if (fPrefs == null) { + fPrefs = new PreferencesBase(); + } + } + } + return fPrefs; + } +} diff --git a/mozilla/grendel/calypso/util/PrefetchEnumeration.java b/mozilla/grendel/calypso/util/PrefetchEnumeration.java new file mode 100644 index 00000000000..9905a68cdf0 --- /dev/null +++ b/mozilla/grendel/calypso/util/PrefetchEnumeration.java @@ -0,0 +1,88 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Frank Tang + */ + +package calypso.util; +import java.util.Enumeration; + +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ +public abstract class PrefetchEnumeration implements Enumeration { + protected boolean initialized = false; + protected Object fFetch = null; +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + private void init() + { + if(! initialized ) + { + fFetch = fetch(); + initialized = true; + } + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public Object nextElement() + { + if(! initialized) + init(); + Object current = fFetch; + fFetch = fetch(); + return current; + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + public boolean hasMoreElements() + { + if(! initialized) + init(); + return (fFetch != null); + } +/* + * + * @author ftang + * @version $Revision: 1.1 $ + * @see + * + */ + abstract protected Object fetch(); + + +} diff --git a/mozilla/grendel/calypso/util/QSort.java b/mozilla/grendel/calypso/util/QSort.java new file mode 100644 index 00000000000..e18368d4232 --- /dev/null +++ b/mozilla/grendel/calypso/util/QSort.java @@ -0,0 +1,108 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** Implements QuickSort, on an array of objects. + An implementor of `Comparer' is passed in to compare two objects. + */ +public class QSort { + + private Comparer comp; + + /** Create a QSort object. + One way to use this would with dynamic class creation: +

+      QSort sorter = new QSort(new Comparer() {
+                                 public int compare(Object a, Object b) {
+                                   if (a.key == b.key) return 0;
+                                   else if (a.key < b.key) return -1;
+                                   else return 1;
+                                 }
+                               });
+      sorter.sort(array);
+      
+ */ + public QSort(Comparer c) { + comp = c; + } + + /** Sorts the array, according to the Comparer. */ + public void sort(Object[] list) { + quicksort(list, 0, list.length - 1); + } + + /** Sorts a subsequence of the array, according to the Comparer. */ + public void sort(Object[] list, int start, int end) { + quicksort(list, start, end - 1); + } + + private void quicksort(Object[] list, int p, int r) { + if (p < r) { + int q = partition(list,p,r); + if (q == r) { + q--; + } + quicksort(list,p,q); + quicksort(list,q+1,r); + } + } + + private int partition (Object[] list, int p, int r) { + Object pivot = list[p]; + int lo = p; + int hi = r; + + while (true) { + + while (comp.compare(list[hi], pivot) >= 0 && + lo < hi) { + hi--; + } + while (comp.compare(list[lo], pivot) < 0 && + lo < hi) { + lo++; + } + if (lo < hi) { + Object T = list[lo]; + list[lo] = list[hi]; + list[hi] = T; + } + else return hi; + } + } + +// public static void main(String argv[]) { +// String array[] = { "7", "3", "0", "4", "5", "6", "2", "1" }; +// QSort sorter = new QSort(new Comparer() { +// public int compare(Object a, Object b) { +// int ai = Integer.parseInt((String) a); +// int bi = Integer.parseInt((String) b); +// if (ai == bi) return 0; +// else if (ai < bi) return -1; +// else return 1; +// } +// }); +// sorter.sort(array); +// for (int i = 0; i < array.length; i++) +// System.out.print(array[i] + " "); +// System.out.println(); +// } + +} diff --git a/mozilla/grendel/calypso/util/RWLock.java b/mozilla/grendel/calypso/util/RWLock.java new file mode 100644 index 00000000000..75938f96efb --- /dev/null +++ b/mozilla/grendel/calypso/util/RWLock.java @@ -0,0 +1,312 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * A "read-write" lock. This lock allows for an arbitrary number of + * simultaneous readers. The lock can be upgraded to a write lock in two + * ways. First, the lock can be upgraded without guaranteeing invariance + * across the transition (in other words, the read lock may need to be + * released before the write lock can be acquired). The other form of + * upgrade guarantees invariance; however, the upgrade can only be + * performed by initially locking the lock using the invariant read lock + * enter method. Upgrading the lock in either case involves waiting until + * there are no more readers. This implementation gives priority to + * upgrades and invariant locks which may lead to reader starvation.

+ * + * Each thread using the lock may re-enter the lock as many times as + * needed. However, attempting to re-enter the lock with the invariant + * read lock will fail unless the lock was originally entered that way by + * the invoking thread.

+ * + * Only one thread may enter the invariant read lock at a time; other + * threads attempting this will block until the owning thread exits the + * lock completely.

+ * + * Note that the implementation assumes that the user of instances of + * this class properly pairs the enters/exits.

+ */ +public final class RWLock { + private int fNumReaders; + private Thread fWriteLockOwner; + private int fWriteLockCount; + private Thread fInvariantLockOwner; + private int fInvariantLockCount; + + public RWLock() { + fStateList = new LockState(); + fStateList.fNext = fStateList; + fStateList.fPrev = fStateList; + fFreeList = new LockState(); + fFreeList.fNext = fFreeList; + fFreeList.fPrev = fFreeList; + } + + public synchronized void enterReadLock() + throws InterruptedException + { + Thread me = Thread.currentThread(); + + // Wait for writer, if any, to release the lock + if ((fWriteLockOwner != null) && (me != fWriteLockOwner)) { + while (fWriteLockOwner != null) { + wait(); + } + } + + // XXX write me: + // If my lock count is zero and there is a writer waiting, block + // until the writer is done + + // Add another state record to the list + appendLockState(me, kRead); + fNumReaders++; + } + + public synchronized void exitReadLock() { + Thread me = Thread.currentThread(); + removeLockState(me, kRead); + fNumReaders--; + notify(); + } + + /** + * Enter the invariant read lock. Only one thread at a time can hold + * the invariant read lock. This lock guarantees to upgrade to a write + * lock without needing to release the read lock. + */ + public synchronized void enterInvariantReadLock() + throws InterruptedException + { + Thread me = Thread.currentThread(); + if (me != fInvariantLockOwner) { + // If we don't have the invariant read lock already then we cannot + // try to get it if we are holding either a read or a write lock + if (isLocked(me, kRead) || isLocked(me, kWrite)) { + throw new Error("enterInvariantReadLock while holding r/w lock"); + } + + /** + * If we get here it means we don't currently hold the invariant read + * lock and we aren't holding any read locks or write locks which + * means we can't be the fWriteLockOwner or the fInvariantOwner. + * Wait until either of those threads, if any, is done with the lock. + */ + while ((fWriteLockOwner != null) || (fInvariantLockOwner != null)) { + wait(); + } + + // Claim the lock + fInvariantLockOwner = me; + } + fInvariantLockCount++; + fNumReaders++; + appendLockState(me, kInvariantRead); + } + + public synchronized void exitInvariantReadLock() { + Thread me = Thread.currentThread(); + removeLockState(me, kInvariantRead); + --fNumReaders; + if (--fInvariantLockCount == 0) { + fInvariantLockOwner = null; + } + notify(); + } + + public synchronized void enterWriteLock() + throws InterruptedException + { + Thread me = Thread.currentThread(); + if (me != fWriteLockOwner) { + // Count up how many read-locks we have currently. The only possible + // type of lock state is kRead or kInvariantRead. + int readCount = 0; + LockState list = fStateList.fPrev; + while (list != fStateList) { + LockState prev = list.fPrev; + if (list.fThread == me) { + Assert.Assertion(list.fState != kWrite); + readCount++; + } + list = prev; + } + + /** + * If we have the invariant read lock then we don't have to release + * our read locks, otherwise we do. When we do we must notify any + * other threads that are waiting for this to occur. When we don't + * we change the wait loop below to wait until all the other + * read locks are released isntead of waiting until all the + * read locks are released. + */ + int baseNumReaders = readCount; + if (me != fInvariantLockOwner) { + // Release our read locks now + baseNumReaders = 0; + fNumReaders -= readCount; + notify(); + } + + // Wait for all other readers or a writer to exit the lock + while ((fWriteLockOwner != null) || (fNumReaders > baseNumReaders)) { + wait(); + } + + // At this point fWriteLockOwner == null and fNumReaders == + // baseNumReaders which means that we can claim the + // write-lock. Restore fNumReaders to account for any of our read + // locks on the list. + if (me != fInvariantLockOwner) { + // Recover our read locks. Note that we left them on the state + // list this entire time, we just reduced the counts to that + // the various wait loops would think they were gone. + fNumReaders += readCount; + } + fWriteLockOwner = me; + } + fWriteLockCount++; + appendLockState(me, kWrite); + } + + public synchronized void exitWriteLock() { + Thread me = Thread.currentThread(); + removeLockState(me, kWrite); + if (--fWriteLockCount == 0) { + fWriteLockOwner = null; + } + notify(); + } + + //---------------------------------------------------------------------- + + static class LockState { + // Linkage for either fStateStack or for fFreeList + LockState fPrev, fNext; + Thread fThread; + int fState; + } + + // Legal state values for a LockState. These indicate what kind + // of enter was done on the lock. + static final int kRead = 0; + static final int kWrite = 1; + static final int kInvariantRead = 2; + static String[] kStateToString = { + "read-lock", "write-lock", "invariant-read-lock" + }; + + /** + * Circular list of LockState objects. When the lock changes state (via + * one of the three enter methods) we append a LockState object to the + * end of the stack. When a thread exits the lock we find it's last + * LockState object and remove it from the list. + */ + private LockState fStateList; + + /** + * A list of free LockState objects used to reduce the execution + * time for allocation. + */ + private LockState fFreeList; + private int fFreeListLength; + private static final int kMaxFreeListLength = 10; + + /** + * See if aThread has the given type of lock + */ + private boolean isLocked(Thread aThread, int aHow) { + LockState list = fStateList.fPrev; + while (list != fStateList) { + if (list.fThread == aThread) { + if (list.fState == aHow) { + return true; + } + } + list = list.fPrev; + } + return false; + } + + private void appendLockState(Thread aThread, int aState) { + LockState ls = newLockState(); + ls.fThread = aThread; + ls.fState = aState; + + // Append ls to the state list + LockState list = fStateList; + ls.fNext = list; + ls.fPrev = list.fPrev; + list.fPrev.fNext = ls; + list.fPrev = ls; + } + + /** + * Walk up the thread state list and look for aThread. Verify that the + * LockState is in the right state and if it is then we remove that + * LockState from the list. + */ + private void removeLockState(Thread aThread, int aState) { + LockState tos = fStateList.fPrev; + while (tos != fStateList) { + if (tos.fThread == aThread) { + if (tos.fState != aState) { + throw new Error("mismatched lock exit: entry=" + + kStateToString[tos.fState] + " exit=" + + kStateToString[aState]); + } + + // Remove tos from the list + tos.fPrev.fNext = tos.fNext; + tos.fNext.fPrev = tos.fPrev; + freeLockState(tos); + return; + } + tos = tos.fPrev; + } + throw new Error("unmatched lock exit"); + } + + private LockState newLockState() { + if (fFreeListLength == 0) { + return new LockState(); + } + + // Get a LockState from the free list + LockState tos = fFreeList.fPrev; + tos.fPrev.fNext = tos.fNext; + tos.fNext.fPrev = tos.fPrev; + fFreeListLength--; + return tos; + } + + private void freeLockState(LockState aState) { + if (fFreeListLength < kMaxFreeListLength) { + LockState list = fFreeList; + + // Append aState to the free list + aState.fNext = list; + aState.fPrev = list.fPrev; + list.fPrev.fNext = aState; + list.fPrev = aState; + fFreeListLength++; + } + } +} diff --git a/mozilla/grendel/calypso/util/RandomAccessFileWithByteLines.java b/mozilla/grendel/calypso/util/RandomAccessFileWithByteLines.java new file mode 100644 index 00000000000..3fcc5edbcc0 --- /dev/null +++ b/mozilla/grendel/calypso/util/RandomAccessFileWithByteLines.java @@ -0,0 +1,174 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +/* RandomAccessFileWithByteLines takes a RandomAccessFile, and provides a way + * to get data a line at a time from it. The API probably needs rethinking. + */ + + + +package calypso.util; + +import calypso.util.Assert; + +import java.io.*; + +public class RandomAccessFileWithByteLines { + static final int INITBUFSIZE = 128; + + protected RandomAccessFile fInput; + protected byte[] fBuffer = new byte[INITBUFSIZE]; + protected int fOffset; + protected int fEnd; + + + public RandomAccessFileWithByteLines(RandomAccessFile f) { + fInput = f; + } + + public boolean readLine(ByteBuf buf) throws IOException { + buf.setLength(0); + do { + for (int i=fOffset ; i 0); + } + + /** + * Eat up one newline if present in the fBuffer. If a newline + * was eaten up return true otherwise false. This will handle + * mac (\r), unix (\n) and windows (\r\n) style of newlines + * transparently. + */ + public boolean eatNewline() throws IOException { + boolean eaten = false; + int amount = fEnd - fOffset; + if (amount < 2) { + if (!fillForCapacity(2)) { + // Couldn't get two characters + if (fEnd - fOffset == 0) { + return false; + } + if ((fBuffer[fOffset] == '\r') || (fBuffer[fOffset] == '\n')) { + fOffset++; + return true; + } + return false; + } + } + if (fBuffer[fOffset] == '\r') { + fOffset++; + eaten = true; + } + if (fBuffer[fOffset] == '\n') { + eaten = true; + fOffset++; + } + return eaten; + } + + + public long getFilePointer() throws IOException { + return fInput.getFilePointer() - (fEnd - fOffset); + } + + public void seek(long loc) throws IOException { + fInput.seek(loc); + fOffset = 0; + fEnd = 0; + } + + protected boolean fill() throws IOException { + if (fEnd - fOffset != 0) { + throw new IOException("fill of non-empty fBuffer"); + } + fOffset = 0; + fEnd = fInput.read(fBuffer); + if (fEnd < 0) { + fEnd = 0; + return false; + } + return true; + } + + /** + * Fill the fBuffer, keeping whatever is unread in the fBuffer and + * ensuring that "capacity" characters of total filled fBuffer + * space is available. Return false if the final amount of data + * in the fBuffer is less than capacity, otherwise return true. + */ + protected boolean fillForCapacity(int capacity) throws IOException { + int remainingData = fEnd - fOffset; + if (remainingData >= capacity) { + // The fBuffer already holds enough data to satisfy capacity + return true; + } + + // The fBuffer needs more data. See if the fBuffer itself must be + // enlarged to statisfy capacity. + if (capacity >= fBuffer.length) { + // Grow the fBuffer to hold enough space + int newBufLen = fBuffer.length * 2; + if (newBufLen < capacity) newBufLen = capacity; + byte newbuf[] = new byte[newBufLen]; + System.arraycopy(fBuffer, fOffset, newbuf, 0, remainingData); + fOffset = 0; + fEnd = remainingData; + fBuffer = newbuf; + } else { + if (remainingData != 0) { + // Slide the data that is currently present in the fBuffer down + // to the start of the fBuffer + System.arraycopy(fBuffer, fOffset, fBuffer, 0, remainingData); + fOffset = 0; + fEnd = remainingData; + } + } + + // Fill up the remainder of the fBuffer + int mustRead = capacity - remainingData; + int nb = fInput.read(fBuffer, remainingData, + fBuffer.length-remainingData); + if (nb < 0) { + return false; + } + fEnd += nb; + if (nb < mustRead) { + return false; + } + return true; + } + + public void readFully(byte[] arr, int off, int length) throws IOException { + Assert.Assertion(fOffset == fEnd); + fInput.readFully(arr, off, length); + } + + +} + diff --git a/mozilla/grendel/calypso/util/Recycler.java b/mozilla/grendel/calypso/util/Recycler.java new file mode 100644 index 00000000000..2075ecf7d68 --- /dev/null +++ b/mozilla/grendel/calypso/util/Recycler.java @@ -0,0 +1,201 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * + * + * @author gess 04-16-97 1:44pm + * @version $Revision: 1.1 $ + * @notes This is a general purpose recycler. + * It contains objects of a specified capacity. + * If you ask for a recycled object and we have + * one, then it's returned as object as you must typecast. + * If we don't have a recycled object, we return null. + */ + +public class Recycler implements MemoryPressure +{ + Object[] fBuffer; + int fCount; + int fCapacity; + final static int gDefaultCapacity = 100; + + /****************************************** + * @author gess 04-16-97 1:40pm + * @param + * @return + * @notes default constructor, assumes + buffer capacity of 100 + *******************************************/ + public Recycler() + { + MemoryManager.Add(this); + fCapacity=gDefaultCapacity; + reset(); + } + + /******************************************************** + * @author gess 04-16-97 1:40pm + * @param aGivenCapacity -- size of underlying buffer + * @return + * @notes constructor with capacity argument + ********************************************************/ + public Recycler(int aGivenCapacity) + { + MemoryManager.Add(this); + fCapacity=aGivenCapacity; + reset(); + } + + /******************************************************** + * @author gess 04-16-97 1:43pm + * @param none + * @return none + * @notes discards original buffer (if present) and creates + a new buffer of size fCapacity + *********************************************************/ + public void reset() + { + fBuffer= new Object[fCapacity]; + fCount = 0; + } + + /******************************************************** + * @author gess 04-16-97 1:43pm + * @param anObject -- object to be recycled + * @return none + * @notes always drop on end of list; when capacity is + reached, replaced last entry + *********************************************************/ + public void recycle(Object anObject) + { + if (null!=anObject) + { + if (null==fBuffer) + { + reset(); + fBuffer[fCount++] = anObject; + } + else + { + if (fCount==fCapacity) + { + // Replace last entry with the new one, + // to improve memory locality + fBuffer[fCount-1] = anObject; + } + else + fBuffer[fCount++] = anObject; + } + } + } + + /** + * Recycles the entire contents of the given list. + * + * @param + * @return + * @exception + * @author gess 04-16-97 4:22pm + **/ + public void recycle(Object anObjectArray[]) + { + int count = anObjectArray.length; + int index; + + for (index = 0; index < count; index++) + { + recycle (anObjectArray[index]); + anObjectArray[index] = null; + } + } + + /******************************************************** + * @author gess 04-16-97 1:43pm + * @param none + * @return object or null + * @notes if we don't have any recycled objects, we + return null. Objects we DO return must be + type cast by you to the appropriate type. + *********************************************************/ + public Object getRecycledObject() + { + Object result=null; + if (fCount>0) + { + result=fBuffer[--fCount]; + fBuffer[fCount]=null; + } + return result; + } + + /****************************************** + * @author gess 04-16-97 1:40pm + * @param none + * @return none + * @notes causes buffer to be released and + count to be reset to 0. + *******************************************/ + public void empty() + { + fCount = 0; + fBuffer = null; + } + + /****************************************** + * @author gess 04-16-97 1:40pm + * @param + * @return + * @notes copied from kipps implementation + *******************************************/ + public void preGC(long aCurrentHeapSpace, long aMaximumHeapSpace) + { + // Discard our array of buffers so that the GC can + // compact the heap nicely. + empty(); + } + + /****************************************** + * @author gess 04-16-97 1:41pm + * @param + * @return + * @notes copied from kipps implementation + *******************************************/ + public void postGC(long aCurrentHeapSpace, long aMaximumHeapSpace) + { + } + + /****************************************** + * @author gess 04-16-97 1:40pm + * @param + * @return + * @notes + *******************************************/ + public void panic() + { + // Get rid of everything, including ourselves + empty(); + } + +} + diff --git a/mozilla/grendel/calypso/util/SelfTest.java b/mozilla/grendel/calypso/util/SelfTest.java new file mode 100644 index 00000000000..887392dfab8 --- /dev/null +++ b/mozilla/grendel/calypso/util/SelfTest.java @@ -0,0 +1,126 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/* #### +import calypso.core.SystemLog; +*/ + +/** + * SelfTest for calypso.util package + * + * @author sclark 04-22-97 3:52pm + * @notes runAllTests must be successfully run after every check-in + */ +public class SelfTest { + public static void main(String args[]) { + SelfTest selfTest = new SelfTest(); + try { + selfTest.runAllTests(); + } catch (SelfTestException e) { + System.err.println("SelfTestException: " + e); + e.printStackTrace(); + } + } + + /** + * Default constructor + * + * @author sclark 04-22-97 3:53pm + **/ + public SelfTest() + { + reset(); + } + + private void reset() + { + } + + public void runAllTests() + throws SelfTestException + { + runUnitTest(); + runThreadTest(); + runStressTest(); + runIDMapTest(); + runAtomTest(); + } + + public void runUnitTest() + throws SelfTestException + { + +/* #### + SystemLog.Instance().log("============================"); + SystemLog.Instance().log("Util unit tests:"); + + SystemLog.Instance().log("Begin RWLock test."); + runRWLockTest(); + SystemLog.Instance().log("End RWLock test."); + + SystemLog.Instance().log("============================"); +*/ + } + + + public void runThreadTest() + throws SelfTestException + { + +/* #### + SystemLog.Instance().log("============================"); + SystemLog.Instance().log("Util thread test."); + + + SystemLog.Instance().log("============================"); +*/ + } + + public void runStressTest() + throws SelfTestException + { + +/* #### + SystemLog.Instance().log("============================"); + SystemLog.Instance().log("Util stress test."); + + SystemLog.Instance().log("============================"); +*/ + } + + public void runIDMapTest() + throws SelfTestException + { + SelfTestIDMap.run(); + } + + public void runAtomTest() + throws SelfTestException + { + SelfTestAtom.run(); + } + + public void runRWLockTest() + throws SelfTestException + { + SelfTestRWLock.run(); + } +} diff --git a/mozilla/grendel/calypso/util/SelfTestAtom.java b/mozilla/grendel/calypso/util/SelfTestAtom.java new file mode 100644 index 00000000000..84358b59229 --- /dev/null +++ b/mozilla/grendel/calypso/util/SelfTestAtom.java @@ -0,0 +1,53 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +class SelfTestAtom { + public static void run() + throws SelfTestException + { + // Create a bunch of atoms + int count = 20000; + int[] ids = new int[count]; + String[] strings = new String[count]; + Atom[] atoms = new Atom[count]; + for (int i = 0; i < count; i++) { + strings[i] = asciify(i); + atoms[i] = Atom.Find(strings[i]); + } + + // Now verify that they act like atoms + for (int i = 0; i < count; i++) { + Atom atom = Atom.Find(strings[i]); + if (atom == atoms[i]) { + if (atom.toString() == strings[i]) { + continue; + } + throw new SelfTestException("atom.toString() != strings[i]"); + } else { + throw new SelfTestException("atom != atoms[i]"); + } + } + } + + static String asciify(int i) { + return Integer.toString(i, 16); + } +} diff --git a/mozilla/grendel/calypso/util/SelfTestException.java b/mozilla/grendel/calypso/util/SelfTestException.java new file mode 100644 index 00000000000..0c10765409a --- /dev/null +++ b/mozilla/grendel/calypso/util/SelfTestException.java @@ -0,0 +1,33 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * Self test exceptions are thrown when self test code is invoked + * and fails to operate properly. + */ +public class SelfTestException extends Exception { + public SelfTestException() { + } + + public SelfTestException(String msg) { + super(msg); + } +} diff --git a/mozilla/grendel/calypso/util/SelfTestIDMap.java b/mozilla/grendel/calypso/util/SelfTestIDMap.java new file mode 100644 index 00000000000..ec8c381abdd --- /dev/null +++ b/mozilla/grendel/calypso/util/SelfTestIDMap.java @@ -0,0 +1,78 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +class SelfTestIDMap { + public static void run() + throws SelfTestException + { + int count = 20000; + int[] ids = new int[count]; + String[] strings = new String[count]; + for (int i = 0; i < count; i++) { + strings[i] = asciify(i); + } + + // Fill the map + long start = System.currentTimeMillis(); + IDMap map = new IDMap(); + for (int i = 0; i < count; i++) { + ids[i] = map.stringToID(strings[i]); + } + long end = System.currentTimeMillis(); + System.out.println("stringToID: took " + (end - start) + "ms"); + + // Test the map + start = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + int id = map.stringToID(strings[i]); + if (ids[i] != id) { + throw new SelfTestException("whoops: id=" + id + " ids[i]=" + ids[i]); + } + } + end = System.currentTimeMillis(); + System.out.println("stringToID: took " + (end - start) + "ms"); + + start = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + String s = map.stringToString(strings[i]); + if (s != strings[i]) { + throw new SelfTestException("whoops: s=" + s + " strings[i]=" + strings[i]); + } + } + end = System.currentTimeMillis(); + System.out.println("stringToString: took " + (end - start) + "ms"); + + // Verify that id's map back to strings properly + start = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + String s = map.idToString(ids[i]); + if (!s.equals(strings[i])) { + throw new SelfTestException("whoops: s=" + s + " strings[i]=" + strings[i]); + } + } + end = System.currentTimeMillis(); + System.out.println("idToString: took " + (end - start) + "ms"); + } + + static String asciify(int i) { + return Integer.toString(i, 16); + } +} diff --git a/mozilla/grendel/calypso/util/SelfTestRWLock.java b/mozilla/grendel/calypso/util/SelfTestRWLock.java new file mode 100644 index 00000000000..a95ba1d81bb --- /dev/null +++ b/mozilla/grendel/calypso/util/SelfTestRWLock.java @@ -0,0 +1,343 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +class SelfTestRWLock { + static void simpleReadTest(RWLock aLock) + throws InterruptedException + { + for (int i = 0; i < 5; i++) { + aLock.enterReadLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitReadLock(); + } + } + + static void simpleReadWriteTest(RWLock aLock) + throws InterruptedException + { + for (int i = 0; i < 5; i++) { + aLock.enterReadLock(); + } + for (int i = 0; i < 5; i++) { + aLock.enterWriteLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitWriteLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitReadLock(); + } + } + + static void simpleInvariantReadTest(RWLock aLock) + throws InterruptedException + { + for (int i = 0; i < 5; i++) { + aLock.enterInvariantReadLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitInvariantReadLock(); + } + } + + static void simpleInvariantReadWriteTest(RWLock aLock) + throws InterruptedException + { + for (int i = 0; i < 5; i++) { + aLock.enterInvariantReadLock(); + } + for (int i = 0; i < 5; i++) { + aLock.enterWriteLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitWriteLock(); + } + for (int i = 0; i < 5; i++) { + aLock.exitInvariantReadLock(); + } + } + + // Verify that an out of order exit throws an error + static void outOfOrderExit0(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterReadLock(); + try { + aLock.exitWriteLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + // Verify that an out of order exit throws an error + static void outOfOrderExit1(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterReadLock(); + try { + aLock.exitInvariantReadLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + static void outOfOrderExit2(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterInvariantReadLock(); + try { + aLock.exitReadLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + static void outOfOrderExit3(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterInvariantReadLock(); + try { + aLock.exitWriteLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + static void outOfOrderExit4(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterWriteLock(); + try { + aLock.exitReadLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + static void outOfOrderExit5(RWLock aLock) + throws InterruptedException, SelfTestException + { + aLock.enterWriteLock(); + try { + aLock.exitInvariantReadLock(); + } catch (Error e) { + return; + } + throw new SelfTestException("whoops"); + } + + public static void run() + throws SelfTestException + { + // Perform simple tests that have no contention with another thread + try { + simpleReadTest(new RWLock()); + simpleReadWriteTest(new RWLock()); + simpleInvariantReadTest(new RWLock()); + simpleInvariantReadWriteTest(new RWLock()); + + outOfOrderExit0(new RWLock()); + outOfOrderExit1(new RWLock()); + outOfOrderExit2(new RWLock()); + outOfOrderExit3(new RWLock()); + outOfOrderExit4(new RWLock()); + outOfOrderExit5(new RWLock()); + + verifyBlocking(new RWLock(), kRead, kWrite); + verifyBlocking(new RWLock(), kInvariantRead, kWrite); + verifyBlocking(new RWLock(), kWrite, kRead); + verifyBlocking(new RWLock(), kWrite, kInvariantRead); + + stressTestContention(new RWLock()); + } catch (InterruptedException e) { + // Test was interrupted + e.printStackTrace(); + } + } + + static final int kRead = 0; + static final int kWrite = 1; + static final int kInvariantRead = 2; + static String[] kStateToString = { + "read-lock", "write-lock", "invariant-read-lock" + }; + + // Verify that blocking is occuring by using the RWLock to + // protect the helper.fValue and guarantee a correct ordering + // of evaluation. + static void verifyBlocking(RWLock aLock, int aMe, int aHelper) + throws SelfTestException, InterruptedException + { + Enter(aLock, aMe); + + RWTestHelper helper = new RWTestHelper(aLock, aHelper, 1, 1, 0); + Thread t1 = new Thread(helper); + t1.start(); + + // Make sure RWTestHelper is waiting to get the write-lock + Thread.sleep(500); + if (helper.fValue != 1) { + throw new SelfTestException("write-lock didn't block"); + } + + // Give RWTestHelper the lock; set value to 2 before it gets + // the lock + helper.fValue = 2; + Exit(aLock, aMe); + + // make sure RWTestHelper is done + Thread.sleep(500); + if (helper.fValue != 0) { + throw new SelfTestException("helper didn't finish"); + } + } + + static void stressTestContention(RWLock aLock) + throws SelfTestException, InterruptedException + { + int count = 500000; + int delay = 123; + + long start = System.currentTimeMillis(); + + // Fire up a bunch of threads + for (int i = 0; i < 3*100; i++) { + RWTestHelper helper = new RWTestHelper(aLock, i % 3, 1, count, delay); + Thread t = new Thread(helper); + t.start(); + ThreadEnter(); + } + + ThreadWait(); + long end = System.currentTimeMillis(); + System.out.println("Stress test: duration=" + (end - start) + "ms"); + } + + static Object gThreadCounterLock = new Object(); + static int gThreadCounter; + + static void ThreadEnter() + throws InterruptedException + { + synchronized (gThreadCounterLock) { + gThreadCounter++; + } + } + + static void ThreadExit() + throws InterruptedException + { + synchronized (gThreadCounterLock) { + if (--gThreadCounter == 0) { + gThreadCounterLock.notify(); + } + } + } + + static void ThreadWait() + throws InterruptedException + { + synchronized (gThreadCounterLock) { + while (gThreadCounter > 0) { + gThreadCounterLock.wait(); + } + } + } + + static final boolean noisy = false; + + static void Enter(RWLock aLock, int aHow) + throws InterruptedException + { + if (noisy) { + System.out.println(Thread.currentThread() + ": enter " + + kStateToString[aHow]); + } + switch (aHow) { + case kRead: + aLock.enterReadLock(); + break; + case kWrite: + aLock.enterWriteLock(); + break; + case kInvariantRead: + aLock.enterInvariantReadLock(); + break; + } + } + + static void Exit(RWLock aLock, int aHow) + throws InterruptedException + { + if (noisy) { + System.out.println(Thread.currentThread() + ": exit " + + kStateToString[aHow]); + } + switch (aHow) { + case kRead: + aLock.exitReadLock(); + break; + case kWrite: + aLock.exitWriteLock(); + break; + case kInvariantRead: + aLock.exitInvariantReadLock(); + break; + } + } + + static class RWTestHelper implements Runnable { + RWLock fLock; + int fHow; + int fValue; + int fCount; + int fDelay; + + RWTestHelper(RWLock aLock, int aHow, int aValue, int aCount, int aDelay) { + fLock = aLock; + fHow = aHow; + fValue = aValue; + fCount = aCount; + fDelay = aDelay; + } + + public void run() { + try { + for (int i = 0; i < fCount; i++) { + SelfTestRWLock.Enter(fLock, fHow); + fValue /= 4; + SelfTestRWLock.Exit(fLock, fHow); + if ((i % 1000) == 0) { + Thread.sleep(fDelay); + } + } + SelfTestRWLock.ThreadExit(); + } catch (InterruptedException e) { + } + } + } +} diff --git a/mozilla/grendel/calypso/util/SignedInteger.java b/mozilla/grendel/calypso/util/SignedInteger.java new file mode 100644 index 00000000000..337549a6760 --- /dev/null +++ b/mozilla/grendel/calypso/util/SignedInteger.java @@ -0,0 +1,175 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +/** + * Utility class that represents the parse state from parsing a + * signed integer. The sign of the integer is maintained so that "10" + * can be distinguished from "+10". In addition, we can optionally + * handle numbers expressed as percentages. + * + * @author Kipp E.B. Hickman + * @see Integer.parseInt + */ +public class SignedInteger { + static public final int PLUS = 1; + static public final int MINUS = -1; + static public final int NONE = 0; + + protected int fValue; + protected int fSign; + protected boolean fPct; + + /** + The integer part works (almost) the same as atoi(): +

    +
  • Skip leading white spaces. +
  • Non-digit char terminates convertion. +
  • If got a valid integer, don't throw NumberFormatException. +
  • Throw exception if first nonspace char is not digit(atoi returns 0). + Example: " +4z" works as 4. + */ + // Nav 4.0 #define XP_ATOI(ss) atoi(ss) + // Typical usage: ns\lib\layout\layutil.c lo_ValueOrPercent(); + public SignedInteger(String aString, boolean aAllowPct) + throws NumberFormatException + { + if( aString == null) + throw new NumberFormatException("1"); // Null string + + int value = 0; + int sign = NONE; + int pp; + + int len = aString.length(); + if (len == 0) + throw new NumberFormatException("2"); // Empty string + + //eat leading whitespace + for(pp=0; pp= len ) + throw new NumberFormatException("3"); // Blank string + + if (pp > 0 ) { + aString = aString.substring( pp ); + len -= pp; + } + + if (aString.charAt(0) == '+') { + sign = PLUS; + aString = aString.substring(1); + len--; + } else if (aString.charAt(0) == '-') { + sign = MINUS; + aString = aString.substring(1); + len--; + } + + // no white space allowed after sign. + if( ! java.lang.Character.isDigit(aString.charAt(0)) ) + throw new NumberFormatException("4"); // start with none digit + + // cut off nondigit at end, otherwise Integer.parseInt() will throw exception. + for(pp=0; pp < len ; pp++) { + if( ! java.lang.Character.isDigit(aString.charAt(pp)) ) + break; + } + + if( pp < len ) { + // it has nondigit at end. + if (aAllowPct && aString.charAt(pp) == '%' ) + fPct = true; + // cut it off + aString = aString.substring(0, pp ); + } + + value = Integer.parseInt(aString, 10); + + fValue = value; + fSign = sign; + if (fPct) { + if ((fSign == MINUS) && (fValue != 0)) { + // Negative percentages are not allowed + throw new NumberFormatException("5"); // negative percentage + } else if (fValue > 100) { + // Percentages larger than 100 are quietly clamped + fValue = 100; + } + } + } + + public SignedInteger(int aValue, int aSign) { + Assert.Assertion(aValue >= 0); + Assert.Assertion((aSign == PLUS) || (aSign == MINUS) || (aSign == NONE)); + fValue = aValue; + fSign = aSign; + } + + public int intValue() { + return fValue; + } + + public boolean isPct() { + return fPct; + } + + /** + * Compute a value based on "aBaseValue". Apply the sign of this + * SignedInteger to determine the value to return. + */ + public int computeValue(int aBaseValue) { + if (fPct) { + return aBaseValue * fValue; + } + switch (fSign) { + case PLUS: + return aBaseValue + fValue; + case MINUS: + return aBaseValue - fValue; + } + return fValue; + } + + /** + * Return the sign of the value. + */ + public int getSign() { + return fSign; + } + + public String toString() { + if (fPct) { + return Integer.toString(fValue, 10) + "%"; + } + switch (fSign) { + case PLUS: + return "+" + Integer.toString(fValue, 10); + case MINUS: + return "-" + Integer.toString(fValue, 10); + } + return Integer.toString(fValue, 10); + } + // XXX equals + // XXX hashcode +} diff --git a/mozilla/grendel/calypso/util/SingleEnumeration.java b/mozilla/grendel/calypso/util/SingleEnumeration.java new file mode 100644 index 00000000000..58b4e575f3e --- /dev/null +++ b/mozilla/grendel/calypso/util/SingleEnumeration.java @@ -0,0 +1,48 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +import java.util.NoSuchElementException; +import java.util.Enumeration; + +/** + * An enumeration to return a single element. + */ +public class SingleEnumeration implements Enumeration { + Object fElement; + boolean fSpent; + + public SingleEnumeration(Object aElement) { + fSpent = false; + fElement = aElement; + } + + public boolean hasMoreElements() { + return !fSpent; + } + + public Object nextElement() throws NoSuchElementException { + if (fSpent) { + throw new NoSuchElementException(); + } + else { + fSpent = true; + return fElement; + } + } +} diff --git a/mozilla/grendel/calypso/util/StringBuf.java b/mozilla/grendel/calypso/util/StringBuf.java new file mode 100644 index 00000000000..ddb8cfef757 --- /dev/null +++ b/mozilla/grendel/calypso/util/StringBuf.java @@ -0,0 +1,742 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * This class is similar to java/lang/StringBuffer with the following changes: + *
    • None of the methods are synchronized + *
    • There is no sharing of the character array + *
    • Converting to a String requires a copy of the character data + *
    • Alloc and Recycle are provided to speed up allocation + *
    • A bunch of useful operations are provided (comparisons, etc.) + *
    + */ +public final class StringBuf { + private char value[]; + private int count; + + static StringBufRecycler gRecycler; + + /** + * Constructs an empty String buffer, reusing one from the + * recycler. + */ + static synchronized public StringBuf Alloc() { + if (gRecycler == null) { + gRecycler = new StringBufRecycler(); + } + return gRecycler.alloc(); + } + + /** + * Release a StringBuf to the recycler. It is up to the caller + * to ensure that buffer is no longer being used otherwise + * unpredicatable program behavior will result. + */ + static synchronized public void Recycle(StringBuf aBuf) { + if (gRecycler == null) { + gRecycler = new StringBufRecycler(); + } + gRecycler.recycle(aBuf); + } + + /** + * Empty the recycler discarding any cached StringBuf objects + */ + static synchronized public void EmptyRecycler() { + if (null != gRecycler) { + gRecycler = null; + } + } + + static void classFinalize() throws Throwable { + // Poof! Now we are unloadable even though we have a static + // variable. + } + + /** + * Constructs an empty String buffer. + */ + public StringBuf() { + this(16); + } + + /** + * Constructs an empty String buffer with the specified initial length. + * @param length the initial length + */ + public StringBuf(int length) { + value = new char[length]; + } + + /** + * Constructs a String buffer with the specified initial value. + * @param str the initial value of the buffer + */ + public StringBuf(String str) { + this(str.length() + 16); + append(str); + } + + public StringBuf(char chars[], int offset, int length) { + value = new char[length]; + System.arraycopy(chars, offset, value, 0, length); + count = length; + } + + /** + * Returns the length (character count) of the buffer. + */ + public int length() { + return count; + } + + /** + * Returns the current capacity of the String buffer. The capacity + * is the amount of storage available for newly inserted + * characters; beyond which an allocation will occur. + */ + public int capacity() { + return value.length; + } + + /** + * Ensures that the capacity of the buffer is at least equal to the + * specified minimum. + * @param minimumCapacity the minimum desired capacity + */ + public void ensureCapacity(int minimumCapacity) { + int maxCapacity = value.length; + + if (minimumCapacity > maxCapacity) { + int newCapacity = (maxCapacity + 1) * 2; + if (minimumCapacity > newCapacity) { + newCapacity = minimumCapacity; + } + + char newValue[] = new char[newCapacity]; + System.arraycopy(value, 0, newValue, 0, count); + value = newValue; + } + } + + /** + * Sets the length of the String. If the length is reduced, characters + * are lost. If the length is extended, the values of the new characters + * are set to 0. + * @param newLength the new length of the buffer + * @exception StringIndexOutOfBoundsException If the length is invalid. + */ + public void setLength(int newLength) { + if (newLength < 0) { + throw new StringIndexOutOfBoundsException(newLength); + } + ensureCapacity(newLength); + + if (count < newLength) { + for (; count < newLength; count++) { + value[count] = '\0'; + } + } + count = newLength; + } + + /** + * Clear the content for reuse. Farster than setLength(0). + */ + public void setEmpty() { + count = 0; + } + + /** + * Returns the character at the specified index. An index ranges + * from 0..length()-1. + * @param index the index of the desired character + * @exception StringIndexOutOfBoundsException If the index is invalid. + */ + public char charAt(int index) { + if ((index < 0) || (index >= count)) { + throw new StringIndexOutOfBoundsException(index); + } + return value[index]; + } + + /** + * Copies the characters of the specified substring (determined by + * srcBegin and srcEnd) into the character array, starting at the + * array's dstBegin location. Both srcBegin and srcEnd must be legal + * indexes into the buffer. + * @param srcBegin begin copy at this offset in the String + * @param srcEnd stop copying at this offset in the String + * @param dst the array to copy the data into + * @param dstBegin offset into dst + * @exception StringIndexOutOfBoundsException If there is an invalid index into the buffer. + */ + public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { + if ((srcBegin < 0) || (srcBegin >= count)) { + throw new StringIndexOutOfBoundsException(srcBegin); + } + if ((srcEnd < 0) || (srcEnd > count)) { + throw new StringIndexOutOfBoundsException(srcEnd); + } + if (srcBegin < srcEnd) { + System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); + } + } + + /** + * Changes the character at the specified index to be ch. + * @param index the index of the character + * @param ch the new character + * @exception StringIndexOutOfBoundsException If the index is invalid. + */ + public void setCharAt(int index, char ch) { + if ((index < 0) || (index >= count)) { + throw new StringIndexOutOfBoundsException(index); + } + value[index] = ch; + } + + /** + * Appends an object to the end of this buffer. + * @param obj the object to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(Object obj) { + return append(String.valueOf(obj)); + } + + /** + * Appends a String to the end of this buffer. + * @param str the String to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(String str) { + if (str == null) { + str = String.valueOf(str); + } + + int len = str.length(); + ensureCapacity(count + len); + str.getChars(0, len, value, count); + count += len; + return this; + } + + /** + * Appends an array of characters to the end of this buffer. + * @param str the characters to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(char str[]) { + int len = str.length; + ensureCapacity(count + len); + System.arraycopy(str, 0, value, count, len); + count += len; + return this; + } + + public StringBuf assign(char str[]) { + count = 0; + return( append(str) ); + } + + public StringBuf assign(String str) { + count = 0; + return( append(str) ); + } + + public StringBuf assign(char str[], int offset, int len) { + count = 0; + return(append( str, offset, len) ); + } + + /** + * Appends a part of an array of characters to the end of this buffer. + * @param str the characters to be appended + * @param offset where to start + * @param len the number of characters to add + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(char str[], int offset, int len) { + ensureCapacity(count + len); + System.arraycopy(str, offset, value, count, len); + count += len; + return this; + } + + /** + * Appends a boolean to the end of this buffer. + * @param b the boolean to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(boolean b) { + return append(String.valueOf(b)); + } + + /** + * Appends a character to the end of this buffer. + * @param ch the character to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(char c) { + ensureCapacity(count + 1); + value[count++] = c; + return this; + } + + /** + * Appends an integer to the end of this buffer. + * @param i the integer to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(int i) { + return append(String.valueOf(i)); + } + + /** + * Appends a long to the end of this buffer. + * @param l the long to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(long l) { + return append(String.valueOf(l)); + } + + /** + * Appends a float to the end of this buffer. + * @param f the float to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(float f) { + return append(String.valueOf(f)); + } + + /** + * Appends a double to the end of this buffer. + * @param d the double to be appended + * @return the StringBuf itself, NOT a new one. + */ + public StringBuf append(double d) { + return append(String.valueOf(d)); + } + + /** + * Inserts an object into the String buffer. + * @param offset the offset at which to insert + * @param obj the object to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, Object obj) { + return insert(offset, String.valueOf(obj)); + } + + /** + * Inserts a String into the String buffer. + * @param offset the offset at which to insert + * @param str the String to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, String str) { + if ((offset < 0) || (offset > count)) { + throw new StringIndexOutOfBoundsException(); + } + int len = str.length(); + ensureCapacity(count + len); + System.arraycopy(value, offset, value, offset + len, count - offset); + str.getChars(0, len, value, offset); + count += len; + return this; + } + + /** + * Inserts an array of characters into the String buffer. + * @param offset the offset at which to insert + * @param str the characters to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, char str[]) { + if ((offset < 0) || (offset > count)) { + throw new StringIndexOutOfBoundsException(); + } + int len = str.length; + ensureCapacity(count + len); + System.arraycopy(value, offset, value, offset + len, count - offset); + System.arraycopy(str, 0, value, offset, len); + count += len; + return this; + } + + /** + * Inserts a boolean into the String buffer. + * @param offset the offset at which to insert + * @param b the boolean to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, boolean b) { + return insert(offset, String.valueOf(b)); + } + + /** + * Inserts a character into the String buffer. + * @param offset the offset at which to insert + * @param ch the character to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset invalid. + */ + public StringBuf insert(int offset, char c) { + ensureCapacity(count + 1); + System.arraycopy(value, offset, value, offset + 1, count - offset); + value[offset] = c; + count += 1; + return this; + } + + /** + * Inserts an integer into the String buffer. + * @param offset the offset at which to insert + * @param i the integer to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, int i) { + return insert(offset, String.valueOf(i)); + } + + /** + * Inserts a long into the String buffer. + * @param offset the offset at which to insert + * @param l the long to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, long l) { + return insert(offset, String.valueOf(l)); + } + + /** + * Inserts a float into the String buffer. + * @param offset the offset at which to insert + * @param f the float to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, float f) { + return insert(offset, String.valueOf(f)); + } + + /** + * Inserts a double into the String buffer. + * @param offset the offset at which to insert + * @param d the double to insert + * @return the StringBuf itself, NOT a new one. + * @exception StringIndexOutOfBoundsException If the offset is invalid. + */ + public StringBuf insert(int offset, double d) { + return insert(offset, String.valueOf(d)); + } + + /** + * Reverse the order of the characters in the String buffer. + */ + public StringBuf reverse() { + int n = count - 1; + for (int j = (n-1) >> 1; j >= 0; --j) { + char temp = value[j]; + value[j] = value[n - j]; + value[n - j] = temp; + } + return this; + } + + public void toUpperCase() { + int n = count; + for (int i = 0; i < n; i++) { + value[i] = Character.toUpperCase(value[i]); + } + } + + public void toLowerCase() { + int n = count; + for (int i = 0; i < n; i++) { + value[i] = Character.toLowerCase(value[i]); + } + } + + /** + * Converts to a String representing the data in the buffer. + */ + public String toString() { + return new String(value, 0, count); + } + + public char[] toChars() { + return value; + } + + /** + * Compress the whitespace present in the buffer. + * XXX I suppose there are i18n issues with this after all a space is + * probably not a space... + */ + public void compressWhitespace(boolean aStripLeadingWhitespace) { + int src = 0; + int dst = 0; + int srcEnd = count; + while (src < srcEnd) { + char ch = value[src]; + if (!Character.isWhitespace(ch)) { + break; + } + if (!aStripLeadingWhitespace) { + dst++; + } + src++; + } + + // Squish down any intermediate spaces + while (src < srcEnd) { + char ch = value[src++]; + value[dst++] = ch; + if (!Character.isWhitespace(ch)) { + continue; + } + while (src < srcEnd) { + ch = value[src]; + if (!Character.isWhitespace(ch)) { + break; + } + src++; + } + } + count = dst; + + if (count != 0) { + // Strip out a trailing space if any. The above loop will leave + // a single space at the end of the value if the value has any + // trailing whitespace. + if (Character.isWhitespace(value[count-1])) { + count--; + } + } + } + + /** + * Return true if the string buffer contains nothing but whitespace + * as defined by Character.isWhitespace() + */ + public boolean isWhitespace() { + int n = count; + for (int i = 0; i < n; i++) { + if (!Character.isWhitespace(value[i])) return false; + } + return true; + } + + /** + * Compares this StringBuf to another String. Returns true if the + * String is equal to this StringBuf; that is, has the same length + * and the same characters in the same sequence. Upper case + * characters are folded to lower case before they are compared. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equalsIgnoreCase(String anotherString) { + return( equalsIgnoreCase( 0, count, anotherString) ); + } + + public boolean equalsIgnoreCase(int aStart, int aLength, String anotherString) { + if( anotherString==null || + aLength!=anotherString.length() || + aStart<0 || + (aStart + aLength) > count ) + return(false); + + for (int i = 0; i < aLength; i++) { + char c1 = value[i+aStart]; + char c2 = anotherString.charAt(i); + if (c1 != c2) { + // If characters don't match but case may be ignored, + // try converting both characters to uppercase. + // If the results match, then the comparison scan should + // continue. + char u1 = Character.toUpperCase(c1); + char u2 = Character.toUpperCase(c2); + if (u1 == u2) + continue; + + // Unfortunately, conversion to uppercase does not work properly + // for the Georgian alphabet, which has strange rules about case + // conversion. So we need to make one last check before + // exiting. + if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) + continue; + return false; + } + } + return true; + } + + /** + * Compares this StringBuf to another StringBuf. Returns true if the + * other StringBuf is equal to this StringBuf; that is, has the same length + * and the same characters in the same sequence. Upper case + * characters are folded to lower case before they are compared. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equalsIgnoreCase(StringBuf anotherString) { + if ((anotherString != null) && (anotherString.count == count)) { + for (int i = 0; i < count; i++) { + char c1 = value[i]; + char c2 = anotherString.value[i]; + if (c1 != c2) { + // If characters don't match but case may be ignored, + // try converting both characters to uppercase. + // If the results match, then the comparison scan should + // continue. + char u1 = Character.toUpperCase(c1); + char u2 = Character.toUpperCase(c2); + if (u1 == u2) + continue; + + // Unfortunately, conversion to uppercase does not work properly + // for the Georgian alphabet, which has strange rules about case + // conversion. So we need to make one last check before + // exiting. + if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) + continue; + return false; + } + } + return true; + } + return false; + } + + public boolean equals(Object aObject) { + if (aObject != null) { + if (aObject instanceof StringBuf) { + return equals((StringBuf) aObject); + } else if (aObject instanceof String) { + return equals((String) aObject); + } + } + return false; + } + + /** + * Compares this StringBuf to another StringBuf. Returns true if the + * other StringBuf is equal to this StringBuf; that is, has the same length + * and the same characters in the same sequence. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equals(StringBuf anotherString) { + if ((anotherString != null) && (anotherString.count == count)) { + for (int i = 0; i < count; i++) { + char c1 = value[i]; + char c2 = anotherString.value[i]; + if (c1 != c2) { + return false; + } + } + return true; + } + return false; + } + + /** + * Compares this StringBuf to another String. Returns true if the + * other String is equal to this StringBuf; that is, has the same length + * and the same characters in the same sequence. + * + * @param anotherString the String to compare this String against + * @return true if the Strings are equal, ignoring case; false otherwise. + */ + public boolean equals(String anotherString) { + if ((anotherString != null) && (anotherString.length() == count)) { + for (int i = 0; i < count; i++) { + char c1 = value[i]; + char c2 = anotherString.charAt(i); + if (c1 != c2) { + return false; + } + } + return true; + } + return false; + } + + public int indexOf(int ch) { + return indexOf(ch, 0); + } + + public int indexOf(int ch, int fromIndex) { + int max = count; + char v[] = value; + + if (fromIndex < 0) { + fromIndex = 0; + } else if (fromIndex >= count) { + // Note: fromIndex might be near -1>>>1. + return -1; + } + for (int i = fromIndex ; i < max ; i++) { + if (v[i] == ch) { + return i; + } + } + return -1; + } + + public void remove(int fromIndex) { + remove(fromIndex, count); + } + + /** + * Remove characters from the StringBuf starting at fromIndex and up + * to but not including toIndex. If toIndex is beyond the length of + * the StringBuf then it is automatically clamped to the end of the + * StringBuf. If fromIndex is out of range a StringIndexOutOfBoundsException + * is thrown. + */ + public void remove(int fromIndex, int toIndex) { + if ((fromIndex >= toIndex) || (fromIndex >= count)) { + throw new StringIndexOutOfBoundsException(fromIndex); + } + if (toIndex > count) toIndex = count; + if (toIndex == count) { + count = fromIndex; + return; + } + System.arraycopy(value, toIndex, value, fromIndex, count - toIndex); + count -= toIndex - fromIndex; + } +} diff --git a/mozilla/grendel/calypso/util/StringBufRecycler.java b/mozilla/grendel/calypso/util/StringBufRecycler.java new file mode 100644 index 00000000000..3c931b8cc6d --- /dev/null +++ b/mozilla/grendel/calypso/util/StringBufRecycler.java @@ -0,0 +1,57 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +class StringBufRecycler extends Recycler +{ + + /******************************************************** + * @author gess 04-16-97 2:02pm + * @param + * @return + * @notes This subclass of the recycler can actually + manufacture instances of stringbuf. This is + behavior that the standard recycler doesn't do, + and the I expect will change in this class, + real soon now. + *********************************************************/ + StringBuf alloc() + { + StringBuf result=null; + if (fCount>0) + { + result = (StringBuf)fBuffer[fCount-1]; + fBuffer[--fCount]=null; + result.setLength(0); + } + else result=new StringBuf(); + return result; + } + + public void panic() + { + // Get rid of everything, including ourselves + super.panic(); + StringBuf.gRecycler = null; + } +} + diff --git a/mozilla/grendel/calypso/util/StringUtils.java b/mozilla/grendel/calypso/util/StringUtils.java new file mode 100644 index 00000000000..f9a359a9951 --- /dev/null +++ b/mozilla/grendel/calypso/util/StringUtils.java @@ -0,0 +1,151 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +public class StringUtils { + /** + * Uppercase the characters in aString. + * + * @see: java.lang.Character + */ + static public String UpperCase(String aString) { + StringBuf buf = StringBuf.Alloc(); + buf.append(aString); + buf.toUpperCase(); + aString = buf.toString(); + StringBuf.Recycle(buf); + return aString; + } + + /** + * Lowercase the characters in aString. + * + * @see: java.lang.Character + */ + static public String LowerCase(String aString) { + StringBuf buf = StringBuf.Alloc(); + buf.append(aString); + buf.toLowerCase(); + aString = buf.toString(); + StringBuf.Recycle(buf); + return aString; + } + + /** + * Return true if the string buffer contains nothing but whitespace + * as defined by Character.isWhitespace() + * + * @see: java.lang.Character + */ + static public boolean IsWhitespace(String aString) { + // XXX this should just loop right here instead of copying + // the string because the loop will be inlined with a good + // compiler + StringBuf buf = StringBuf.Alloc(); + buf.append(aString); + boolean rv = buf.isWhitespace(); + StringBuf.Recycle(buf); + return rv; + } + + /** + * Translate an integer into a string that is at least aDigits + * wide. Pad with zeros if necessary. + */ + static public String ToHex(int i, int aDigits) { + String rv = Integer.toString(i, 16); + int len = rv.length(); + if (len < aDigits) { + StringBuf buf = StringBuf.Alloc(); + len = aDigits - len; + while (--len >= 0) { + buf.append('0'); + } + buf.append(rv); + rv = buf.toString(); + StringBuf.Recycle(buf); + } + return rv; + } + + /** + * Compress the whitespace out of a string and return a new string + */ + static public String CompressWhitespace(String aString, boolean aLeading) { + StringBuf buf = StringBuf.Alloc(); + buf.append(aString); + buf.compressWhitespace(aLeading); + String rv = buf.toString(); + StringBuf.Recycle(buf); + return rv; + } + + /** + * Quote a string using java source file rules. The result is a new + * string with all the appropriate data quoted. The outer quotes that + * would be required in a java source file are not provided by this + * routine unless aProvideOuterQuotes is true. + */ + static char[] hex; + static { + hex = new char[16]; + "0123456789abcdef".getChars(0, 16, hex, 0); + } + static public String JavaQuote(String aString, boolean aProvideOuterQuotes) + { + StringBuf buf = StringBuf.Alloc(); + if (aProvideOuterQuotes) { + buf.append('"'); + } + for (int i = 0, n = aString.length(); i < n; i++) { + char ch = aString.charAt(i); + switch (ch) { + case '\n': + buf.append("\\n"); + break; + case '\r': + buf.append("\\r"); + break; + case '\t': + buf.append("\\t"); + break; + case '"': + buf.append("\\\""); + break; + default: + if ((ch < 32) || (ch >= 127)) { + buf.append("\\u"); + buf.append(hex[(ch >> 12) & 0xf]); + buf.append(hex[(ch >> 8) & 0xf]); + buf.append(hex[(ch >> 4) & 0xf]); + buf.append(hex[ch & 0xf]); + } else { + buf.append(ch); + } + } + } + if (aProvideOuterQuotes) { + buf.append('"'); + } + String rv = buf.toString(); + StringBuf.Recycle(buf); + return rv; + } +} diff --git a/mozilla/grendel/calypso/util/TempFile.java b/mozilla/grendel/calypso/util/TempFile.java new file mode 100644 index 00000000000..4d39c188067 --- /dev/null +++ b/mozilla/grendel/calypso/util/TempFile.java @@ -0,0 +1,158 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.io.*; +import java.util.*; + +/** + * Utility class for generating temporary file names. + * + * @author Kipp E.B. Hickman + */ +public class TempFile { + static private boolean gHaveSetFinalizersOnExit; + static private int gNextID; + static private Random gGenerator = new Random(); + + String fTempFileName; + + /** + * Create a new temporary file name that the calling + * thread can use that is unique and has the given extension. + */ + // XXX need security access check + static public synchronized TempFile TempName(String aExtension) { + /* XXX seems to cause jdk1.1.3b to hang... + if (!gHaveSetFinalizersOnExit) { + // Make sure temp files get removed on exit + System.runFinalizersOnExit(true); + gHaveSetFinalizersOnExit = true; + }*/ + + // Fix up temp dir to have a trailing separator + String tmpDir = GetTempDir(); + int tmpDirLen = tmpDir.length(); + if ((tmpDirLen > 0) && + (tmpDir.charAt(tmpDirLen - 1) != File.separatorChar)) { + tmpDir = tmpDir + File.separatorChar; + SetTempDir(tmpDir); + } + + for (;;) { + int value = gGenerator.nextInt() & 0xffffff; + String name = tmpDir + "ns" + StringUtils.ToHex(value, 6) + aExtension; + File file = new File(name); + if (file.exists()) { + continue; + } + return new TempFile(name); + } + } + + /** + * Return the native path name for the system's temporary directory. + */ + static final String propertyName = "system.tempdir"; + static public synchronized String GetTempDir() { + Properties p = System.getProperties(); + String dir = p.getProperty(propertyName); + if (dir == null) { + String osname = p.getProperty("os.name"); + if (osname.startsWith("Windows")) { + dir = "c:\\windows\temp"; + if (TryDirectory(dir)) { + return dir; + } + dir = "c:\\temp"; + if (TryDirectory(dir)) { + return dir; + } + dir = "\temp"; + if (TryDirectory(dir)) { + return dir; + } + dir = "\tmp"; + if (TryDirectory(dir)) { + return dir; + } + // Oh well, fall back to the current directory wherever that is! + dir = "."; + if (TryDirectory(dir)) { + return dir; + } + throw new Error("whoops: no windoze tempdir!"); + } else { + // XXX for now, assume it's unix + dir = "/tmp"; + if (TryDirectory(dir)) { + return dir; + } + throw new Error("whoops: no unix tempdir!"); + } + } + return dir; + } + + static private boolean TryDirectory(String aDir) { + File f = new File(aDir); + if (f.exists() && f.isDirectory()) { + SetTempDir(aDir); + return true; + } + return false; + } + + /** + * Set the native path name for the system's temporary directory. + */ + // XXX need security access check + static public synchronized void SetTempDir(String aDir) { + Properties p = System.getProperties(); + p.put(propertyName, aDir); + } + + protected TempFile(String aName) { + fTempFileName = aName; + } + + public OutputStream create() throws IOException { + return new BufferedOutputStream(new FileOutputStream(fTempFileName)); + } + + public OutputStream append() throws IOException { + return new BufferedOutputStream(new FileOutputStream(fTempFileName, true)); + } + + public String getName() { + return fTempFileName; + } + + public void delete() { + if (fTempFileName != null) { + new File(fTempFileName).delete(); + fTempFileName = null; + } + } + + protected void finalize() { + delete(); + } +} diff --git a/mozilla/grendel/calypso/util/TimeBomb.java b/mozilla/grendel/calypso/util/TimeBomb.java new file mode 100644 index 00000000000..7f85f648746 --- /dev/null +++ b/mozilla/grendel/calypso/util/TimeBomb.java @@ -0,0 +1,47 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +// original timebomb code to search for. + + // This will expire on April 1st at 12pm local time + //java.util.Date before = new Date(97, 9, 1, 0, 0); + //java.util.Date now = new Date(); + //java.util.Date then = new Date(97, 11, 25, 12, 00); + //if (now.before(before) || now.after(then)) { + // System.err.println("This software has expired"); + // System.exit(-1); + //} + +public class TimeBomb +{ + public static final boolean Ticking = true; + + // Before is 12-01-97 + + public static final int BeforeYear = 97; + public static final int BeforeMonth = 11; // Zero based + public static final int BeforeDate = 1; + + // Expire is 4-01-98 + + public static final int ExpiresYear = 98; + public static final int ExpiresMonth = 3; // Zero based + public static final int ExpiresDate = 1; + public static final int ExpiresHour = 12; +} diff --git a/mozilla/grendel/calypso/util/URLClassLoader.java b/mozilla/grendel/calypso/util/URLClassLoader.java new file mode 100644 index 00000000000..fa842c6e2e0 --- /dev/null +++ b/mozilla/grendel/calypso/util/URLClassLoader.java @@ -0,0 +1,341 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.net.*; +import java.util.*; +import java.util.zip.*; +import java.io.*; + +// Simple class loader: +// - no signatures +// - no security +// - no mayscript + +/** + * A URL based class loader. This class knows how to load classes from + * a given base URL + */ +public class URLClassLoader extends ClassLoader { + private Hashtable classes; + private URL codeBaseURL; + private URL archiveURL; + private ZipFile fZipFile; + private TempFile fTempFile; + + static final boolean XXXnoisy = false; + + /** + * Create a new URL based class loader. aBaseURL specifies the URL + * to load classes relative to. If aArchiveURL is not null then the + * archive will be searched first (if it fails then the load will be + * attempted from aBaseURL). + */ + public URLClassLoader(URL aBaseURL, URL aArchiveURL) { + codeBaseURL = aBaseURL; + archiveURL = aArchiveURL; + classes = new Hashtable(); + if (XXXnoisy) { + System.out.println("####### Creating TagClassLoader"); + System.out.println("### CodeBaseURL=" + codeBaseURL); + System.out.println("### ArchiveURL=" + archiveURL); + } + } + + /** + * Load a class from a URL. This does the actual work of loading the + * class and then defining it. + */ + private Class loadClass(String name, URL url, String pathname) + throws IOException + { + byte[] data = readURL(url); + if (XXXnoisy) { + System.out.println("# loadClass: url="+url+" bytes="+data.length); + } + // XXX update for netscape security model + return defineClass(name, data, 0, data.length); + } + + private Class loadClassFromArchive(String name, URL url, String pathname) + throws IOException + { + if (fZipFile == null) { + // First time in we copy over the archive URL + fTempFile = TempFile.TempName(".zip"); + copyURL(fTempFile.create(), url); + if (XXXnoisy) { + System.out.println("### Copying zip file: tempName=" + + fTempFile.getName() + " url=" + url); + } + fZipFile = new ZipFile(fTempFile.getName()); + } + + // Dig up the class's bits using the zip loader + byte[] data = readZipFile(pathname); + if (XXXnoisy) { + System.out.println("# loadClass: url="+url+"(" + + pathname + ") bytes=" + data.length); + } + // XXX update for netscape security model + return defineClass(name, data, 0, data.length); + } + + /** + * Load a class from this class loader. This method is used by applets + * that want to explicitly load a class. + */ + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, true); + } + + /** + * Load and resolve a class. This method is called by the java runtime + * to get a class that another class needs (e.g. a superclass). + */ + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + Class cl = (Class)classes.get(name); + if (cl == null) { + // XXX: We should call a Security.checksPackageAccess() native + // method, and pass name as arg + SecurityManager security = System.getSecurityManager(); + if (security != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + security.checkPackageAccess(name.substring(0, i)); + } + } + try { + return findSystemClass(name); + } catch (ClassNotFoundException e) { + } + cl = findClass(name); + } + if (cl == null) { + throw new ClassNotFoundException(name); + } + if (resolve) { + resolveClass(cl); + } + return cl; + } + + /** + * This method finds a class. The returned class + * may be unresolved. This method has to be synchronized + * to avoid two threads loading the same class at the same time. + * Must be called with the actual class name. + */ + private synchronized Class findClass(String name) + throws ClassNotFoundException + { + Class cl = (Class)classes.get(name); + if (cl != null) { + return cl; + } + + // XXX: We should call a Security.checksPackageDefinition() native + // method, and pass name as arg + SecurityManager security = System.getSecurityManager(); + if (security != null) { + int i = name.lastIndexOf('.'); + if (i >= 0) { + security.checkPackageDefinition(name.substring(0, i)); + } + } + + boolean system_class = true; + String cname = name.replace('.', '/') + ".class"; + if (cl == null) { + URL url; + try { + url = new URL(codeBaseURL, cname); + } catch (MalformedURLException e) { + throw new ClassNotFoundException(name); + } + if (XXXnoisy) { + System.err.println("# Fetching " + url); + } + + try { + if (archiveURL != null) { + cl = loadClassFromArchive(name, archiveURL, cname); + // XXX try regular file if archive load fails? + } else { + cl = loadClass(name, url, cname); + } + system_class = false; + } catch (IOException e) { + if (XXXnoisy) { + System.out.println("# IOException loading class: " + e); + e.printStackTrace(); + } + throw new ClassNotFoundException(name); + } + } + if (!name.equals(cl.getName())) { + Class oldcl = cl; + cl = null; + throw new ClassFormatError(name + " != " + oldcl.getName()); + } + if (system_class == false) { + // setPrincipalAry(cl, cname); + } + classes.put(name, cl); + return cl; + } + + // collapse the i/o loops between this code and readZipFile + + private byte[] readURL(URL url) throws IOException { + byte[] data; + InputStream in = null; + try { + URLConnection c = url.openConnection(); + c.setAllowUserInteraction(false); + in = c.getInputStream(); + + int len = c.getContentLength(); + data = new byte[(len == -1) ? 4096 : len]; + int total = 0, n; + + while ((n = in.read(data, total, data.length - total)) >= 0) { + if ((total += n) == data.length) { + if (len < 0) { + byte newdata[] = new byte[total * 2]; + System.arraycopy(data, 0, newdata, 0, total); + data = newdata; + } else { + break; + } + } + } + } finally { + if (in != null) { + in.close(); + } + } + return data; + } + + /** + * Load a given file from the underlying zip file named "aName". Return + * an array of bytes which contain the decompressed contents of the + * file in the zip file. + */ + private byte[] readZipFile(String aName) + throws IOException + { + ZipEntry entry = fZipFile.getEntry(aName); + if (entry == null) { + throw new IOException("file not found: " + aName); + } + + InputStream in = null; + int len = (int) entry.getSize(); + byte[] data = new byte[(len == -1) ? 4096 : len]; + try { + in = fZipFile.getInputStream(entry); + int total = 0, n; + while ((n = in.read(data, total, data.length - total)) >= 0) { + if ((total += n) == data.length) { + if (len < 0) { + byte newdata[] = new byte[total * 2]; + System.arraycopy(data, 0, newdata, 0, total); + data = newdata; + } else { + break; + } + } + } + } finally { + if (in != null) { + in.close(); + } + } + return data; + } + + private void copyURL(OutputStream aOut, URL aURLSource) + throws IOException + { + InputStream in = null; + try { + byte[] inputBuf = new byte[4096]; + + // open the destination file for writing + //SecurityManager.enablePrivilege("UniversalFileAccess"); + //SecurityManager.enablePrivilege("UniversalConnect"); + + URLConnection c = archiveURL.openConnection(); + c.setAllowUserInteraction(false); + in = c.getInputStream(); + + // Read all the bytes from the url into the temp file + Thread thread = Thread.currentThread(); + int total = 0, n; + while (((n = in.read(inputBuf)) >= 0) && !thread.isInterrupted()) { + aOut.write(inputBuf, 0, n); + total += n; + } + if (thread.isInterrupted()) { + throw new IOException("interrupted: " + this); + } + if (XXXnoisy) { + System.out.println("# Copying archive: url=" + archiveURL + + " tempFile=" + fTempFile.getName() + + " copiedBytes=" + total); + } + } finally { + if (in != null) { + in.close(); + } + if (aOut != null) { + aOut.close(); + } + } + // SecurityManager.revertPrivilege(); + } + + public URL getCodeBaseURL() { + return codeBaseURL; + } + + protected void finalize() { + if (fZipFile != null) { + try { + // First close the zip file + fZipFile.close(); + } catch (Exception e) { + } + fZipFile = null; + } + if (fTempFile != null) { + try { + // Then remove the temporary file + fTempFile.delete(); + } finally { + fTempFile = null; + } + } + } +} diff --git a/mozilla/grendel/calypso/util/Vec.java b/mozilla/grendel/calypso/util/Vec.java new file mode 100644 index 00000000000..d5981dac307 --- /dev/null +++ b/mozilla/grendel/calypso/util/Vec.java @@ -0,0 +1,138 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * Some more methods for Vector + */ +public class Vec extends Vector { + public Vec(int initialCapacity, int capacityIncrement) { + super(initialCapacity, capacityIncrement); + } + + public Vec(int initialCapacity) { + super(initialCapacity, 0); + } + + public Vec() { + } + + public boolean containsIdentical(Object aElement) { + return indexOfIdentical(aElement, 0) != -1; + } + + public int indexOfIdentical(Object aElement) { + return indexOfIdentical(aElement, 0); + } + + public synchronized int indexOfIdentical(Object aElement, int aIndex) { + for (int i = aIndex; i < elementCount ; i++) { + if (aElement == elementData[i]) { + return i; + } + } + return -1; + } + + public boolean insertElementAfter(Object aElement, Object aExistingElement) + { + if (aElement == null) { + throw new NullPointerException( + "It is illegal to store nulls in Vectors."); + } + if (aExistingElement == null) { + return false; + } + int index = indexOf(aExistingElement); + if (index == -1) { + return false; + } + if (index >= elementCount - 1) { + addElement(aElement); + } else { + insertElementAt(aElement, index + 1); + } + return true; + } + + public void addElements(Vector aVector) { + int addCount, i; + + if (aVector == null) + return; + addCount = aVector.size(); + if (elementData == null || (elementCount + addCount) >= elementData.length) + ensureCapacity(elementCount + addCount); + + for (i = 0; i < addCount; i++) + addElement(aVector.elementAt(i)); + } + + public synchronized boolean removeElementIdentical(Object aElement) { + int i = indexOfIdentical(aElement); + if (i >= 0) { + removeElementAt(i); + return true; + } + return false; + } + + public synchronized Object removeFirstElement() { + if (elementCount == 0) { + return null; + } + Object rv = elementData[0]; + removeElementAt(0); + return rv; + } + + public synchronized Object removeLastElement() { + if (elementCount == 0) { + return null; + } + int index = elementCount - 1; + Object rv = elementData[index]; + removeElementAt(index); + return rv; + } + + static Comparer gStringComparer; + + public synchronized void sort() { + if (gStringComparer == null) { + gStringComparer = new StringComparer(); + } + QSort s = new QSort(gStringComparer); + s.sort(elementData, 0, elementCount); + } + + public synchronized void sort(Comparer aComparer) { + QSort s = new QSort(aComparer); + s.sort(elementData, 0, elementCount); + } + + static class StringComparer implements Comparer { + public int compare(Object a, Object b) { + return ((String)a).compareTo((String)b); + } + } +} diff --git a/mozilla/grendel/calypso/util/VectorRecycler.java b/mozilla/grendel/calypso/util/VectorRecycler.java new file mode 100644 index 00000000000..2ffb0743637 --- /dev/null +++ b/mozilla/grendel/calypso/util/VectorRecycler.java @@ -0,0 +1,119 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.*; + +/** + * Static recycler class for java.util.Vector + * + * @author psl 04-23-97 6:51pm + * @see + */ +public final class VectorRecycler +{ + static Recycler gRecycler; + + /** + * Get one + * + * @return + * @exception + * @author psl 04-23-97 6:52pm + */ + public static synchronized Vector Alloc () + { + Vector result = null; + + if (null != gRecycler) + result = (Vector)gRecycler.getRecycledObject (); + if (null == result) + result = new Vector (); + return result; + } + + /** + * Get one with an initial capacity + * a recycled vector may be bigger than capacity + * + * @param aCapacity minimum desired capacity + * @return + * @exception + * @author psl 04-23-97 6:52pm + */ + public static synchronized Vector Alloc (int aCapacity) + { + Vector result = null; + + if (null != gRecycler) + result = (Vector)gRecycler.getRecycledObject (); + if (null != result) + result.ensureCapacity (aCapacity); + else + result = new Vector (aCapacity); + return result; + } + + /** + * Recycle a vector + * (it will be emptied by the recycler) + * + * @param aVector vector to be recycled + * @return + * @exception + * @author psl 04-23-97 6:52pm + */ + public static synchronized void Recycle (Vector aVector) + { + if (null == gRecycler) + gRecycler = new Recycler (); + aVector.removeAllElements (); + gRecycler.recycle (aVector); + } + + /** + * Empty the recycler, it's earth day + * + * @param + * @return + * @exception + * @author psl 04-21-97 4:29pm + */ + static synchronized public void EmptyRecycler () + { + if (null != gRecycler) + gRecycler.empty (); + } + + /** + * finalize class for unloading + * + * @param + * @return + * @exception + * @author psl 04-21-97 4:29pm + */ + static void classFinalize () throws Throwable + { + // Poof! Now we are unloadable even though we have a static + // variable. + } +} + diff --git a/mozilla/grendel/calypso/util/WeakLink.java b/mozilla/grendel/calypso/util/WeakLink.java new file mode 100644 index 00000000000..e16e100c432 --- /dev/null +++ b/mozilla/grendel/calypso/util/WeakLink.java @@ -0,0 +1,38 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import sun.misc.Ref; + +/** + * Weak link class. + */ +public class WeakLink extends sun.misc.Ref { + public WeakLink() { + } + + public WeakLink(Object obj) { + setThing(obj); + } + + public Object reconstitute() { + return null; + } +} diff --git a/mozilla/grendel/calypso/util/WeakLinkArray.java b/mozilla/grendel/calypso/util/WeakLinkArray.java new file mode 100644 index 00000000000..a6bbe464c09 --- /dev/null +++ b/mozilla/grendel/calypso/util/WeakLinkArray.java @@ -0,0 +1,95 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.Enumeration; + +/** + * An array of objects whose references are weak. Weak references + * hold a reference to the object that the GC can break when the + * object no longer has any hard references. + */ +public class WeakLinkArray { + WeakLink fArray[]; + int fCount; + + public WeakLinkArray() { + } + + public void addElement(Object aObject) { + if (fArray == null) { + fArray = new WeakLink[10]; + } else { + // Grow the array if it's too small + int len = fArray.length; + if (fCount == len) { + // Try to use a slot in the array that contains a dead weak + // link instead of growing the array. + for (int i = 0; i < fCount; i++) { + WeakLink w = fArray[i]; + if ((w == null) || (w.get() == null)) { + if (w == null) { + fArray[i] = new WeakLink(aObject); + } else { + w.setThing(aObject); + } + return; + } + } + + // Grow the array. + len += 10; + WeakLink newArray[] = new WeakLink[len]; + System.arraycopy(fArray, 0, newArray, 0, fCount); + fArray = newArray; + } + } + fArray[fCount++] = new WeakLink(aObject); + } + + public void removeElement(Object aObject) { + int n = fCount; + for (int i = 0; i < n; i++) { + WeakLink w = fArray[i]; + if (w != null) { + Object obj = w.get(); + if ((obj == null) || (obj == aObject)) { + fArray[i] = null; + } + } + } + } + + public boolean contains(Object aObject) { + int n = fCount; + for (int i = 0; i < n; i++) { + WeakLink w = fArray[i]; + if ((w != null) && (w.get() == aObject)) { + return true; + } + } + return false; + } + + public Enumeration elements() { + return new WeakLinkArrayEnumeration(this); + } + // XXX hashCode, equals, toString +} diff --git a/mozilla/grendel/calypso/util/WeakLinkArrayEnumeration.java b/mozilla/grendel/calypso/util/WeakLinkArrayEnumeration.java new file mode 100644 index 00000000000..b6d8ba28524 --- /dev/null +++ b/mozilla/grendel/calypso/util/WeakLinkArrayEnumeration.java @@ -0,0 +1,45 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package calypso.util; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +class WeakLinkArrayEnumeration implements Enumeration { + WeakLink fArray[]; + int fCount; + int fOffset; + + WeakLinkArrayEnumeration(WeakLinkArray aArray) { + fArray = aArray.fArray; + fCount = aArray.fCount; + } + + public boolean hasMoreElements() { + return fOffset < fCount; + } + + public Object nextElement() { + if (fOffset == fCount) { + throw new NoSuchElementException(); + } + return fArray[fOffset++]; + } +} diff --git a/mozilla/grendel/composition/AddressBar.java b/mozilla/grendel/composition/AddressBar.java new file mode 100644 index 00000000000..8438f62e146 --- /dev/null +++ b/mozilla/grendel/composition/AddressBar.java @@ -0,0 +1,61 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.awt.*; +import com.sun.java.swing.*; + +public class AddressBar extends NSTabbedPane { + AddressList mAddressList; + AttachmentsList mAttachmentsList; + OptionsPanel mOptionsPanel; + + public AddressBar() { + //address panel + ImageIcon addressIcon = new ImageIcon(getClass().getResource("images/small_address.gif")); + mAddressList = new AddressList (); + + //attachments panel + ImageIcon attachmentsIcon = new ImageIcon(getClass().getResource("images/small_attachments.gif")); + mAttachmentsList = new AttachmentsList(); + + //otpions panel + ImageIcon optionsIcon = new ImageIcon(getClass().getResource("images/small_otpions.gif")); + mOptionsPanel = new OptionsPanel(); + + //tabbed panel holds address, attachments and otpions. + addTab("", addressIcon, mAddressList); + addTab("", attachmentsIcon, mAttachmentsList); + addTab("", optionsIcon, mOptionsPanel); + setSelectedIndex(0); + } + + public AddressList getAddressList() { + return mAddressList; + } + + public AttachmentsList getAttachmentsList() { + return mAttachmentsList; + } + + public OptionsPanel getOptionsPanel() { + return mOptionsPanel; + } +} diff --git a/mozilla/grendel/composition/AddressDialog.java b/mozilla/grendel/composition/AddressDialog.java new file mode 100644 index 00000000000..a7e2482bd60 --- /dev/null +++ b/mozilla/grendel/composition/AddressDialog.java @@ -0,0 +1,389 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; + +import com.sun.java.swing.*; +import com.sun.java.swing.table.*; +import com.sun.java.swing.border.*; + +class AddressDialog extends Dialog implements ActionListener { + private final int BUTTON_WIDTH = 100; + private final int BUTTON_HEIGHT = 28; + + private List mSendToList; //ListBox gadget that holds recipients names. + private JTable mTable; //Tbale displays the address book entries. + private Vector mAddresses; //a parallel list of recipients with mSendToList. + private boolean wasCanceled = true; //true if Cancel button was used to dismiss dialog. + + //FIX remove + final Object[][] data = { + {"Name", + "Lester Schueler", + "Don Vale", + "Chris Dreckman", + "Fred Cutler", + "Rod Spears" + }, + {"Email Address", + "lesters@netscape.com", + "dvale@netscape.com", + "chrisd@netscape.com", + "fredc@netscape.com", + "rods@netscape.com" + }, + {"Organization", + "Netscape Comm.", + "Netscape Comm.", + "Netscape Comm.", + "Netscape Comm.", + "Netscape Comm." + }, + {"Phone", + "(619)618-2227", + "(619)618-2224", + "(619)618-2211", + "(619)618-2206", + "(619)618-2228" + } + }; + + //FIX remove + public static void main(String[] args) { + Frame frame = new JFrame(); + frame.setBackground(Color.lightGray); + + AddressDialog aDialog = new AddressDialog(frame); + aDialog.show (); + aDialog.dispose(); + + frame.show(); + } + + /** + * + */ + AddressDialog(Frame aParent) { + //FIX: Resource + super(aParent, "Select Addresses", true); + + mAddresses = new Vector(); + + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + JComponent lookupPanel = createLookupPanel (); + add (lookupPanel); + + JComponent SendToPanel = createSendToPanel (); + add (SendToPanel); + + JComponent bottomPanel = createBottomPanel (); + add (bottomPanel); + + setResizable(false); + setSize (716, 480); + } + + /** + * + */ + private JPanel createLookupPanel () { + //the outer most panel has groove etched into it. + JPanel outerPanel = new JPanel(true); + + //FIX: Resource + outerPanel.setBorder(new TitledBorder(new EtchedBorder(), "Type in the name you are looking for:")); + outerPanel.setLayout (new BorderLayout ()); + outerPanel.setPreferredSize(new Dimension (500, 225)); + + // "North" panel + JPanel northPanel = new JPanel (true); + northPanel.setLayout (new FlowLayout (FlowLayout.LEFT, 5, 5)); + northPanel.setPreferredSize(new Dimension (500, 35)); + + //Search text field + JTextField searchTxt = new JTextField (20); + northPanel.add (searchTxt); + + //FIX: Resource + northPanel.add (new JLabel ("In:")); + + JComboBox combo = new JComboBox(); + combo.setPreferredSize(new Dimension (275, 25)); + northPanel.add (combo); + + //FIX: Resource + JButton b = makeButton ("Search", BUTTON_WIDTH, BUTTON_HEIGHT); + ButtonModel bm = b.getModel(); + bm.setEnabled(false); + northPanel.add (b); + + // Center table + TableModel dataModel; + + // Create the table data model + dataModel = new AbstractTableModel() { + public int getRowCount() { return data[0].length - 1; } + + public int getColumnCount() { return data.length; } + + public Object getValueAt(int rowIndex, int columnIndex) { + return data[columnIndex][rowIndex+1]; + } + + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + data[columnIndex][rowIndex+1] = aValue; + } + + public int getColumnIndex(Object aID) { + for (int i = 0; i < data.length; i++) { + if (data[i][0].equals(aID)) { + return i; + } + } + return -1; + } + }; + + // Create the table + mTable = new JTable(dataModel); + + // Added our columns into the column model + for (int columnIndex = 0; columnIndex < data.length; columnIndex++){ + // Create a column object for each column of data + TableColumn newColumn = new TableColumn(columnIndex); + + newColumn.setWidth(200); + mTable.addColumn(newColumn); + } + + mTable.setColumnSelectionAllowed(false); + mTable.setShowGrid(false); + + // Put the table and header into a scrollPane + JScrollPane scrollpane = new JScrollPane(); + JTableHeader tableHeader = mTable.getTableHeader(); + + // create and add the column heading to the scrollpane's + // column header viewport + JViewport headerViewport = new JViewport(); + headerViewport.setLayout(new BoxLayout(headerViewport, BoxLayout.X_AXIS)); + headerViewport.add(tableHeader); + scrollpane.setColumnHeader(headerViewport); + + // add the table to the viewport + JViewport mainViewPort = scrollpane.getViewport(); + mainViewPort.add(mTable); + + // speed up resizing repaints by turning off live cell updates + tableHeader.setUpdateTableInRealTime(false); + + // South panel + JPanel southPanel = new JPanel (true); + + //FIX: Resource + southPanel.setLayout (new FlowLayout (FlowLayout.LEFT, 10, 0)); + southPanel.add (makeButton ("To:", BUTTON_WIDTH, BUTTON_HEIGHT)); + southPanel.add (makeButton ("Cc:", BUTTON_WIDTH, BUTTON_HEIGHT)); + southPanel.add (makeButton ("Bcc:", BUTTON_WIDTH, BUTTON_HEIGHT)); + + //FIX + JButton jb = makeButton ("Add to Address Book", 184, BUTTON_HEIGHT); + ButtonModel jbm = jb.getModel(); + jbm.setEnabled(false); + southPanel.add (jb); + + JPanel panel2 = new JPanel (new BorderLayout (5, 5), true); + panel2.setBorder (new EmptyBorder (5, 5, 5, 5)); + panel2.add (northPanel, BorderLayout.NORTH); + panel2.add (scrollpane, BorderLayout.CENTER); + panel2.add (southPanel, BorderLayout.SOUTH); + + outerPanel.add (panel2, BorderLayout.CENTER); + return outerPanel; + } + + /** + * The Send To Panel holds the list people. + */ + private JPanel createSendToPanel () { + //the outer most panel has groove etched into it. + JPanel etchedPanel = new JPanel(); + + //FIX: Resource + etchedPanel.setBorder(new TitledBorder(new EtchedBorder(), "This message will be sent to:")); + etchedPanel.setLayout (new BorderLayout (5, 5)); + etchedPanel.setPreferredSize(new Dimension (500, 175)); + + JPanel panel2 = new JPanel (new BorderLayout (5, 5), true); + panel2.setBorder (new EmptyBorder(5, 5, 5, 5)); + + // Center Listbox panel + mSendToList = new List(); + mSendToList.setBackground (Color.white); + panel2.add (mSendToList, BorderLayout.CENTER); + + JPanel panel3 = new JPanel (new BorderLayout(), true); + + //FIX: Resource + panel3.add (makeButton ("Remove", BUTTON_WIDTH, BUTTON_HEIGHT), BorderLayout.WEST); + panel2.add (panel3, BorderLayout.SOUTH); + + etchedPanel.add (panel2, BorderLayout.CENTER); + return etchedPanel; + } + + /** + * The bottom panel has the ok, cancel and help buttons. + */ + private JPanel createBottomPanel () { + JPanel panel = new JPanel (true); + panel.setLayout (new FlowLayout (FlowLayout.CENTER, 25, 8)); + + //FIX: Resource + panel.add (makeButton ("OK", BUTTON_WIDTH, BUTTON_HEIGHT)); + panel.add (makeButton ("Cancel", BUTTON_WIDTH, BUTTON_HEIGHT)); + + //FIX + JButton b = makeButton ("Help", BUTTON_WIDTH, BUTTON_HEIGHT); + panel.add (b); + ButtonModel bm = b.getModel(); + bm.setEnabled(false); + + return panel; + } + + private JButton makeButton (String aLabel, int aWidth, int aHeight) { + JButton button = new JButton (aLabel); + button.setPreferredSize(new Dimension (aWidth, aHeight)); + button.addActionListener (this); + return button; + } + + public Insets getInsets () { + return (new Insets (25, 10, 10, 10)); + } + + /** + * Adds an entry to the send list. + * @param The Addressee to be added to the list. + */ + private void addToSendList (Addressee aAddressee) { + if (null != aAddressee) { + mSendToList.add (aAddressee.deliveryString () + " " + aAddressee.getText()); + mAddresses.addElement (aAddressee); + } + } + + /** + * Removes the specified entry from the list. + * @param The index of the entry to remove. + */ + private void removeFromSendList (int aIndex) { + if (-1 < aIndex) { + mSendToList.remove (aIndex); + mAddresses.removeElementAt (aIndex); + } + } + + /** + * Sets the address from aa Array. + * @param aAddresses An array of addresses. + * @see getAddresses + */ + public void setAddresses (Addressee[] aAddresses) { + mAddresses.removeAllElements(); + mSendToList.removeAll(); + + if (null != aAddresses) { + for (int i = 0; i < aAddresses.length; i++) { + addToSendList (aAddresses[i]); + } + } + } + + /** + * Returns the addresses in the form of an array. + * @returns An array of addresses. + * @see setAddresses + */ + public Addressee[] getAddresses () { + int arrSize = mAddresses.size(); + + if (0 < arrSize) { + Addressee[] anArray = new Addressee[arrSize]; + mAddresses.copyInto(anArray); + return anArray; + } + + return null; + } + + /** + * Return true if cancel was pressed. + */ + public boolean getCanceled () { return wasCanceled; } + + /** + * Actions to respond to are button pressed for "To", "Cc", "Bcc", "Ok", and "Canel" + */ + public void actionPerformed (ActionEvent e) { + String s = e.getActionCommand (); + + //FIX: Resource + if (s.equals ("To:")) { + int selectedRow = mTable.getSelectedRow(); + if (-1 != selectedRow) + addToSendList (new Addressee ((String) data[1][selectedRow + 1], Addressee.TO)); + } + //FIX: Resource + else if (s.equals ("Cc:")) { + int selectedRow = mTable.getSelectedRow(); + if (-1 != selectedRow) + addToSendList (new Addressee ((String) data[1][selectedRow + 1], Addressee.CC)); + } + //FIX: Resource + else if (s.equals ("Bcc:")) { + int selectedRow = mTable.getSelectedRow(); + if (-1 != selectedRow) + addToSendList (new Addressee ((String) data[1][selectedRow + 1], Addressee.BCC)); + } + //FIX: Resource + else if (s.equals ("Remove")) { + int index = mSendToList.getSelectedIndex(); + + if (-1 < index) + removeFromSendList (index); + } + //FIX: Resource + else if (s.equals ("Cancel")) { + wasCanceled = true; + setVisible(false); + } + //FIX: Resource + else if (s.equals ("OK")) { + wasCanceled = false; + setVisible(false); + } + } +} diff --git a/mozilla/grendel/composition/AddressList.java b/mozilla/grendel/composition/AddressList.java new file mode 100644 index 00000000000..d75468dafb5 --- /dev/null +++ b/mozilla/grendel/composition/AddressList.java @@ -0,0 +1,917 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +/* Gadget organization: + an AddressList has one AddressPanel. + an AddressPanel has many AddressLine(s). + an AddressLine has one DeliveryButton, one AddressIcon and one AddressTextField. + */ + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.util.*; + +import com.sun.java.swing.*; +import com.sun.java.swing.JTable; + +import netscape.orion.misc.*; + +public class AddressList extends JScrollPane implements Serializable { + protected AddressPanel mAddressPanel; + + public AddressList() { + super(); + + //scroll panel + JViewport spViewPort = getViewport(); + + //create addressList panel + mAddressPanel = new AddressPanel (); + + //add address list panel to scroll panel. + spViewPort.add(mAddressPanel); + + setBackground (Color.white); + } + + int mProp1 = 0; + public void setProp1 (int aVal) { + mProp1 = aVal; + } + + public int getProp1 () { + return mProp1; + } + + int mProp2 = 0; + public void setProp2 (int aVal) { + mProp2 = aVal; + } + + public int getProp2 () { + return mProp2; + } + + /** + * Clears and sets the addresses from an Array. + * @param aAddresses An array of addresses. + * @see getAddresses() + */ + public void setAddresses (Addressee[] aAddresses) { + mAddressPanel.removeAllAddressLines (); //delete all entries. + + if (null != aAddresses) { + for (int i = 0; i < aAddresses.length; i++) { + + if (null != aAddresses[i]) + mAddressPanel.addAddresseLine (aAddresses[i]); + } + } + + mAddressPanel.repack(); + } + + /** + */ + public Dimension getPreferredSize() { + return mAddressPanel.getPreferredSize(); + } + + /** + * Returns the addresses in the form of an array. + * @returns An array of addresses. + * @see setAddresses() + */ + public Addressee[] getAddresses () { + Vector temp = mAddressPanel.mAddressees; + int i; + + //reject blank entries. + for (i = 0; i < temp.size(); i++) { + AddressLine al = (AddressLine) temp.elementAt(i); + + if (al.isBlank()) { + temp.removeElement (al); + } + } + + //copy over from vector into array. + Addressee[] anArray = new Addressee[temp.size()]; + for (i = 0; i < temp.size(); i++) { + anArray[i] = ((AddressLine) temp.elementAt(i)).getAddressee(); + } + + return anArray; + } + + /** + * + */ + public class AddressPanel extends JPanel implements KeyListener { + protected Vector mAddressees; //Addresses + private Dimension mAddLineSize; //The size of one AddressLine. For layout. + private Dimension mPerfSize; //The preferred size of this panel = 4 * mAddLineSize. + + public AddressPanel () { + super(); + setLayout (null); + + //vector for holding addressee list. + mAddressees = new Vector(); + + //create the first addressee to start them off. + repack(); + } + + /** + * layout Addressee lines. + */ + public void doLayout() { + int i; + Enumeration e; + Dimension size = getSize(); + + /*place each addressee line in one column multiple rows. + +---------------+ + | addressee #1 | + |---------------| + | addressee #2 | + |---------------| + | ... | + |---------------| + | addressee #n | + +---------------+ + */ + for (e = mAddressees.elements(), i = 0; e.hasMoreElements(); i++) { + AddressLine al = (AddressLine) e.nextElement(); + al.setBounds(0, i * mAddLineSize.height, size.width, mAddLineSize.height); + } + } + +// public Dimension getMaximumSize() { +// return getPreferredSize(); +// } + +// public Dimension getMinimumSize() { +// return getPreferredSize(); +// } + + public void addNotify () { + super.addNotify (); + + //store the first AddressLine's size for layout. + AddressLine al = (AddressLine) mAddressees.elementAt(0); + mAddLineSize = al.getPreferredSize(); + + //the preferred size of the panel is 4 AddressLines tall and full parent width. + mPerfSize = new Dimension (mAddLineSize.width, 5 * mAddLineSize.height); + } + + /** + * Returns the preferred size. + * The preferred size of the panel is 4 AddressLines tall and full parent width. + * @return + */ + public Dimension getPreferredSize() { + //the preferred size of the panel is 4 AddressLines tall and full parent width. + return (mPerfSize); + } + + /** + * Adds a new address line to the list. + * @param aAddressee the Addresee you which to add. + * @see removeAddressLine + */ + private synchronized void addAddresseLine (Addressee aAddressee) { + //create the new AddressLine. + AddressLine al = new AddressLine (aAddressee); + + //Add a keyboard listener for navigation + //so they may navigate BETWEEN AddressLines using arrow keys. + al.atfAddKeyListener (this); + + add (al); //add AddressLine gadget to panel + mAddressees.addElement (al); //remember in vector table + validate(); //layout control again (scrollbar may appear). + } + + /** + * Remove any blank lines from the middle + * and appends a single blank line to the list. + */ + private void repack () { + //remove blank lines + for (int i = 0; i < mAddressees.size() - 2; i++) { + AddressLine al = (AddressLine) mAddressees.elementAt(i); + if (al.isBlank()) { + removeAddressLine(al); + } + } + + //no entries at all so add a blank line. + if (0 == mAddressees.size()) { + addAddresseLine (new Addressee ("", Addressee.TO)); + } + else { + //add a blank last line if needed. + AddressLine lastAL = (AddressLine) mAddressees.elementAt(mAddressees.size() - 1); + if (!lastAL.isBlank()) { + addAddresseLine (new Addressee ("", lastAL.getDeliveryMode())); + } + } + } + + /* + * Place focus on the last addressLine. + */ + private void focusOnLast () { + int last = mAddressees.size() - 1; + + //get its TextFiled and set focus on it. + if (last > -1) { + AddressLine al = (AddressLine) mAddressees.elementAt(last); + al.atfRequestFocus(); + } + } + + /** + * Removes an address line from the list. + * @param aAddressLine The address line you wish to remove. + * @see addAddresseLine + */ + private synchronized void removeAddressLine (AddressLine aAddressLine) { + remove (aAddressLine); //remove AddressLine gadget from panel. + + aAddressLine.atfRemoveKeyListener (this); //stop listening for key events. + + mAddressees.removeElement (aAddressLine); //remove from vector table. + validate(); //layout control again (scrollbar). + repaint(); + } + + /** + * Removes all addressee lines. + * @see removeAddressLine + * @see addAddresseLine + */ + private synchronized void removeAllAddressLines () { + removeAll(); //remove all AddressLines gadget from panel. + mAddressees.removeAllElements(); //clear vector table. + + validate(); //layout control again (scrollbar). + repaint(); + } + + /** + * Resonds to keyboard events for navigation (up, down, enter, etc.) + * @see addAddresseLine + */ + //implements KeyListener... + public void keyReleased (KeyEvent e) {} + public void keyTyped (KeyEvent e) {} + + /* Recieve keyPressed events for this->AddressLine->AddressTextField. + KeyEvent response table + KEY_PRESSED Old Position State New Positon Behavior + --------------------------------------------------------------------------------------- + VK_UP NOT first - Previous + VK_UP First - First + VK_DOWN NOT last - Next + VK_DOWN Last - Last + VK_BACK_SPACE First line empty First + VK_BACK_SPACE NOT first line empty Previous Delete this address line. + VK_DELETE Last line empty Last + VK_DELETE NOT last line empty Same Delete this address line. + */ + public void keyPressed (KeyEvent e) { + + //filter out only the keys we're interested in. + if ((KeyEvent.KEY_PRESSED == e.getID()) && + ( (KeyEvent.VK_UP == e.getKeyCode()) || + (KeyEvent.VK_BACK_SPACE == e.getKeyCode()) || + (KeyEvent.VK_DELETE == e.getKeyCode()) || + (KeyEvent.VK_DOWN == e.getKeyCode()) + ) + ) { + + //find out which AddressLine created this key press. + Component sourceComp = e.getComponent(); + + //check that this is an AddressTextField. + if (sourceComp instanceof AddressTextField) { + AddressTextField sourceATF = (AddressTextField)sourceComp; + + //get its parent AddressLine gadget. + AddressLine sourceAL = (AddressLine) sourceATF.getParent(); + + //locate the AddressLine in the vector table mAddressees. + int lastIndex = mAddressees.lastIndexOf(sourceAL); + + // This should never happen. + // (i.e. You should never get a KeyEvent from a gadget not in the mAddressees Vector) + // FIX: do something. + if (-1 == lastIndex) { + //System.out.println ("Internal error."); + } + else { + + //calculate where to send focus depending on the keypress. + int incFocus = 0; //increment focus (-1, 0, +1) + + //UP + if (KeyEvent.VK_UP == e.getKeyCode()) { + incFocus = -1; //focus previous. + } + + //DOWN + else if (KeyEvent.VK_DOWN == e.getKeyCode()) { + incFocus = +1; //focus next. + } + + //BACKSPACE or DELETE + else if ((KeyEvent.VK_BACK_SPACE == e.getKeyCode()) || + (KeyEvent.VK_DELETE == e.getKeyCode())) { + //if they've pressed backspace or delete and this address line + // is already empty them delete this AddressLine. + + //no matter what, if there is only one AddressLine left + // then don't delete it or try to change focus. + if (1 >= mAddressees.size()) + return; + + //Check to see if the current AddressTextField (ATF) is empty. + // If so then delete it. + if (sourceAL.isBlank()) { + + //remove the AddressLine from this container and vector list. + removeAddressLine (sourceAL); + + if (KeyEvent.VK_BACK_SPACE == e.getKeyCode()) + incFocus = -1; //focus previous. + else //KeyEvent.VK_DELETE == e.getKeyCode() + incFocus = 0; //stay where you are. + } + } + + //set focus to the next AddressTextField. + { + int nextIndex = lastIndex + incFocus; + + if (nextIndex < 0) { + nextIndex = 0; + } + else if (nextIndex > (mAddressees.size() - 1)) { + nextIndex = mAddressees.size() - 1; + } + + //get its TextFiled and set focus on it. + AddressLine nextAL = (AddressLine) mAddressees.elementAt(nextIndex); + nextAL.atfRequestFocus(); + } + } + } + } + } + + public void paint(Graphics g) { + //paint the AddressLine gadets. + super.paint(g); + + Dimension size = getSize(); + + g.setColor (Color.blue); + + //draw horizonttal lines BELOW the AddressLine gadgets. + for (int i = mAddressees.size() * mAddLineSize.height; i < size.height; i += mAddLineSize.height) { + g.drawLine (0, i, size.width, i); + } + + //draw Vertical line BELOW the AddressLine gadgets and lined up with the left side addressee button. + if (0 < mAddressees.size()) { + AddressLine firstAddressLine = (AddressLine) mAddressees.elementAt(0); + int buttonWidth = firstAddressLine.getButtonWidth(); + g.drawLine (buttonWidth, mAddressees.size() * mAddLineSize.height, buttonWidth, size.height); + } + } + } + + //************************* + /** + * An AddressLine has one DeliveryButton and one AddressTextField. + */ + public class AddressLine extends JPanel { + private DeliveryButton mDeliveryButton; + private AddressTextField mAddressTextField; + private DragIcon mDragIcon; +// private boolean mShowDeliveryButton = true; + + public AddressLine (Addressee aAddressee) { + super(); + setLayout (null); //see doLayout. + + //left side delivery button ("To:") + mDeliveryButton = new DeliveryButton (aAddressee.getDelivery()); + add (mDeliveryButton); + + //center DragIcon + mDragIcon = new DragIcon(); + add (mDragIcon); + + //right side text field ("john_doe@company.com") + mAddressTextField = new AddressTextField (aAddressee.getText(), mDeliveryButton); + add (mAddressTextField); + } + + protected void atfRemoveKeyListener(KeyListener kl){ mAddressTextField.removeKeyListener (kl); } + protected void atfAddKeyListener(KeyListener kl) { mAddressTextField.addKeyListener (kl); } + protected void atfRequestFocus() { mAddressTextField.requestFocus(); } + protected int getButtonWidth() { return mDeliveryButton.getPreferredSize ().width; } + protected boolean isBlank() { return mAddressTextField.getText().equals (""); } + protected int getDeliveryMode() { return mDeliveryButton.getDeliveryMode (); } + + /** + * layout Delivery Button, DragIcon and AddressTextField. + */ + public void doLayout() { + /* Layout + +----------------+----------+---------------------+ + | DeliveryButton | DragIcon | AddressTextField >>>| + +----------------+----------+---------------------+ + */ + Dimension mySize = getSize(); //get this containers size. + + //DeliveryButton + Dimension dbSize = mDeliveryButton.getPreferredSize (); + mDeliveryButton.setBounds(0, 0, dbSize.width, mySize.height); + int x = dbSize.width; + + //DragIcon + Dimension diSize = mDragIcon.getPreferredSize (); + mDragIcon.setBounds(x, 0, diSize.width, mySize.height); + x += diSize.width; + + //AddressTetField takes up the rest. + Dimension atfSize = mAddressTextField.getPreferredSize (); + mAddressTextField.setBounds(x, (mySize.height - atfSize.height)/2, mySize.width - x, mySize.height); + } + + /* + */ + public Dimension getPreferredSize() { + return mDeliveryButton.getPreferredSize (); + } + + /* + * Returns an Addressee for the line. + */ + protected Addressee getAddressee() { + return (new Addressee (mAddressTextField.getText(), mDeliveryButton.getDeliveryMode ())); + } + + /** + * Paint the blue lines in the background. + */ + public void paint(Graphics g) { + super.paint(g); + + Dimension size = getSize(); + FontMetrics fm = g.getFontMetrics(); + int buttonWidth = getButtonWidth(); + + g.setColor (Color.blue); + g.drawLine (buttonWidth, 0, size.width, 0); //top + g.drawLine (buttonWidth, size.height, size.width, size.height); //bottom + } + } + + //************************* + /** + * Image icon for drag and drop. + */ + public class DragIcon extends JPanel { + private ImageIcon mIcon; + + public DragIcon () { + super(); + + setBorder(BorderFactory.createEmptyBorder (5, 8, 5, 8)); + + //create image icon for drag and drop. + + mIcon = new ImageIcon(getClass().getResource("images/card.gif")); + } + + public void paint (Graphics g) { + Dimension size = getSize(); + + //try to center the icon. + int x = (size.width - mIcon.getIconWidth())/2; + x = (x < 0) ? 0 : x; + int y = (size.height - mIcon.getIconHeight())/2; + y = (y < 0) ? 0 : y; + + mIcon.paintIcon (this, g, x, y); + } + } + +// public class AddressTextField extends JTextField implements FocusListener { + public class AddressTextField extends ATC_Field implements FocusListener { + private final String ADDRESS_SEPERATORS = ","; + private final String ADDRESS_QUOTES = "\""; + + private DeliveryButton mDeliveryButton; + + public AddressTextField (String aString, DeliveryButton aDeliveryButton) { + super (aString, new TestDataSource2()); + mDeliveryButton = aDeliveryButton; + + //red completion text. + setCompletionColor (Color.red); + + //NO border. + setBorder(null); + + //get focus gained/lost events. + addFocusListener(this); + + //catch tabs and enters before anyone else. + enableEvents (AWTEvent.KEY_EVENT_MASK); //see processKeyEvent + } + + /** + * catch tabs before anyone else. + */ + public void processKeyEvent (KeyEvent e) { + //TAB + if ('\t' == e.getKeyChar()) + return; //ignore tab characters. + + //ENTER + if ('\n' == e.getKeyChar()) { + evaluate (); //evaluate this line for mutilple entries. + mAddressPanel.focusOnLast(); //Put focus on the last blank entry. + } + + super.processKeyEvent(e); + } + + /** + * stub + */ + public void focusGained(FocusEvent evt) { + } + + /** + * On focusLost, evaluate line for multiple entries. + */ + public void focusLost(FocusEvent evt) { + evaluate (); + } + + /** + * Evaluate line for multiple addresses, notify parent to add the new entries. + */ + private void evaluate() { + String [] tokens = parseLine (getText()); + + //we've lost focus and they type nothing or a bunch of ADDRESS_SEPERATORS on this line. + if (tokens.length == 0) { + setText (""); + } + + //else they typed something.... + else { + //we keep the first. + setText (tokens[0]); + + //if more than one address is on this line then add the others. + if (tokens.length > 1) { + for (int i = 1; i < tokens.length; i++) { + mAddressPanel.addAddresseLine (new Addressee (tokens[i], mDeliveryButton.getDeliveryMode())); + } + } + } + + //repack the lines. (remove blanks) + mAddressPanel.repack(); + } + + /** + * Parses up the string. + * @param aString The String to parse. + * @return returns an array of strings. + * @see ADDRESS_SEPERATORS + * @see ADDRESS_QUOTES + */ + private String[] parseLine (String aString) { + Vector tokenVec = new Vector (); + boolean quoted = false; + int tail = 0; + int head = 0; + + //step through each character in the string. + for (head = 0; head < aString.length(); head++) { + + //is this a quote character? + if (-1 != ADDRESS_QUOTES.indexOf(aString.charAt(head))) { + + //are we already in a quoted string? + if (quoted) { + String token = aString.substring(tail, head + 1).trim(); + + //if this is not a blank then increment count. + if (!token.equals("")) + tokenVec.addElement(token); + + tail = head + 1; + quoted = false; //quoting off + } + else { + tail = head; //remember the quote char. + quoted = true; //quoting on. + } + } + + //is this a seperator character? + else if (-1 != ADDRESS_SEPERATORS.indexOf(aString.charAt(head))) { + + //if not in a quote and not + if (!quoted) { + String token = aString.substring(tail, head).trim(); + + //if this is not a blank then increment count. + if (!token.equals("")) + tokenVec.addElement(token); + + tail = head + 1; + } + } + } + + //last token + String quotingChar = ""; + + //did we finish with an open quote? + if (quoted) { + quotingChar = aString.substring(tail, tail + 1); //add a matching quote. + } + + String token = aString.substring(tail, head).trim(); + + //if this is not a blank then increment count. + if (!token.equals("")) + tokenVec.addElement(token + quotingChar); + + //return an array of Strings. + String [] tokenArray = new String [tokenVec.size()]; + tokenVec.copyInto(tokenArray); + return tokenArray; + } + } + + //************************* + /** + * DeliveryButton displays "To:", "Cc:", etc and has a popup menu to change values. + */ + public class DeliveryButton extends JPanel implements + MouseListener, //show popup menu on mouse click + FocusListener, //display focus when tabbed to. + ActionListener, //get popup menu selection. + KeyListener { //show popup menu when focus and spacebar pressed. + + private int mDeliveryMode; + private Dimension mPerfSize = null; + private Insets mInsets = new Insets (4, 4, 4, 8); + private PopupMenu mPopup; + + public DeliveryButton (int aDeliveryMode) { + //get your own mouse events for popup menu. + addMouseListener (this); + + //get your own focus events for button highlight. + addFocusListener (this); + + //display popup menu when in focus and spacebar pressed. + addKeyListener (this); + + //create the popup menu.. + mPopup = createPopup (); + + //add popup to me. + add (mPopup); + + setDeliveryMode (aDeliveryMode); + } + + /** + * Creates the popup menu. + */ + private PopupMenu createPopup () { + PopupMenu pm = new PopupMenu (Addressee.getDeliveryTitle()); + + //added text commands to popup + for (int i = 0; i < Addressee.mDeliveryStr.length; i++) { + MenuItem mi = new MenuItem (Addressee.mDeliveryStr[i]); + pm.add (mi); + mi.addActionListener (this); + } + + return pm; + } + + /* + * Set the button delivery mode. + * @param aDeliveryMode a value like Addressee.TO or Addressee.BCC + * @see getDeliveryMode + */ + protected void setDeliveryMode (int aDeliveryMode) { + mDeliveryMode = aDeliveryMode; + repaint(); + } + + /* + * Return the button delivery mode. + * @return a delivery mode a value like Addressee.TO or Addressee.BCC + * @see setDeliveryMode + */ + protected int getDeliveryMode () { return mDeliveryMode; } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + /** + * addNotify creates the preferred size dimension because only + * after the peer has been created can we get the font metrics. + */ + public void addNotify () { + super.addNotify (); + + if (null == mPerfSize) { + Font fnt = getFont(); + if (null != fnt) { + FontMetrics fm = getToolkit().getFontMetrics(fnt); + if (null != fm) { + String longestString = Addressee.getLongestString(); + + mPerfSize = new Dimension(fm.stringWidth(longestString) + mInsets.left + mInsets.right, + fm.getMaxAscent() + fm.getMaxDescent() + mInsets.top + mInsets.bottom); + } + } + } + } + + /** + * Return PerfSize created in addNotify. + */ + public Dimension getPreferredSize() { + if (null != mPerfSize) { + return mPerfSize; + } + return new Dimension(50, 20); + } + + public boolean isFocusTraversable() { + return true; + } + + /** + * Display button highlight. + */ + public void focusGained(FocusEvent evt) { + repaint(); + } + + /** + * Remove button highlight. + */ + public void focusLost(FocusEvent evt) { + repaint(); + } + + /** + * Paint the button's bevels and arrow and text string (i.e. "To:") + */ + public void paint(Graphics g) { + Dimension size = getSize(); + + //gray background + g.setColor (Color.lightGray); + g.fillRect (0, 0, size.width - 1, size.height - 1); + + //draw bezel borders + g.setColor (Color.white); + g.drawLine (0, 0, 0, size.height - 1); //top + g.drawLine (0, 0, size.width - 1, 0); //left + g.setColor (Color.black); + g.drawLine (size.width - 1, 0, size.width - 1, size.height - 1); //right + g.drawLine (0, size.height - 1, size.width - 1, size.height - 1); //bottom + + //down arrow + g.setColor (Color.gray); + + int xPoints[] = {0, 10, 5}; + int yPoints[] = {0, 0, 10}; + Polygon arrow = new Polygon (xPoints, yPoints, 3); + Rectangle arrowRect = arrow.getBounds(); + + int deltaX = mInsets.left; + int deltaY = (size.height - arrowRect.height)/2; + + arrow.translate (deltaX, deltaY); + + g.fillPolygon(arrow); + + g.setColor (Color.white); + g.drawLine (xPoints[1] + deltaX, + yPoints[1] + deltaY, + xPoints[2] + deltaX, + yPoints[2] + deltaY); //right edge + + //Delivery mode text (ie. "To:") + g.setColor (Color.black); + FontMetrics fm = g.getFontMetrics(); + + //Font height is wacky. + //g.drawString (mLabel, arrowRect.width + mInsets.left + 4, (fm.getHeight() + size.height)/2); + g.drawString (Addressee.deliveryToString(mDeliveryMode), arrowRect.width + mInsets.left + 4, fm.getHeight()); + + //draw focus rectangle (AWT 1.1 doesn't have support for line styles) + if (true == hasFocus()) { + final int offset = 3; //insets of focus rectangle. + + //top and bottom + int bottomOffset = size.height - offset - 1; + for (int x = offset; x < size.width - offset; x += 2) { + g.drawLine (x, offset, x, offset); //top + g.drawLine (x, bottomOffset, x, bottomOffset); //bottom + } + + //left and right + int rightOffset = size.width - offset - 1; + for (int y = offset; y < size.height - offset; y += 2) { + g.drawLine (offset, y, offset, y); //left + g.drawLine (rightOffset, y, rightOffset, y); //right + } + } + } + + //implements KeyListener... + public void keyReleased (KeyEvent e) {} + public void keyTyped (KeyEvent e) {} + + /* + * Display popup menu when spacebar pressed. + */ + public void keyPressed (KeyEvent e) { + if ((KeyEvent.KEY_PRESSED == e.getID()) && + (KeyEvent.VK_SPACE == e.getKeyCode())) { + mPopup.show (e.getComponent (), 0, 0); + } + } + + /** + * Called when popup menu item is selected. + */ + public void actionPerformed (ActionEvent e) { + String menuCommand = e.getActionCommand(); + setDeliveryMode (Addressee.deliveryToInt (menuCommand)); + } + + public void mouseEntered (MouseEvent e) {} + public void mouseExited (MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + public void mouseClicked (MouseEvent e) {} + + /** + * On any mouse click show the popup menu. + * Windows has bug for getX(), getY(). (UNIX OK) + */ + public void mousePressed (MouseEvent e) { + mPopup.show (e.getComponent (), e.getX(), e.getY()); + } + } +} diff --git a/mozilla/grendel/composition/AddressListBeanInfo.java b/mozilla/grendel/composition/AddressListBeanInfo.java new file mode 100644 index 00000000000..9fc16a660eb --- /dev/null +++ b/mozilla/grendel/composition/AddressListBeanInfo.java @@ -0,0 +1,72 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.beans.*; + +/** BeanInfo for AddressList: +*/ +public class AddressListBeanInfo extends SimpleBeanInfo { + private final static Class beanClass = AddressList.class; + + // getAdditionalBeanInfo method allows to return any number of additional + // BeanInfo objects which provide information about the Bean that this BeanInfo + // describes. + public BeanInfo[] getAdditionalBeanInfo() { + try { + java.util.Vector v = new java.util.Vector(); + BeanInfo[] rv; + BeanInfo b; + Class c = beanClass.getSuperclass(); + + while (c.isAssignableFrom(Object.class) != true) { + b = Introspector.getBeanInfo(c); + v.addElement(b); + c = c.getSuperclass(); + } + rv = new BeanInfo[v.size()]; + v.copyInto(rv); + + return rv; + } + catch (IntrospectionException e) { + throw new Error(e.toString()); + } + } + + // getIcon returns an image object which can be used by toolboxes, toolbars + // to represent this bean. Icon images are in GIF format. + public java.awt.Image getIcon(int iconKind) + { + if (iconKind == BeanInfo.ICON_COLOR_16x16 || + iconKind == BeanInfo.ICON_MONO_16x16) + { + java.awt.Image img = loadImage("images/AttachmentsListIcon16.gif"); + return img; + } + if (iconKind == BeanInfo.ICON_COLOR_32x32 || + iconKind == BeanInfo.ICON_MONO_32x32) + { + java.awt.Image img = loadImage("images/AttachmentsListIcon32.gif"); + return img; + } + return null; + } +} diff --git a/mozilla/grendel/composition/Addressee.java b/mozilla/grendel/composition/Addressee.java new file mode 100644 index 00000000000..98fa1083951 --- /dev/null +++ b/mozilla/grendel/composition/Addressee.java @@ -0,0 +1,118 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.util.*; + +import javax.mail.Address; + +public class Addressee { + private static final String ResourceFile = "addressee.res"; + + public static final int TO = 0; + public static final int CC = 1; + public static final int BCC = 2; + public static final int GROUP = 3; + public static final int REPLY_TO = 4; + public static final int FOLLOWUP_TO = 5; + + public static final String[] mDeliveryStr = {"To:", "Cc:", "Bcc:", "Group:", "Reply-To:", "Followup-To:"}; +// public static final String[] mDeliveryStr; + + private String mAddress; + private int mDelivery; + private ResourceBundle resources; +/* + static { + String[] mDeliveryName = { + "address_to", + "address_cc", + "address_bcc", + "address_group", + "address_reply_to", + "address_followup_to"}; + + mDeliveryStr = new String[mDeliveryName.length]; + ResourceBundle resources = ResourceBundle.getBundle(ResourceFile, Locale.getDefault()); + + for (int i = 0; i < mDeliveryName.length; i++) { + String str; + + try { + str = resources.getString(mDeliveryName[i]); + } + catch (MissingResourceException mre) { + str = ""; + } + + mDeliveryStr[i] = str; + } + } +*/ + public Addressee (String aAddress, int aDelivery) { + init (aAddress, aDelivery); + } + + public Addressee () { + init ("", TO); + } + + public Addressee(Address addr, int delivery) { + init(addr.toString(), delivery); + } + + + public void init (String aAddress, int aDelivery) { + setText (aAddress); + setDelivery (aDelivery); + } + + public String getText () { return mAddress; } + public void setText (String aAddress) { mAddress = aAddress; } + + public int getDelivery () { return mDelivery; } + public void setDelivery (int aDelivery) { mDelivery = aDelivery; } + + public String deliveryString () { return deliveryToString(mDelivery); }; + + public static String getLongestString() { + return "Followup-To:"; + } + + public static String getDeliveryTitle() { + return "Delivery"; + } + + public static String deliveryToString (int aDelivery) { + if ((aDelivery > -1) && (aDelivery < mDeliveryStr.length)) + return mDeliveryStr [aDelivery]; + + return mDeliveryStr [TO]; + } + + protected static int deliveryToInt(String deliverString) { + for (int i = 0; i < mDeliveryStr.length; i++) { + if (deliverString.equalsIgnoreCase (mDeliveryStr[i])) + return (i); + } + + return (TO); //default + } +} diff --git a/mozilla/grendel/composition/AttachmentsList.java b/mozilla/grendel/composition/AttachmentsList.java new file mode 100644 index 00000000000..e9856f45cb5 --- /dev/null +++ b/mozilla/grendel/composition/AttachmentsList.java @@ -0,0 +1,374 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +/* Gadget oganization: + an AttachmentsList has one AttachmentsPanel. + an AttachmentsPanel has many AttachmentLabel(s). + */ + +import java.io.Serializable; +import java.util.*; +import java.awt.*; +import java.awt.event.*; + +import com.sun.java.swing.*; + +/* +*/ +//public class AttachmentsList extends JScrollPane implements Serializable, FocusListener { +public class AttachmentsList extends JScrollPane implements Serializable { + private AttachmentsPanel mAP; + + public AttachmentsList () { + super (); + + //add attachments panel. + mAP = new AttachmentsPanel (); + JViewport spViewPort = getViewport(); + spViewPort.add(mAP); + + //keyboard navigation (arrows). + addKeyListener (mAP); + +// mAP.addFocusListener (this); + + setBackground (Color.white); + } + + /** + * Sets this gadgets enabled state. + * @param aEnabled Boolean value for enabled. + */ + public void setEnabled (boolean aEnabled) { + } + + /** + * showDialog requests that the AddressList display its file dialog. + * @see showDialog() + */ + public void showDialog () { + mAP.showDialog (); + } + + /** + * Delets all attachments from the list. + * @see addAttachment + * @see removeAttachment + */ + public void removeAllAttachments () { + } + + /** + * Remove an entry at index from the list. + * @param aIndex The index of the attachment to remove. + * @see removeAllAttachments + */ + public void removeAttachment (int aIndex) { + } + + /** + * Adds an attachment to the list. + * @param aFilePath A file path. + * @see removeAllAttachments + */ + public void addAttachment (String aFilePath) { + mAP.addAttachment (aFilePath); + } + + /** + * Tranverse up a component's tree to find the top most frame. + * @param aComp the component for which you want its parent frame for. + * @return The parent frame. + */ + public static Frame getTopLevelFrame(Component aComp) { + Component c = aComp; + + while (null != c.getParent()) + c = c.getParent(); + + if (c instanceof Frame) + return (Frame) c; + + return null; + } + + //***************** + /** + * + */ + class AttachmentsPanel extends JPanel implements MouseListener, KeyListener { + private AttachmentLabel mCurrentSelection; + + public AttachmentsPanel () { + super (); + + //do your own layout. + setLayout (null); + + //Get your own mouse events. + // used to diaply file dialog when mouse down on background. + addMouseListener (this); + + setBackground (Color.white); + } + + /** + * Gets the currently selected label. + * @return The currently selected label. + * @see setSelection + */ + private AttachmentLabel getSelection () { return mCurrentSelection; } + + /** + * Sets the currently selected label. + * @param One of the AttachmentLabel's + * @see getSelection + */ + private void setSelection (AttachmentLabel aLabel) { + //un-hightlight old. + if (null != mCurrentSelection) { + mCurrentSelection.setForeground (Color.black); + mCurrentSelection.setBackground (Color.white); + } + + mCurrentSelection = aLabel; + + //highlight new + if (null != mCurrentSelection) { + mCurrentSelection.setForeground (Color.white); + mCurrentSelection.setBackground (Color.blue); + } + + repaint(); + } + + /** + * Adds a file attachment to the list. + * @param aFilePath A file path. + * @see AttachmentsList.addAttachment + */ + protected void addAttachment (String aFilePath) { + AttachmentLabel fileLabel = new AttachmentLabel (aFilePath); + add (fileLabel); + + //mouse listener for selection via mouse (click on the line). + fileLabel.addMouseListener (this); + + //select the newly added attachment. + setSelection (fileLabel); + doLayout(); + } + + /** + * Remove a file attachment from the list. + */ + private void removeAttachment (AttachmentLabel aAttLabel) { + if (null != aAttLabel) { + remove (aAttLabel); + + //if the AttachmentLabel that's being removed is also the currently + // selected label then set the current selection to null. + if (aAttLabel == getSelection()) + setSelection(null); + + //force a re-layout of the labels. + doLayout(); + } + } + + /** + * Remove a file attachment from the list. + */ + private void removeAttachment (int aIndex) { + } + + /** + * + */ + public void doLayout() { + Graphics g = getGraphics(); + FontMetrics fm = g.getFontMetrics(); + Dimension size = getSize(); + int y, i; + Component comps[] = getComponents(); + int height = new Double(fm.getHeight() * 1.2).intValue(); + + for ( i = 0, y = 0; + (i < comps.length) && (y < size.height); + i++, y += fm.getHeight()) { + + AttachmentLabel attLabel = (AttachmentLabel) comps[i]; + attLabel.setBounds(3, y, size.width, height); + } + } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + public Dimension getPreferredSize() { + Graphics g = getGraphics(); + FontMetrics fm = g.getFontMetrics(); + Component comps[] = getComponents(); + int totalHeight = new Double((comps.length + 1) * fm.getHeight() * 1.2).intValue(); + + return new Dimension(100, totalHeight); + } + + /** + * Display the file dialog so the user can select a file to add to the list. + * @see this.mousePressed and AttachmentsList.showDialog() + */ + protected void showDialog () { + //get the parent frame for the file dialog. + Frame parentFrame = getTopLevelFrame(this); + + if (null != parentFrame) { + FileDialog file = new FileDialog (parentFrame, "Attach File", FileDialog.LOAD); + + file.setFile ("*.*"); //all files. + file.show(); //show the dialog. This blocks. + + //didi they select a file? + if (null != file.getFile()) { + + //assemble the full path. + String fullPath = file.getDirectory() + file.getFile(); + addAttachment (fullPath); + } + } + } + + /** + * + */ + public void mouseEntered (MouseEvent e) {} + public void mouseExited (MouseEvent e) {} + public void mouseReleased(MouseEvent e) {} + public void mouseClicked (MouseEvent e) {} + + //on any mouse click show the popup menu. + public void mousePressed (MouseEvent e) { + Component comp = e.getComponent(); + + //mouse down on background (this panel) so dispaly file dialog. + if (comp == this) { + showDialog (); + } + + //else mouse down on an AttachmentLabel so select line. + else if (comp instanceof AttachmentLabel) { + setSelection ((AttachmentLabel)e.getComponent()); + } + } + + /** + * keyPressed responds to up and down arrows keys. + */ + //implements KeyListener... + public void keyReleased (KeyEvent e) {} + public void keyTyped (KeyEvent e) {} + + public void keyPressed (KeyEvent e) { + if (KeyEvent.KEY_PRESSED == e.getID()) { + + //delete the selected attachment. + if (KeyEvent.VK_DELETE == e.getKeyCode()) { + AttachmentLabel current = getSelection(); + + if (null != current) + removeAttachment (current); + } + + //else arrow up or down (change selection) + else if ((KeyEvent.VK_UP == e.getKeyCode()) || + (KeyEvent.VK_DOWN == e.getKeyCode())) + { + //get the currently selected attachment and all attachments. + AttachmentLabel current = getSelection(); + Component comps[] = getComponents(); + int i = 0; + + //find the index of the current attachment + for (i = 0; i < comps.length; i++) { + if (current == (AttachmentLabel) comps[i]) + break; + } + + //leave if not found. + if (i == comps.length) + return; + + //increment/decrement + if (KeyEvent.VK_UP == e.getKeyCode()) + i += comps.length -1; + else //(KeyEvent.VK_DOWN == e.getKeyCode()) + i += comps.length +1; + + //select next and wrap using the modulus. + setSelection ((AttachmentLabel) comps[i % comps.length]); + } + } + } + } + + //***************** + /** An AttachmentLabel displays the file name. + */ + class AttachmentLabel extends JLabel { + AttachmentLabel (String aLabel) { + super (aLabel); + } + + /** + * Processes all Mouse events + */ + public void processMouseEvent(MouseEvent e) { + switch(e.getID()) { + case MouseEvent.MOUSE_PRESSED: + requestFocus(); + break; + + case MouseEvent.MOUSE_RELEASED: + break; + case MouseEvent.MOUSE_EXITED: + break; + } + super.processMouseEvent(e); + } + + public void paint (Graphics g) { + Dimension size = getSize(); + FontMetrics fm = g.getFontMetrics(); + + g.setColor (getBackground()); + g.fillRect (0, 0, size.width, size.height); + + g.setColor (getForeground()); + g.drawString (getText(), 0, fm.getHeight()); + } + } +} + diff --git a/mozilla/grendel/composition/AttachmentsListBeanInfo.java b/mozilla/grendel/composition/AttachmentsListBeanInfo.java new file mode 100644 index 00000000000..7d493ae45c3 --- /dev/null +++ b/mozilla/grendel/composition/AttachmentsListBeanInfo.java @@ -0,0 +1,127 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.beans.*; +import java.lang.reflect.Method; + +/** BeanInfo for AttachmentsList: +*/ +public class AttachmentsListBeanInfo extends SimpleBeanInfo { + private final static Class beanClass = AttachmentsList.class; + + // getIcon returns an image object which can be used by toolboxes, toolbars + // to represent this bean. Icon images are in GIF format. + public java.awt.Image getIcon(int iconKind) + { + if (iconKind == BeanInfo.ICON_COLOR_16x16 || + iconKind == BeanInfo.ICON_MONO_16x16) + { + java.awt.Image img = loadImage("images/AttachmentsListIcon16.gif"); + return img; + } + if (iconKind == BeanInfo.ICON_COLOR_32x32 || + iconKind == BeanInfo.ICON_MONO_32x32) + { + java.awt.Image img = loadImage("images/AttachmentsListIcon32.gif"); + return img; + } + return null; + } + + // getPropertyDescriptors returns an array of PropertyDescriptors which describe + // the editable properties of this bean. + public PropertyDescriptor[] getPropertyDescriptors() { +/* + try { + PropertyDescriptor prop1 = new PropertyDescriptor("prop1", beanClass); + prop1.setBound(true); + prop1.setDisplayName("Property 1"); + + PropertyDescriptor rv[] = {prop1, prop2}; + return rv; + } + catch (IntrospectionException e) { + throw new Error(e.toString()); + } +*/ + return null; + } + + public MethodDescriptor[] getMethodDescriptors() { + try { + Method mthd; + + //showDialog requests that the AddressList display its file dialog. + //public void showDialog () { + mthd = beanClass.getMethod ("showDialog", null); + MethodDescriptor showDialogDesc = new MethodDescriptor (mthd); + showDialogDesc.setShortDescription ("Displays the file dialog"); + + //Sets this gadgets enabled state. + //public void setEnabled (boolean aEnabled) { + ParameterDescriptor setEnabledPD = new ParameterDescriptor (); + setEnabledPD.setShortDescription ("Boolean value for enabled"); + ParameterDescriptor[] setEnabledPDA = { setEnabledPD }; + + Class[] setEnabledParams = { java.lang.Boolean.TYPE }; + mthd = beanClass.getMethod ("setEnabled", setEnabledParams); + MethodDescriptor setEnabledDesc = new MethodDescriptor (mthd, setEnabledPDA); + setEnabledDesc.setShortDescription ("Sets this gadgets enabled state"); + + //Delets all attachments from the list. + //public void removeAllAttachments () { + mthd = beanClass.getMethod ("removeAllAttachments", null); + MethodDescriptor removeAllAttachmentsDesc = new MethodDescriptor (mthd); + removeAllAttachmentsDesc.setShortDescription ("Delets all attachments from the list"); + + //Remove an entry at index from the list. + //public void removeAttachment (int aIndex) { + ParameterDescriptor removeAttachmentPD = new ParameterDescriptor (); + removeAttachmentPD.setShortDescription ("The index of the attachment to remove"); + ParameterDescriptor[] removeAttachmentPDA = { removeAttachmentPD }; + + Class[] removeAttachmentParams = { java.lang.Integer.TYPE }; + mthd = beanClass.getMethod ("removeAttachment", removeAttachmentParams); + MethodDescriptor removeAttachmentDesc = new MethodDescriptor (mthd, removeAttachmentPDA); + removeAttachmentDesc.setShortDescription ("Remove an entry at index from the list"); + + //Adds an file to the list. + //public void addAttachment (String aFilePath) { + ParameterDescriptor addAttachmentPD = new ParameterDescriptor (); + addAttachmentPD.setShortDescription ("A file path"); + ParameterDescriptor[] addAttachmentPDA = { addAttachmentPD }; + + Class[] addAttachmentParams = { String.class }; + mthd = beanClass.getMethod ("addAttachment", addAttachmentParams); + MethodDescriptor addAttachmentDesc = new MethodDescriptor (mthd, addAttachmentPDA); + addAttachmentDesc.setShortDescription ("Adds an file to the list"); + + //pull it all together now. + MethodDescriptor [] mda = { showDialogDesc, setEnabledDesc, removeAllAttachmentsDesc, removeAttachmentDesc, addAttachmentDesc }; return mda; + } + catch (SecurityException e) { + return null; + } + catch (NoSuchMethodException e) { + return null; + } + } +} diff --git a/mozilla/grendel/composition/Composition.java b/mozilla/grendel/composition/Composition.java new file mode 100644 index 00000000000..a003cc47a08 --- /dev/null +++ b/mozilla/grendel/composition/Composition.java @@ -0,0 +1,177 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import calypso.util.Assert; + +import java.awt.BorderLayout; +import java.util.Properties; +import java.util.Vector; + +//import com.sun.java.swing.*; +import com.sun.java.swing.event.*; + +import netscape.orion.toolbars.*; +import netscape.orion.menus.NsMenuManager; + +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; +import grendel.widgets.Animation; +import grendel.ui.GeneralFrame; +import grendel.ui.StoreFactory; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; + +/** + * + * @author Lester Schueler + */ +public class Composition extends GeneralFrame { + CompositionPanel mCompositionPanel; + AddressBar mAddressBar; + + public static void main(String[] args) { + //check arguments + if (2 != args.length) { + System.out.println ("Usage: composition mail_server_name user_name"); + System.exit(0); + } + + Composition compFrame = new Composition (); + compFrame.show(); + } + + /** + * + */ + public Composition() { + super("Composition", "mail.composition"); + fResourceBase = "grendel.composition"; + + Session session = StoreFactory.Instance().getSession(); + + //session.setDebug(true); + mCompositionPanel = new CompositionPanel(session); + + mCompositionPanel.addCompositionPanelListener(new PanelListener()); + + //create menubar (top) + fMenu = buildMenu("mainMenubar", + mCompositionPanel.getActions()); + getRootPane().setMenuBar(fMenu); + + fToolBar = mCompositionPanel.getToolBar(); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(ToolBarLayout.CreateSpring(), + null)); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(fAnimation, null)); + + mAddressBar = mCompositionPanel.getAddressBar(); + + //top collapsible item + fToolBarPanel.add(fToolBar); + //bottom collapsible item + fToolBarPanel.add(mAddressBar); + + fStatusBar = buildStatusBar(); + fPanel.add(BorderLayout.SOUTH, fStatusBar); + + fPanel.add(mCompositionPanel); + + restoreBounds(); + } + + public void dispose() { + saveBounds(); + super.dispose(); // call last + } + + protected void startAnimation() { + super.startAnimation(); + } + + protected void stopAnimation() { + super.stopAnimation(); + } + + /** Initialize the headers of this composition as being a reply to + the given message. */ + public void initializeAsReply(Message msg, boolean replyall) { + mCompositionPanel.setReferredMessage(msg); + MessageExtra mextra = MessageExtraFactory.Get(msg); + int i; + Vector dests = new Vector(); + Address from[] = null; + try { + from = msg.getReplyTo(); + } catch (MessagingException e) { + } + if (from == null || from.length == 0) { + try { + from = msg.getFrom(); + } catch (MessagingException e) { + } + } + + if (from != null) { + for (i=0 ; i, 20 Oct 1997. + */ + +package grendel.composition; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FileDialog; +import java.awt.Font; +import java.awt.Frame; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.util.Hashtable; +import java.util.StringTokenizer; +import java.util.Properties; + +import com.sun.java.swing.BorderFactory; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JButton; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.JTextField; +import com.sun.java.swing.JTextArea; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.EventListenerList; +import com.sun.java.swing.text.BadLocationException; +import com.sun.java.swing.text.Document; +import com.sun.java.swing.text.JTextComponent; +import com.sun.java.swing.text.TextAction; + +import javax.mail.Address; +import javax.mail.Session; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.InternetAddress; + +import netscape.orion.toolbars.NSButton; +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; +import grendel.ui.ActionFactory; +import grendel.ui.GeneralPanel; + +public class CompositionPanel extends GeneralPanel { + private Hashtable mCommands; + private Hashtable mMenuItems; + + private AddressBar mAddressBar; + private AddressList mAddressList; + private AttachmentsList mAttachmentsList; + private JTextComponent mEditor; + private JTextField mSubject; + private Session mSession; + private String mMailServerHostName; + private String mMailUserName; + private Message referredMsg; + + private EventListenerList mListeners = new EventListenerList(); + + /** + * + */ + public CompositionPanel(Session aSession) { + fResourceBase = "grendel.composition"; + + mSession = aSession; + + //Create message panel + // The MessagePanel is... + // 1) The subject JLabel + // 2) The subject JTextField and + // 3) An editor (JTextArea) + // The editor in the message panel must be created first so + // the supported JActions can be registered with the menu "mMenubar". + MessagePanel messagePanel = new MessagePanel (); + mEditor = messagePanel.getEditor (); + mSubject = messagePanel.getSubject (); + + //toolbar buttons + fToolBar = createToolbar(); + mAddressBar = new AddressBar(); + mAddressList = mAddressBar.getAddressList(); + + //the splitPane holds collapseble panels and message panels. +// JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, false, collapsePanel, messagePanel); + + + add(messagePanel, BorderLayout.CENTER); + } + + public void dispose() { + } + + /** + * Fetch the list of actions supported by this + * editor. It is implemented to return the list + * of actions supported by the embedded JTextComponent + * augmented with the actions defined locally. + */ + public IUICmd[] getActions() { + return defaultActions; + // XXX WHS need to translate Actions to UICmds + // return TextAction.augmentList(mEditor.getActions(), defaultActions); + } + + /** + * Return the addressing bar + */ + + public AddressBar getAddressBar() { + return mAddressBar; + } + + void setSubject(String subject) { + mSubject.setText(subject); + } + + String getSubject() { + return mSubject.getText(); + } + + void setReferredMessage(Message m) { + referredMsg = m; + } + + Message getReferredMessage() { + return referredMsg; + } + + /** + * Add a CompositionPanelListener + */ + + public void addCompositionPanelListener(CompositionPanelListener l) { + mListeners.add(CompositionPanelListener.class, l); + } + + /** + * Remove a CompositionPanelListener + */ + + public void removeCompositionPanelListener(CompositionPanelListener l) { + mListeners.remove(CompositionPanelListener.class, l); + } + + protected void notifySendingMail() { + Object[] listeners = mListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == CompositionPanelListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((CompositionPanelListener)listeners[i+1]).sendingMail(changeEvent); + } + } + } + + protected void notifyMailSent() { + Object[] listeners = mListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == CompositionPanelListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((CompositionPanelListener)listeners[i+1]).doneSendingMail(changeEvent); + } + } + } + + protected void notifySendFailed() { + Object[] listeners = mListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == CompositionPanelListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((CompositionPanelListener)listeners[i+1]).sendFailed(changeEvent); + } + } + } + + /** + * Find the hosting frame, for the file-chooser dialog. + */ + protected Frame getParentFrame() { + for (Container p = getParent(); p != null; p = p.getParent()) { + if (p instanceof Frame) { + return (Frame) p; + } + } + return null; + } + + //"File" actions + public static final String saveDraftTag ="saveDraft"; + public static final String saveAsTag ="saveAs"; + public static final String sendNowTag ="sendNow"; + public static final String sendLaterTag ="sendLater"; + public static final String quoteOriginalTextTag ="quoteOriginalText"; + public static final String selectAddressesTag ="selectAddresses"; + public static final String goOfflineTag ="goOffline"; + public static final String closeWindowTag ="closeWindow"; + + // "file->new" actions + public static final String navigatorWindowTag ="navigatorWindow"; + public static final String messageTag ="message"; + public static final String blankPageTag ="blankPage"; + public static final String pageFromTemplateTag ="pageFromTemplate"; + public static final String pageFromWizardTag ="pageFromWizard"; + + // "file->attach" actions + public static final String fileTag ="attachFile"; + public static final String webPageTag ="webPage"; + public static final String addressBookCardTag ="addressBookCard"; + public static final String myAddressBookCardTag ="myAddressBookCard"; + + //"Edit" actions + public static final String undoTag ="undo"; + public static final String pasteAsQuotationTag ="pasteAsQuotation"; + public static final String DeleteTag ="delete"; + public static final String selectAllTag ="selectAll"; + public static final String findInMessageTag ="findInMessage"; + public static final String findAgainTag ="findAgain"; + public static final String searchDirectoryTag ="searchDirectory"; + public static final String preferencesTag ="appPrefs"; + + //"View" actions + public static final String hideMessageToolbarTag ="hideMessageToolbar"; + public static final String hideAddressingAreaTag ="hideAddressingArea"; + public static final String viewAddressTag ="viewAddress"; + public static final String viewAttachmentsTag ="viewAttachments"; + public static final String viewOptionsTag ="viewOptions"; + public static final String wrapLongLinesTag ="wrapLongLines"; + + // --- action implementations ----------------------------------- + private IUICmd[] defaultActions = { + //"File" actions +// new SaveDraft(), + new SaveAs(), + new SendNow(), +// new SendLater(), + new QuoteOriginalText(), + new SelectAddresses(), +// new GoOffline(), + ActionFactory.GetExitAction(), + + // "file->new" actions +// new NavigatorWindow(), +// new Message(), +// new BlankPage(), +// new PageFromTemplate(), +// new PageFromWizard(), + + // "file->attach" actions + new AttachFile(), +// new WebPage(), +// new AddressBookCard(), +// new MyAddressBookCard(), + + //"Edit" actions +// new Undo(), + //new Cut(), Handled by editor. + //new Copy(), Handled by editor. + //new Paste(), Handled by editor. + new PasteAsQuotation(), +// new Delete(), +// new FindInMessage(), +// new FindAgain(), +// new SelectAll(), +// new SearchDirectory(), + ActionFactory.GetPreferencesAction(), + + //"View" actions +// new HideMessageToolbar(), +// new HideAddressingArea(), + new ViewAddress(), + new ViewAttachments(), + new ViewOptions(), +// new WrapLongLines() + }; + + //----------------------- + //"File" actions + //----------------------- + /** + * Try to save the message to the "draft" mailbox. + * @see SaveAs + */ + class SaveDraft extends AbstractUICmd { + SaveDraft() { + super(saveDraftTag); + setEnabled(true); + } + public void actionPerformed(ActionEvent e) {} + } + + /** + * Try to save the message to a text file. + * @see SaveDraft + */ + class SaveAs extends AbstractUICmd { + SaveAs() { + super(saveAsTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + FileDialog fileDialog = new FileDialog (getParentFrame(), "Save As", FileDialog.SAVE); + fileDialog.setFile("untitled.txt"); + fileDialog.show(); //blocks + + String fileName = fileDialog.getFile(); + //check for canel + if (fileName == null) { + return; + } + + //try to retieve all text from message area. + String messageText = mEditor.getText(); + + //try to write text to file. + try { + //assemble fill path to file. + String directory = fileDialog.getDirectory(); + File f = new File(directory, fileName); + FileOutputStream fstrm = new FileOutputStream(f); + + //write text to the file. + fstrm.write(messageText.getBytes()) ; + fstrm.flush(); + } catch (IOException io) { + } + } + } + + /** + * Send the mail message now. + */ + class SendNow extends AbstractUICmd { + SendNow() { + super(sendNowTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent ae) { + //try to retieve all text from message area. + notifySendingMail(); + boolean success = false; + + String messageText = mEditor.getText(); + + if (messageText == null) { + System.err.println("****Null messageText"); + } + + //try to send the message + //get the current list of recipients. + Addressee[] recipients = mAddressList.getAddresses (); + + //Check that is at least one recipient. + if (0 < recipients.length) { + //get the users name who's sending this message. + Properties props = mSession.getProperties(); + String userName = + props.getProperty("user.email_address"); + + if (userName != null) { + //create a mime message + Message msg = new PatchedMimeMessage(mSession); + + try { + //set who's sending this message. + msg.setFrom (new InternetAddress(userName)); + + //add the recipients one at a time. + for (int i = 0; i < recipients.length; i++) { + javax.mail.Address[] toAddress = new InternetAddress[1]; + toAddress[0] = new InternetAddress(recipients[i].getText()); + + int deliverMode = Message.TO; + + //map grendel.composition.Addressee delivery modes + // into javax.mail.Message delivery modes. + switch (recipients[i].getDelivery()) { + case Addressee.TO: + deliverMode = Message.TO; + break; + case Addressee.CC: + deliverMode = Message.CC; + break; + case Addressee.BCC: + deliverMode = Message.BCC; + break; + } + msg.addRecipients(deliverMode, toAddress); + } + + msg.setSubject(mSubject.getText()); //set subject from text + //field. + msg.setSentDate(new java.util.Date()); //set date to now. + msg.setContent(messageText, "text/plain"); //contents. + msg.send(); //send the message. + success = true; + } catch (javax.mail.SendFailedException sex) { + sex.printStackTrace(); + Address addr[] = sex.getInvalidAddresses(); + if (addr != null) { + System.err.println("Addresses: "); + for (int i = 0; i < addr.length; i++) { + System.err.println(" " + addr[i].toString()); + } + } + } catch (MessagingException mex) { + mex.printStackTrace(); + } + } else { + System.err.println("user.email_address undefined"); + } + } + + if (success) { + //hide this frame after sending mail. + notifyMailSent(); + } else { + notifySendFailed(); + } + } + } + + /** + * Quote the original text message into the editor. + * @see PasteAsQuotation + */ + class QuoteOriginalText extends AbstractUICmd { + QuoteOriginalText() { + super(quoteOriginalTextTag); + setEnabled(true); + } + public void actionPerformed(ActionEvent event) { + if (referredMsg == null) return; // Or beep or whine??? ### + // ### Get the message as a stream of text. This involves + // complicated things like invoking the MIME parser, and throwing + // away non-text parts, and translating HTML to text, and so on. + // Yeah, right. + InputStream plaintext = null; + try { + plaintext = referredMsg.getInputStream(); + } catch (MessagingException e) { + } + if (plaintext == null) return; // Or beep or whine??? ### + + + int position = mEditor.getCaretPosition(); + Document doc = mEditor.getDocument(); + + MessageExtra mextra = MessageExtraFactory.Get(referredMsg); + String author; + try { + author = mextra.getAuthor(); + } catch (MessagingException e) { + author = "???"; // I18N? ### + } + String tmp = "\n" + author + " wrote:"; // I18N ### + try { + doc.insertString(position, tmp, null); + position += tmp.length(); + } catch (BadLocationException e) { + } + + // OK, now insert the data from the plaintext, and precede each + // line with "> ". + + ByteLineBuffer linebuffer = new ByteLineBuffer(); + ByteBuf empty = new ByteBuf(); + linebuffer.setOutputEOL(empty); + + boolean eof = false; + + ByteBuf buf = new ByteBuf(); + ByteBuf line = new ByteBuf(); + + while (!eof) { + buf.setLength(0); + try { + eof = (buf.read(plaintext, 1024) < 0); + } catch (IOException e) { + eof = true; + } + if (eof) { + linebuffer.pushEOF(); + } else { + linebuffer.pushBytes(buf); + } + while (linebuffer.pullLine(line)) { + try { + doc.insertString(position, "\n> ", null); + position += 3; + doc.insertString(position, line.toString(), null); + position += line.length(); + } catch (BadLocationException e) { + } + } + } + repaint(); + try { + plaintext.close(); + } catch (IOException e) { + } + } + } + + class SelectAddresses extends AbstractUICmd { + SelectAddresses() { + super(selectAddressesTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + AddressDialog aDialog = new AddressDialog(getParentFrame()); + Addressee[] mAddresses; + + //get the current list to hand to the dialog. + mAddresses = mAddressList.getAddresses (); + + //display the addressee dialog + aDialog.setAddresses (mAddresses); //initialize the dialog + aDialog.show (); //blocks + + if (false == aDialog.getCanceled()) { + //get the addresses from dialog + mAddresses = aDialog.getAddresses(); + + //update addresses panel with data from dialog. + mAddressList.setAddresses (mAddresses); + } + + aDialog.dispose(); + } + } + + //----------------------- + // "file->new" actions + //----------------------- + + //----------------------- + // "file->attach" actions + //----------------------- + class AttachFile extends AbstractUICmd { + AttachFile() { + super(fileTag); + setEnabled(true); + } + + //display the AttachmentsList and have the file dialog "popup" on them. + public void actionPerformed(ActionEvent e) { + mAddressBar.setSelectedIndex(1); + mAttachmentsList.showDialog(); + } + } + + //----------------------- + //"Edit" actions + //----------------------- + + /** + * Quote and paste whatever string that's on the clipboard into the editor. + * @see QuoteOriginalText + */ + class PasteAsQuotation extends AbstractUICmd { + PasteAsQuotation() { + super(pasteAsQuotationTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + Clipboard board=Toolkit.getDefaultToolkit().getSystemClipboard(); + + /* an instance of the system clipboard */ + Transferable transfer=board.getContents(this); + + // Try to get a string from the clipboard + try { + String clipboardData = (String) + (transfer.getTransferData(DataFlavor.stringFlavor)); + String quotingString = "> "; + StringBuffer quotedString = new StringBuffer(""); + boolean appendQuotaion = true; + + //Assemble the quoted string. + StringTokenizer st = new StringTokenizer(clipboardData, "\n", true); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + + if (appendQuotaion) + quotedString.append (quotingString); + + appendQuotaion = true; + quotedString.append (token); + + //if this token is a line then skip inserting a quotitona on the line + if (!token.equals ("\n")) + appendQuotaion = false; + } + + //try to insert the quoted string. + Document doc = mEditor.getDocument(); + try { + doc.insertString(0 , quotedString.toString(), null); + repaint(); + } catch (BadLocationException bl) { + } + } + catch (UnsupportedFlavorException ufe){ + } + catch(IOException ioe){ + } + } + } + + //----------------------- + //"View" actions + //----------------------- + + class ViewAddress extends AbstractUICmd { + ViewAddress() { + super(viewAddressTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + mAddressBar.setSelectedIndex(0); + } + } + + class ViewAttachments extends AbstractUICmd { + ViewAttachments() { + super(viewAttachmentsTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + mAddressBar.setSelectedIndex(1); + } + } + + class ViewOptions extends AbstractUICmd { + ViewOptions() { + super(viewOptionsTag); + setEnabled(true); + } + + public void actionPerformed(ActionEvent e) { + mAddressBar.setSelectedIndex(2); + } + } + + /** + * Create a Toolbar + * @see addToolbarButton + */ + private NSToolbar createToolbar() { + + NSToolbar toolBar = new NSToolbar(); + addToolbarButton(toolBar, new SendNow(), + "send", "Send this message"); + addToolbarButton(toolBar, new QuoteOriginalText(), + "quote", "Quote the previous document"); + addToolbarButton(toolBar, new SelectAddresses(), + "address", "Address this message"); + addToolbarButton(toolBar, new AttachFile(), + "attach", "Include an attachment"); + addToolbarButton(toolBar, null, + "spelling", "Check Spelling"); + addToolbarButton(toolBar, new SaveDraft(), + "save", "Save this message as a draft"); + addToolbarButton(toolBar, null, + "security", "Show security Information"); + addToolbarButton(toolBar, null, + "stop", "Stop the current Transfer (ESC)" ); + + return toolBar; + } + + /** + * create a toolbar button + * @param aToolBar The parent toolbar to add this button to. + * @param aActionListener Who you want to be notified when the button is + * pressed. + * @param aImageName The image name for the button. like "save.gif" + * @param aToolTip The buttons tool tip. like "Save the current file". + * @see createToolbar + */ + public void addToolbarButton(NSToolbar aToolBar, + IUICmd aActionListener, + String aImageName, + String aToolTip) { + NSButton b = new NSButton(); + + b.setHorizontalTextPosition(JButton.CENTER); + b.setVerticalTextPosition(JButton.BOTTOM); + b.setToolTipText(aToolTip); + + URL iconUrl = getClass().getResource("images/" + aImageName + ".gif"); + b.setIcon(new ImageIcon(iconUrl)); + + iconUrl = getClass().getResource("images/" + + aImageName + "-disabled.gif"); + b.setDisabledIcon(new ImageIcon(iconUrl)); + + iconUrl = getClass().getResource("images/" + + aImageName + "-pressed.gif"); + b.setPressedIcon(new ImageIcon(iconUrl)); + + iconUrl = getClass().getResource("images/" + + aImageName + "-rollover.gif"); + b.setRolloverIcon(new ImageIcon(iconUrl)); + + +// JButton b = new JButton(new ImageIcon(aImageName)); +// b.setToolTipText(aToolTip); +// b.setPad(new Insets(3,3,3,3)); + + if (aActionListener != null) { + b.addActionListener(aActionListener); + } else { + b.setEnabled(false); + } + + aToolBar.addItem(b); + } + + /** + * MessagePanel holds the subject and message body fields. + * The MessagePanel is... + * 1) The subject label "Subject:" (JLabel) + * 2) The subject text field (JTextField) + * 3) The editor (JTextArea) + * + * @see Composition + */ + class MessagePanel extends JPanel { + JTextArea mEditor; + JTextField mSubjectText; + + public MessagePanel() { + super(true); + setLayout (new BorderLayout()); + + //Subject (label and text field) + JPanel subjectPanel = new JPanel (new BorderLayout(5, 0)); + subjectPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); + + JLabel subjectLabel = new JLabel("Subject:"); + subjectPanel.add (subjectLabel, BorderLayout.WEST); + + mSubjectText = new JTextField(""); + subjectPanel.add (mSubjectText, BorderLayout.CENTER); + + //add the subject panel to this panel. + add(subjectPanel, BorderLayout.NORTH); + + //message body (text area) + mEditor = createEditor(); + mEditor.setBorder(new BevelBorder(BevelBorder.LOWERED)); + add(new JScrollPane(mEditor), BorderLayout.CENTER); + + mEditor.setFont(Font.decode("Monospaced-12")); + } + + /** + * Creates a text editor for the message panel. + * @return an editor. + * @see getEditor + */ + private JTextArea createEditor () { + return new JTextArea(0, 70); + } + + /** + * return the messsagePanel's subject text field. + * @return the text field used for the subject. + */ + public JTextField getSubject () { return mSubjectText; } + + /** + * return the messsagePanel's editor. + * @return the editor used for the message panel. + * @see createEditor + */ + public JTextArea getEditor () { return mEditor; } + } + + class PatchedMimeMessage extends MimeMessage { + PatchedMimeMessage(Session s) { + super(s); + } + public void writeTo(OutputStream o) throws MessagingException + { + try { + if (getContentType().equals("text/plain")) { + try { + o.write(new String("Subject: ").getBytes()); + o.write(mSubject.getText().getBytes()); + o.write(new String("\n").getBytes()); + String text = (String) getContent(); + if (text == null) { + text = mEditor.getText(); + } + if (text != null) { + o.write(text.getBytes()); + } else { + System.err.println("Couldn't writeTo(): null content"); + } + } catch (IOException e) { + e.printStackTrace(); + throw new MessagingException("Couldn't writeTo(): " + + e); + } + } else { + super.writeTo(o); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/mozilla/grendel/composition/CompositionPanelListener.java b/mozilla/grendel/composition/CompositionPanelListener.java new file mode 100644 index 00000000000..2f7353f3a82 --- /dev/null +++ b/mozilla/grendel/composition/CompositionPanelListener.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 23 Oct 1997. + */ + +package grendel.composition; + +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +public interface CompositionPanelListener extends EventListener { + public void sendingMail(ChangeEvent aEvent); + public void doneSendingMail(ChangeEvent aEvent); + public void sendFailed(ChangeEvent aEvent); +} diff --git a/mozilla/grendel/composition/CompositionTest.java b/mozilla/grendel/composition/CompositionTest.java new file mode 100644 index 00000000000..a9d64e757b5 --- /dev/null +++ b/mozilla/grendel/composition/CompositionTest.java @@ -0,0 +1,104 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + + Test program for the Composition Editor. + + This program's only function is to get a SMTP server name and + a mail user name then perform the following two lines... + + Composition CompFrame = new Composition(mailServerName, userName); + CompFrame.show(); + + (look near the bottom of this file) + */ + +package grendel.composition; + +import com.sun.java.swing.text.*; +import com.sun.java.swing.*; + +import java.awt.event.*; +import java.awt.*; + +class CompositionTest extends JPanel implements ActionListener { + JTextField mServerName; + JTextField mUserName; + JButton mSendMailButton; + + public static void main(String[] args) { + JFrame frame = new JFrame("Composition test"); + frame.addWindowListener(new AppCloser()); + frame.setLayout (new BorderLayout ()); + frame.add (new CompositionTest(), BorderLayout.CENTER); + //frame.setSize(450, 200); + frame.pack(); + frame.show(); + } + + /** + * Closes the appliction. + */ + protected static final class AppCloser extends WindowAdapter { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + } + + CompositionTest () { + super (true); + + setBackground (Color.lightGray); + setLayout(new FlowLayout()); + + add (new JLabel("Mail Server:")); + mServerName = new JTextField ("nsmail-2.mcom.com", 25); + add (mServerName); + + add (new JLabel("User Name:")); + mUserName = new JTextField ("", 25); + add (mUserName); + + mSendMailButton = new JButton ("Send Mail"); + add (mSendMailButton); + mSendMailButton.addActionListener (this); + } + + public void actionPerformed(ActionEvent evt) { + if (mSendMailButton == evt.getSource()) { + //check fields + String mailServerName = mServerName.getText().trim(); + + if (mailServerName.equals("")) { + System.out.println ("Please enter mail server name."); + return; + } + + String userName = mUserName.getText().trim(); + + if (userName.equals("")) { + System.out.println ("Please enter a user name."); + return; + } + + //This is where the compsiton editor is started. + Composition CompFrame = new Composition(); + CompFrame.pack(); + CompFrame.show(); + } + } +} diff --git a/mozilla/grendel/composition/Makefile b/mozilla/grendel/composition/Makefile new file mode 100644 index 00000000000..97cf7b04ab8 --- /dev/null +++ b/mozilla/grendel/composition/Makefile @@ -0,0 +1,36 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + AddressBar.java \ + AddressDialog.java \ + AddressList.java \ + AddressListBeanInfo.java \ + Addressee.java \ + AttachmentsList.java \ + AttachmentsListBeanInfo.java \ + Composition.java \ + CompositionPanel.java \ + CompositionPanelListener.java \ + CompositionTest.java \ + NSTabbedPane.java \ + OptionsPanel.java \ + TestDataSource2.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/composition/MenuLabels.properties b/mozilla/grendel/composition/MenuLabels.properties new file mode 100644 index 00000000000..5d758e408e4 --- /dev/null +++ b/mozilla/grendel/composition/MenuLabels.properties @@ -0,0 +1,85 @@ +Title=Composer + +# "file" menu labels +fileLabel=File +fileAccel=F +newLabel =New +newAccel =N +saveDraftLabel =Save Draft +saveDraftAccel =S +saveAsLabel =Save As... +saveAsAccel =A +sendNowLabel =Send Now +sendNowAccel =N +sendLaterLabel =Send Later +sendLaterAccel =L +quoteOriginalTextLabel =Quote Original Text +quoteOriginalTextAccel =Q +selectAddressesLabel =Select Addresses... +selectAddressesAccel =e +attachLabel =Attach +attachAccel =t +appExitLabel =Exit +appExitAccel =x + +saveDraftKeyboard =CTRL+S +sendNowKeyboard =CTRL+ENTER +closeKeyboard =CTRL+W +appExitKeyboard =CTRL+Q + + # "file->attach" menu labels + attachFileLabel =File... + attachFileAccel =F + webPageLabel =Web Page... + webPageAccel =W + addressBookCardLabel =Address Book Card... + addressBookCardAccel =A + +# "edit" menu labels +editLabel =Edit +editAccel =E +undoLabel =Undo +undoAccel =U +cutLabel =Cut +cutAccel =t +copyLabel =Copy +copyAccel =C +pasteLabel =Paste +pasteAccel =P +pasteAsQuotationLabel =Paste As Quotation +pasteAsQuotationAccel =Q +DeleteLabel =Delete +DeleteAccel =D +selectAllLabel =Select All +selectAllAccel =A +findInMessageLabel =Find In Message +findInMessageAccel =F +findAgainLabel =Find Again +findAgainAccel =g +searchDirectoryLabel =Search Directory +searchDirectoryAccel =D +appPrefsLabel =Preferences... +appPrefsAccel =r + +undoKeyboard =CTRL+Z +cutKeyboard =CTRL+X +copyKeyboard =CTRL+C +pasteKeyboard =CTRL+V +DeleteKeyboard =DEL +selectAllKeyboard =CTRL+A +findInMessageKeyboard =CTRL+F +findAgainKeyboard =CTRL+G + +# "view" menu definition +viewLabel =View +viewAccel =V +addressLabel =Address +addressAccel =A +attachmentsLabel =Attachments +attachmentsAccel =t +optionsLabel =Options +optionsAccel =O + + + + diff --git a/mozilla/grendel/composition/NSTabbedPane.java b/mozilla/grendel/composition/NSTabbedPane.java new file mode 100644 index 00000000000..e48f1a1996e --- /dev/null +++ b/mozilla/grendel/composition/NSTabbedPane.java @@ -0,0 +1,139 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.awt.*; +import com.sun.java.swing.JTabbedPane; +import com.sun.java.swing.basic.BasicTabbedPaneUI; +import com.sun.java.swing.*; +import com.sun.java.swing.plaf.*; + +import netscape.orion.toolbars.*; +import pk.core.*; + +public class NSTabbedPane extends BaseDelegate implements INSToolbar, IFloatingToolbar { + protected JTabbedPane fTabbedPane; + protected Object fID = null; + protected String fName = null; + protected String fFloatingTitle = ""; + + public NSTabbedPane() { + fTabbedPane = new JTabbedPane(); + fTabbedPane.setUI(new MyTabbedPaneUI()); + } + + /** + * the toolbar is about to be docked + */ + public void dock() { + } + + /** + * tells the toolbar it is about to float + */ + public void floatToolbar() { + } + + /** + * sets the title to be used when the toolbar is floating + */ + public void setFloatingTitle(String aTitle) { + fFloatingTitle = aTitle; + } + + /** + * gets the title used when the toolbar is floating + */ + public String getFloatingTitle() { + return fFloatingTitle; + } + + /** + * adds a new tab. + */ + public void addTab(String aLabel, ImageIcon aIcon, Component mAddressList) { + fTabbedPane.addTab(aLabel, aIcon, mAddressList); + } + + /** + * set tab selection + */ + public void setSelectedIndex(int aIndex) { + fTabbedPane.setSelectedIndex (aIndex); + } + + /** + * sets an ID for this toolbar + */ + public void setID(Object anID) { + fID = anID; + } + + /** + * gets the toolbar's id + */ + public Object getID() { + return fID; + } + + /** + * sets the name of the toolbar + */ + public void setName(String aName) { + fName = aName; + } + + /** + * gets the name of the toolbar + */ + public String getName() { + return fName; + } + + /** + * gets the component associated with the toolbar + */ + public Component getComponent() { + return fTabbedPane; + } + + //*************************** + public class MyTabbedPaneUI extends BasicTabbedPaneUI { + public Dimension getPreferredSize(JComponent container) { + int widest = 0; + int tallest = 0; + + //return the component with the largst preferred size. + Component[] comps = fTabbedPane.getComponents(); + for (int i = 0; i < comps.length; i++) { + Dimension dim = comps[i].getPreferredSize(); + + if (dim.width > widest) + widest = dim.width; + + if (dim.height > tallest) + tallest = dim.height; + } + + return new Dimension(widest, tallest); + } + } +} + diff --git a/mozilla/grendel/composition/OptionsPanel.java b/mozilla/grendel/composition/OptionsPanel.java new file mode 100644 index 00000000000..20cd48e0e99 --- /dev/null +++ b/mozilla/grendel/composition/OptionsPanel.java @@ -0,0 +1,109 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.io.Serializable; +import java.awt.*; +import com.sun.java.swing.*; +import com.sun.java.swing.border.*; + +public class OptionsPanel extends JPanel implements Serializable { + private final int BOX_WIDTH = 300; + private final int BOX_HEIGHT = 30; + + public OptionsPanel () { + super (); + + setLayout(new GridLayout(3, 2, 5, 5)); + + addCheck ("Encrypted", "Encrypted", 'e', false); + + addCheck ("Return Receipt", "Return Receipt", 'r', false); + + addCheck ("Signed", "Signed", 'd', false); + + LabeledCombo priority = new LabeledCombo("Priority"); + priority.addPossibleValue("Lowest"); + priority.addPossibleValue("Low"); + priority.addPossibleValue("Normal"); + priority.addPossibleValue("High"); + priority.addPossibleValue("Highest"); + add(new FixedSizedPanel (BOX_WIDTH, BOX_HEIGHT, priority)); + + addCheck ("Uuencoded instead of MIME for attachments", "", 'u', false); + + LabeledCombo format = new LabeledCombo("Format"); + format.addPossibleValue("Ask me"); + format.addPossibleValue("Plain Text only"); + format.addPossibleValue("HTML Text only"); + format.addPossibleValue("Plain Text and HTML"); + add(new FixedSizedPanel (BOX_WIDTH, BOX_HEIGHT, format)); + } + + private void addCheck (String aLabel, String aToolTip) { + addCheck (aLabel, aToolTip, ' ', false); + } + + private void addCheck (String aLabel, String aToolTip, boolean isChecked) { + addCheck (aLabel, aToolTip, ' ', isChecked); + } + + private void addCheck (String aLabel, String aToolTip, char aAccelerator, boolean isChecked) { + JCheckBox checkButton = new JCheckBox(aLabel, isChecked); + + if (!aToolTip.equals ("")) checkButton.setToolTipText(aToolTip); + if (' ' != aAccelerator) checkButton.setKeyAccelerator(aAccelerator); + + //create a fixed sized panel. + add(new FixedSizedPanel (BOX_WIDTH, BOX_HEIGHT, checkButton)); + } + + class FixedSizedPanel extends JPanel { + FixedSizedPanel (int aWidth, int aHeight, Component aComp) { + setPreferredSize (new Dimension (aWidth, aHeight)); + setLayout (new BorderLayout ()); + setBorder (BorderFactory.createEmptyBorder (0, 5, 0, 0)); + add ("West", aComp); + } + } + + class LabeledCombo extends JPanel { + JComboBox mComboBox; + JLabel mLabel; + + public LabeledCombo (String aLabel) { + + mComboBox = new JComboBox (); + mLabel = new JLabel (aLabel); + + mComboBox.setPreferredSize (new Dimension (150, 20)); + + setLayout (new BorderLayout(5, 5)); +// setInsets (new Insets (5, 5, 5, 5)); + + add ("West", mLabel); + add ("Center", mComboBox); + } + + public void addPossibleValue(String aValue) { + mComboBox.insertItemAt (aValue, mComboBox.getItemCount()); + } + } +} diff --git a/mozilla/grendel/composition/README b/mozilla/grendel/composition/README new file mode 100644 index 00000000000..dcfafb1f17f --- /dev/null +++ b/mozilla/grendel/composition/README @@ -0,0 +1,5 @@ +This is Grendel -- a Java mail/news client. + +The grendel.composition package is the UI to composing messages. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/composition/TestDataSource2.java b/mozilla/grendel/composition/TestDataSource2.java new file mode 100644 index 00000000000..c80eddd1f46 --- /dev/null +++ b/mozilla/grendel/composition/TestDataSource2.java @@ -0,0 +1,111 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.composition; + +import java.util.Vector; +import com.sun.java.swing.event.*; +import netscape.orion.misc.*; + +public class TestDataSource2 extends ATC_SourceAdapter { + String[] stringTable = { + "Bob Schenefelt", + "Tom Jones", + "Tim Smith", + "John Simon", + "Jim", + "Jimmy", + "Les", + "Lester", + "Lesters@netscape.com", + "Lester Schueler" + }; + + private Vector mSearchResults; //holds search results. (see search) + private int mCurrentIndex = 0; //current index if search results. + + public TestDataSource2 () { + } + + /** + * get the next match from the results vector. + */ + public String getNext () { + if (mSearchResults.size() > 0) { + mCurrentIndex = (mCurrentIndex + 1) % mSearchResults.size(); + return (String) mSearchResults.elementAt(mCurrentIndex); + } + + return null; + } + + /** + * get the previous match from the results vector. + */ + public String getPrevious () { + if (mSearchResults.size() > 0) { + mCurrentIndex = mCurrentIndex + mSearchResults.size() - 1; + mCurrentIndex %= mSearchResults.size(); + return (String) mSearchResults.elementAt(mCurrentIndex); + } + + return null; + } + + /** + * Gives notification the document has changed so perform a new search. + */ + public void documentChanged(ATC_Document doc) { + //get the text from the document to search on. + String textToSearchFor = getDocumentText(doc); + + //find all matches for the string. + findAllMatches (textToSearchFor); + + //Inform the document of how many strings were found. + doc.setQueryResults(mSearchResults.size()); + } + + /** + * Find all partial matches for a search string. + * Store the result in the vector mSearchResults. + */ + private void findAllMatches (String subString) { + int subStringLength = subString.length(); + + //store search results in s vector for later + mSearchResults = new Vector (); + mCurrentIndex = 0; + + //don't match emtpy substrings. + if ((null == subString) || (subString.equals (""))) + return; + + for (int i = 0; i < stringTable.length; i++) { + if ((stringTable[i].length() >= subStringLength) && + (stringTable[i].substring (0, subStringLength).equalsIgnoreCase (subString))) { + + //matching substring + mSearchResults.addElement (stringTable[i].substring (subStringLength)); + } + } + + return; + } +} diff --git a/mozilla/grendel/composition/contenthandlers b/mozilla/grendel/composition/contenthandlers new file mode 100644 index 00000000000..0a7137a96ee --- /dev/null +++ b/mozilla/grendel/composition/contenthandlers @@ -0,0 +1,2 @@ +text/plain: com.sun.mail.handlers.text_plain +multipart/mixed: com.sun.mail.handlers.multipart_mixed \ No newline at end of file diff --git a/mozilla/grendel/composition/images/AttachmentsListIcon16.gif b/mozilla/grendel/composition/images/AttachmentsListIcon16.gif new file mode 100644 index 0000000000000000000000000000000000000000..f510f3480316a0f8861744d80c300b56982e1929 GIT binary patch literal 101 zcmZ?wbh9u|6krfw*vtR|4Pe{=rW$~}hK2(N4m310{0EBvXZR1MfHaT=1Uf)LpiTh> k1{SFgCp}lESY+q!{(nU+Eg{qHamBh=txW|wn*|xH0b^ApHvj+t literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/AttachmentsListIcon32.gif b/mozilla/grendel/composition/images/AttachmentsListIcon32.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f77b638aa84dcaedade88ed7c397d7a38216cdf GIT binary patch literal 87 zcmZ?wbh9u|RA5kGn8?8J|NsBUnAcMBj~N&kbbuHrs=&a&WYE&T^7LE&$6SkUb${mF r^qaqJkxS;YlfAD_ZHuowHrxC9H?Gq~Rp0;d^Ki8vS#&X+fx#L8Xm=y| literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/README b/mozilla/grendel/composition/images/README new file mode 100644 index 00000000000..fd9524084fe --- /dev/null +++ b/mozilla/grendel/composition/images/README @@ -0,0 +1,16 @@ +# +# The contents of this directory are subject to the Mozilla Public +# License Version 1.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License +# at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. diff --git a/mozilla/grendel/composition/images/address-disabled.gif b/mozilla/grendel/composition/images/address-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..285a1760a38a1d077a7fc4653294df4ab0073eb9 GIT binary patch literal 206 zcmZ?wbhEHb6lV}+c+9}iz`$T^oc2G>I4$iAgSpLt0|yuy8vZl%|K989?i#N~_NJy>Wmfi%j+W-Qy11T+#V*xTYx+cMi`&ZN<_QThSOWm6Ku#(E literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/address-pressed.gif b/mozilla/grendel/composition/images/address-pressed.gif new file mode 100644 index 0000000000000000000000000000000000000000..ee7b4be0830f2e5ec617ea8a86ac6fb86f969b42 GIT binary patch literal 228 zcmZ?wbhEHb6lV}+c+9}iz`$T^oc2G>I4$iAgSpLt0|yuy8vZl5^$&QF{#bgsCvhY2BpL z);U8ggmasMM0@kig-6a<&hBtbG?<~_vC&CMq1Ka~XYp1}?iqo^>=g2x7C2*iNM{>6Gv|VZvt~}2%DH$2 VbFc8kwd+>RT{CC%98p0AYXGknRXzX! literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/address-rollover.gif b/mozilla/grendel/composition/images/address-rollover.gif new file mode 100644 index 0000000000000000000000000000000000000000..0d567da3ed4c5cc57ebb0cc971ed49f77e162d3a GIT binary patch literal 319 zcmZ?wbhEHb6lV}+Xkh?>1_lOW<1~i<4FA)N)6&i`nA;pUZ~(~r50q+Xm^l+f{|5n( z(gvVvpi-iN;!hSv1_llW9gq%?oeZpd6qNc>GUsKin&z^5lJA-=ElR6>C*A0MY;D;7 zNaV6AQ~R;x1Pcujr^}NCtRtrLA6{=D^EK&#K}2AIh=TIG3PDX4#k3Aa-A#;#Cvq_J z*h`CL@-kI2*|PGa+6oj2a5E`~I7|}sWEXVku4m|13SpYhS1FiN#n;U}dk)9M_~uHU z4ZOXxXDhKXgzaSAy@!>3JHtw6Zg%ct$4?wTFrUf)NcPEN2WIX~z4z{4 SQ91MY;L~Re`Y%-!8LRI4$iAgSpLt0|yuy8vZllax4z@`{NsUKYq%&3ki~u>Q`0!AFwc30{|Z`FIE5m literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/attach-pressed.gif b/mozilla/grendel/composition/images/attach-pressed.gif new file mode 100644 index 0000000000000000000000000000000000000000..1696658a4215a677c0a39afa41d78ddce1229cbf GIT binary patch literal 129 zcmZ?wbhEHb6lV}+IK;qUY@C*scIH3B|AvMJB_$;W7*PDl!pOkD#GnHb0I6kQcGK8( z=bynTjt*AgusKH=9vt#LU_K`~Y+91~n&$SUQ#AJ*X4dc6b>5Qm&9O@xD_lJl!kpMz aH#%snOb#gB`T7a-rCs|Td@yHZum%8eQz3{~6NK&Kx*!;6KCvhK7cjGyj7D149FYl9CbwP#Ojl zf3h$#Ft9S{fCNBhFtB8AIN`Z^?+h)&W2YW`YZN)maM{C%)!0MBa^pLZ(>{kJf&%U+ zuq9phW{A^vX}E5r)NGaZuA|#)*?FGF0ojTxRy!Yj&Z^Gc?C@YVuaX+eW*rBG11Dsg c?Tif=8M3mfd=#o=8JU}nx_f&26a*Qp0WQ!BX=e@`IPjn0e?vpV%$fgzK=CIFBLf2qgAPaqNG$_% zn8&U=|0c9rE?&AVQPfDiQK9UfxSj)N)1m2AcRA%8W*;qcxGjGAt-<{@VpCprNH!*# tK8a;xImi;hYjtoZmAsit}F^18hK$plU5TquK zrHKrV65wDIk5OR<9eNEco`e`?1`GEE>P1PMFEESZ%modO1<4j;a_s&9`=T$tecI2{ zr{AtvxqJa>N`Vv@0U${d%d%|Sh7clzFvf%srIgk>48wtefsv6BjF4j#mK8-Iqy#hS zP+}wKcrM`#GY@$bapr23jpLytiQ?EC#H94&m}@P=(1B2dA@hA6h9aAF5P}B7In~-# z$~Cj^8!DufXzdV!ILAua#*h#egf^uXLI6S@_pog2MOhkU-5_+shy)=CBN_LIY%F_n zJj!w-2tw)yUKqLBcl`iMg`|>ER`=wXscWAFq2~v#RMZHRCQ@N8h|-X8KkngvK+TfU zUKAt7X%w?4#*Di`M09|Z?`LCeK-!ma4oi)Mf?V2|SO^1%2;)R*EEMr0tV75IlggEv z2t$#G2^(P~eb)$704W0w6hTDI7ABC#9Z!VWSQ!sOxm-FXO0FQ2+T4^0ssh9e%OtNH zPh!fDCm`iEBHXYDb!7lONti^Yh$*B3yPjj-)@2SNHpEdj#*|}ELgG1N0_hNDBiC_- zL~D29al zQ9l#D$2DcjMB*~vGT}kR&4sM%V%t z_@9v;tB?H!U?rfuYIdfrH&Zgss!6x)+TylPD7T%=gEd14fWBr+S2kj zj}JGM_?ynHzkcrSjlGj+PV9|r0 ze<=R`&*$q`RlmIB?1rMg8R?@f^VU~&9f3`tyR4=(Q~TwDW2d4M{b!$(i#L6KzA0$D zUN#rh?Py(5(R*p*ntPppt(t<4&YZuxWn#vsoAc#v>yg`0kRQpT1Y8E7$IC zvjD#|(;fFhNqa+a#dJ_yvAOI*-?WD_$FEE{9r7|TSk-p@{av|KW6=|t;iYA}O14Zc ze*IK^CTz>4`aV2Zy>s@GV*9}D)2}{q?e>|smwdIo^LWji3^+NXrWbVV`{T|hAMIP1 I$$&-w0rDg6k^lez literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/quote-disabled.gif b/mozilla/grendel/composition/images/quote-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..1eb03b6733f5053d06afca60efbac4ffaf6d5828 GIT binary patch literal 168 zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNRH#9WNocSLJl$4Yj8W^ww#h)yU3=GT+ zIv`n)84N6;1uc%N_f{~S`MoUodFU*Qp!u51+7wpYxOPrIW~*N8>XK>><4`{b2BF># z*<&5aT$7X@Ur5(+nd=($;+At5N3+PX?j1SSml-P#ZQ!BX=e@`IPjn0e?vpV%$fgzKuJjnEQwAi{$ycfU|?a; z0ZD+&U|=bjaKdx-T-S!vs|_60g%|?_Zkh!x{Id1+m1)lf!rvD1eA8v!bujO_@H;N+ z#QY`Md;%vAu1ua{#?2x3<`Pd@C3EA7E}OdT(_RV6h;_HUF?MU1rf}0{`{@O{rW|CH bR8*~%VRd3;lCLdQYO%M|nP?#<$Y2csg+oDO literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/quote-rollover.gif b/mozilla/grendel/composition/images/quote-rollover.gif new file mode 100644 index 0000000000000000000000000000000000000000..f93ed9542050199e910f153272030285abfd0cf5 GIT binary patch literal 188 zcmZ?wbhEHb6lV}+c+3C-#>Q!BX=e@`IPjn0e?vpV%$fhefI&$~si6U^0*O%k$->CM zz`~#d5(AmRz>>Lv!(r8432xy{-x&i0guXUjyEa!tQ0(}Dy4Q)Pjm2)%%!zK6?rER> zHL3I73y~gP{?;ds2d(&)m}v!{=uYE}R^?xKp{?3;lk28aEP@3)W^OR7y;ro2JK??- WOQ~3{y`oyPy=Z$!XLq+CgEas({6hZ# literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/quote.gif b/mozilla/grendel/composition/images/quote.gif new file mode 100644 index 0000000000000000000000000000000000000000..bbc5900a6e53ce117a8b2ee026d46f34b2864447 GIT binary patch literal 180 zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNRH#9WNocSLJl$4Yj8W^ww#h)yU3=GT+ zIv`n)84N6m6IcvZ?UiVsSm#4_ zM9ve!mpD9_7g_XNT6TH?pBJ~trjxQt0?F?d-4I-!d~GTF%1~)XsfLi7g?pOz-~DN) M)2QCuD9B(903!Z7r2qf` literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/save-disabled.gif b/mozilla/grendel/composition/images/save-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..afee775f202610b0c5991a88da291605a382e882 GIT binary patch literal 195 zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNRH#9WNocSLJl$4Yj8W^ww#h)yU3=GT+ zIv`n)84N511t&aL@4dvc_Vlr-Vm!*<8z-+!RbQdV{O1^RW1UuAcS?Srh@;2!#T7O> zHwqO*nAE-fixpCv7F}7B&cYnB;l|AC*;i(5Z!Y{*eU6Ji+erFP<6+$|OTX7SwzJo0 bm?^i|n#GxEw91)vnu{p+_D>cTWUvMRHTpwb literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/save-pressed.gif b/mozilla/grendel/composition/images/save-pressed.gif new file mode 100644 index 0000000000000000000000000000000000000000..555b6e8e488092494dae9ecfa7f4bd2dcaa9aef4 GIT binary patch literal 221 zcmZ?wbhEHb6lV}+c+3C-#>Q!BX=e@`IPjn0e?vpV%$fhefI&$~si6U^0*O%k$->CM zz`~#d5(AmRz|uA0gy-tL7LN;en4ehSJ2z0oxvE6@VX5r)uBhcA-xD?dtlfJcL@_{` z!|#OSLgob=8A1v=Q zUBO!2oK>Ie##z%P+Lpu4+1V*p*1$L+OO%aic3H&)-H6$XI#_ju7S3i^!TQRKaFCUHT4!*XU9F^k5kh0ML*4O}#|?th68Fkt5K|2lmQ2gAFD9d{h` z9(;_gPGI7!udFI!=WJ~iV`FM!XXtM4W18MSlPRTJN62#e!Z~#eOxB7EX0X*+%oAC# LX5A)XK?Z98wLMX; literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/save.gif b/mozilla/grendel/composition/images/save.gif new file mode 100644 index 0000000000000000000000000000000000000000..1a788aff6697d0021e877f1491483f16d185bb0d GIT binary patch literal 218 zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNRH#9WNocSLN7?hNh8X6cdf#OdVMg|6E z1|5(T$P5OS_JR|htM?v$k$cK8VkxWV0?ufQLg`tnq9V&Y=L#6CX5Z;Ki7j|SkU&6* zbyDKYPSt6yjUfkSJFE}ZD4o3`X8$dQg=`uc2d_WiJ-{~SVynW!yW0*;4`I3c*G*cW zs!EKNv8AoGm5rgHrkSz7y~VLXONga^24iJ0Lz33?8Q#S#7E^^6EMB%sSdhUQ0IAwc A`v3p{ literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/security-disabled.gif b/mozilla/grendel/composition/images/security-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..e020e2612d683a671bed0290cb935d6f0a90f15b GIT binary patch literal 179 zcmZ?wbhEHb6lV}+c+3C-4Gawb(~Q&7&irTi-_X!7bLM}b*#H0kXELNU0Oc5fTm(@3 z$->CMz`~#d5(1gQz>+ZGgy-tL7O$;5-b=eHv^;7JPG6-S6}Id)>tu%uc{YsU>2o%R z9s3?7Sh3(tlG7xK${9;CtE_T_Sk{y#M7$}6#*!cErlDhK+m zFgJ!U^*(suwX~|ifvH1Kg)M@G>9-A!Hq%jsXEl2Jr<{KSnl@c|rXYhg03v}#%>V!Z literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/security-rollover.gif b/mozilla/grendel/composition/images/security-rollover.gif new file mode 100644 index 0000000000000000000000000000000000000000..477e60dca95ee45767355efaeb5ce5dc601e9164 GIT binary patch literal 207 zcmZ?wbhEHb6lV}+c+3C-4Gawb(~Q&7&Kx*!;6KCvhK7cjGyemH|Ns9#lOe4ED2)UZ zf3h$#Ft9S{fW$y%FtF5YIN`Z^uf=PtGe3?W6Xe{eGhc)EP?AEk^$J%WAG5PclM7jP zvHBM=+_7jroGvNA(;zt^(aM*JsrlhjXDz{f-0F{Zn5sm*FvyZI)ff4&L&I+R$Et`EXP*X-I~=fF`A39m&?s$-h2r`25SK0B}Z2P literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/security.gif b/mozilla/grendel/composition/images/security.gif new file mode 100644 index 0000000000000000000000000000000000000000..605ea33541c383b0ff3c267b5d0426b3386872c8 GIT binary patch literal 204 zcmZ?wbhEHb6lV}+c+3C-4Gawb(~Q&7&Kx*!;6KCvhK7d!X=i56{124*|Ns9?hO`Et z3I-q-1QdU=FfuT(GU$LfATt0utB!^TyG4Aua%Ku|6K literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/send-disabled.gif b/mozilla/grendel/composition/images/send-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..a33a50edd36e0e3497898ca0db846f25258fe268 GIT binary patch literal 191 zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1Pl!f3=BX46rlK% zg^_`Qg+T`-3NnL%C1=73&veCAS-KwXM>h6~XCyr?;dnGtV1dV>FO{dnvaZ``)S58+ zG6~Joo9@`3kn+jskYCz%;aICl-r1a73oLf28Z8yA4-T4@(R(h{C2pdDsptN=hbmcL czbv<^DzUGvY!YG+ZEUJ(>+S285@fIj07(Qxp8x;= literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/send-pressed.gif b/mozilla/grendel/composition/images/send-pressed.gif new file mode 100644 index 0000000000000000000000000000000000000000..58a6cc55ecab51e23f579e393950cd871d01e6ad GIT binary patch literal 212 zcmZ?wbhEHb6lV}+c+9}S@c(~W+L;3f4*UnQ7#JEF8fMM}(f@(G29PumBLl^sEQ|~c z%nUjpL68{?ER6*xJXi0n5D+SQ<|H8?$J*9;Bb6<&C1VoT7cp+!)_*qu$Y_k*2|o`A!PbKs`1SpqcZE* uNNq93;y?>shDKH9S|%2AkXfQ4r@l&K{s~#{HO+f+AMT#< z@QY>|3)78EgBY{+qt{alvcl%>x!{zbpiu0{vPI2^Lx*|kx(cobyQb!E2$SgXC@o#z zF!_KwgHVyWesmp6Y^+Cyr4pldM`wp_~e1@T*lqTOMTWa*Jf_qyKdf-?a+g+_}?JNU=0A|>NoZP literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/spelling-disabled.gif b/mozilla/grendel/composition/images/spelling-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..23577352af0e259814f5030b6bf13f30d6a78517 GIT binary patch literal 200 zcmZ?wbhEHb6lV}+c+9|HY@C*scIH3B|AvN!nKS=0Ff{!C|6fT-X(mG&12&-elZBCi zfr&u}BndKufu%Iygy-tLj!)KBvxzH8h_gmXuI$s{oHtd2+gRgJh}v0m-G=rF>&^Zo zs&UNs)#QkBIr(H#hm%xlLwN9w!|!^o>MK^8S|x9}{Q3%a_Q}Y%GmDy=XXYu-tdQXN iBW!D6lVD-x+Tx(lAS5r>kvehC0Oeb7Gl literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/spelling-rollover.gif b/mozilla/grendel/composition/images/spelling-rollover.gif new file mode 100644 index 0000000000000000000000000000000000000000..7b80357fa54cf72229582cf6cc55e49526011fb9 GIT binary patch literal 245 zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNR|7T!mXlR%@6GZ<90R|-{rI`$A3`jun zCkrD312cmTNDO2K1Iwa<6P~NLD!x9;XvWl}@+hLKA)Ixg=RQ?|6(R~M33?LUOsBZI zc{gfHad5R9WDvPmay(^Sq@)ms^^&6FJS?lXAAO`{8kDUVm!EARvn$?Y;z_$R=Y^&7 zZr0tNULoY1Yh9M-Rp7_b#?+pd-^lFT-#b;QGoib?w?4*!bwQS!p54?H8l8}~7TGW^WNY@m@(fEBN@yPx;Yj z-%o!oY{HaM)2xzK-BuB6$I{nm+UnTjZaBfwEH943ouyGyT`G1q!#q)6MuwH@EUP)# M#5Zmd5@fIj0AUtOVgLXD literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/stop-disabled.gif b/mozilla/grendel/composition/images/stop-disabled.gif new file mode 100644 index 0000000000000000000000000000000000000000..cb239f28d0d36e5ff44cdb9168154b1d5409c80b GIT binary patch literal 216 zcmZ?wbhEHb6lV}+c+3C-4Gav%#%XD3XZ|z%{}1FgG|Zg&Ur9*`EQwAi{$ycfU|?a; z0ZD+&U|?yTaKh4K@3a{wf7-EVq}^3Xc~DuzaB*6U!-?EkYhzW#H+z3omFX{MdYLe( ztHs@zapL4o;;1X$KgTF}&}vUZwj@WDc@*;?Pz+m>Fx{O#uVdmg)%u1~AIwI|*$ x!%nxXx|3 zvaIF4zktj!riRA~k_I}A`e*r#*H+DDk8kjHs1R;&DKlSpX4(3d7~iB@cWsWm6+5@6 zr^41J!&u4HM?9E8xj8~t(^9Ofhl#VbY@)DEL1LMlpHHBb4SRK_Q+|VRVpnmCij>hz lA&*T{4eL5OSy-BT?vgYH%vd@{zS(BrM2a4FKCXPz?Y8 literal 0 HcmV?d00001 diff --git a/mozilla/grendel/composition/images/stop-rollover.gif b/mozilla/grendel/composition/images/stop-rollover.gif new file mode 100644 index 0000000000000000000000000000000000000000..fe9821bd91003b08f384bf2743fc6483f72adc16 GIT binary patch literal 269 zcmZ?wbhEHb6lV}+c+3C-4Gav%#%cf4jMLK295`^`Kg0k3K*5HF|7mAt&ioGs3`$B$ zV6_a2KUo+V7}yweKwOX+3@p1YczCYb+O*)LdSI(k-zg=IyDdUTgE$%*TDS|3%?;Rn zP>_Ye#Z%$XQx0zC`O6g@r&@(#IqOok^MZz!ri tTc~w26K9y!yl7VI@|~F*E9Ir6wTp@k + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mozilla/grendel/dnd/DnDConstants.java b/mozilla/grendel/dnd/DnDConstants.java new file mode 100644 index 00000000000..f0a9d5758c4 --- /dev/null +++ b/mozilla/grendel/dnd/DnDConstants.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public class DnDConstants { + public static int ACTION_NONE= 0x0; + public static int ACTION_COPY= 0x1; + public static int ACTION_MOVE= 0x2; + public static int ACTION_COPY_OR_MOVE= ACTION_COPY | + ACTION_MOVE; + public static int ACTION_REFERENCE = 0x40000000; +} diff --git a/mozilla/grendel/dnd/DragSource.java b/mozilla/grendel/dnd/DragSource.java new file mode 100644 index 00000000000..b608f82db97 --- /dev/null +++ b/mozilla/grendel/dnd/DragSource.java @@ -0,0 +1,102 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.*; +import java.util.*; + +public class DragSource { + static DragSource fInstance = new DragSource(); + + Cursor fDefaultDragCursor = + Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + Cursor fDefaultDropCursor = + Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); + Cursor fDefaultNoDropCursor = + Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + + public static DragSource getDragSource(Component c) { + return fInstance; + } + + public DragSourceContext startDrag(Component c, + AWTEvent trigger, + int actions, + Image dragImage, + Point dragImageOffset, + Transferable transferable, + DragSourceListener dsl) + throws InvalidDnDOperationException { + DragSourceContext res = null; + if (trigger instanceof MouseEvent) { + res = new DragSourceContext(this, c, trigger, transferable); + res.setSourceActions(actions); + try { + res.addDragSourceListener(dsl); + } catch (TooManyListenersException e) {} + } else { + throw new InvalidDnDOperationException(); + } + + return res; + } + + public void setDefaultDragCursor(Cursor c) { + fDefaultDropCursor = c; + } + + public Cursor getDefaultDragCursor() { + return fDefaultDropCursor; + } + + public void setDefaultDropCursor(Cursor c) { + fDefaultDragCursor = c; + } + + public Cursor getDefaultDropCursor() { + return fDefaultDragCursor; + } + + public void setDefaultNoDropCursor(Cursor c) { + fDefaultNoDropCursor = c; + } + + public Cursor getDefaultNoDropCursor() { + return fDefaultNoDropCursor; + } + + public boolean isDragTrigger(AWTEvent trigger) { + if (trigger instanceof MouseEvent) { + MouseEvent e = (MouseEvent) trigger; + return (e.getID() == MouseEvent.MOUSE_DRAGGED); + } + return false; + } + + public Rectangle getDragThresholdBBox(Component c, + Point hotspot) { + return new Rectangle(hotspot.x - 4, hotspot.y - 4, + 8, 8); + } +} diff --git a/mozilla/grendel/dnd/DragSourceContext.java b/mozilla/grendel/dnd/DragSourceContext.java new file mode 100644 index 00000000000..c83cf0fe02f --- /dev/null +++ b/mozilla/grendel/dnd/DragSourceContext.java @@ -0,0 +1,192 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.*; +import java.util.*; +import com.sun.java.swing.*; + +public class DragSourceContext { + static final int ACTION_MASK = + DnDConstants.ACTION_MOVE| + DnDConstants.ACTION_COPY| + DnDConstants.ACTION_REFERENCE; + + DragSource fSource; + Component fComponent; + AWTEvent fTrigger; + Transferable fTransferable; + int fActions; + int fModifiers; + Cursor fCursor; + Cursor fOldCursor; + DragSourceListener fListener; + + public DragSourceContext(DragSource aSource, Component aComponent, + AWTEvent aTrigger, Transferable aTransferable) { + fSource = aSource; + fComponent = aComponent; + fTrigger = aTrigger; + fTransferable = aTransferable; + fOldCursor = fComponent.getCursor(); + } + + public DragSource getDragSource() { + return fSource; + } + + public Component getComponent() { + return fComponent; + } + + public AWTEvent getTrigger() { + return fTrigger; + } + + public Transferable getTransferable() { + return fTransferable; + } + + public void cancelDrag() throws InvalidDnDOperationException { + notifyDropEnd(true, false); + fComponent.setCursor(fOldCursor); + } + + public void commitDrop() throws InvalidDnDOperationException { + notifyDropEnd(false, true); + fComponent.setCursor(fOldCursor); + } + + public int getSourceActions() { + return fActions; + } + + public void setSourceActions(int actions) + throws InvalidDnDOperationException { + if ((actions & (~ACTION_MASK)) != 0) { + throw new InvalidDnDOperationException(); + } + fActions = actions; + } + + public void setCursor(Cursor cursor) + throws InvalidDnDOperationException { + fCursor = cursor; + fComponent.setCursor(fCursor); + } + + public Cursor getCursor() { + return fCursor; + } + + public void addDragSourceListener(DragSourceListener dsl) + throws TooManyListenersException { + if (fListener != null) { + throw new TooManyListenersException(); + } + fListener = dsl; + } + + public void removeDragSourceListener(DragSourceListener dsl) { + if (fListener == dsl) { + fListener = null; + } + } + + void notifyDragEnter() { + DragSourceDragEvent event = new DragSourceDragEvent(this, 0, 0); + if (fListener != null) { + fListener.dragEnter(event); + } + } + + void notifyDragOver() { + DragSourceDragEvent event = new DragSourceDragEvent(this, 0, 0); + if (fListener != null) { + fListener.dragOver(event); + } + } + + void notifyDragGestureChanged() { + DragSourceDragEvent event = new DragSourceDragEvent(this, 0, 0); + if (fListener != null) { + fListener.dragGestureChanged(event); + } + } + + void notifyDragExit() { + DragSourceDragEvent event = new DragSourceDragEvent(this, 0, 0); + if (fListener != null) { + fListener.dragExit(event); + } + } + + void notifyDrop() { + DragSourceDragEvent event = new DragSourceDragEvent(this, 0, 0); + if (fListener != null) { + fListener.drop(event); + } + } + + void notifyDropEnd(boolean aCancelled, boolean aSuccessful) { + DragSourceDropEvent event = + new DragSourceDropEvent(this, aCancelled, aSuccessful); + if (fListener != null) { + fListener.dragDropEnd(event); + } + } + + class DragMouseListener extends MouseAdapter implements MouseMotionListener { + public void mouseReleased(MouseEvent aEvent) { + + } + + public void mouseDragged(MouseEvent aEvent) { + Container top = findTopContainer(fComponent); + Point pt = SwingUtilities.convertPoint(fComponent, aEvent.getPoint(), + top); + Component mouseOver = + findTopContainer(fComponent).getComponentAt(pt); + + if (mouseOver != null && mouseOver instanceof DropTargetComponent) { + DropTarget target = ((DropTargetComponent) mouseOver).getDropTarget(); + if (target != null) { + + } + } + + } + + public void mouseMoved(MouseEvent aEvent) { + + } + + Container findTopContainer(Component c) { + while (c.getParent() != null) { + c = c.getParent(); + } + return (Container) c; + } + } +} diff --git a/mozilla/grendel/dnd/DragSourceDragEvent.java b/mozilla/grendel/dnd/DragSourceDragEvent.java new file mode 100644 index 00000000000..cdc1c06a6c8 --- /dev/null +++ b/mozilla/grendel/dnd/DragSourceDragEvent.java @@ -0,0 +1,47 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public class DragSourceDragEvent extends java.util.EventObject { + DragSourceContext fContext; + int fActions; + int fModifiers; + + public DragSourceDragEvent(DragSourceContext context, + int actions, int modifiers) { + super(context); + fActions = actions; + fModifiers = modifiers; + } + + public DragSourceContext getDragSourceContext() { + return (DragSourceContext) getSource(); + } + + public int getTargetActions() { + return 0; + } + + public int getGestureModifiers() { + return 0; + } +} diff --git a/mozilla/grendel/dnd/DragSourceDropEvent.java b/mozilla/grendel/dnd/DragSourceDropEvent.java new file mode 100644 index 00000000000..3ef79ce46de --- /dev/null +++ b/mozilla/grendel/dnd/DragSourceDropEvent.java @@ -0,0 +1,43 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public class DragSourceDropEvent + extends java.util.EventObject { + boolean fCancelled; + boolean fSucessful; + + public DragSourceDropEvent(Object aSource, boolean cancelled, + boolean sucessful) { + super(aSource); + fSucessful = sucessful; + fCancelled = cancelled; + } + + public boolean isDragCancelled() { + return fCancelled; + } + + public boolean isDropSuccessful() { + return fSucessful; + } +} diff --git a/mozilla/grendel/dnd/DragSourceListener.java b/mozilla/grendel/dnd/DragSourceListener.java new file mode 100644 index 00000000000..030dbd90a83 --- /dev/null +++ b/mozilla/grendel/dnd/DragSourceListener.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public interface DragSourceListener + extends java.util.EventListener { + void dragEnter (DragSourceDragEvent dsde); + void dragOver (DragSourceDragEvent dsde); + void dragGestureChanged(DragSourceDragEvent dsde); + void dragExit (DragSourceDragEvent dsde); + void drop (DragSourceDragEvent dsde); + void dragDropEnd (DragSourceDropEvent dsde); +} diff --git a/mozilla/grendel/dnd/DropTarget.java b/mozilla/grendel/dnd/DropTarget.java new file mode 100644 index 00000000000..81056e511ba --- /dev/null +++ b/mozilla/grendel/dnd/DropTarget.java @@ -0,0 +1,88 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.util.*; + +public class DropTarget { + Component fComponent = null; + Insets fScrollInsets = null; + int fTargetActions = DnDConstants.ACTION_COPY_OR_MOVE; + boolean fActive = true; + + public DropTarget() { + + } + + public DropTarget(Component c) { + fComponent = c; + } + + public Component getComponent() { + return fComponent; + } + + public void setComponent(Component c) + throws IllegalArgumentException { + fComponent = c; + } + + public DropTargetContext getDropTargetContext() { + return null; + } + + public boolean supportsAutoScrolling() { + return fScrollInsets != null; + } + + public void setAutoScrollInsets(Insets scrollInsets) { + fScrollInsets = scrollInsets; + } + + public Insets getAutoScrollInsets() { + return fScrollInsets; + } + + public void setDefaultTargetActions(int actions) { + fTargetActions = actions; + } + + public int getDefaultTargetActions() { + return fTargetActions; + } + + public void addDropTargetListener(DropTargetListener dte) + throws TooManyListenersException { + } + + public void removeDropTargetListener(DropTargetListener dte) { + } + + public void setActive(boolean active) { + fActive = active; + } + + public boolean isActive() { + return fActive; + } +} diff --git a/mozilla/grendel/dnd/DropTargetComponent.java b/mozilla/grendel/dnd/DropTargetComponent.java new file mode 100644 index 00000000000..4f9ef0b1fde --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetComponent.java @@ -0,0 +1,28 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 11 Nov 1997. + */ + +package grendel.dnd; + +public interface DropTargetComponent { + public void setDropTarget(DropTarget dt) + throws IllegalArgumentException; + public DropTarget getDropTarget(); +} diff --git a/mozilla/grendel/dnd/DropTargetContext.java b/mozilla/grendel/dnd/DropTargetContext.java new file mode 100644 index 00000000000..87b2001bd25 --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetContext.java @@ -0,0 +1,63 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.awt.datatransfer.*; + +public class DropTargetContext { + public DropTarget getDropTarget() { + return null; + } + + public Component getComponent() { + return null; + } + + public void setTargetActions(int actions) + throws InvalidDnDOperationException { + } + + public int getTargetActions() { + return 0; + } + + public DataFlavor[] getDataFlavors() { + return null; + } + + public void getTransferable() throws InvalidDnDOperationException { + } + + public void dropComplete(boolean success) + throws InvalidDnDOperationException { + + } + + protected void acceptDrop(int action) { + + } + + protected void rejectDrop() { + + } +} diff --git a/mozilla/grendel/dnd/DropTargetDragEvent.java b/mozilla/grendel/dnd/DropTargetDragEvent.java new file mode 100644 index 00000000000..d43d53935a1 --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetDragEvent.java @@ -0,0 +1,41 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.awt.datatransfer.*; + +public abstract class DropTargetDragEvent + extends DropTargetEvent { + DataFlavor fFlavors[]; + public DropTargetDragEvent(Object aSource, DropTargetContext context, + Point cursor, int actions, + DataFlavor flavors[]) { + super(aSource, context, cursor, actions); + fFlavors = flavors; + } + + public DataFlavor[] getDataFlavors() { + return fFlavors; + } +} + diff --git a/mozilla/grendel/dnd/DropTargetDropEvent.java b/mozilla/grendel/dnd/DropTargetDropEvent.java new file mode 100644 index 00000000000..c51ea35b881 --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetDropEvent.java @@ -0,0 +1,49 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; +import java.awt.datatransfer.*; + +public class DropTargetDropEvent + extends DropTargetEvent { + Transferable fTransferable; + + public DropTargetDropEvent(Object aSource, DropTargetContext context, + Point cursor, int actions, + Transferable transferable) { + super(aSource, context, cursor, actions); + fTransferable = transferable; + } + + public void acceptDrop(int dropAction) { + fContext.acceptDrop(dropAction); + } + + public void rejectDrop() { + fContext.rejectDrop(); + } + + public Transferable getTransferable() { + return fTransferable; + } +} diff --git a/mozilla/grendel/dnd/DropTargetEvent.java b/mozilla/grendel/dnd/DropTargetEvent.java new file mode 100644 index 00000000000..d6ce1b5ae20 --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetEvent.java @@ -0,0 +1,50 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +import java.awt.*; + +public abstract class DropTargetEvent extends java.util.EventObject { + DropTargetContext fContext; + Point fCursor; + int fActions; + + public DropTargetEvent(Object aSource, DropTargetContext context, + Point cursor, int actions) { + super(aSource); + fContext = context; + fCursor = cursor; + fActions = actions; + } + + public DropTargetContext getDropTargetContext() { + return fContext; + } + + Point getCursorLocation() { + return fCursor; + } + + public int getSourceActions() { + return fActions; + } +} diff --git a/mozilla/grendel/dnd/DropTargetListener.java b/mozilla/grendel/dnd/DropTargetListener.java new file mode 100644 index 00000000000..5ea32a9ded6 --- /dev/null +++ b/mozilla/grendel/dnd/DropTargetListener.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public interface DropTargetListener + extends java.util.EventListener { + void dragEnter (DropTargetDragEvent dtde); + void dragOver (DropTargetDragEvent dtde); + void dragExit (DropTargetDragEvent dtde); + void dragScroll (DropTargetDragEvent dtde); + void drop (DropTargetDropEvent dtde); +} diff --git a/mozilla/grendel/dnd/InvalidDnDOperationException.java b/mozilla/grendel/dnd/InvalidDnDOperationException.java new file mode 100644 index 00000000000..3556f7b22e5 --- /dev/null +++ b/mozilla/grendel/dnd/InvalidDnDOperationException.java @@ -0,0 +1,25 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.dnd; + +public class InvalidDnDOperationException extends Exception { +} diff --git a/mozilla/grendel/dnd/Makefile b/mozilla/grendel/dnd/Makefile new file mode 100644 index 00000000000..0dc07142663 --- /dev/null +++ b/mozilla/grendel/dnd/Makefile @@ -0,0 +1,36 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + DnDConstants.java \ + DragSource.java \ + DragSourceContext.java \ + DragSourceDragEvent.java \ + DragSourceDropEvent.java \ + DragSourceListener.java \ + DropTarget.java \ + DropTargetComponent.java \ + DropTargetContext.java \ + DropTargetDragEvent.java \ + DropTargetDropEvent.java \ + DropTargetEvent.java \ + DropTargetListener.java \ + InvalidDnDOperationException.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/filters/DeleteFilterActionFactory.java b/mozilla/grendel/filters/DeleteFilterActionFactory.java new file mode 100644 index 00000000000..443ab02ff49 --- /dev/null +++ b/mozilla/grendel/filters/DeleteFilterActionFactory.java @@ -0,0 +1,72 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class DeleteFilterActionFactory. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import java.io.Reader; +import javax.mail.Message; +import javax.mail.Flags; +import javax.mail.MessagingException; + +import grendel.filters.IFilterAction; + +public class DeleteFilterActionFactory extends Object + implements IFilterActionFactory { + + static private class DeleteFilterAction extends Object + implements IFilterAction { + // IFilterAction methods: + public void exorcise(Message message) { + String subject = null; + try { + if (message != null) + subject = message.getSubject(); + } catch (MessagingException e) { + subject = "getSubject() threw exception"; + } + System.err.println("Message: " + subject + " -> discard;"); + try { + message.setFlags(Flags.DeletedFlag, true); + } catch (MessagingException e) { + System.err.println("DeleteFilterAction.exercise() failed: " + e); + } + } + } + + // IFilterActionFactory methods: + public IFilterAction Make(String[] notused) { + return new DeleteFilterAction(); + } + public IFilterAction Make(FilterRulesParser notused) { + return new DeleteFilterAction(); + } + public String getName() { + return "discard"; // Do not localize, part of save format + } + public String toString() { + return "Delete"; // FIXME localize + } + static public IFilterAction Make() { + return new DeleteFilterAction(); + } +} diff --git a/mozilla/grendel/filters/FilterBase.java b/mozilla/grendel/filters/FilterBase.java new file mode 100644 index 00000000000..f1d883ccb65 --- /dev/null +++ b/mozilla/grendel/filters/FilterBase.java @@ -0,0 +1,61 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterBase. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +import grendel.filters.IFilter; +import grendel.filters.IFilterAction; + +class FilterBase implements IFilter { + // instance slots + private String fName; + private SearchTerm fMatchTerm; + private IFilterAction fAction; + + // Constructors + FilterBase(String name, SearchTerm term, IFilterAction action) { + fName = name; + fMatchTerm = term; + fAction = action; + } + /* + FilterBase(Reader reader) { // construct from character stream + // impliment parse that can find folder lists + } + */ + + // Instance methods + public boolean match(Message message) { + return fMatchTerm.match(message); + } + public void exorcise(Message message) { + fAction.exorcise(message); + } + public String toString() { + return fName; + } +} diff --git a/mozilla/grendel/filters/FilterFactory.java b/mozilla/grendel/filters/FilterFactory.java new file mode 100644 index 00000000000..336b582f7a8 --- /dev/null +++ b/mozilla/grendel/filters/FilterFactory.java @@ -0,0 +1,38 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterFactory + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.filters; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +import grendel.filters.IFilter; +import grendel.filters.IFilterAction; +import grendel.filters.FilterBase; + +public class FilterFactory extends Object { + static public IFilter Make(String name, SearchTerm term, + IFilterAction action) { + return new FilterBase(name, term, action); + } +} diff --git a/mozilla/grendel/filters/FilterMaster.java b/mozilla/grendel/filters/FilterMaster.java new file mode 100644 index 00000000000..e95fe34628a --- /dev/null +++ b/mozilla/grendel/filters/FilterMaster.java @@ -0,0 +1,226 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterMaster. + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.filters; + +import java.io.Reader; +import java.io.FileReader; +import java.io.File; +import java.io.EOFException; +import java.io.IOException; +import java.io.FileNotFoundException; +import java.util.Hashtable; +import java.util.Enumeration; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Store; +import javax.mail.Session; +import javax.mail.MessagingException; +import javax.mail.search.SearchTerm; +import javax.mail.search.SubjectTerm; +import javax.mail.search.FromTerm; +import javax.mail.event.MessageCountEvent; + +import grendel.storage.BerkeleyStore; + +import grendel.ui.StoreFactory; + +import grendel.filters.FilterSyntaxException; +import grendel.filters.MoveFilterActionFactory; +import grendel.filters.DeleteFilterActionFactory; +import grendel.filters.SubjectTermFactory; + +public class FilterMaster extends Object { + // class slots + static private FilterMaster fTheMaster = null; + + // class methods + public static FilterMaster Get() { + if (fTheMaster == null) + fTheMaster = new FilterMaster(); + return fTheMaster; + } + + // instance slots + private Hashtable fFilters; // to hold named filters + private Hashtable fFilterActionFactories; + private Hashtable fFilterTermFactories; + + // constructor API + private FilterMaster() { + fFilters = new Hashtable(); + fFilterTermFactories = new Hashtable(); + fFilterActionFactories = new Hashtable(); + + // Register built-in terms. + registerFilterTermFactory(new SubjectTermFactory()); + + // Register built-in filter actions. + registerFilterActionFactory(new MoveFilterActionFactory()); + registerFilterActionFactory(new DeleteFilterActionFactory()); + + // Locate and read the filters description file. + String home_dir = System.getProperties().getProperty("user.home"); + File home_dir_file = new File(home_dir); + File mail_filters_file = new File(home_dir_file, "mail.filters"); + FileReader fr = null; + try { + fr = new FileReader(mail_filters_file); + } catch (FileNotFoundException e) { + System.err.println("mail.filters not found, skipping loadFilters()"); + fr = null; + } + + // We must do this as loadFilters calls Get(), and we don't want to + // go recursive. This is a reasonable thing to do, because at this + // point, the constructor has succeeded, so we know we will be the + // FilterMaster. + fTheMaster = this; + + if (fr != null) + loadFilters(fr); + } + + // instance methods + private void loadFilters(Reader reader) { + + IFilter filter; + + FilterRulesParser parser = new FilterRulesParser(reader); + + try { + while ((filter = parser.getNext()) != null) { + String name = filter.toString(); + // System.err.println("got filter: " + name); + fFilters.put(name, filter); + } + } catch (FilterSyntaxException e) { + System.err.println("Error in filter rules: " + e); + } catch (IOException e) { + System.err.println("IO error reading filter rules: " + e); + } + + parser = null; + } + /*package*/ + void registerFilterActionFactory(IFilterActionFactory factory) { + String key = factory.getName(); + if (fFilterActionFactories.get(key) != null) { + // FIXME + System.err.println("FilterMaster.registerFilterAction(): duplicate"); + return; + } + fFilterActionFactories.put(key, factory); + } + /*package*/ + IFilterActionFactory getFilterActionFactory(String name) { + return (IFilterActionFactory)fFilterActionFactories.get(name); + } + /*package*/ + void registerFilterTermFactory(IFilterTermFactory factory) { + String key = factory.getName(); + if (fFilterTermFactories.get(key) != null) { + // FIXME + System.err.println("FilterMaster.registerFilterTerm(): duplicate"); + return; + } + fFilterTermFactories.put(key, factory); + } + /*package*/ + IFilterTermFactory getFilterTermFactory(String name) { + return (IFilterTermFactory)fFilterTermFactories.get(name); + } + + // tell me what you can do. Something like this will need to be created + // so that you can build a UI. Caller uses object.toString() on the + // elements to create user visible labels, etc.. + public Enumeration getFilterActionFactories() { + return fFilterActionFactories.elements(); + } + public Enumeration getFilterTermFactories() { + return fFilterTermFactories.elements(); + } + public Enumeration getFilters() { + return fFilters.elements(); + } + + // The verb that this whole sub-system is about. Apply all the defined + // filters to the specified message. + public void applyFilters(Message message) { + boolean deleted = false; + try { + deleted = message.isSet(Flags.Flag.DELETED); + } catch (MessagingException e) { + deleted = false; + } + if (deleted) + return; // don't need to be here + + Enumeration filters = getFilters(); + while (filters.hasMoreElements()) { + IFilter filter = (IFilter)filters.nextElement(); + if (filter.match(message)) { + filter.exorcise(message); + break; // only do first match + } + } + } + // Utilities: + public Folder getFolder(String name) { + Session session = StoreFactory.Instance().getSession(); + // ### Definitely wrong: + Store store = BerkeleyStore.GetDefaultStore(session); + Folder folder; + + try { + folder = store.getFolder(name); + if (!folder.exists()) + folder = null; + } catch (MessagingException e) { + folder = null; + } + return folder; + } + // For Testing. + public void applyFiltersToTestInbox() { + + Folder inbox = getFolder("TestInbox"); + + if (inbox != null) { + synchronized (inbox) { + Message[] messages; + try { + messages = inbox.getMessages(); + } catch (MessagingException e) { + messages = null; + } + if (messages != null) { + for (int i = 0; i < messages.length; i++) { + applyFilters(messages[i]); + } + } + } + } + } +} diff --git a/mozilla/grendel/filters/FilterRulesParser.java b/mozilla/grendel/filters/FilterRulesParser.java new file mode 100644 index 00000000000..edf5eb08ad8 --- /dev/null +++ b/mozilla/grendel/filters/FilterRulesParser.java @@ -0,0 +1,354 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterRulesParser + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.filters; + +import java.io.Reader; +import java.io.IOException; + +import java.util.Vector; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +import grendel.filters.IFilter; +import grendel.filters.IFilterAction; +import grendel.filters.FilterBase; +import grendel.filters.FilterMaster; + +public class FilterRulesParser extends Object { + + // Internal classes. + public static class Token extends Object { + public boolean isSubclassOf(Class ref_class) { + Class my_class = this.getClass(); + while (my_class != Object.class) { + if (my_class == ref_class) + return true; + my_class = my_class.getSuperclass(); + } + return false; + } + // public String toString(); - object has this + } + private static class EofToken extends Token { + public String toString() { + return "EOF"; + } + } + private static class DelimitToken extends Token { + public String toString() { + return ":"; + } + } + private static class EndToken extends Token { + public String toString() { + return ";"; + } + } + private static class OpenToken extends Token { + public String toString() { + return "("; + } + } + private static class CloseToken extends Token { + public String toString() { + return ")"; + } + } + private static class CommaToken extends Token { + public String toString() { + return ","; + } + } + private static class MatchesToken extends Token { + public String toString() { + return "contains"; + } + } + + // Class slots + public static final Token kEOF_TOKEN = new EofToken(); + public static final Token kDELIMIT_TOKEN = new DelimitToken(); + public static final Token kEND_TOKEN = new EndToken(); + public static final Token kOPEN_TOKEN = new OpenToken(); + public static final Token kCLOSE_TOKEN = new CloseToken(); + public static final Token kCOMMA_TOKEN = new CommaToken(); + public static final Token kCONTAINS_TOKEN = new MatchesToken(); + + // Instance classes + private class StringToken extends Token { + private String fString; + StringToken(String s) { + fString = s; + } + public String toString() { + return fString; + } + } + private class BadToken extends StringToken { + BadToken(String s) { + super(s); + } + BadToken(char c) { + super(null); + char buf[] = new char[2]; + buf[0] = c; + buf[1] = '\0'; + fString = new String(buf); + } + } + + // Instance slots + private Reader fReader; + private int fLastc; + + // Constructors + public FilterRulesParser(Reader reader) { + + fReader = reader; + fLastc = -1; + } + + // Instance methods + private int getc() throws IOException { + int rv; + if (fLastc != -1) { + rv = fLastc; + fLastc = -1; + } else { + rv = fReader.read(); + } + return rv; + } + private void ungetc(int c) { + fLastc = c; + } + + public Token getToken() throws IOException { + + int c; + + for (;;) { + if ((c = getc()) == -1) { + return kEOF_TOKEN; + } + + if (Character.isWhitespace((char)c)) { + continue; // keep going + } + + else if (c == ':') { + return kDELIMIT_TOKEN; + } + else if (c == ',') { + return kCOMMA_TOKEN; + } + else if (c == ';') { + return kEND_TOKEN; + } + else if (c == '(') { + return kOPEN_TOKEN; + } + else if (c == ')') { + return kCLOSE_TOKEN; + } + + else if (c == '/') { // comment? + c = getc(); + + if (c != '/') { + ungetc(c); + return new BadToken((char)'/'); + } + + // gobble till eol + while (c != '\n') { + c = getc(); + if (c == -1) + return kEOF_TOKEN; + } + continue; + } + + else if (c == '"') { + StringBuffer s = new StringBuffer(64); + + // gobble till '"' + c = getc(); + while (c != '"') { + if (c == -1) + return kEOF_TOKEN; + s.append((char)c); + c = getc(); + } + + return new StringToken(s.toString()); + } + + else { // string + StringBuffer s = new StringBuffer(16); + + // gobble till whitespace + while (Character.isLetterOrDigit((char)c)) { + s.append((char)c); + c = getc(); + if (c == -1) + break; + } + ungetc(c); + + // look for string amongst known tokens + // this needs to go into a symbol table but there is only one + // now. + if (s.toString().compareTo(kCONTAINS_TOKEN.toString()) == 0) + return kCONTAINS_TOKEN; + + return new BadToken(s.toString()); + } + + } + } + + // A utility routine + public String[] getArgs() throws IOException, FilterSyntaxException { + Token token; + + if ((token = getToken()) != kOPEN_TOKEN) { + throw new + FilterSyntaxException("Expected '(': " + token); + } + + if ((token = getToken()) == kCLOSE_TOKEN) + return null; + + Vector vector = new Vector(4); + + for(;;) { + + if (!token.isSubclassOf(StringToken.class)) { + throw new + FilterSyntaxException("Expected argument found: " + token); + } + + vector.addElement(token.toString()); + + if ((token = getToken()) != kCOMMA_TOKEN) + break; + } + + if (token != kCLOSE_TOKEN) { + throw new + FilterSyntaxException("Expected ')' found: " + token); + } + + String[] rv = new String[vector.size()]; + vector.copyInto(rv); + return rv; + } + public IFilter getNext() throws IOException, FilterSyntaxException { + Token token; + String name; + SearchTerm term; + IFilterAction action; + IFilterTermFactory term_factory; + IFilterActionFactory action_factory; + + FilterMaster filter_master = FilterMaster.Get(); + + if ((token = getToken()) == kEOF_TOKEN) + return null; + + // expecting a string which is name of filter + if (!token.isSubclassOf(StringToken.class)) { + throw new + FilterSyntaxException("Expected name of filter rule found: " + token); + } + name = token.toString(); + + // expecting delimiter + token = getToken(); + if (token != kDELIMIT_TOKEN) { + throw new + FilterSyntaxException("Expected delimiter found: " + token); + } + + // expecting search match expression (for now just name()) + // could do this by registering names as tokens.... + token = getToken(); + if (!token.isSubclassOf(StringToken.class)) { + throw new + FilterSyntaxException("Expected search term found: " + token); + } + + term_factory = filter_master.getFilterTermFactory(token.toString()); + + if (term_factory == null) { + throw new + FilterSyntaxException("Unknown search term found: " + token); + } + + // Call the term factory to parse the term, and create it. + term = term_factory.Make(this); + + // expecting delimiter + token = getToken(); + if (token != kDELIMIT_TOKEN) { + throw new + FilterSyntaxException("Expected delimiter found: " + token); + } + + // expecting search match expression (for now just name()) + // could do this by registering names as tokens.... + token = getToken(); + if (!token.isSubclassOf(StringToken.class)) { + throw new + FilterSyntaxException("Expected filter action found: " + token); + } + + action_factory = filter_master.getFilterActionFactory(token.toString()); + + if (action_factory == null) { + throw new + FilterSyntaxException("Unknown filter action found: " + token); + } + + /* + // collect action arguments. + String[] args; + args = getArgs(); + + action = action_factory.Make(args); + */ + action = action_factory.Make(this); + + // expecting delimiter + token = getToken(); + if (token != kEND_TOKEN) { + throw new + FilterSyntaxException("Expected ';' found: " + token); + } + + return FilterFactory.Make(name, term, action); + } +} diff --git a/mozilla/grendel/filters/FilterSyntaxException.java b/mozilla/grendel/filters/FilterSyntaxException.java new file mode 100644 index 00000000000..ab8121d3809 --- /dev/null +++ b/mozilla/grendel/filters/FilterSyntaxException.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterSyntaxException. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import javax.mail.MessagingException; + +// This guy should probably provide line number info and such for the +// catcher. +public class FilterSyntaxException extends MessagingException { + FilterSyntaxException(String s) { + super(s); + } +} diff --git a/mozilla/grendel/filters/IFilter.java b/mozilla/grendel/filters/IFilter.java new file mode 100644 index 00000000000..6e96aad8ad6 --- /dev/null +++ b/mozilla/grendel/filters/IFilter.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Interface IFilter. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import javax.mail.Message; + +public interface IFilter { + public boolean match(Message message); + public String toString(); // must be implimented + public void exorcise(Message message); +} diff --git a/mozilla/grendel/filters/IFilterAction.java b/mozilla/grendel/filters/IFilterAction.java new file mode 100644 index 00000000000..85ff4c40e30 --- /dev/null +++ b/mozilla/grendel/filters/IFilterAction.java @@ -0,0 +1,30 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Interface IFilterAction. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import javax.mail.Message; + +public interface IFilterAction { + public void exorcise(Message message); +} diff --git a/mozilla/grendel/filters/IFilterActionFactory.java b/mozilla/grendel/filters/IFilterActionFactory.java new file mode 100644 index 00000000000..3cc690123c4 --- /dev/null +++ b/mozilla/grendel/filters/IFilterActionFactory.java @@ -0,0 +1,40 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Interface FilterActionFactory. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import java.io.IOException; + +import javax.mail.Message; + +import grendel.filters.IFilterAction; +import grendel.filters.FilterRulesParser; +import grendel.filters.FilterSyntaxException; + +public interface IFilterActionFactory { + public IFilterAction Make(FilterRulesParser p) + throws IOException, FilterSyntaxException; + public IFilterAction Make(String[] args) throws FilterSyntaxException; + public String getName(); + public String toString(); // should be implimented +} diff --git a/mozilla/grendel/filters/IFilterTermFactory.java b/mozilla/grendel/filters/IFilterTermFactory.java new file mode 100644 index 00000000000..35ee20f9afb --- /dev/null +++ b/mozilla/grendel/filters/IFilterTermFactory.java @@ -0,0 +1,38 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Interface IFilterTermFactory. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import java.io.IOException; + +import javax.mail.search.SearchTerm; + +import grendel.filters.FilterRulesParser; +import grendel.filters.FilterSyntaxException; + +public interface IFilterTermFactory { + public SearchTerm Make(FilterRulesParser p) + throws IOException, FilterSyntaxException; + public String getName(); + public String toString(); // must be implimented +} diff --git a/mozilla/grendel/filters/Makefile b/mozilla/grendel/filters/Makefile new file mode 100644 index 00000000000..c7d259dbbf3 --- /dev/null +++ b/mozilla/grendel/filters/Makefile @@ -0,0 +1,34 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + DeleteFilterActionFactory.java \ + FilterBase.java \ + FilterFactory.java \ + FilterMaster.java \ + FilterRulesParser.java \ + FilterSyntaxException.java \ + IFilter.java \ + IFilterAction.java \ + IFilterActionFactory.java \ + IFilterTermFactory.java \ + MoveFilterActionFactory.java \ + SubjectTermFactory.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/filters/MoveFilterActionFactory.java b/mozilla/grendel/filters/MoveFilterActionFactory.java new file mode 100644 index 00000000000..6ccead8d73d --- /dev/null +++ b/mozilla/grendel/filters/MoveFilterActionFactory.java @@ -0,0 +1,134 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class MoveFilterActionFactory. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.filters; + +import java.io.IOException; + +import javax.mail.Message; +import javax.mail.Folder; +import javax.mail.Flags; +import javax.mail.MessagingException; + +import grendel.filters.IFilterAction; +import grendel.filters.FilterMaster; +import grendel.filters.FilterRulesParser; +import grendel.filters.FilterSyntaxException; + +public class MoveFilterActionFactory extends Object +implements IFilterActionFactory { + static class MoveFilterAction extends Object implements IFilterAction { + // slots + private Folder fTargetFolder; + + // constructors + private MoveFilterAction(Folder folder) { + fTargetFolder = folder; + } + + // IFilterAction methods: + public void exorcise(Message message) { + String subject = null; + try { + if (message != null) + subject = message.getSubject(); + } catch (MessagingException e) { + subject = "getSubject() threw exception"; + } + System.err.println("Message: " + subject + + "-> fileinto \"" + fTargetFolder.getName() + "\";"); + if (fTargetFolder != null && message != null) { + boolean set; + try { + set = message.isSet(Flags.Flag.DELETED); + } catch (MessagingException e) { + set = false; + } + + if (set) + System.out.println("MoveFilterAction.exercise() message is deleted"); + + Message mlist[] = { message }; + try { + fTargetFolder.appendMessages(mlist); + } catch (MessagingException e) { + System.err.println("MoveFilterAction.exercise() move failed: " + e); + } + try { + message.setFlags(new Flags(Flags.Flag.DELETED), true); + } catch (MessagingException e) { + System.err.println("MoveFilterAction.exercise() delete failed: " +e); + } + } + } + } + + // IFilterActionFactory methods: + // first arg is name of folder + public IFilterAction Make(String[] args) throws FilterSyntaxException { + if (args == null || args.length != 1) { + throw new FilterSyntaxException("Wrong number of args to " + + getName() + " action"); + } + FilterMaster filter_master = FilterMaster.Get(); + Folder folder = filter_master.getFolder(args[0]); + + if (folder == null) { + throw new FilterSyntaxException("Folder " + args[0] + "not found in" + + getName() + " action"); + } + return new MoveFilterAction(folder); + } + public IFilterAction Make(FilterRulesParser parser) + throws IOException, FilterSyntaxException { + // We expect: + // moveTo "folder" + // folder is the folder to file the message in + FilterRulesParser.Token token; + + token = parser.getToken(); + + String name = token.toString(); + Folder folder = FilterMaster.Get().getFolder(name); + + if (folder == null) { + throw new FilterSyntaxException("Folder " + name + " not found in " + + getName() + " action"); + } + return new MoveFilterAction(folder); + } + + public String getName() { + return "fileinto"; // Do not localize, part of save format + } + public String toString() { + return "Move to folder"; // FIXME localize + } + + /* + // convenience + static public IFilterAction Make(Folder folder) { + return new MoveFilterAction(folder); + } + */ +} diff --git a/mozilla/grendel/filters/README b/mozilla/grendel/filters/README new file mode 100644 index 00000000000..9b042252bfc --- /dev/null +++ b/mozilla/grendel/filters/README @@ -0,0 +1,13 @@ +This is Grendel -- a Java mail/news client. + +The grendel.view package is what gathers together the messages in a +Folder, calculates a sorting/threading order on them, and allows for a +UI to present them. + +Much of the UI work is in the "thread list" -- a scrollable list of +messages, possibly organized in a threaded tree, which the user can +scroll through and select and drag and manipulate. Most of the smarts +of the "thread list" are really implemented here in the grendel.view +package. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/filters/SubjectTermFactory.java b/mozilla/grendel/filters/SubjectTermFactory.java new file mode 100644 index 00000000000..494edab8a7c --- /dev/null +++ b/mozilla/grendel/filters/SubjectTermFactory.java @@ -0,0 +1,62 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class SubjectTermFactory. + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.filters; + +import java.io.IOException; + +import javax.mail.search.SearchTerm; +import javax.mail.search.SubjectTerm; + +import grendel.filters.IFilterAction; +import grendel.filters.FilterRulesParser; +import grendel.filters.FilterSyntaxException; + +public class SubjectTermFactory extends Object + implements IFilterTermFactory { + // IFilterTermFactory methods: + public String getName() { + return "subject"; // Do not localize, part of save format + } + public String toString() { + return "Subject contains"; // FIXME localize + } + public SearchTerm Make(FilterRulesParser parser) + throws IOException, FilterSyntaxException { + // We expect: + // subject matches name + // name is the subject term to match + FilterRulesParser.Token token; + + token = parser.getToken(); + + if (token != FilterRulesParser.kCONTAINS_TOKEN) { + throw new FilterSyntaxException("Expected 'contains': " + token); + } + + token = parser.getToken(); + + String name = token.toString(); + + return new SubjectTerm(name); + } +} diff --git a/mozilla/grendel/integrator/CompositionShell.java b/mozilla/grendel/integrator/CompositionShell.java new file mode 100644 index 00000000000..848c94790c4 --- /dev/null +++ b/mozilla/grendel/integrator/CompositionShell.java @@ -0,0 +1,263 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Oct 1997. + */ + +package grendel.integrator; + +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.util.Enumeration; +import java.util.Vector; + +import javax.activation.DataHandler; + +import com.sun.java.swing.AbstractAction; +import com.sun.java.swing.event.EventListenerList; + +import calypso.util.NullJavaEnumeration; + +import netscape.orion.uimanager.AbstractUICmd; + +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; +import netscape.shell.ShellViewCtxListener; + +import grendel.composition.CompositionPanel; +import grendel.prefs.Prefs; + +public class CompositionShell implements IShellViewCtx { + IShellIntegrator fIntegrator; + IShellViewCtx fParent; + DataHandler fDataHandler; + + EventListenerList fListeners = new EventListenerList(); + + /** + * Initializes this view e.g., the Integrator calls this first so you can + * identify the view. + * + */ + public void initialize( IShellIntegrator shell, IShellViewCtx parent ) { + fParent = parent; + fIntegrator = shell; + + // Tweek some UI behaviors + grendel.ui.GeneralFrame.SetExternalShell(true); + grendel.ui.ActionFactory.SetComposeMessageAction(new ComposeAction()); + } + + /** + * Provides an enumeration for the subviews in this view. + * + * @param iFlags Flags determining which items to iclude in the enumeration + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An Enumeration for the view context's children + */ + public Enumeration children( int iFlags ) { + return NullJavaEnumeration.kInstance; + } + + /** + * Provides an array of the view context's children + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An array of the view context's children + */ + public IShellViewCtx[] getChildren( int iFlags ) { + return null; + } + + /** + * Returns the number of children for the view context. + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return the number of children for the view context + */ + public int getChildCount( int iFlags ) { + return 0; + } + + /** + * Returns the view context's direct parent view context. + * + * @return the view context's parent. + */ + public IShellViewCtx getParent() { + return fParent; + } + + /** + * Sets the view context's parent. + * + * @param viewCtx the parent of this view context. + */ + public void setParent( IShellViewCtx viewCtx ) { + fParent = viewCtx; + } + + /** + * Compares two subviews. + * + * @param subview1 identifies the first subview to compare + * @param subview2 identifies the second subview to compare + * + * @return Less than zero - The first subview should precede the second + * Greater than zero - The first subview should follow the second + * Zero - The two subviews are the same + */ + public int compareIDs( IShellViewCtx subview1, IShellViewCtx subview2 ) { + return 0; + } + + /** + * Creates an IShellView object. Note the object created must be different + * than this view i.e., different references, because the Integrator may + * instruct this view to create more than one independent view. + * + * @return an IShellView object representing this view + */ + public IShellView createView(Object aObject) { + return new CompositionView(); + } + + /** + * Returns the view's preferences that display in the shell's shared + * preference window. + * + * @return an object specifying property information for the view's + * preferences. Note the property information is found via introspection. + * @see java.beans.BeanInfo + */ + public Object getGlobalPreferences() { + return new Prefs(); + } + + /** + * Returns attributes of this view. + * + * @return one or more flags describing the specified subview's attributes + * @see #CANCOPY + * @see #CANDELETE + * @see #CANMOVE + * @see #CANRENAME + * @see #READONLY + * @see #HASSUBFOLDER + * @see #FOLDER + */ + public int getAttributes() { + return READONLY|FOLDER; + } + + /** + * Supplies an icon for this view + * + * @param iTypeFlags one or more flags specifying the requested icon's type + * @see java.beans.BeanInfo#ICON_COLOR_16x16 + * @see java.beans.BeanInfo#ICON_MONO_16x16 + * @see java.beans.BeanInfo#ICON_COLOR_32x32 + * @see java.beans.BeanInfo#ICON_MONO_32x32 + * @see #OPEN + * + * @return an icon for the subview + */ + public Image getIcon( int iTypeFlags ) { + Image res = null; + + switch (iTypeFlags) { + case java.beans.BeanInfo.ICON_COLOR_16x16: + case java.beans.BeanInfo.ICON_MONO_16x16: + res = (Image) Toolkit.getDefaultToolkit().getImage(ClassLoader.getSystemResource("grendel/ui/images/GrendelIcon16.gif")); + break; + + case java.beans.BeanInfo.ICON_COLOR_32x32: + case java.beans.BeanInfo.ICON_MONO_32x32: + res = (Image) Toolkit.getDefaultToolkit().getImage(ClassLoader.getSystemResource("grendel/ui/images/GrendelIcon32.gif")); + break; + } + return res; + } + + /** + * Returns a human readable name for this view + * + * @return a string representation of this view + */ + public String getDisplayName() { + return "Grendel Composition"; + } + + /** + * Sets the display name for the specified subview + * + * @param name the new display name for this view + */ + public void setDisplayName( String name ) { + } + + /** + * Adds a change listener for monitoring changes on the ctx. + * e.g., the view ctx child state may have changed. + */ + public void addShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.add(ShellViewCtxListener.class, l); + } + + /** + * Removes a change listener for monitoring changes on the ctx. + */ + public void removeShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.remove(ShellViewCtxListener.class, l); + } + + public void setDataHandler(DataHandler aHandler) { + fDataHandler = aHandler; + } + + public DataHandler getDataHandler() { + return fDataHandler; + } + + class ComposeAction extends AbstractUICmd { + public ComposeAction() { + super("msgNew"); + + setEnabled(true); + } + + public void actionPerformed(ActionEvent aEvent) { + fIntegrator.browseViewCtx(new CompositionShell(), + IShellIntegrator.NEWWINDOW| + IShellIntegrator.ROOTEDMODE); + } + } +} diff --git a/mozilla/grendel/integrator/CompositionView.java b/mozilla/grendel/integrator/CompositionView.java new file mode 100644 index 00000000000..5b9ee7a8bb4 --- /dev/null +++ b/mozilla/grendel/integrator/CompositionView.java @@ -0,0 +1,158 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Oct 1997. + */ + +package grendel.integrator; + +import java.awt.Component; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.Properties; + +import com.sun.java.swing.Action; +import com.sun.java.swing.JMenu; +import com.sun.java.swing.JMenuItem; +import com.sun.java.swing.event.ChangeEvent; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import netscape.orion.toolbars.NSToolbar; + +import netscape.shell.IShellAnimation; +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; + +import grendel.composition.AddressBar; +import grendel.composition.CompositionPanel; +import grendel.composition.CompositionPanelListener; +import grendel.ui.ActionFactory; + +import javax.mail.Session; + +class CompositionView implements IShellView { + IShellIntegrator fIntegrator; + IShellAnimation fAnimation; + + NSToolbar fToolBar; + AddressBar fAddressBar; + CompositionPanel fPanel; + + /** + * Instructs the view to create the ui component for this view. + * + * @param prevView the view that was active prior to this view + * @param integrator the instance of the Integrator containing this view + * @param rect a rectangle describing the size and position of the view + * + * @return the component ui object for displaying this view's data + */ + public Component createViewComponent( IShellView prevView, + IShellIntegrator communicator, + Rectangle rect ) { + + fIntegrator = communicator; + fAnimation = + (IShellAnimation) fIntegrator.getService(IShellAnimation.class, this); + + + Preferences prefs = PreferencesFactory.Get(); + String host = prefs.getString("smtp.host", "mail"); + String addr = prefs.getString("user.email_address", "john@doe.com"); + + Properties props = new Properties(); + props.put("mail.smtp.user", host); + props.put("mail.smtp.host", addr); + + // create some properties and get the default Session + Session session = Session.getDefaultInstance(props, null); + fPanel = new CompositionPanel(session); + fPanel.addCompositionPanelListener(new PanelListener()); + + fToolBar = fPanel.getToolBar(); + fAddressBar = fPanel.getAddressBar(); + + return fPanel; + } + + /** + * Instructs the view to dispose the ui component for this view. + */ + public void disposeViewComponent() { + fPanel.dispose(); + fPanel = null; + } + + /** + * Returns the Component object for this view. + */ + public Component getViewComponent() { + return fPanel; + } + + /** + * Called by the Integrator when the view is created or is about to be + * disposed. View's should use this opportunity to merge menus etc. + * + * @param bActivate specifies the requested state. + */ + public void activateUI( boolean bActivate ) { + if (bActivate) { + fIntegrator.addBar(fToolBar.getComponent(), "", false); + fIntegrator.addBar(fAddressBar.getComponent(), "", false); + } + } + + /** + * Refreshes the contents of this view. + */ + public void refresh() { + + } + + class PanelListener implements CompositionPanelListener { + public void sendingMail(ChangeEvent aEvent) { + if (fAnimation != null) { + fAnimation.start(); + } + } + public void doneSendingMail(ChangeEvent aEvent) { + if (fAnimation != null) { + fAnimation.stop(); + } + fIntegrator.closeShell(); + } + public void sendFailed(ChangeEvent aEvent) { + if (fAnimation != null) { + fAnimation.stop(); + } + } + } +} + + diff --git a/mozilla/grendel/integrator/FolderCtx.java b/mozilla/grendel/integrator/FolderCtx.java new file mode 100644 index 00000000000..0286dc79896 --- /dev/null +++ b/mozilla/grendel/integrator/FolderCtx.java @@ -0,0 +1,290 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + + */ + + +package grendel.integrator; + +import java.awt.Image; +import java.awt.Toolkit; +import java.util.Enumeration; +import java.util.Vector; + +import javax.activation.DataHandler; + +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.EventListenerList; + +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; +import netscape.shell.ShellViewCtxListener; + +import javax.mail.Folder; +import javax.mail.MessagingException; + +import grendel.view.ViewedFolder; +import grendel.ui.UIFactory; + +public class FolderCtx implements IShellViewCtx { + IShellIntegrator fIntegrator; + IShellViewCtx fParent; + ViewedFolder fFolder; + Vector fChildren = new Vector(); + DataHandler fDataHandler; + EventListenerList fListeners = new EventListenerList(); + boolean fInited; + + + public FolderCtx(ViewedFolder aFolder) { + fFolder = aFolder; + } + + /** + * Initializes this view e.g., the Integrator calls this first so you can + * identify the view. + * + */ + + public void initialize( IShellIntegrator shell, IShellViewCtx aParent ) { + fIntegrator = shell; + fParent = aParent; + } + + /** + * Provides an enumeration for the subviews in this view. + * + * @param iFlags Flags determining which items to iclude in the enumeration + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An Enumeration for the view context's children + */ + public Enumeration children( int iFlags ) { + if (!fInited) { + initChildren(); + } + return fChildren.elements(); + } + + /** + * Provides an array of the view context's children + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An array of the view context's children + */ + public IShellViewCtx[] getChildren( int iFlags ) { + if (!fInited) { + initChildren(); + } + IShellViewCtx res[] = new IShellViewCtx[fChildren.size()]; + fChildren.copyInto(res); + return res; + } + + /** + * Returns the number of children for the view context. + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return the number of children for the view context + */ + public int getChildCount( int iFlags ) { + if (!fInited) { + return 1; + } else { + return fChildren.size(); + } + } + + /** + * Returns the view context's direct parent view context. + * + * @return the view context's parent. + */ + public IShellViewCtx getParent() { + return fParent; + } + + /** + * Sets the view context's parent. + * + * @param viewCtx the parent of this view context. + */ + public void setParent( IShellViewCtx viewCtx ) { + fParent = viewCtx; + } + + /** + * Compares two subviews. + * + * @param subview1 identifies the first subview to compare + * @param subview2 identifies the second subview to compare + * + * @return Less than zero - The first subview should precede the second + * Greater than zero - The first subview should follow the second + * Zero - The two subviews are the same + */ + public int compareIDs( IShellViewCtx subview1, IShellViewCtx subview2 ) { + return 0; + } + + /** + * Creates an IShellView object. Note the object created must be different + * than this view i.e., different references, because the Integrator may + * instruct this view to create more than one independent view. + * + * @return an IShellView object representing this view + */ + public IShellView createView(Object aObject) { + return new FolderView(fFolder.getFolder()); + } + + /** + * Returns the view's preferences that display in the shell's shared + * preference window. + * + * @return an object specifying property information for the view's + * preferences. Note the property information is found via introspection. + * @see java.beans.BeanInfo + */ + public Object getGlobalPreferences() { + return null; + } + + /** + * Returns attributes of this view. + * + * @return one or more flags describing the specified subview's attributes + * @see #CANCOPY + * @see #CANDELETE + * @see #CANMOVE + * @see #CANRENAME + * @see #READONLY + * @see #HASSUBFOLDER + * @see #FOLDER + */ + public int getAttributes() { + return FOLDER|READONLY|HASSUBFOLDER; + } + + /** + * Supplies an icon for this view + * + * @param iTypeFlags one or more flags specifying the requested icon's type + * @see java.beans.BeanInfo#ICON_COLOR_16x16 + * @see java.beans.BeanInfo#ICON_MONO_16x16 + * @see java.beans.BeanInfo#ICON_COLOR_32x32 + * @see java.beans.BeanInfo#ICON_MONO_32x32 + * @see #OPEN + * + * @return an icon for the subview + */ + public Image getIcon( int iTypeFlags ) { + Image res = null; + + switch (iTypeFlags) { + case java.beans.BeanInfo.ICON_COLOR_16x16: + case java.beans.BeanInfo.ICON_MONO_16x16: + res = UIFactory.Instance().getFolderImage(fFolder, + false, false); + break; + + case java.beans.BeanInfo.ICON_COLOR_32x32: + case java.beans.BeanInfo.ICON_MONO_32x32: + res = UIFactory.Instance().getFolderImage(fFolder, + false, true); + break; + } + + return res; + } + + /** + * Returns a human readable name for this view + * + * @return a string representation of this view + */ + public String getDisplayName() { + return fFolder.getFolder().getName(); + } + + /** + * Sets the display name for the specified subview + * + * @param name the new display name for this view + */ + public void setDisplayName( String name ) { + } + + /** + * Adds a change listener for monitoring changes on the ctx. + * e.g., the view ctx child state may have changed. + */ + public void addShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.add(ShellViewCtxListener.class, l); + } + + /** + * Removes a change listener for monitoring changes on the ctx. + */ + public void removeShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.remove(ShellViewCtxListener.class, l); + } + + public void setDataHandler(DataHandler aHandler) { + fDataHandler = aHandler; + } + + public DataHandler getDataHandler() { + return fDataHandler; + } + + void initChildren() { + fChildren.removeAllElements(); + ViewedFolder folder = fFolder.getFirstSubFolder(); + while (folder != null) { + FolderCtx child = new FolderCtx(folder); + fChildren.addElement(child); + child.initialize(fIntegrator, this); + folder = folder.getNextFolder(); + } + fInited = true; + notifyChange(); + } + + void notifyChange() { + Object[] listeners = fListeners.getListenerList(); + ChangeEvent event = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + // Lazily create the event: + if (event == null) + event = new ChangeEvent(this); + ((ShellViewCtxListener)listeners[i+1]).childrenChanged(event); + } + } +} diff --git a/mozilla/grendel/integrator/FolderView.java b/mozilla/grendel/integrator/FolderView.java new file mode 100644 index 00000000000..496716aa8df --- /dev/null +++ b/mozilla/grendel/integrator/FolderView.java @@ -0,0 +1,240 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.integrator; + +import java.awt.Component; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.Vector; + +import javax.mail.Folder; + +import com.sun.java.swing.Action; +import com.sun.java.swing.JMenu; +import com.sun.java.swing.JMenuItem; +import com.sun.java.swing.event.ChangeEvent; + +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.UIMConstants; +import netscape.orion.uimanager.IUICmd; + +import netscape.shell.IShellAnimation; +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; + +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.ConfigFormatException; +import netscape.orion.uimanager.IUICmd; +import netscape.orion.uimanager.IUIMMenuBar; +import netscape.orion.uimanager.UIMConstants; + +import xml.tree.TreeBuilder; +import xml.tree.XMLNode; + +import grendel.ui.ActionFactory; +import grendel.ui.FolderPanel; +import grendel.ui.FolderPanelListener; +import grendel.ui.MessagePanel; +import grendel.ui.MessagePanelListener; +import grendel.view.ViewedMessage; +import grendel.widgets.Splitter; +import grendel.widgets.StatusEvent; +import grendel.widgets.TreePath; + +class FolderView implements IShellView { + IShellIntegrator fIntegrator; + IShellAnimation fAnimation; + + Splitter fSplitter; + MessagePanel fMessagePanel; + FolderPanel fFolderPanel; + + PanelListener fListener; + + Folder fFolder; + + NSToolbar fToolBar; + + public FolderView(Folder aFolder) { + fFolder = aFolder; + } + + /** + * Instructs the view to create the ui component for this view. + * + * @param prevView the view that was active prior to this view + * @param integrator the instance of the Integrator containing this view + * @param rect a rectangle describing the size and position of the view + * + * @return the component ui object for displaying this view's data + */ + public Component createViewComponent( IShellView aPrevView, + IShellIntegrator aIntegrator, + Rectangle aRect ) { + fIntegrator = aIntegrator; + fAnimation = + (IShellAnimation) fIntegrator.getService(IShellAnimation.class, this); + + fSplitter = new Splitter(Splitter.VERTICAL); + fFolderPanel = new FolderPanel(); + fMessagePanel = new MessagePanel(); + + NSToolbar folderToolBar = fFolderPanel.getToolBar(); + NSToolbar messageToolBar = fMessagePanel.getToolBar(); + + fToolBar = grendel.ui.Util.MergeToolBars(folderToolBar, + messageToolBar); + + fListener = new PanelListener(); + fFolderPanel.addFolderPanelListener(fListener); + fMessagePanel.addMessagePanelListener(fListener); + + fSplitter.add(fFolderPanel, new Float(1.0)); + fSplitter.add(fSplitter.createSeparator(4)); + fSplitter.add(fMessagePanel, new Float(1.0)); + + fFolderPanel.setFolder(fFolder); + + return fSplitter; + } + + /** + * Instructs the view to dispose the ui component for this view. + */ + public void disposeViewComponent() { + fFolderPanel.removeFolderPanelListener(fListener); + fFolderPanel.dispose(); + fMessagePanel.dispose(); + } + + /** + * Returns the Component object for this view. + */ + public Component getViewComponent() { + return fSplitter; + } + + /** + * Called by the Integrator when the view is created or is about to be + * disposed. View's should use this opportunity to merge menus etc. + * + * @param bActivate specifies the requested state. + */ + public void activateUI( boolean bActivate ) { + IUICmd actions[] = { + ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction(), + }; + + if (bActivate) { + try { + URL url = getClass().getResource("menus.xml"); + XMLNode root = TreeBuilder.build(url, getClass()); + XMLNode node = root.getChild(UIMConstants.kMenubarType, + UIMConstants.kIDAttribute, + "FolderView"); + + fIntegrator.mergeMenus(node, actions, this); + } catch (ConfigFormatException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + fIntegrator.addBar(fToolBar.getComponent(), "", false); + } + } + + /** + * Refreshes the contents of this view. + */ + public void refresh() { + } + + // + // MessageSelectionListener class + // + + class PanelListener implements FolderPanelListener, MessagePanelListener { + // FolderPanelListener + + public void loadingFolder(ChangeEvent aEvent) { + // start animation + if (fAnimation != null) { + fAnimation.start(); + } + } + + public void folderLoaded(ChangeEvent aEvent) { + // stop animation + if (fAnimation != null) { + fAnimation.stop(); + } + } + + public void folderStatus(StatusEvent aEvent) { + fIntegrator.setStatusText(aEvent.getStatus()); + } + + public void folderSelectionChanged(ChangeEvent aEvent) { + TreePath path = null; + Enumeration selection = ((FolderPanel) aEvent.getSource()).getSelection(); + if (selection.hasMoreElements()) { + path = (TreePath) selection.nextElement(); + } + if (path != null && !selection.hasMoreElements()) { // not multiple selection + ViewedMessage node = (ViewedMessage) path.getTip(); + fMessagePanel.setMessage(node.getMessage()); + } else { + fMessagePanel.setMessage(null); + } + } + public void folderSelectionDoubleClicked(ChangeEvent aEvent) { + } + + // MessagePanelListener + + public void loadingMessage(ChangeEvent aEvent) { + // start animation + if (fAnimation != null) { + fAnimation.start(); + } + } + + public void messageLoaded(ChangeEvent aEvent) { + // stop animation + if (fAnimation != null) { + fAnimation.stop(); + } + } + + public void messageStatus(StatusEvent aEvent) { + fIntegrator.setStatusText(aEvent.getStatus()); + } + } +} diff --git a/mozilla/grendel/integrator/Makefile b/mozilla/grendel/integrator/Makefile new file mode 100644 index 00000000000..0a28aa10859 --- /dev/null +++ b/mozilla/grendel/integrator/Makefile @@ -0,0 +1,29 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + CompositionShell.java \ + CompositionView.java \ + FolderCtx.java \ + FolderView.java \ + SessionView.java \ + Shell.java \ + StoreCtx.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/integrator/MenuLabels.properties b/mozilla/grendel/integrator/MenuLabels.properties new file mode 100644 index 00000000000..30c97e1a670 --- /dev/null +++ b/mozilla/grendel/integrator/MenuLabels.properties @@ -0,0 +1,134 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + + +# +# menu labels +# + +fileLabel=File +fileAccel=F +editLabel=Edit +editAccel=E +viewLabel=View +viewAccel=V +messageLabel=Message +messageAccel=M + +# +# Application menu item labels +# + +appSearchLabel=Search +appSearchAccel=S +appRunFiltersLabel=Run Filters on TestInbox +appRunFiltersAccel=F + +# +# Edit menu item labels +# + +editUndoLabel=Undo +editUndoAccel=U +editCutLabel=Cut +editCutAccel=t +editCopyLabel=Copy +editCopyAccel=C +editPasteLabel=Paste +editPasteAccel=P +editClearLabel=Delete +editClearAccel=D + +# +# Folder menu item labels +# + +folderOpenLabel=Open Folder +folderOpenAccel=O + +# +# Message menu item labels +# + +folderNewLabel=New Folder... +folderNewAccel=F +folderDeleteLabel=Delete Folder +folderDeleteAccel=D + +msgGetNewLabel=Get New Messages +msgGetNewAccel=G +msgOpenLabel=Open Message +msgOpenAccel=M +msgSaveAsLabel=Save Message As... +msgSaveAsAccel=A +msgNewLabel=New Message +msgNewAccel=N +msgReplyLabel=Reply +msgReplyAccel=R +msgReplyAllLabel=Reply All +msgReplyAllAccel=A +msgForwardLabel=Forward +msgForwardAccel=F +msgForwardQuotedLabel=Forward Quoted +msgForwardQuotedAccel=Q + +msgMoveLabel=File to +msgMoveAccel=F +msgCopyLabel=Copy to +msgCopyAccel=C +msgDeleteLabel=Delete Message +msgDeleteAccel=D + +msgMarkLabel=Mark +msgMarkAccel=M + +# +# Mark menu labels +# + +markMsgReadLabel=As Read +markMsgReadAccel=R +markThreadReadLabel=Thread Read +markThreadReadAccel=T +markAllReadLabel=All Read +markAllReadAccel=A + +# +# Sort menu labels +# + +viewSortLabel=Sort +viewSortAccel=S + +toggleThreadingLabel=Toggle Threading +toggleThreadingAccel=T +sortDateLabel=by Date +sortDateAccel=D +sortSubjectLabel=by Subject +sortSubjectAccel=S +sortAuthorLabel=by Author +sortAuthorAccel=A +sortNumberLabel=by Number +sortNumberAccel=N + +# +# Window menu item labels +# + +windowListLabel=Window List... +windowListAccel=W + diff --git a/mozilla/grendel/integrator/SessionView.java b/mozilla/grendel/integrator/SessionView.java new file mode 100644 index 00000000000..1a348958c87 --- /dev/null +++ b/mozilla/grendel/integrator/SessionView.java @@ -0,0 +1,148 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 10 Nov 1997. + */ + +package grendel.integrator; + +import java.awt.Component; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.Vector; + +import javax.mail.Folder; + +import com.sun.java.swing.Action; +import com.sun.java.swing.JMenu; +import com.sun.java.swing.JMenuItem; +import com.sun.java.swing.event.ChangeEvent; + +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.ConfigFormatException; +import netscape.orion.uimanager.IUICmd; +import netscape.orion.uimanager.IUIMMenuBar; +import netscape.orion.uimanager.UIMConstants; + +import xml.tree.TreeBuilder; +import xml.tree.XMLNode; + +import netscape.shell.IShellAnimation; +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; + +import grendel.ui.ActionFactory; +import grendel.ui.MasterPanel; +import grendel.ui.MasterPanelListener; +import grendel.widgets.StatusEvent; +import grendel.widgets.TreePath; + +class SessionView implements IShellView { + IShellIntegrator fIntegrator; + IShellAnimation fAnimation; + + MasterPanel fMasterPanel; + NSToolbar fToolBar; + + public SessionView() { + } + + /** + * Instructs the view to create the ui component for this view. + * + * @param prevView the view that was active prior to this view + * @param integrator the instance of the Integrator containing this view + * @param rect a rectangle describing the size and position of the view + * + * @return the component ui object for displaying this view's data + */ + public Component createViewComponent( IShellView aPrevView, + IShellIntegrator aIntegrator, + Rectangle aRect ) { + fIntegrator = aIntegrator; + fAnimation = + (IShellAnimation) fIntegrator.getService(IShellAnimation.class, this); + + fMasterPanel = new MasterPanel(); + + fToolBar = fMasterPanel.getToolBar(); + + return fMasterPanel; + } + + /** + * Instructs the view to dispose the ui component for this view. + */ + public void disposeViewComponent() { + fMasterPanel.dispose(); + } + + /** + * Returns the Component object for this view. + */ + public Component getViewComponent() { + return fMasterPanel; + } + + /** + * Called by the Integrator when the view is created or is about to be + * disposed. View's should use this opportunity to merge menus etc. + * + * @param bActivate specifies the requested state. + */ + public void activateUI( boolean bActivate ) { + IUICmd actions[] = { + ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction(), + }; + + if (bActivate) { + try { + URL url = getClass().getResource("menus.xml"); + XMLNode root = TreeBuilder.build(url, getClass()); + XMLNode node = root.getChild(UIMConstants.kMenubarType, + UIMConstants.kIDAttribute, + "FolderView"); + + fIntegrator.mergeMenus(node, actions, this); + } catch (ConfigFormatException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + fIntegrator.addBar(fToolBar.getComponent(), "", false); + } + } + + /** + * Refreshes the contents of this view. + */ + public void refresh() { + } +} diff --git a/mozilla/grendel/integrator/Shell.java b/mozilla/grendel/integrator/Shell.java new file mode 100644 index 00000000000..6a75207abb7 --- /dev/null +++ b/mozilla/grendel/integrator/Shell.java @@ -0,0 +1,311 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + + */ + +package grendel.integrator; + +import java.awt.Image; +import java.awt.Toolkit; +import java.util.Enumeration; +import java.util.Vector; + +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.ChangeListener; +import com.sun.java.swing.event.EventListenerList; + +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; +import netscape.shell.ShellViewCtxListener; + +import javax.activation.DataHandler; +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; + +import grendel.prefs.Prefs; +import grendel.view.ViewedFolder; +import grendel.view.ViewedStore; + +import grendel.ui.StoreFactory; + +public class Shell implements IShellViewCtx { + IShellIntegrator fIntegrator; + Vector fChildren = new Vector(); + IShellViewCtx fParent; + EventListenerList fListeners = new EventListenerList(); + DataHandler fDataHandler; + + StoreChangeListener fStoreChangeListener; + + /** + * Initializes this view e.g., the Integrator calls this first so you can + * identify the view. + * + */ + public void initialize( IShellIntegrator shell, IShellViewCtx aParent ) { + fIntegrator = shell; + fParent = aParent; + + // ###HACKHACKHACK Remove me when javamail fixes their stuff. + java.io.File mailcapfile = new java.io.File("mailcap"); + if (!mailcapfile.exists()) { + try { + (new java.io.RandomAccessFile(mailcapfile, "rw")).close(); + System.out.println("*** Created empty mailcap file in current"); + System.out.println("*** directory (to work around buggy javamail"); + System.out.println("*** software from JavaSoft)."); + } catch (java.io.IOException e) { + System.out.println("*** Couldn't create empty mailcap file: " + e); + System.out.println("*** Immanent crash is likely due to buggy"); + System.out.println("*** javamail software from JavaSoft."); + } + } + + initChildren(); + + fStoreChangeListener = new StoreChangeListener(); + StoreFactory.Instance().addChangeListener(fStoreChangeListener); + + // Tweek some UI behaviors + grendel.ui.GeneralFrame.SetExternalShell(true); + } + + /** + * Provides an enumeration for the subviews in this view. + * + * @param iFlags Flags determining which items to iclude in the enumeration + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An Enumeration for the view context's children + */ + public Enumeration children( int iFlags ) { + return fChildren.elements(); + } + + /** + * Provides an array of the view context's children + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return An array of the view context's children + */ + public IShellViewCtx[] getChildren( int iFlags ) { + IShellViewCtx res[] = new IShellViewCtx[fChildren.size()]; + fChildren.copyInto(res); + return res; + } + + /** + * Returns the number of children for the view context. + * + * @param iFlags Flags determining which items to iclude in the array + * @see #FOLDERS + * @see #NONFOLDERS + * @see #INCLUDEHIDDEN + * + * @return the number of children for the view context + */ + public int getChildCount( int iFlags ) { + return fChildren.size(); + } + + /** + * Returns the view context's direct parent view context. + * + * @return the view context's parent. + */ + public IShellViewCtx getParent() { + return fParent; + } + + /** + * Sets the view context's parent. + * + * @param viewCtx the parent of this view context. + */ + public void setParent( IShellViewCtx viewCtx ) { + fParent = viewCtx; + } + + /** + * Compares two subviews. + * + * @param subview1 identifies the first subview to compare + * @param subview2 identifies the second subview to compare + * + * @return Less than zero - The first subview should precede the second + * Greater than zero - The first subview should follow the second + * Zero - The two subviews are the same + */ + public int compareIDs( IShellViewCtx subview1, IShellViewCtx subview2 ) { + return 0; + } + + /** + * Creates an IShellView object. Note the object created must be different + * than this view i.e., different references, because the Integrator may + * instruct this view to create more than one independent view. + * + * @return an IShellView object representing this view + */ + public IShellView createView(Object aObject) { + return new SessionView(); + } + + /** + * Returns the view's preferences that display in the shell's shared + * preference window. + * + * @return an object specifying property information for the view's + * preferences. Note the property information is found via introspection. + * @see java.beans.BeanInfo + */ + public Object getGlobalPreferences() { + return new Prefs(); + } + + /** + * Returns attributes of this view. + * + * @return one or more flags describing the specified subview's attributes + * @see #CANCOPY + * @see #CANDELETE + * @see #CANMOVE + * @see #CANRENAME + * @see #READONLY + * @see #HASSUBFOLDER + * @see #FOLDER + */ + public int getAttributes() { + return FOLDER|HASSUBFOLDER|READONLY; + } + + /** + * Supplies an icon for this view + * + * @param iTypeFlags one or more flags specifying the requested icon's type + * @see java.beans.BeanInfo#ICON_COLOR_16x16 + * @see java.beans.BeanInfo#ICON_MONO_16x16 + * @see java.beans.BeanInfo#ICON_COLOR_32x32 + * @see java.beans.BeanInfo#ICON_MONO_32x32 + * @see #OPEN + * + * @return an icon for the subview + */ + public Image getIcon( int iTypeFlags ) { + Image res = null; + + switch (iTypeFlags) { + case java.beans.BeanInfo.ICON_COLOR_16x16: + case java.beans.BeanInfo.ICON_MONO_16x16: + res = (Image) Toolkit.getDefaultToolkit().getImage(ClassLoader.getSystemResource("grendel/ui/images/GrendelIcon16.gif")); + break; + + case java.beans.BeanInfo.ICON_COLOR_32x32: + case java.beans.BeanInfo.ICON_MONO_32x32: + res = (Image) Toolkit.getDefaultToolkit().getImage(ClassLoader.getSystemResource("grendel/ui/images/GrendelIcon32.gif")); + break; + } + + return res; + } + + /** + * Returns a human readable name for this view + * + * @return a string representation of this view + */ + public String getDisplayName() { + return "Grendel"; + } + + /** + * Sets the display name for the specified subview + * + * @param name the new display name for this view + */ + public void setDisplayName( String name ) { + + } + + /** + * Adds a change listener for monitoring changes on the ctx. + * e.g., the view ctx child state may have changed. + */ + public void addShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.add(ShellViewCtxListener.class, l); + } + + /** + * Removes a change listener for monitoring changes on the ctx. + */ + public void removeShellViewCtxListener( ShellViewCtxListener l ) { + fListeners.remove(ShellViewCtxListener.class, l); + } + + public void setDataHandler(DataHandler aHandler) { + fDataHandler = aHandler; + } + + public DataHandler getDataHandler() { + return fDataHandler; + } + + void initChildren() { + ViewedStore stores[] = StoreFactory.Instance().getStores(); + + fChildren.removeAllElements(); + + try { + for (int i = 0; i < stores.length; i++) { + StoreCtx child = new StoreCtx(stores[i]); + child.initialize(fIntegrator, this); + fChildren.addElement(child); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + class StoreChangeListener implements ChangeListener { + public void stateChanged(ChangeEvent aEvent) { + initChildren(); + notifyChange(); + } + } + + void notifyChange() { + Object[] listeners = fListeners.getListenerList(); + ChangeEvent event = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + // Lazily create the event: + if (event == null) + event = new ChangeEvent(this); + ((ShellViewCtxListener)listeners[i+1]).childrenChanged(event); + } + } +} + diff --git a/mozilla/grendel/integrator/StoreCtx.java b/mozilla/grendel/integrator/StoreCtx.java new file mode 100644 index 00000000000..efb634da26a --- /dev/null +++ b/mozilla/grendel/integrator/StoreCtx.java @@ -0,0 +1,54 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 14 Nov 1997. + */ + +package grendel.integrator; + +import java.awt.Image; +import java.text.MessageFormat; + +import netscape.shell.IShellIntegrator; +import netscape.shell.IShellView; +import netscape.shell.IShellViewCtx; + +import grendel.view.ViewedFolder; +import grendel.view.ViewedStore; + +public class StoreCtx extends FolderCtx { + ViewedStore fStore; + public StoreCtx(ViewedStore aStore) { + super(aStore); + fStore = aStore; + } + + /** + * Returns a human readable name for this view + * + * @return a string representation of this view + */ + public String getDisplayName() { + String host = fStore.getHost(); + if (host != null) { + return MessageFormat.format("Messages on {0}", new Object[] {host}); + } else { + return "Local Messages"; + } + } +} diff --git a/mozilla/grendel/integrator/menus.xml b/mozilla/grendel/integrator/menus.xml new file mode 100644 index 00000000000..398f9bdfe5b --- /dev/null +++ b/mozilla/grendel/integrator/menus.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mozilla/grendel/mime/HeaderCharsetDecoder.java b/mozilla/grendel/mime/HeaderCharsetDecoder.java new file mode 100644 index 00000000000..e5f04893716 --- /dev/null +++ b/mozilla/grendel/mime/HeaderCharsetDecoder.java @@ -0,0 +1,72 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 7 Oct 1997. + */ + +package grendel.mime; + +import calypso.util.ByteBuf; + +/** This class knows how to convert Unicode to and from the MIME encoding + defined in RFC 2047. + */ +class HeaderCharsetDecoder { + + static HeaderCharsetDecoder decoder = null; + + private HeaderCharsetDecoder() { } // only one instance + + static public HeaderCharsetDecoder Get() { + if (decoder == null) + synchronized(HeaderCharsetDecoder.class) { + if (decoder == null) + decoder = new HeaderCharsetDecoder(); + } + return decoder; + } + + + /** Converts a ByteBuf of raw header bytes to a StringBuffer of Unicode + characters, decoding the bytes as per the encoding rules of RFC 2047. + This is used by getUnicodeHeader(). + + @param in The raw bytes to be decoded. + @param out The resultant Unicode characters. + @param tokenizable_p Whether the header from which these bytes came + was a tokenizable header (like "From") or an + unstructured-text header (like "Subject"). + This information is needed because the decoding + rules are subtly different for the two types + of headers. + */ + public void mimeToUnicode(ByteBuf in, StringBuffer out, + boolean tokenizable_p) { + // #### write me + byte inb[] = in.toBytes(); + int L = in.length(); + for (int i = 0; i < L; i++) + out.append((char) inb[i]); + } + + public void unicodeToMime(String in, ByteBuf out, boolean tokenizable_p) { + // #### write me + out.append(in.getBytes()); + } + +} diff --git a/mozilla/grendel/mime/IMimeObject.java b/mozilla/grendel/mime/IMimeObject.java new file mode 100644 index 00000000000..c56523f5276 --- /dev/null +++ b/mozilla/grendel/mime/IMimeObject.java @@ -0,0 +1,75 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Interface to extracting information from MIME objects. + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime; + +import javax.mail.internet.InternetHeaders; +import java.util.Enumeration; + +/** + This is an interface for extracting information from a MIME part, + or a tree of MIME parts. + @see IMimeParser + @see IMimeOperator + */ + +public interface IMimeObject { + + /** Returns the MIME content-type of this object. + Note that under certain conditions, this may disagree with the + content-type in the object's headers: the value returned by + contentType() takes priority. + + Note also that the content-type may be null, in the specific + case of the top-level body of a non-MIME message. + */ + String contentType(); + + /** Returns the headers which describe this object. + This may be null in some cases, including the case of the + outermost object. These headers describe the object itself, + not the contents of the object: so, for a message/rfc822 object, + these would be the headers which contained a Content-Type of + message/rfc822. The interior headers, those describing the + body of the object, are found in the child. + */ + InternetHeaders headers(); + + /** Returns the children of the object, or null if it has no children + or is not a container at all. + */ + Enumeration children(); + + /** Returns the ID string of the MIME part. + This is the parent-relative index string, like "1.2.3" + as found in mailbox: and news: URLs. + This will be null for the outermost part. + The first child of the outer part is "1"; + the third child of that child is "1.3"; and so on. + */ + String partID(); + +// IMimeObject findChild(String partNumber); +// String suggestedFileName(); +// ...various other utility routines... + +} diff --git a/mozilla/grendel/mime/IMimeOperator.java b/mozilla/grendel/mime/IMimeOperator.java new file mode 100644 index 00000000000..5723e5d1b87 --- /dev/null +++ b/mozilla/grendel/mime/IMimeOperator.java @@ -0,0 +1,61 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime; + +import calypso.util.ByteBuf; +import java.util.Enumeration; + +/** This is the interface of objects which operate on a parsed tree of + MIME objects. When an IMimeParser is created to parse a stream, an + implementor of IMimeOperator is created along with it, to operate + on the MIME objects as they are parsed. Each IMimeOperator has + an IMimeObject associated with it, representing the node currently + being parsed. It may use that to interrogate information like + content-types and other header values. + + @see grendel.mime.html.MimeHTMLOperatorFactory + @see IMimeObject + @see IMimeParser + */ + +public interface IMimeOperator { + + /** If the MimeObject is a leaf node, this method is called with the bytes + of the body. It may be called any number of times, including 0, and the + blocks will not necessarily fall on line boundaries. + */ + void pushBytes(ByteBuf b); + + /** If the MimeObject is not a leaf node, this method will be called each + time a new child object is seen. It should create and return a + corresponding MimeOperator (which may or may not be of the same type + as `this'.) + */ + IMimeOperator createChild(IMimeObject object); + + /** This method is called when the end of the MimeObject's body has been + seen. It is called exactly once, and is called both for leaf and + non-leaf MIME objects. Neither pushBytes() nor createChild() will + be called after pushEOF(). + */ + void pushEOF(); +} diff --git a/mozilla/grendel/mime/IMimeParser.java b/mozilla/grendel/mime/IMimeParser.java new file mode 100644 index 00000000000..f22cf4950a3 --- /dev/null +++ b/mozilla/grendel/mime/IMimeParser.java @@ -0,0 +1,55 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime; +import calypso.util.ByteBuf; + +/** + This is an interface for streaming parsing of MIME data. + @see grendel.mime.parser.MimeParserFactory + @see IMimeOperator + @see IMimeObject + */ + +public interface IMimeParser { + + /** Set the IMimeOperator associated with this parser. + Since the parsers and operators both need access to each other, + one will create a parser, then create an operator for it, + then point the parser at the operator using this method. + It is an error to try and change the operator once it has + already been set. + */ + void setOperator(IMimeOperator op); + + /** Feed bytes to be parsed into the parser. + */ + void pushBytes(ByteBuf buffer); + + /** Inform the parser that no more bytes will be forthcoming. + */ + void pushEOF(); + + /** Returns a corresponding object implementing the IMimeObject interface. + This will very likely be the same object. + */ + IMimeObject getObject(); +} diff --git a/mozilla/grendel/mime/Makefile b/mozilla/grendel/mime/Makefile new file mode 100644 index 00000000000..03c642c1fb2 --- /dev/null +++ b/mozilla/grendel/mime/Makefile @@ -0,0 +1,34 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SUBDIRS= \ + html \ + parser \ + encoder \ + extractor \ + $(NULL) + +SRCS= \ + HeaderCharsetDecoder.java \ + IMimeObject.java \ + IMimeOperator.java \ + IMimeParser.java \ + TestMime.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/mime/README b/mozilla/grendel/mime/README new file mode 100644 index 00000000000..50ed734f7d7 --- /dev/null +++ b/mozilla/grendel/mime/README @@ -0,0 +1,7 @@ +This is Grendel -- a Java mail/news client. + +The grendel.mime package implements a general MIME parser. +Sub-packages build specific tools on top of that MIME parser, +for example, converting a MIME object to an HTML representation. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/mime/TestMime.java b/mozilla/grendel/mime/TestMime.java new file mode 100644 index 00000000000..911866a69f3 --- /dev/null +++ b/mozilla/grendel/mime/TestMime.java @@ -0,0 +1,108 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 29 Aug 1997. + */ + +package grendel.mime; + +import java.io.RandomAccessFile; +import java.io.IOException; + +import calypso.util.ByteBuf; +import grendel.mime.parser.MimeParserFactory; +import grendel.mime.html.MimeHTMLOperatorFactory; + + +/* This silly little class implements an operator which draws every MIME + part as a labelled box, to give an overview of the MIME structure (and + incidentally to show that all of the operators are closed properly, + since the table won't show up if they're not.) + */ +class MimeDebugOperator implements IMimeOperator { + String type; + public MimeDebugOperator(IMimeObject obj) { + type = obj.contentType(); + System.out.print("

    "); + System.out.print(""); + System.out.print("
    " + + type + + (obj.partID() == null ? "" : + "
    " + obj.partID()) + + "
    \n"); + } + public void pushBytes(ByteBuf b) { } + public IMimeOperator createChild(IMimeObject object) { + return new MimeDebugOperator(object); + } + public void pushEOF() { + System.out.println("

    "); + } +} + + + +class TestMime { + public static void main(String args[]) { + + for (int i = 0; i < args.length; i++) { + try { + System.err.println("Starting..."); + long start = System.currentTimeMillis(); + RandomAccessFile file = new RandomAccessFile(args[i], "r"); + + String toplevel_type = "message/rfc822"; + + IMimeParser parser = MimeParserFactory.Make(toplevel_type); + IMimeOperator op; + + if (true) + op = MimeHTMLOperatorFactory.Make(parser.getObject()); + else + op = new MimeDebugOperator(parser.getObject()); + + parser.setOperator(op); + + byte[] bytes = new byte[1024]; + ByteBuf buf = new ByteBuf(bytes.length); + int bread; + do { + bread = file.read(bytes, 0, bytes.length); + if (bread > 0) { + buf.setLength(0); + buf.append(bytes, 0, bread); // this sucks! + parser.pushBytes(buf); + } + } while (bread >= 0); + bytes = null; + buf = null; + parser.pushEOF(); + + long end = System.currentTimeMillis(); + System.err.println("Took " + (end - start) + " milliseconds."); + + } catch (IOException e) { + System.err.println("Got an IO exception: " + e.getMessage()); + e.printStackTrace(); + return; + } + } + } +} diff --git a/mozilla/grendel/mime/encoder/Makefile b/mozilla/grendel/mime/encoder/Makefile new file mode 100644 index 00000000000..3ada214ec87 --- /dev/null +++ b/mozilla/grendel/mime/encoder/Makefile @@ -0,0 +1,29 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + MimeBase64Decoder.java \ + MimeBase64Encoder.java \ + MimeEncoder.java \ + MimeQuotedPrintableDecoder.java \ + MimeQuotedPrintableEncoder.java \ + MimeUUDecoder.java \ + MimeUUEncoder.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/mime/encoder/MimeBase64Decoder.java b/mozilla/grendel/mime/encoder/MimeBase64Decoder.java new file mode 100644 index 00000000000..a03b80a7617 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeBase64Decoder.java @@ -0,0 +1,181 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; + +/** Implements a plaintext -> Base64 encoder. + */ +public final class MimeBase64Decoder extends MimeEncoder { + + private byte token[] = new byte[4]; // input buffer + private byte bytes[] = new byte[3]; // output buffer + private int token_length = 0; // input buffer length + + static private final byte NUL = 127; // must be out of range 0-64 + static private final byte EOF = 126; // must be out of range 0-64 + + static private final byte map[] = { + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 000-007 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 010-017 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 020-027 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 030-037 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 040-047 !"#$%&' + NUL, NUL, NUL, 62, NUL, NUL, NUL, 63, // 050-057 ()*+,-./ + 52, 53, 54, 55, 56, 57, 58, 59, // 060-067 01234567 + 60, 61, NUL, NUL, NUL, EOF, NUL, NUL, // 070-077 89:;<=>? + + NUL, 0, 1, 2, 3, 4, 5, 6, // 100-107 @ABCDEFG + 7, 8, 9, 10, 11, 12, 13, 14, // 110-117 HIJKLMNO + 15, 16, 17, 18, 19, 20, 21, 22, // 120-127 PQRSTUVW + 23, 24, 25, NUL, NUL, NUL, NUL, NUL, // 130-137 XYZ[\]^_ + NUL, 26, 27, 28, 29, 30, 31, 32, // 140-147 `abcdefg + 33, 34, 35, 36, 37, 38, 39, 40, // 150-157 hijklmno + 41, 42, 43, 44, 45, 46, 47, 48, // 160-167 pqrstuvw + 49, 50, 51, NUL, NUL, NUL, NUL, NUL, // 170-177 xyz{|}~ + + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 200-207 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 210-217 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 220-227 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 230-237 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 240-247 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 250-257 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 260-267 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 270-277 + + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 300-307 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 310-317 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 320-327 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 330-337 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 340-347 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 350-357 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 360-367 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 370-377 + }; + + + // Fast routine that assumes full 4-char tokens with no '=' in them. + // + private final void decode_token(ByteBuf out) { + + int num = ((map[token[0]] << 18) | + (map[token[1]] << 12) | + (map[token[2]] << 6) | + (map[token[3]])); + + bytes[0] = (byte) (0xFF & (num >> 16)); + bytes[1] = (byte) (0xFF & (num >> 8)); + bytes[2] = (byte) (0xFF & num); + + out.append(bytes); + } + + + // Hairier routine that deals with the final token, which can have fewer + // than four characters, and that might be padded with '='. + // + private final void decode_final_token(ByteBuf out) { + + byte b0 = map[token[0]]; + byte b1 = map[token[1]]; + byte b2 = map[token[2]]; + byte b3 = map[token[3]]; + + int eq_count = 0; + + if (b0 == EOF) { b0 = 0; eq_count++; } + if (b1 == EOF) { b1 = 0; eq_count++; } + if (b2 == EOF) { b2 = 0; eq_count++; } + if (b3 == EOF) { b3 = 0; eq_count++; } + + int num = ((b0 << 18) | (b1 << 12) | (b2 << 6) | (b3)); + + // eq_count will be 0, 1, or 2. + // No "=" padding means 4 bytes mapped to 3, the normal case, + // not handled in this routine. + // "xxx=" means 3 bytes mapped to 2. + // "xx==" means 2 bytes mapped to 1. + // "x===" can't happen, because "x" would then be encoding + // only 6 bits, not 8, the minimum possible. + + out.append((byte) (num >> 16)); // byte 1, count = 0 or 1 or 2 + if (eq_count <= 1) { + out.append((byte) ((num >> 8) & 0xFF)); // byte 2, count = 0 or 1 + if (eq_count == 0) { + out.append((byte) (num & 0xFF)); // byte 3, count = 0 + } + } + } + + + /** Given a sequence of input bytes using the Base64 encoding, produces a + sequence of unencoded output bytes. Note that some (small) amount of + buffering may be necessary, if the input byte stream didn't fall on an + appropriate boundary. If there are bytes in `out' already, the new + bytes are appended, so the caller should do `out.setLength(0)' first + if that's desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + + if (token == null) // already saw eof marker? + return; + + byte inb[] = in.toBytes(); + int in_length = in.length(); + + for (int i = 0; i < in_length; i++) { + byte b = inb[i]; + byte t = map[b]; + + if (t != NUL) + token[token_length++] = b; + + if (t == EOF) { + eof(out); + return; + + } else if (token_length == 4) { + decode_token(out); + token_length = 0; + } + } + } + + + /** Tell the Base64 decoder that no more input data will be forthcoming. + This may result in output, as a result of flushing the internal buffer. + This object must not be used again after calling eof(). If there are + bytes in `out' already, the new bytes are appended, so the caller should + do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + if (token != null && token_length != 0) { + while (token_length < 4) + token[token_length++] = '='; + decode_final_token(out); + } + token_length = 0; + token = null; + bytes = null; + } + +} diff --git a/mozilla/grendel/mime/encoder/MimeBase64Encoder.java b/mozilla/grendel/mime/encoder/MimeBase64Encoder.java new file mode 100644 index 00000000000..a9fb7f2f01b --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeBase64Encoder.java @@ -0,0 +1,123 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; + +/** Implements a Base64 -> plaintext decoder. + */ +public final class MimeBase64Encoder extends MimeEncoder { + + private int buf = 0; // a 24-bit quantity + private int buf_bytes = 0; // how many octets are set in it + private byte line[] = new byte[74]; // output buffer + private int line_length = 0; // output buffer fill pointer + + static private final byte crlf[] = "\r\n".getBytes(); + + static private final byte map[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0-7 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 8-15 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 16-23 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 24-31 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 32-39 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 40-47 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 48-55 + '4', '5', '6', '7', '8', '9', '+', '/', // 56-63 + }; + + private final void encode_token() { + int i = line_length; + line[i] = map[0x3F & (buf >> 18)]; // sextet 1 (octet 1) + line[i+1] = map[0x3F & (buf >> 12)]; // sextet 2 (octet 1 and 2) + line[i+2] = map[0x3F & (buf >> 6)]; // sextet 3 (octet 2 and 3) + line[i+3] = map[0x3F & buf]; // sextet 4 (octet 3) + line_length += 4; + buf = 0; + buf_bytes = 0; + } + + private final void encode_partial_token() { + int i = line_length; + line[i] = map[0x3F & (buf >> 18)]; // sextet 1 (octet 1) + line[i+1] = map[0x3F & (buf >> 12)]; // sextet 2 (octet 1 and 2) + + if (buf_bytes == 1) + line[i+2] = '='; + else + line[i+2] = map[0x3F & (buf >> 6)]; // sextet 3 (octet 2 and 3) + + if (buf_bytes <= 2) + line[i+3] = '='; + else + line[i+3] = map[0x3F & buf]; // sextet 4 (octet 3) + line_length += 4; + buf = 0; + buf_bytes = 0; + } + + private final void flush_line(ByteBuf out) { + line[line_length] = '\r'; + line[line_length+1] = '\n'; + out.append(line, 0, line_length + 2); + line_length = 0; + } + + /** Given a sequence of input bytes, produces a sequence of output bytes + using the base64 encoding. If there are bytes in `out' already, the + new bytes are appended, so the caller should do `out.setLength(0)' + first if that's desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + + byte inb[] = in.toBytes(); + int in_length = in.length(); + + for (int i = 0; i < in_length; i++) { + if (buf_bytes == 0) + buf = (buf & 0x00FFFF) | (inb[i] << 16); + else if (buf_bytes == 1) + buf = (buf & 0xFF00FF) | (inb[i] << 8); + else + buf = (buf & 0xFFFF00) | (inb[i]); + + if ((++buf_bytes) == 3) { + encode_token(); + if (line_length >= 72) + flush_line(out); + } + } + } + + /** Tell the base64 encoder that no more input data will be forthcoming. + This may result in output, as a result of flushing the internal buffer. + This object must not be used again after calling eof(). If there are + bytes in `out' already, the new bytes are appended, so the caller should + do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + if (buf_bytes != 0) + encode_partial_token(); + flush_line(out); + line = null; + } +} diff --git a/mozilla/grendel/mime/encoder/MimeEncoder.java b/mozilla/grendel/mime/encoder/MimeEncoder.java new file mode 100644 index 00000000000..be6da58ba44 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeEncoder.java @@ -0,0 +1,53 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +/** This abstract class is the parent of those classes which implement + MIME encoding and decoding: base64, quoted-printable, and uuencode. + @see MimeBase64Encoder + @see MimeBase64Decoder + @see MimeQuotedPrintableEncoder + @see MimeQuotedPrintableDecoder + @see MimeUUEncoder + @see MimeUUDecoder + */ + +import calypso.util.ByteBuf; + +public abstract class MimeEncoder { + + /** Given a sequence of input bytes, produces a sequence of output bytes. + Note that some (small) amount of buffering may be necessary, if the + input byte stream didn't fall on an appropriate boundary. If there + are bytes in `out' already, the new bytes are appended, so the + caller should do `out.setLength(0)' first if that's desired. + */ + abstract public void translate(ByteBuf in, ByteBuf out); + + /** Tell the decoder that no more input data will be forthcoming. + This may result in output, as a result of flushing the internal + buffer. This object must not be used again after calling eof(). + If there are bytes in `out' already, the new bytes are appended, + so the caller should do `out.setLength(0)' first if that's desired. + */ + abstract public void eof(ByteBuf out); +} diff --git a/mozilla/grendel/mime/encoder/MimeQuotedPrintableDecoder.java b/mozilla/grendel/mime/encoder/MimeQuotedPrintableDecoder.java new file mode 100644 index 00000000000..b0a687536b2 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeQuotedPrintableDecoder.java @@ -0,0 +1,164 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; + +/** Implements a Quoted-Printable -> plaintext decoder. + */ +public class MimeQuotedPrintableDecoder extends MimeEncoder { + + private byte token[] = new byte[3]; // input and output buffer + private int token_length = 0; // input buffer length + + static private final byte NUL = 127; // must be greater than 16 + static private final byte ESC = 126; // must be greater than 16 + static private final byte CR = 125; // must be greater than 16 + static private final byte LF = 124; // must be greater than 16 + + static private final byte map[] = { + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 000-007 + NUL, NUL, LF, NUL, NUL, CR, NUL, NUL, // 010-017 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 020-027 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 030-037 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 040-047 !"#$%&' + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 050-057 ()*+,-./ + 0, 1, 2, 3, 4, 5, 6, 7, // 060-067 01234567 + 8, 9, NUL, NUL, NUL, ESC, NUL, NUL, // 070-077 89:;<=>? + + NUL, 10, 11, 12, 13, 14, 15, NUL, // 100-107 @ABCDEFG + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 110-117 HIJKLMNO + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 120-127 PQRSTUVW + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 130-137 XYZ[\]^_ + NUL, 10, 11, 12, 13, 14, 15, NUL, // 140-147 `abcdefg + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 150-157 hijklmno + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 160-167 pqrstuvw + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 170-177 xyz{|}~ + + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 200-207 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 210-217 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 220-227 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 230-237 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 240-247 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 250-257 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 260-267 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 270-277 + + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 300-307 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 310-317 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 320-327 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 330-337 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 340-347 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 350-357 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 360-367 + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 370-377 + }; + + + /** Given a sequence of input bytes using the quoted-printable encoding, + produces a sequence of unencoded output bytes. Note that some (small) + amount of buffering may be necessary, if the input byte stream didn't + fall on an appropriate boundary. If there are bytes in `out' already, + the new bytes are appended, so the caller should do `out.setLength(0)' + first if that's desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + + /* To Do, maybe someday: when decoding quoted-printable, we are required + to strip trailing whitespace from lines -- since when encoding in QP, + one is required to quote such trailing whitespace, any trailing + whitespace which remains must have been introduced by a stupid gateway. + We don't do this right now, as it would require arbitrary lookahead + instead of just a fixed distance of three bytes. + */ + + byte inb[] = in.toBytes(); + int in_length = in.length(); + + int i = 0; + + while (i < in_length) { + + // First, copy as many consecutive bytes as we can. + if (token_length == 0) { + int j = i; + while (i < in_length && map[inb[i]] != ESC) + i++; + if (i != j) + out.append(inb, j, i-j); + + if (i >= in_length) + break; + } + + // at this point, either token_length > 0, or we've seen ESC + + byte b = inb[i]; + byte c = map[b]; + i++; + + token[token_length++] = b; + + if (token_length != 3) + continue; + + token_length = 0; + + // assert (map[token[0]] == ESC) + + byte n1 = map[token[1]]; + byte n2 = map[token[2]]; + + if (n1 == CR || n1 == LF) { // "=\r", "=\n", or "=\r\n" + if (n1 == LF || n2 != LF) { + // lone CR or LF -- push out or put back following character. + if (n2 == ESC) { + token[0] = token[2]; + token_length = 1; + } else { + out.append(token[2]); + } + } + + } else if (n1 <= 15 || n2 <= 15) { // "=xy" where x and y are hex + out.append((byte) ((n1 << 4) | n2)); + + } else { // something unparsable. + out.append(token); // write it out unchanged. + } + + } + } + + + /** Tell the quoted-printable decoder that no more input data will be + forthcoming. This may result in output, as a result of flushing the + internal buffer. This object must not be used again after calling eof(). + If there are bytes in `out' already, the new bytes are appended, + so the caller should do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + // ignore the last incomplete token, if any -- it means "no trailing CRLF". + token_length = 0; + token = null; + } +} diff --git a/mozilla/grendel/mime/encoder/MimeQuotedPrintableEncoder.java b/mozilla/grendel/mime/encoder/MimeQuotedPrintableEncoder.java new file mode 100644 index 00000000000..9bd13be6109 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeQuotedPrintableEncoder.java @@ -0,0 +1,170 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; +import calypso.util.Assert; + +/** Implements a plaintext -> Quoted-Printable encoder. + */ +public class MimeQuotedPrintableEncoder extends MimeEncoder { + + boolean had_whitespace = false; + int output_column = 0; + byte last_code = NUL; + + static private final byte NUL = 0; // don't encode + static private final byte ESC = 1; // always encode + static private final byte SPC = 2; // horizontal whitespace + static private final byte BOL = 3; // encode if at beginning of line + static private final byte CR = 4; // carriage return + static private final byte LF = 5; // linefeed + + static private final byte map[] = { + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 000-007 + ESC, SPC, LF, ESC, ESC, CR, ESC, ESC, // 010-017 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 020-027 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 030-037 + SPC, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 040-047 !"#$%&' + NUL, NUL, NUL, NUL, NUL, NUL, BOL, NUL, // 050-057 ()*+,-./ + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 060-067 01234567 + NUL, NUL, NUL, NUL, NUL, ESC, NUL, NUL, // 070-077 89:;<=>? + + NUL, NUL, NUL, NUL, NUL, NUL, BOL, NUL, // 100-107 @ABCDEFG + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 110-117 HIJKLMNO + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 120-127 PQRSTUVW + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 130-137 XYZ[\]^_ + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 140-147 `abcdefg + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 150-157 hijklmno + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, // 160-167 pqrstuvw + NUL, NUL, NUL, NUL, NUL, NUL, NUL, ESC, // 170-177 xyz{|}~ + + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 200-207 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 210-217 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 220-227 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 230-237 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 240-247 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 250-257 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 260-267 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 270-277 + + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 300-307 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 310-317 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 320-327 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 330-337 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 340-347 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 350-357 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 360-367 + ESC, ESC, ESC, ESC, ESC, ESC, ESC, ESC, // 370-377 + }; + + static private final byte hex[] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + static private final byte crlf[] = "\r\n".getBytes(); + static private final byte eq_crlf[] = "=\r\n".getBytes(); + static private final byte eq_crlf_crlf[] = "=\r\n\r\n".getBytes(); + + + /** Given a sequence of input bytes, produces a sequence of output bytes + using the quoted-printable encoding. If there are bytes in `out' + already, the new bytes are appended, so the caller should do + `out.setLength(0)' first if that's desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + + byte inb[] = in.toBytes(); + int in_length = in.length(); + + for (int i = 0; i < in_length; i++) { + byte b = inb[i]; + byte c = map[b]; + + if (c == NUL) { // non-special character + out.append(b); + output_column++; + had_whitespace = false; + + } else if (c == SPC) { // whitespace + out.append(b); + output_column++; + had_whitespace = true; + + } else if (c == LF && last_code == CR) { + // We already processed this linebreak; + // ignore the LF in the CRLF pair. + + } else if (c == CR || c == LF) { // start of linebreak + + if (had_whitespace) { + // Whitespace cannot be allowed to occur at the end of the line. + // So we encode " \n" as " =\n\n", that is, the whitespace, a + // soft line break, and then a hard line break. + // + out.append(eq_crlf_crlf); + had_whitespace = false; + } else { + out.append(crlf); + } + output_column = 0; + + } else if (c == BOL && output_column != 0) { // beginning-of-line + out.append(b); // special, while not + output_column++; // at beginning-of-line + had_whitespace = false; + + } else { // special character + out.append((byte) '='); + out.append((byte) hex[0xF & (b >> 4)]); + out.append((byte) hex[0xF & b]); + output_column += 3; + had_whitespace = false; + } + + last_code = c; + + Assert.Assertion(output_column <= 76); + + if (output_column >= 73) { + out.append(eq_crlf); + output_column = 0; + had_whitespace = false; + } + } + } + + /** Tell the quoted-printable encoder that no more input data will be + forthcoming. This may result in output, as a result of flushing the + internal buffer. This object must not be used again after calling eof(). + If there are bytes in `out' already, the new bytes are appended, + so the caller should do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + if (output_column != 0) { + out.append(eq_crlf); + output_column = 0; + had_whitespace = false; + } + } +} diff --git a/mozilla/grendel/mime/encoder/MimeUUDecoder.java b/mozilla/grendel/mime/encoder/MimeUUDecoder.java new file mode 100644 index 00000000000..6b1591cb817 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeUUDecoder.java @@ -0,0 +1,196 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; + +/** Implements a uuencode -> plaintext decoder. + */ +public final class MimeUUDecoder extends MimeEncoder { + + // Maps ASCII characters to their corresponding uuencode 6-bit values. + static private final byte map[] = { + 0, 0, 0, 0, 0, 0, 0, 0, // 000-007 + 0, 0, 0, 0, 0, 0, 0, 0, // 010-017 + 0, 0, 0, 0, 0, 0, 0, 0, // 020-027 + 0, 0, 0, 0, 0, 0, 0, 0, // 030-037 + 0, 1, 2, 3, 4, 5, 6, 7, // 040-047 !"#$%&' + 8, 9, 10, 11, 12, 13, 14, 15, // 050-057 ()*+,-./ + 16, 17, 18, 19, 20, 21, 22, 23, // 060-067 01234567 + 24, 25, 26, 27, 28, 29, 30, 31, // 070-077 89:;<=>? + + 32, 33, 34, 35, 36, 37, 38, 39, // 100-107 @ABCDEFG + 40, 41, 42, 43, 44, 45, 46, 47, // 110-117 HIJKLMNO + 48, 49, 50, 51, 52, 53, 54, 55, // 120-127 PQRSTUVW + 56, 57, 58, 59, 60, 61, 62, 63, // 130-137 XYZ[\]^_ + 0, 0, 0, 0, 0, 0, 0, 0, // 140-147 `abcdefg + 0, 0, 0, 0, 0, 0, 0, 0, // 150-157 hijklmno + 0, 0, 0, 0, 0, 0, 0, 0, // 160-167 pqrstuvw + 0, 0, 0, 0, 0, 0, 0, 0, // 170-177 xyz{|}~ + + 0, 0, 0, 0, 0, 0, 0, 0, // 200-207 + 0, 0, 0, 0, 0, 0, 0, 0, // 210-217 + 0, 0, 0, 0, 0, 0, 0, 0, // 220-227 + 0, 0, 0, 0, 0, 0, 0, 0, // 230-237 + 0, 0, 0, 0, 0, 0, 0, 0, // 240-247 + 0, 0, 0, 0, 0, 0, 0, 0, // 250-257 + 0, 0, 0, 0, 0, 0, 0, 0, // 260-267 + 0, 0, 0, 0, 0, 0, 0, 0, // 270-277 + + 0, 0, 0, 0, 0, 0, 0, 0, // 300-307 + 0, 0, 0, 0, 0, 0, 0, 0, // 310-317 + 0, 0, 0, 0, 0, 0, 0, 0, // 320-327 + 0, 0, 0, 0, 0, 0, 0, 0, // 330-337 + 0, 0, 0, 0, 0, 0, 0, 0, // 340-347 + 0, 0, 0, 0, 0, 0, 0, 0, // 350-357 + 0, 0, 0, 0, 0, 0, 0, 0, // 360-367 + 0, 0, 0, 0, 0, 0, 0, 0, // 370-377 + }; + + static private final int BEGIN = 100; + static private final int BODY = 101; + static private final int END = 102; + int state = BEGIN; + + + ByteBuf line_buf = new ByteBuf(80); // we buffer by lines while decoding + ByteBuf out_buf = new ByteBuf(80); // line buffering output is faster + + + private final void decode_line(ByteBuf out) { + + byte line[] = line_buf.toBytes(); + int length = line_buf.length(); + + if (state == BODY && + length == 3 && + line[0] == 'e' && + line[1] == 'n' && + line[2] == 'd') { + state = END; + line_buf = null; + + } else if (state == BEGIN) { + if (length >= 9 && + line[0] == 'b' && + line[1] == 'e' && + line[2] == 'g' && + line[3] == 'i' && + line[4] == 'n' && + line[5] == ' ') { + state = BODY; + } + + } else if (length > 1) { // state == BODY + + // We map down `line', reading four bytes and writing three. + + out_buf.ensureCapacity(length); + byte outb[] = out_buf.toBytes(); + int outi = 0; + + int i = map[line[0]]; + int lost = i - (((length - 1) * 3) / 4); + + // If lost is >0 get here, then the line is shorter than the length byte + // at the beginning says it should be: it's internally inconsistent. We + // will parse from it what we can... + // + // This probably happened because some gateway stripped trailing + // whitespace from the end of the line -- so pretend the line was padded + // with spaces (which conveniently map to \000...) + if (lost > 0) + i -= lost; + + int in = 0; + for (++in; i > 0; in += 4, i -= 3) { + if (i >= 3) { + // We read four; write three. + outb[outi++] = (byte) (map[line[in]] << 2 | map[line[in+1]] >> 4); + outb[outi++] = (byte) (map[line[in+1]] << 4 | map[line[in+2]] >> 2); + outb[outi++] = (byte) (map[line[in+2]] << 6 | map[line[in+3]]); + + } else { + // Handle a line that isn't a multiple of 4 long. + // (We read 1, 2, or 3, and will write 1 or 2.) + outb[outi++] = (byte)(map[line[in]] << 2 | map[line[in+1]] >> 4); + if (i == 2) { + outb[outi++] = (byte)(map[line[in+1]] << 4 | map[line[in+2]] >> 2); + } + } + } + + // If the line was truncated, pad the missing bytes with 0 (SPC). */ + while (lost > 0) { + outb[outi++] = 0; + lost--; + } + + // Append to `out' a line at a time, so that we don't make one method + // call per *byte*... (It's more like one per 38 or something.) + out.append(outb, 0, outi); + } + + if (line_buf != null) + line_buf.setLength(0); + } + + + /** Given a sequence of input bytes in the uuencode encoding, produces a + sequence of unencoded output bytes. Note that some (small) amount of + buffering may be necessary, if the input byte stream didn't fall on an + appropriate boundary. If there are bytes in `out' already, the new + bytes are appended, so the caller should do `out.setLength(0)' first + if that's desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + if (line_buf == null) + return; + byte inb[] = in.toBytes(); + int in_length = in.length(); + for (int i = 0; i < in_length; i++) { + if (inb[i] == '\r' || inb[i] == '\n') { + if (line_buf.length() != 0) { + decode_line(out); + if (line_buf == null) + return; + } + } else { + line_buf.append(inb[i]); + } + } + } + + /** Tell the uudecoder that no more input data will be forthcoming. This + may result in output, as a result of flushing the internal buffer. + This object must not be used again after calling eof(). If there are + bytes in `out' already, the new bytes are appended, so the caller should + do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + if (line_buf != null && + line_buf.length() != 0) + decode_line(out); + line_buf = null; + out_buf = null; + } +} diff --git a/mozilla/grendel/mime/encoder/MimeUUEncoder.java b/mozilla/grendel/mime/encoder/MimeUUEncoder.java new file mode 100644 index 00000000000..35e79dc1f30 --- /dev/null +++ b/mozilla/grendel/mime/encoder/MimeUUEncoder.java @@ -0,0 +1,125 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.encoder; + +import calypso.util.ByteBuf; + +/** Implements a plaintext -> uuencode encoder. + */ +public final class MimeUUEncoder extends MimeEncoder { + + private byte name[] = null; // name for the "begin" line + private int buf = 0; // a 24-bit quantity + private int buf_bytes = 0; // how many octets are set in it + private byte line[] = new byte[63]; // output buffer + private int line_length = 1; // output buffer fill pointer + + static private final byte begin_644[] = "begin 644 ".getBytes(); + static private final byte crlf[] = "\r\n".getBytes(); + static private final byte end_crlf[] = "end\r\n".getBytes(); + + static private final byte map[] = { + '`', '!', '"', '#', '$', '%', '&', '\'', // 0-7 + '(', ')', '*', '+', ',', '-', '.', '/', // 8-15 + '0', '1', '2', '3', '4', '5', '6', '7', // 16-23 + '8', '9', ':', ';', '<', '=', '>', '?', // 24-31 + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', // 32-39 + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', // 40-47 + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', // 48-55 + 'X', 'Y', 'Z', '[', '\\',']', '^', '_', // 56-63 + }; + + + public MimeUUEncoder(String file_name) { + this.name = file_name.getBytes(); + } + + + private final void encode_token() { + int i = line_length; + line[i] = map[0x3F & (buf >> 18)]; // sextet 1 (octet 1) + line[i+1] = map[0x3F & (buf >> 12)]; // sextet 2 (octet 1 and 2) + line[i+2] = map[0x3F & (buf >> 6)]; // sextet 3 (octet 2 and 3) + line[i+3] = map[0x3F & buf]; // sextet 4 (octet 3) + line_length += 4; + buf = 0; + buf_bytes = 0; + } + + private final void flush_line(ByteBuf out, int offset) { + line[0] = map[(line_length * 3 / 4) - offset]; + line[line_length] = '\r'; + line[line_length+1] = '\n'; + out.append(line, 0, line_length + 2); + line_length = 1; + } + + /** Given a sequence of input bytes, produces a sequence of output bytes + using uu encoding. If there are bytes in `out' already, the new bytes + are appended, so the caller should do `out.setLength(0)' first if that's + desired. + */ + public final void translate(ByteBuf in, ByteBuf out) { + if (name != null) { + out.append(begin_644); + out.append(name); + out.append(crlf); + name = null; + } + + byte inb[] = in.toBytes(); + int in_length = in.length(); + + for (int i = 0; i < in_length; i++) { + if (buf_bytes == 0) + buf = (buf & 0x00FFFF) | (inb[i] << 16); + else if (buf_bytes == 1) + buf = (buf & 0xFF00FF) | (inb[i] << 8); + else + buf = (buf & 0xFFFF00) | (inb[i]); + + if ((++buf_bytes) == 3) { + encode_token(); + if (line_length >= 60) + flush_line(out, 0); + } + } + } + + /** Tell the uu encoder that no more input data will be forthcoming. + This may result in output, as a result of flushing the internal buffer. + This object must not be used again after calling eof(). If there are + bytes in `out' already, the new bytes are appended, so the caller should + do `out.setLength(0)' first if that's desired. + */ + public final void eof(ByteBuf out) { + if (buf_bytes != 0) { + int off = 3 - buf_bytes; + encode_token(); + flush_line(out, off); + } + flush_line(out, 0); + name = null; + line = null; + out.append(end_crlf); + } +} diff --git a/mozilla/grendel/mime/encoder/README b/mozilla/grendel/mime/encoder/README new file mode 100644 index 00000000000..e0d58a240d9 --- /dev/null +++ b/mozilla/grendel/mime/encoder/README @@ -0,0 +1,6 @@ +This is Grendel -- a Java mail/news client. + +The grendel.mime.encoder package implements encoding of MIME objects, +including the various MIME content-transfer-encodings. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/mime/extractor/Makefile b/mozilla/grendel/mime/extractor/Makefile new file mode 100644 index 00000000000..a5081e76134 --- /dev/null +++ b/mozilla/grendel/mime/extractor/Makefile @@ -0,0 +1,23 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + MimeExtractorOperatorFactory.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/mime/extractor/MimeExtractorOperatorFactory.java b/mozilla/grendel/mime/extractor/MimeExtractorOperatorFactory.java new file mode 100644 index 00000000000..4c706ca1ae5 --- /dev/null +++ b/mozilla/grendel/mime/extractor/MimeExtractorOperatorFactory.java @@ -0,0 +1,73 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 2 Sep 1997. + */ + +package grendel.mime.extractor; + +import calypso.util.ByteBuf; +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; + + +/** Creates an IMimeOperator to extract the raw data of a MIME sub-part. + The ID is of the form used by IMimeObject.partID() -- it is a string + listing the index of the part in the MIME tree, like "1.2.5". + */ +public class MimeExtractorOperatorFactory { + public static IMimeOperator Make(IMimeObject root_object, String target_id) { + return new MimeExtractorOperator(root_object, target_id); + } +} + + +/** This class extracts a particular MIME part, by number. + */ +class MimeExtractorOperator implements IMimeOperator { + + String target_id = null; + boolean this_one = false; + boolean done = false; + + MimeExtractorOperator(IMimeObject object, String target_id) { + this.target_id = target_id; + if (!done) { + String id = object.partID(); + if (id == target_id || + (id != null && target_id != null && + id.equalsIgnoreCase(target_id))) + this_one = true; + } + } + + public IMimeOperator createChild(IMimeObject child_object) { + return new MimeExtractorOperator(child_object, target_id); + } + + public void pushBytes(ByteBuf b) { + if (this_one) { + System.out.print(b.toString()); + } + } + + public void pushEOF() { + if (this_one) + done = true; + } +} diff --git a/mozilla/grendel/mime/extractor/README b/mozilla/grendel/mime/extractor/README new file mode 100644 index 00000000000..37ed689995e --- /dev/null +++ b/mozilla/grendel/mime/extractor/README @@ -0,0 +1,7 @@ +This is Grendel -- a Java mail/news client. + +The grendel.mime.extractor package implements an IMimeOperator that +knows how to extract a given part from a compound MIME object, by +part-number. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/mime/html/BriefHeaderFormatter.java b/mozilla/grendel/mime/html/BriefHeaderFormatter.java new file mode 100644 index 00000000000..b7983e53cbf --- /dev/null +++ b/mozilla/grendel/mime/html/BriefHeaderFormatter.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 31 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; + +class BriefHeaderFormatter extends HeaderFormatter { + public void formatHeaders(InternetHeaders headers, StringBuffer output) { + throw new Error("unimplemented"); + } +} diff --git a/mozilla/grendel/mime/html/FullHeaderFormatter.java b/mozilla/grendel/mime/html/FullHeaderFormatter.java new file mode 100644 index 00000000000..7cd1e2d2e5c --- /dev/null +++ b/mozilla/grendel/mime/html/FullHeaderFormatter.java @@ -0,0 +1,72 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 31 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import javax.mail.Header; +import java.util.Enumeration; + +class FullHeaderFormatter extends HeaderFormatter { + + public void formatHeaders(InternetHeaders headers, StringBuffer output) { + + startHeaderOutput(output); + + for (Enumeration e = headers.getAllHeaders(); e.hasMoreElements();) { + Header hh = (Header) e.nextElement(); + String h = hh.getName(); + String v = hh.getValue(); + + if (h.equalsIgnoreCase("Subject")) + writeSubjectHeader(h, v, output); + + else if (h.equalsIgnoreCase("References") || + h.equalsIgnoreCase("Message-ID") || + h.equalsIgnoreCase("Resent-Message-ID") || + h.equalsIgnoreCase("Supersedes")) + writeIDHeader(h, v, output); + + else if (h.equalsIgnoreCase("Sender") || + h.equalsIgnoreCase("From") || + h.equalsIgnoreCase("Resent-Sender") || + h.equalsIgnoreCase("Resent-From") || + h.equalsIgnoreCase("Reply-To") || + h.equalsIgnoreCase("Resent-To") || + h.equalsIgnoreCase("Resent-CC") || + h.equalsIgnoreCase("To") || + h.equalsIgnoreCase("CC") || + h.equalsIgnoreCase("BCC")) + + writeAddressHeader(h, v, output); + + else if (h.equalsIgnoreCase("Newsgroups") || + h.equalsIgnoreCase("Followup-To") || + h.equalsIgnoreCase("Posted-To")) + writeNewsgroupHeader(h, v, output); + + else + writeRandomHeader(h, v, output); + } + finishHeaderOutput(output); + } +} diff --git a/mozilla/grendel/mime/html/HeaderFormatter.java b/mozilla/grendel/mime/html/HeaderFormatter.java new file mode 100644 index 00000000000..fe5dc972e9f --- /dev/null +++ b/mozilla/grendel/mime/html/HeaderFormatter.java @@ -0,0 +1,214 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 31 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; + + +/** This class provides a method which knows how to convert an + InternetHeaders object to an HTML representation. + */ + +abstract class HeaderFormatter { + abstract public void formatHeaders(InternetHeaders headers, + StringBuffer output); + + /** Translates an RFC-mandated message header name (like "Subject") + into a localized string which should be presented to the user. + */ + String localizeHeaderName(String header) { + return header; + } + + /** Called to translate plain-text to an HTML-presentable form. */ + void quoteHTML(StringBuffer string) { + TextHTMLConverter.quoteForHTML(string, true, false); + } + + /** Called when beginning to output a header block. This opens the table. */ + void startHeaderOutput(StringBuffer output) { + output.append(""); + } + + /** Called when done filling a header block. This closes the table. */ + void finishHeaderOutput(StringBuffer output) { + output.append("
    "); + } + + /*************************************************************************/ + + boolean writeAddressHeader(String header, InternetHeaders headers, + StringBuffer output) { + String values[] = headers.getHeader(header); + if (values == null || values.length == 0) + return false; + else { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeRandomHeader(header, value, output); + } + } + + boolean writeAddressHeader(String header, String value, + StringBuffer output) { + return writeAddressHeader(header, new StringBuffer(value), output); + } + + boolean writeAddressHeader(String header, StringBuffer value, + StringBuffer output) { + // #### write me + return writeRandomHeader(header, value, output); + } + + /*************************************************************************/ + + boolean writeNewsgroupHeader(String header, InternetHeaders headers, + StringBuffer output) { + String values[] = headers.getHeader(header); + if (values == null || values.length == 0) + return false; + else { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeNewsgroupHeader(header, value, output); + } + } + + boolean writeNewsgroupHeader(String header, String value, + StringBuffer output) { + return writeNewsgroupHeader(header, new StringBuffer(value), output); + } + + boolean writeNewsgroupHeader(String header, StringBuffer value, + StringBuffer output) { + // #### write me + return writeRandomHeader(header, value, output); + } + + + /*************************************************************************/ + + boolean writeIDHeader(String header, InternetHeaders headers, + StringBuffer output) { + String values[] = headers.getHeader(header); + if (values == null || values.length == 0) + return false; + else { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeIDHeader(header, value, output); + } + } + + boolean writeIDHeader(String header, String value, StringBuffer output) { + return writeIDHeader(header, new StringBuffer(value), output); + } + + boolean writeIDHeader(String header, StringBuffer value, + StringBuffer output) { + // #### write me + return writeRandomHeader(header, value, value); + } + + + /*************************************************************************/ + + boolean writeSubjectHeader(String header, InternetHeaders headers, + StringBuffer output) { + String values[] = headers.getHeader(header); + if (values == null || values.length == 0) + return false; + else { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeSubjectHeader(header, value, output); + } + } + + boolean writeSubjectHeader(String header, String value, + StringBuffer output) { + return writeSubjectHeader(header, new StringBuffer(value), output); + } + + boolean writeSubjectHeader(String header, StringBuffer value, + StringBuffer output) { + // #### write me + return writeRandomHeader(header, value, output); + } + + + /*************************************************************************/ + + boolean writeRandomHeader(String header, InternetHeaders headers, + StringBuffer output, + boolean all_p) { + String values[] = headers.getHeader(header); + if (values == null || values.length == 0) + return false; + else { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeRandomHeader(header, value, output); + } + } + + boolean writeRandomHeader(String header, String value, StringBuffer output) { + return writeRandomHeader(header, new StringBuffer(value), output); + } + + boolean writeRandomHeader(String header, String values[], + StringBuffer output) { + String value = ""; + for (int i = 0; i < values.length; i++) { + if (i > 0) value += "\r\n\t"; + value += values[i]; + } + return writeRandomHeader(header, value, output); + } + + boolean writeRandomHeader(String header, StringBuffer value, + StringBuffer output) { + output.append(""); + output.append(localizeHeaderName(header)); + output.append(": "); + quoteHTML(value); + output.append(value); + output.append(""); + return true; + } +} diff --git a/mozilla/grendel/mime/html/Makefile b/mozilla/grendel/mime/html/Makefile new file mode 100644 index 00000000000..b8347c37543 --- /dev/null +++ b/mozilla/grendel/mime/html/Makefile @@ -0,0 +1,43 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + BriefHeaderFormatter.java \ + FullHeaderFormatter.java \ + HeaderFormatter.java \ + MimeContainerOperator.java \ + MimeExternalBodyOperator.java \ + MimeExternalObjectOperator.java \ + MimeHTMLOperator.java \ + MimeHTMLOperatorFactory.java \ + MimeLeafOperator.java \ + MimeMessageOperator.java \ + MimeMultipartAlternativeOperator.java \ + MimeMultipartAppleDoubleOperator.java \ + MimeMultipartRelatedOperator.java \ + MimeMultipartSignedOperator.java \ + MimeTextEnrichedOperator.java \ + MimeTextHTMLOperator.java \ + MimeTextOperator.java \ + MimeTextRichtextOperator.java \ + MimeTextVCardOperator.java \ + NormalHeaderFormatter.java \ + TextHTMLConverter.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/mime/html/MimeContainerOperator.java b/mozilla/grendel/mime/html/MimeContainerOperator.java new file mode 100644 index 00000000000..ecf9f3c4387 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeContainerOperator.java @@ -0,0 +1,57 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts multipart/ types to HTML: mainly this involves + emitting HRs between the children. + */ + +class MimeContainerOperator extends MimeHTMLOperator { + + boolean any_kids = false; + + MimeContainerOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public IMimeOperator createChild(IMimeObject child) { + + if (any_kids) + // Push out an HR between each child. + getOut().print("


    "); + any_kids = true; + + return super.createChild(child); + } + + public void pushBytes(ByteBuf b) { + throw new Error("containers don't get bytes!"); + } + + public void pushEOF() { + } +} diff --git a/mozilla/grendel/mime/html/MimeExternalBodyOperator.java b/mozilla/grendel/mime/html/MimeExternalBodyOperator.java new file mode 100644 index 00000000000..46a43a9475f --- /dev/null +++ b/mozilla/grendel/mime/html/MimeExternalBodyOperator.java @@ -0,0 +1,227 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.io.PrintStream; + +/** This class converts the message/external-body MIME type to HTML + that points at the remote document. + */ + +class MimeExternalBodyOperator extends MimeExternalObjectOperator { + + InternetHeaders body_headers; + MimeExternalBodyBody body; + + MimeExternalBodyOperator(IMimeObject object, PrintStream out) { + super(object, out); + body_headers = new InternetHeaders(); + } + + void writeBoxEarly() {} // we need to wait for EOF to write the box. + + public void pushBytes(ByteBuf b) { + throw new Error("containers don't get bytes"); + } + + public IMimeOperator createChild(IMimeObject child) { + // Children of message/external-body are always operators of this type. + // The parent and child types know about each other. + body = new MimeExternalBodyBody(child, getOut()); + return body; + } + + private String getHeaderValue(String name, InternetHeaders ih, + boolean strip) { + String hh[] = ih.getHeader(name); + if (hh == null || hh.length == 0) + return null; + String v = hh[0]; + // #### handle strip + return v; + } + + void make_header(String param_name, String header_value, + InternetHeaders input_headers, + InternetHeaders output_headers) { + ByteBuf scratch = new ByteBuf(); +//#### +// ByteBuf bb = new ByteBuf(header_value); +// if (input_headers.getParameter(bb, param_name, scratch)) { +// +// // Kludge for URLs... if this is a URL parameter, remove all whitespace +// // from it. (The URL parameter to one of these headers is stored with +// // lines broken every 40 characters or less; it's assumed that all +// // significant whitespace was URL-hex-encoded, and all the rest of it +// // was inserted just to keep the lines short.) +// if (param_name.equalsIgnoreCase("URL")) { +// int i = 0; +// while (i < scratch.length()) { +// if ((scratch.toBytes())[i] <= ' ') +// scratch.remove(i, i+1); +// else +// i++; +// } +// } +// +// scratch.insert(0, param_name + ": "); +// output_headers.addLine(scratch); +// } + } + + public void pushEOF() { + + ByteBuf line = new ByteBuf(); + + // silly constructed headers used for display purposes + InternetHeaders dpy_headers = new InternetHeaders(); + + // the *outer* content-type (message/external-body with parameters.) + String ct = getHeaderValue("Content-Type", headers, false); + + make_header("Access-Type", ct, headers, dpy_headers); + make_header("URL", ct, headers, dpy_headers); + make_header("Site", ct, headers, dpy_headers); + make_header("Server", ct, headers, dpy_headers); + make_header("Directory", ct, headers, dpy_headers); + make_header("Name", ct, headers, dpy_headers); + + // the *inner* content-type + if (body != null && + body.headers != null) { + String ict = getHeaderValue("Content-Type", body.headers, true); + if (ict != null) { + line.insert(0, "Type: "); + dpy_headers.addHeaderLine(ict); + } + } + + make_header("Size", ct, headers, dpy_headers); + make_header("Expiration", ct, headers, dpy_headers); + make_header("Permission", ct, headers, dpy_headers); + make_header("Mode", ct, headers, dpy_headers); + make_header("Subject", ct, headers, dpy_headers); + + String body_str = (body != null && body.buffer != null + ? body.buffer.toString() + : null); + if (body_str != null) { + body_str = body_str.trim(); + if (body_str.length() == 0) + body_str = null; + } + + String url = makeURL(dpy_headers, body_str); + String lname = (url == null + ? "Document Info" // #### I18N + : "Link to Document"); // #### I18N + + formatAttachmentBox(dpy_headers, null, lname, url, body_str, true); + dpy_headers = null; + body = null; + } + + String makeURL(InternetHeaders headers, String body) { + String at = getHeaderValue("Access-Type", headers, true); + if (at == null) + return null; + else if (at.equalsIgnoreCase("ftp") || at.equalsIgnoreCase("anon-ftp")) { + String site = getHeaderValue("Site", headers, true); + String dir = getHeaderValue("Directory", headers, true); + String name = getHeaderValue("name", headers, true); + if (site == null) + return null; + else { + String result = "ftp://" + site + "/"; + if (dir != null) { + if (dir.charAt(0) == '/') + dir = dir.substring(1); + result += dir; + if (result.charAt(result.length() - 1) != '/') + result += "/"; + } + if (name != null) + result += name; + return result; + } + } + + else if (at.equalsIgnoreCase("local-file") || at.equalsIgnoreCase("afs")) { + String name = getHeaderValue("Name", headers, true); + if (name == null) + return null; + // name = NET_Escape(name, URL_PATH); // #### + return "file:" + name; + } + + else if (at.equalsIgnoreCase("mail-server")) { + String server = getHeaderValue("Server", headers, true); + String subject = getHeaderValue("Subject", headers, false); + + if (server == null) + return null; + // server = NET_Escape(server, URL_XALPHAS); // #### + // subject = NET_Escape(subject, URL_XALPHAS); // #### + // body = NET_Escape(body, URL_XALPHAS); // #### + + String result = "mailto:" + server; + if (subject != null) + result += "?subject=" + subject; + if (body != null) { + if (subject != null) + result += "&"; + else + result += "?"; + result += "body=" + body; + } + return result; + } + + else if (at.equalsIgnoreCase("url")) { // RFC 2017 + return getHeaderValue("URL", headers, false); + } + + else + return null; + } +} + + +class MimeExternalBodyBody extends MimeTextOperator { + + MimeExternalBodyBody(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + // Store decoded bytes in the `buffer' StringBuffer. + // Store the whole body there. + decodeBytesToUnicode(b, buffer); + } + + public void pushEOF() { + } +} diff --git a/mozilla/grendel/mime/html/MimeExternalObjectOperator.java b/mozilla/grendel/mime/html/MimeExternalObjectOperator.java new file mode 100644 index 00000000000..2eaa9a7f6aa --- /dev/null +++ b/mozilla/grendel/mime/html/MimeExternalObjectOperator.java @@ -0,0 +1,151 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 26 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.io.PrintStream; + +/** This class converts unknown MIME types to HTML representing + an "attachment". + @see MimeHTMLOperator + */ + +class MimeExternalObjectOperator extends MimeLeafOperator { + + String part_id; + + MimeExternalObjectOperator(IMimeObject object, PrintStream out) { + super(object, out); + part_id = object.partID(); + writeBoxEarly(); + } + + void writeBoxEarly() { + String url = "about:jwz"; // #### + formatAttachmentBox(headers, content_type, part_id, url, null, false); + } + + void formatAttachmentBox(InternetHeaders headers, String type, + String link_name, String link_url, String body, + boolean all_headers_p) { + + String icon = "internal-gopher-binary"; // #### + + getOut().print("
    " + + "" + + "
    "); + + if (icon != null) { + getOut().print("" + + "
    "); + if (link_url != null) + getOut().print(""); + getOut().print("\"\""); + if (link_url != null) getOut().print(""); + getOut().print(""); + } + + if (link_url != null) getOut().print(""); + getOut().print(link_name); + if (link_url != null) getOut().print(""); + + if (icon != null) + getOut().print("
    "); + getOut().print("
    "); + + if (all_headers_p) { + + FullHeaderFormatter formatter = new FullHeaderFormatter(); + StringBuffer output = new StringBuffer(200); + formatter.formatHeaders(headers, output); + getOut().print(output.toString()); + + } else { + String cde[] = headers.getHeader("Content-Description"); + String desc = (cde == null || cde.length == 0 ? null : cde[0]); + String name = null; + + ByteBuf buf = new ByteBuf(50); + + String cdi[] = headers.getHeader("Content-Disposition"); + String disp = (cdi == null || cdi.length == 0 ? null : cdi[0]); +// #### +// if (disp != null) +// name = headers.getParameter(buf, "filename"); +// +// if (name == null) +// if (headers.getHeaderValue("Content-Type", buf, false, false)) +// name = headers.getParameter(buf, "name"); + + if (name == null) { + String n[] = headers.getHeader("Content-Name"); + if (n != null && n.length != 0) + name = n[0]; + } + + getOut().print(""); + + if (name != null) + getOut().print(""); + + if (type != null) + getOut().print(""); + + if (desc != null) + getOut().print(""); + + getOut().print("
    " + + "Name" + // #### l10n + ": " + + name + + "
    " + + "Type" + // #### l10n + ": " + + type + + "
    " + + "Description" + // #### l10n + ": " + + desc + + "
    "); + } + + if (body != null) { + getOut().print("

    ");
    +      // #### quote HTML
    +      getOut().print(body);
    +      getOut().print("
    "); + } + + getOut().print("
    "); + } + + + public void pushBytes(ByteBuf b) { + // Nothing to do with the bytes -- we only use the headers. + } + + public void pushEOF() { + } +} diff --git a/mozilla/grendel/mime/html/MimeHTMLOperator.java b/mozilla/grendel/mime/html/MimeHTMLOperator.java new file mode 100644 index 00000000000..1530eddc67f --- /dev/null +++ b/mozilla/grendel/mime/html/MimeHTMLOperator.java @@ -0,0 +1,111 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 26 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.io.PrintStream; + +/** This is the base class of objects which convert a MIME object into HTML. + + @see MimeOperator + */ + +abstract class MimeHTMLOperator implements IMimeOperator { + + String content_type; + InternetHeaders headers; + private PrintStream out; + + MimeHTMLOperator(IMimeObject object, PrintStream o) { + content_type = object.contentType(); + headers = object.headers(); + out = o; + } + + final PrintStream getOut() { + return out; + } + + public IMimeOperator createChild(IMimeObject child_object) { + return createChild(child_object, out); + } + + public IMimeOperator createChild(IMimeObject child_object, + PrintStream child_out) { + + String child_type = child_object.contentType(); + MimeHTMLOperator result; + + if (child_type == null) // the untyped-text container + result = new MimeContainerOperator(child_object, child_out); + + else if (child_type.equalsIgnoreCase("text/html")) + result = new MimeTextHTMLOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("text/enriched")) +// result = new MimeTextEnrichedOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("text/richtext")) +// result = new MimeTextRichtextOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("text/x-vcard")) +// result = new MimeTextVCardOperator(child_object, child_out); + + else if (child_type.regionMatches(true, 0, "text/", 0, 5)) + result = new MimeTextOperator(child_object, child_out); + + // #### ... more here ... + + else if (child_type.equalsIgnoreCase("message/rfc822") || + child_type.equalsIgnoreCase("message/news")) + result = new MimeMessageOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("multipart/alternative")) +// result = new MimeMultipartAlternativeOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("multipart/related")) +// result = new MimeMultipartRelatedOperator(child_object, child_out); + +// else if (child_type.equalsIgnoreCase("multipart/signed")) +// result = new MimeMultipartSignedOperator(child_object, child_out); + + else if (child_type.equalsIgnoreCase("multipart/appledouble") || + child_type.equalsIgnoreCase("multipart/header-set")) + result = new MimeMultipartAppleDoubleOperator(child_object, child_out); + + else if (child_type.regionMatches(true, 0, "multipart/", 0, 10) || + child_type.equalsIgnoreCase("x-sun-attachment")) + result = new MimeContainerOperator(child_object, child_out); + + else if (child_type.equalsIgnoreCase("message/external-body")) + result = new MimeExternalBodyOperator(child_object, child_out); + + else + result = new MimeExternalObjectOperator(child_object, child_out); + + return result; + } + +} diff --git a/mozilla/grendel/mime/html/MimeHTMLOperatorFactory.java b/mozilla/grendel/mime/html/MimeHTMLOperatorFactory.java new file mode 100644 index 00000000000..1f4c94284f4 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeHTMLOperatorFactory.java @@ -0,0 +1,45 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 29 Aug 1997. + */ + +package grendel.mime.html; + +import java.lang.NullPointerException; +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; +import java.io.PrintStream; + +public class MimeHTMLOperatorFactory { + public static IMimeOperator Make(IMimeObject root_object) { + return Make(root_object, System.out); + } + + public static IMimeOperator Make(IMimeObject root_object, PrintStream out) + { + String ct = root_object.contentType(); + if (ct != null && + ct.equalsIgnoreCase("message/rfc822") || + ct.equalsIgnoreCase("message/news")) + return new MimeMessageOperator(root_object, out); + else + throw new Error("currently, only message/rfc822 and message/news are" + + " valid top-level content types."); + } +} diff --git a/mozilla/grendel/mime/html/MimeLeafOperator.java b/mozilla/grendel/mime/html/MimeLeafOperator.java new file mode 100644 index 00000000000..22fd7ab3b21 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeLeafOperator.java @@ -0,0 +1,38 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; +import java.io.PrintStream; + +abstract class MimeLeafOperator extends MimeHTMLOperator { + + MimeLeafOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public IMimeOperator createChild(IMimeObject child) { + // #### do this better? + throw new Error("can't create a child of a leaf"); + } +} diff --git a/mozilla/grendel/mime/html/MimeMessageOperator.java b/mozilla/grendel/mime/html/MimeMessageOperator.java new file mode 100644 index 00000000000..97b4014fdb2 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeMessageOperator.java @@ -0,0 +1,50 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts message/rfc822 to HTML: mainly this involves + emitting the headers, then letting the body handle itself. + */ + +class MimeMessageOperator extends MimeContainerOperator { + + MimeMessageOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public IMimeOperator createChild(IMimeObject object) { + + NormalHeaderFormatter formatter = new NormalHeaderFormatter(); + StringBuffer output = new StringBuffer(200); + formatter.formatHeaders(object.headers(), output); + output.append("

    "); + getOut().print(output.toString()); + + return super.createChild(object); + } + +} diff --git a/mozilla/grendel/mime/html/MimeMultipartAlternativeOperator.java b/mozilla/grendel/mime/html/MimeMultipartAlternativeOperator.java new file mode 100644 index 00000000000..b6721782e08 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeMultipartAlternativeOperator.java @@ -0,0 +1,92 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts the multipart/alternative MIME type to HTML, + by buffering the sub-parts and extracting the most appropriate one. + */ + +class MimeMultipartAlternativeOperator extends MimeHTMLOperator { + + boolean done = false; + IMimeObject saved_child = null; + IMimeOperator current_child = null; + ByteBuf saved_output_buffer = null; + ByteBuf current_output_buffer = null; + + MimeMultipartAlternativeOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + private void closeOpenChild() { +/* + if (current_child != null) { + // we've just buffered up one direct child of this + // multipart/alternative. Examine it and see if it's + // one we can display inline. If it is, save it + // (replacing the last one we saved, if any.) + if (displayableInline(current_child.object)) { + saved_child = current_child; + saved_output_buffer = current_output_buffer; + } + } +*/ + } + + public IMimeOperator createChild(IMimeObject obj) { + closeOpenChild(); + // make a new stream for this part, which writes into a + // newly-allocated byte buffer. (There may be two such + // in existance at a time, the one we're filling, and the + // last decent one we've seen.) +/* + current_output_buffer = new CharArrayWriter(); + current_child = + super.createChild(child, + new MimeOutputBuffer(current_output_buffer)); +*/ + return current_child; + } + + public void pushEOF() { + // we've seen the end of this mult/alternative, and thus of + // its last child. Now is the time to write the output: + // the buffer of the last "good" child we've seen, or if + // there isn't one, of the last child we've seen period. +/* + closeOpenChild(); + if (saved_output_buffer) + writeOutput(saved_output_buffer); + else if (current_output_buffer) + writeOutput(current_output_buffer); +*/ + } + + public void pushBytes(ByteBuf b) { + throw new Error("containers don't get bytes!"); + } +} diff --git a/mozilla/grendel/mime/html/MimeMultipartAppleDoubleOperator.java b/mozilla/grendel/mime/html/MimeMultipartAppleDoubleOperator.java new file mode 100644 index 00000000000..b18dd730a26 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeMultipartAppleDoubleOperator.java @@ -0,0 +1,71 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.io.PrintStream; + +/** This class converts the multipart/appledouble and multipart/header-set + MIME type to HTML, by presenting one link for both parts, and then + attempting to display the second part inline. + */ + +class MimeMultipartAppleDoubleOperator extends MimeContainerOperator { + + boolean got_first_child = false; + String part_id = null; + + MimeMultipartAppleDoubleOperator(IMimeObject object, PrintStream out) { + super(object, out); + part_id = object.partID(); + } + + public IMimeOperator createChild(IMimeObject child) { + + if (got_first_child) + return super.createChild(child); + else { + got_first_child = true; + return new MimeMultipartAppleDoubleRSRCOperator(child, getOut(), + headers, part_id); + } + } +} + +class MimeMultipartAppleDoubleRSRCOperator extends MimeExternalObjectOperator { + + MimeMultipartAppleDoubleRSRCOperator(IMimeObject object, + PrintStream out, + InternetHeaders parent_headers, + String parent_id) { + super(object, out); + + String url = "about:jwz"; // #### + String type = "Macintosh File"; // #### I18N + formatAttachmentBox(parent_headers, type, parent_id, url, null, false); + } + + void writeBoxEarly() { } +} diff --git a/mozilla/grendel/mime/html/MimeMultipartRelatedOperator.java b/mozilla/grendel/mime/html/MimeMultipartRelatedOperator.java new file mode 100644 index 00000000000..e4fa708a360 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeMultipartRelatedOperator.java @@ -0,0 +1,45 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts the multipart/related MIME type to HTML, in some + as-yet-to-be-determined way. + */ + +class MimeMultipartRelatedOperator extends MimeContainerOperator { + + MimeMultipartRelatedOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + throw new Error("unimplemented"); + } + + public void pushEOF() { + throw new Error("unimplemented"); + } +} diff --git a/mozilla/grendel/mime/html/MimeMultipartSignedOperator.java b/mozilla/grendel/mime/html/MimeMultipartSignedOperator.java new file mode 100644 index 00000000000..05fec691c17 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeMultipartSignedOperator.java @@ -0,0 +1,45 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts the multipart/signed MIME type to HTML, in some + as-yet-to-be-determined way. + */ + +class MimeMultipartSignedOperator extends MimeContainerOperator { + + MimeMultipartSignedOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + throw new Error("unimplemented"); + } + + public void pushEOF() { + throw new Error("unimplemented"); + } +} diff --git a/mozilla/grendel/mime/html/MimeTextEnrichedOperator.java b/mozilla/grendel/mime/html/MimeTextEnrichedOperator.java new file mode 100644 index 00000000000..302d382334c --- /dev/null +++ b/mozilla/grendel/mime/html/MimeTextEnrichedOperator.java @@ -0,0 +1,38 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import java.io.PrintStream; + +/** This class converts text/enriched to HTML. + @see MimeHTMLOperator + */ + +class MimeTextEnrichedOperator extends MimeTextRichtextOperator { + + static boolean enriched = true; + + MimeTextEnrichedOperator(IMimeObject object, PrintStream out) { + super(object, out); + } +} diff --git a/mozilla/grendel/mime/html/MimeTextHTMLOperator.java b/mozilla/grendel/mime/html/MimeTextHTMLOperator.java new file mode 100644 index 00000000000..6a1ec78210c --- /dev/null +++ b/mozilla/grendel/mime/html/MimeTextHTMLOperator.java @@ -0,0 +1,54 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts text/html MIME parts to HTML. + @see MimeHTMLOperator + */ + +class MimeTextHTMLOperator extends MimeTextOperator { + + MimeTextHTMLOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + buffer.setLength(0); + decodeBytesToUnicode(b, buffer); + getOut().print(buffer.toString()); + } + + public void pushEOF() { + + getOut().print("" + + "" + + "" + + "" + + "

"); + + buffer = null; + } +} diff --git a/mozilla/grendel/mime/html/MimeTextOperator.java b/mozilla/grendel/mime/html/MimeTextOperator.java new file mode 100644 index 00000000000..9b44bb49d38 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeTextOperator.java @@ -0,0 +1,60 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 26 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import grendel.mime.IMimeObject; +import java.io.PrintStream; + +/** This class converts text/plain (or unknown text/ types) to HTML. + @see MimeHTMLOperator + */ + +class MimeTextOperator extends MimeLeafOperator { + + StringBuffer buffer = null; + + MimeTextOperator(IMimeObject object, PrintStream out) { + super(object, out); + buffer = new StringBuffer(200); + getOut().print("
");
+  }
+
+  void decodeBytesToUnicode(ByteBuf in, StringBuffer out) {
+    // #### need to look at charset and do deep magic
+    for (int i = 0; i < in.length(); i++) {
+      out.append((char) in.byteAt(i));
+    }
+  }
+
+  public void pushBytes(ByteBuf b) {
+    buffer.setLength(0);
+    decodeBytesToUnicode(b, buffer);
+    TextHTMLConverter.quoteForHTML(buffer, true, true);
+    getOut().print(buffer.toString());
+  }
+
+  public void pushEOF() {
+    buffer = null;
+    getOut().print("
"); + } +} diff --git a/mozilla/grendel/mime/html/MimeTextRichtextOperator.java b/mozilla/grendel/mime/html/MimeTextRichtextOperator.java new file mode 100644 index 00000000000..41dcaf5a055 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeTextRichtextOperator.java @@ -0,0 +1,47 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import grendel.mime.IMimeObject; +import java.io.PrintStream; + +/** This class converts text/richtext to HTML. + @see MimeHTMLOperator + */ + +class MimeTextRichtextOperator extends MimeTextOperator { + + static boolean enriched = false; + + MimeTextRichtextOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + throw new Error("unimplemented"); + } + + public void pushEOF() { + throw new Error("unimplemented"); + } +} diff --git a/mozilla/grendel/mime/html/MimeTextVCardOperator.java b/mozilla/grendel/mime/html/MimeTextVCardOperator.java new file mode 100644 index 00000000000..4b409f4c140 --- /dev/null +++ b/mozilla/grendel/mime/html/MimeTextVCardOperator.java @@ -0,0 +1,45 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Aug 1997. + */ + +package grendel.mime.html; + +import grendel.mime.IMimeObject; +import calypso.util.ByteBuf; +import java.io.PrintStream; + +/** This class converts the the abomination known as VCARD to HTML, in + some as-yet-undetermined way. + */ + +class MimeTextVCardOperator extends MimeLeafOperator { + + MimeTextVCardOperator(IMimeObject object, PrintStream out) { + super(object, out); + } + + public void pushBytes(ByteBuf b) { + throw new Error("unimplemented"); + } + + public void pushEOF() { + throw new Error("unimplemented"); + } +} diff --git a/mozilla/grendel/mime/html/NormalHeaderFormatter.java b/mozilla/grendel/mime/html/NormalHeaderFormatter.java new file mode 100644 index 00000000000..3b21a38f59c --- /dev/null +++ b/mozilla/grendel/mime/html/NormalHeaderFormatter.java @@ -0,0 +1,192 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 31 Aug 1997. + */ + +package grendel.mime.html; + +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; + +/** This class provides a method which knows how to convert an + InternetHeaders object to a brief HTML representation, containing + only the ``interesting'' headers. + */ + +class NormalHeaderFormatter extends HeaderFormatter { + + // #### need a way to customize this list + public String interesting_headers[] = { + "Subject", + "Resent-Comments", + "Resent-Date", + "Resent-Sender", // Not shown if Resent-From is present. + "Resent-From", + "Resent-To", + "Resent-Cc", + "Date", + "Sender", // Not shown if From is present. + "From", + "Reply-To", // Not shown if From has the same addr. + "Organization", + "To", + "CC", + "BCC", + "Posted-To", + "Newsgroups", // Not shown if Posted-To was the same. + "Followup-To", + "References", + + "X-Mailer", // jwz finds these useful for debugging... + "X-Newsreader", + "X-Posting-Software", + "X-News-Posting-Software", + }; + + public void formatHeaders(InternetHeaders headers, StringBuffer output) { + + boolean did_from = false; + boolean did_resent_from = false; + boolean did_posted_to = false; + + startHeaderOutput(output); + + for (int i = 0; i < interesting_headers.length; i++) { + String header = interesting_headers[i]; + + // The Subject header gets written in bold. + // + if (header.equalsIgnoreCase("Subject")) + writeSubjectHeader(header, headers, output); + + // Message-IDs get displayed clickably. + // + else if (header.equalsIgnoreCase("References") || + header.equalsIgnoreCase("Message-ID") || + header.equalsIgnoreCase("Resent-Message-ID") || + header.equalsIgnoreCase("Supersedes")) + writeIDHeader(header, headers, output); + + // The From header supercedes the Sender header. + // + else if (header.equalsIgnoreCase("Sender") || + header.equalsIgnoreCase("From")) { + if (!did_from) { + did_from = true; + if (!writeAddressHeader("From", headers, output)) + writeAddressHeader("Sender", headers, output); + } + } + + // Likewise, the Resent-From header supercedes the Resent-Sender header. + // + else if (header.equalsIgnoreCase("Resent-Sender") || + header.equalsIgnoreCase("Resent-From")) { + if (!did_resent_from) { + did_resent_from = true; + if (!writeAddressHeader("Resent-From", headers, output)) + writeAddressHeader("Resent-Sender", headers, output); + } + } + + // Emit the Newsgroups header unless we've already emitted a Posted-To + // header that had the same value. + // + else if (header.equalsIgnoreCase("Posted-To")) { + did_posted_to = true; + writeNewsgroupHeader(header, headers, output); + } + else if (header.equalsIgnoreCase("Newsgroups")) + if (!did_posted_to) + writeNewsgroupHeader(header, headers, output); + else { + did_posted_to = true; + String newsgroups[] = headers.getHeader("Newsgroups"); + String posted_to[] = headers.getHeader("Posted-To"); + if (posted_to == null || posted_to.length == 0 || + newsgroups == null || newsgroups.length == 0 || + !posted_to[0].equalsIgnoreCase(newsgroups[0])) + writeNewsgroupHeader(header, headers, output); + } + + // Emit the Reply-To header only if it differs from the From header. + // (we just compare the `address' part.) + // + else if (header.equalsIgnoreCase("Reply-To")) { + String reply_to[] = headers.getHeader("Reply-To"); + if (reply_to != null && reply_to.length != 0) { + String froms[] = headers.getHeader("From"); + String from_addrs = (froms == null || froms.length == 0 ? null + : froms[0]); + String repl_addrs = reply_to[0]; + + // #### extract 822 addr-specs from from_addrs + // #### extract 822 addr-specs from repl_addrs + + if (from_addrs == null || repl_addrs == null || + !from_addrs.equalsIgnoreCase(repl_addrs)) { + writeAddressHeader("Reply-To", headers, output); + } + } + } + + // Random other address headers. + // These headers combine all occurences: that is, if there is more than + // one CC field, all of them will be combined and presented as one. + // + else if (header.equalsIgnoreCase("Resent-To") || + header.equalsIgnoreCase("Resent-CC") || + header.equalsIgnoreCase("To") || + header.equalsIgnoreCase("CC") || + header.equalsIgnoreCase("BCC")) + writeAddressHeader(header, headers, output); + + // Random other newsgroup headers. + // These headers combine all occurences, as with address headers. + // + else if (header.equalsIgnoreCase("Newsgroups") || + header.equalsIgnoreCase("Followup-To") || + header.equalsIgnoreCase("Posted-To")) + writeNewsgroupHeader(header, headers, output); + + // Everything else. + // These headers don't combine: only the first instance of the header + // will be shown, if there is more than one. + // + else + writeRandomHeader(header, headers, output, false); + } + + finishHeaderOutput(output); + } + + +// public static void main(String args[]) { +// InternetHeaders h = new InternetHeaders(); +// h.addLine("From: Jamie Zawinski "); +// h.addLine("organization: about:jwz"); +// h.addLine("Subject: this http://home/ neat URL"); +// h.addLine("Newsgroups: alt.foo,alt.bar"); +// +// StringBuffer o = new StringBuffer(); +// NormalHeaderFormatter f = new NormalHeaderFormatter(); +// f.formatHeaders(h, o); +// getOut().println(o.toString()); +// } +} diff --git a/mozilla/grendel/mime/html/README b/mozilla/grendel/mime/html/README new file mode 100644 index 00000000000..5ffc2361293 --- /dev/null +++ b/mozilla/grendel/mime/html/README @@ -0,0 +1,7 @@ +This is Grendel -- a Java mail/news client. + +The grendel.mime.extractor package implements an IMimeOperator that +knows how to convert a compound MIME object to an HTML representation +suitable for display. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/mime/html/TextHTMLConverter.java b/mozilla/grendel/mime/html/TextHTMLConverter.java new file mode 100644 index 00000000000..f2091113573 --- /dev/null +++ b/mozilla/grendel/mime/html/TextHTMLConverter.java @@ -0,0 +1,267 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 31 Aug 1997. + */ + +package grendel.mime.html; + + +/** This class knows how to convert plain-text to HTML. + */ + +class TextHTMLConverter { + + /** Given a StringBuffer of text, alters that text in place to be displayable + as HTML: the <, >, and & characters are converted to entities. + + @arg urls_too If this argument is true, then any text in the + buffer that looks like a URL will have a link + wrapped around it pointing at that URL. + + @arg citations_too If this argument is true, then if the line begins + with Usenet-style citation marks, it will be + wrapped in tags. + + For either `urls_too' or `citations_too' to work, + the buffer must contain exactly one line of text. + If both of these arguments are false, the buffer + may fall on any boundary. + */ + public static void quoteForHTML(StringBuffer text, boolean urls_too, + boolean citations_too) { + + int in_length = text.length(); + + for (int i = 0; i < in_length; i++) { + char c = text.charAt(i); + if (c == '<') { + text.setCharAt(i, '&'); + text.insert(i+1, "lt;"); + in_length += 3; + i += 3; + + } else if (c == '&') { + text.setCharAt(i, '&'); + text.insert(i+1, "amp;"); + in_length += 4; + i += 4; + + } else if (urls_too) { + if (c > ' ' && + (i == 0 || + !Character.isLetterOrDigit((char) text.charAt(i-1))) && + isURLProtocol(text, i, in_length)) { + int end; + + // found a URL protocol. Now find the end of the URL. + + for (end = i; end < in_length; end++) { + // These characters always mark the end of the URL. + if (text.charAt(end) <= ' ' || + text.charAt(end) == '<' || text.charAt(end) == '>' || + text.charAt(end) == '`' || text.charAt(end) == ')' || + text.charAt(end) == '\'' || text.charAt(end) == '"' || + text.charAt(end) == ']' || text.charAt(end) == '}') + break; + } + + // Check for certain punctuation characters on the end, and strip + // them off. + while (end > i && + (text.charAt(end-1) == '.' || text.charAt(end-1) == ',' || + text.charAt(end-1) == '!' || text.charAt(end-1) == ';' || + text.charAt(end-1) == '-' || text.charAt(end-1) == '?' || + text.charAt(end-1) == '#')) + end--; + + // if the url is less than 7 characters then we screwed up and got a + // "news:" url or something which is worthless to us. Exclude the A + // tag in this case. + // + // Also exclude any URL that ends in a colon; those tend to be + // internal and magic and uninteresting. + // + if ((end-i) > 7 && + text.charAt(end-1) != ':') { + // extract the URL + int url_length = end-i; + char url[] = new char[url_length]; + text.getChars(i, end, url, 0); + + text.insert(i, ""); + i += 2; + in_length += 2; + + text.insert(i+1, url); + in_length += url_length; + + i++; + end = i + url_length; + while (i < end) { + + c = text.charAt(i); + if (c == '<') { + text.setCharAt(i, '&'); + text.insert(i+1, "lt;"); + in_length += 3; + end += 3; + i += 3; + + } else if (c == '&') { + text.setCharAt(i, '&'); + text.insert(i+1, "amp;"); + in_length += 4; + end += 4; + i += 4; + } + + i++; + } + + text.insert(i, ""); + i += 4; + in_length += 4; + + } else { + // move to the end of this URL for our next trip through the loop. + i = end-1; + } + } + } + } + + if (citations_too) { + + // Decide whether this line is a quotation, and should be italicized. + // This implements the following case-sensitive regular expression: + // + // ^[ \t]*[A-Z]*[]>] + // + // Which matches these lines: + // + // > blah blah blah + // > blah blah blah + // LOSER> blah blah blah + // LOSER] blah blah blah + // + int i; + + // skip over whitespace + for (i = 0; i < in_length; i++) + if (text.charAt(i) > ' ') break; + + // skip over ASCII uppercase letters + for (; i < in_length; i++) + if (text.charAt(i) < 'A' || text.charAt(i) > 'Z') break; + + if (i < in_length && + (text.charAt(i) == '>' || text.charAt(i) == ']') && + !sendmailFuckage(text, i, in_length)) { + text.insert(i, ""); + in_length += 6; + text.insert(in_length, ""); + } + } + } + + private final static boolean isURLProtocol(StringBuffer buf, + int start, int length) { + switch(buf.charAt(start)) { + case 'a': case 'A': + return matchSubstring(buf, start, length, "about:"); + case 'f': case 'F': + return (matchSubstring(buf, start, length, "ftp:") || + matchSubstring(buf, start, length, "file:")); + case 'g': case 'G': + return matchSubstring(buf, start, length, "gopher:"); + case 'h': case 'H': + return (matchSubstring(buf, start, length, "http:") || + matchSubstring(buf, start, length, "https:")); + case 'm': case 'M': + return (matchSubstring(buf, start, length, "mailto:") || + matchSubstring(buf, start, length, "mailbox:")); + case 'n': case 'N': + return matchSubstring(buf, start, length, "news:"); + case 'r': case 'R': + return matchSubstring(buf, start, length, "rlogin:"); + case 's': case 'S': + return matchSubstring(buf, start, length, "snews:"); + case 't': case 'T': + return (matchSubstring(buf, start, length, "telnet:") || + matchSubstring(buf, start, length, "tn3270:")); + case 'w': case 'W': + return matchSubstring(buf, start, length, "wais:"); + case 'u': case 'U': + return matchSubstring(buf, start, length, "urn:"); + default: + return false; + } + } + + private final static boolean matchSubstring(StringBuffer buf, int start, + int length, String string) { + int L = string.length(); + if (length - start <= L) return false; + for (int i = 0; i < L; i++) { + if (Character.toLowerCase(string.charAt(i)) != + Character.toLowerCase(buf.charAt(start+i))) + return false; + } + return true; + } + + private final static boolean sendmailFuckage(StringBuffer buf, + int start, int length) { + return ((length - start) > 5 && + buf.charAt(start ) == '>' && + buf.charAt(start+1) == 'F' && + buf.charAt(start+2) == 'r' && + buf.charAt(start+3) == 'o' && + buf.charAt(start+4) == 'm' && + buf.charAt(start+5) == ' '); + } + + static void test(String x) { + System.out.println("Testing: " + x); + StringBuffer buf = new StringBuffer(x); + quoteForHTML(buf, true, false); + System.out.println(" " + buf.toString()); + } + + public static void main(String args[]) { + test("foobar"); + test(""); + test("gabba gabba "); + test("this is not http: a url"); + test("this is not hxxp:adssfafsa a url"); + test("this is http:adssfafsa a url"); + test("this is mailto: a url"); + test("this is mailto:jwz a url"); + test("this is ABOUT:JWZ?LOSSAGE=SPECTACULAR a url"); + test("this is ABOUT:JWZ?LOSSAGE=SPECTACULAR&egregious=very. a url"); + test("http://somewhere/fbi.cgi?huzza=<zorch>"); + test("---http://somewhere/fbi.cgi?huzza=<zorch>..."); + } +} diff --git a/mozilla/grendel/mime/parser/Makefile b/mozilla/grendel/mime/parser/Makefile new file mode 100644 index 00000000000..e55096d25f9 --- /dev/null +++ b/mozilla/grendel/mime/parser/Makefile @@ -0,0 +1,32 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + MimeContainer.java \ + MimeDwimText.java \ + MimeLeaf.java \ + MimeMessageExternalBody.java \ + MimeMessageRFC822.java \ + MimeMultipart.java \ + MimeMultipartDigest.java \ + MimeObject.java \ + MimeParserFactory.java \ + MimeXSunAttachment.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/mime/parser/MimeContainer.java b/mozilla/grendel/mime/parser/MimeContainer.java new file mode 100644 index 00000000000..4109fe9b043 --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeContainer.java @@ -0,0 +1,236 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + + +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; + +import javax.mail.internet.InternetHeaders; + +import java.util.Enumeration; +import java.util.Vector; +import java.util.StringTokenizer; + +//import java.lang.reflect.Constructor; +//import java.lang.reflect.InvocationTargetException; + + +/** This abstract class represents the parsers for all MIME objects which can + contain other MIME objects within them. + */ +abstract class MimeContainer extends MimeObject { + + Vector kids = null; + MimeObject open_child = null; + + public MimeContainer(String content_type, InternetHeaders headers) { + super(content_type, headers); + } + + public Enumeration children() { return kids.elements(); } + + /** This method creates a new child part, making the decision about which + MIME content-type strings correspond to which subclasses of MimeObject. + It does this by mapping the type name to a class name, and attempting + to load it from the `grendel.mime.parser' package. + */ + public MimeObject makeChild(String child_type, + InternetHeaders child_headers) { + + if (child_type == null && child_headers != null) { + String hh[] = child_headers.getHeader("Content-Type"); + if (hh != null && hh.length != 0) { + child_type = hh[0]; + // #### strip to first token + int i = child_type.indexOf(';'); + if (i > 0) + child_type = child_type.substring(0, i); + child_type = child_type.trim(); + } + } + + if (child_type == null) + return new MimeDwimText(child_type, child_headers); + + else if (child_type.equalsIgnoreCase("message/rfc822") || + child_type.equalsIgnoreCase("message/news")) + return new MimeMessageRFC822(child_type, child_headers); + + else if (child_type.equalsIgnoreCase("message/external-body")) + return new MimeMessageExternalBody(child_type, child_headers); + + else if (child_type.equalsIgnoreCase("multipart/digest")) + return new MimeMultipartDigest(child_type, child_headers); + + else if (child_type.regionMatches(true, 0, "multipart/", 0, 10)) + return new MimeMultipart(child_type, child_headers); + + else if (child_type.equalsIgnoreCase("x-sun-attachment")) + return new MimeXSunAttachment(child_type, child_headers); + else + return new MimeLeaf(child_type, child_headers); + } + + + /* This hairy mess does the same thing as the above version, but it + does so by dynamically looking up classes at runtime: if it sees + a MIME type of "multipart/foobar-baz", it would try to instantiate, + in order, + + grendel.mime.parser.MimeMultipartFoobarBaz + grendel.mime.parser.MimeMultipart + grendel.mime.parser.Leaf + + This is all well and good, but it turns out to be a bit slower, and + I'm not convinced this generality is actually useful here in the + parser, so I'm going to punt on it for now. + */ +// public MimeObject makeChild(String child_type, InternetHeaders child_headers) { +// +// if (child_type == null && child_headers != null) +// child_type = child_headers.getHeaderValue("Content-Type", true, false); +// +// String pkg = "grendel.mime.parser"; +// String base_class_name = null; +// String full_class_name = null; +// +// if (child_type == null) { +// full_class_name = "MimeDwimText"; +// } else { +// +// if (child_type.equalsIgnoreCase("message/news")) // synonym kludge +// child_type = "message/rfc822"; +// else if (child_type.equalsIgnoreCase("multipart/header-set")) +// child_type = "multipart/appledouble"; +// +// StringTokenizer st = new StringTokenizer(child_type, " /-_", false); +// while (st.hasMoreTokens()) { +// String t = st.nextToken(); +// t = (String.valueOf(Character.toUpperCase(t.charAt(0))) + +// t.substring(1).toLowerCase()); +// if (t.equals("Rfc822")) t = "RFC822"; // typographical kludge +// if (base_class_name == null) +// base_class_name = "Mime" + t; +// if (full_class_name == null) +// full_class_name = "Mime"; +// full_class_name += t; +// } +// } +// +// if (full_class_name != null) +// full_class_name = pkg + "." + full_class_name; +// if (base_class_name != null) +// base_class_name = pkg + "." + base_class_name; +// +// +// Class types[] = { "".getClass(), child_headers.getClass() }; +// Object args[] = { child_type, child_headers }; +// MimeObject result; +// +// // First try to find and instantiate the full-class version... +// result = try_class(full_class_name, types, args); +// if (result != null) return result; +// +// // If that failed, try to find and instantiate the base-class version. +// result = try_class(base_class_name, types, args); +// if (result != null) return result; +// +// // If that failed, use the leaf class (this better work.) +// result = try_class(pkg + ".MimeLeaf", types, args); +// if (result == null) throw new Error("internal error: no classes"); +// return result; +// } +// +// // Tries to find a class of the given name, and construct an instance +// // with a two-arg constructor (String, InternetHeaders). Returns null if either +// // fails. +// private final MimeObject try_class(String class_name, +// Class[] types, Object[] args) { +// if (class_name == null) return null; +// try { +// Class c = Class.forName(class_name); +// +// Constructor cc = c.getConstructor(types); +// return (MimeObject) cc.newInstance(args); +// +// } catch (ClassNotFoundException c) { // Class.forName +// } catch (NoSuchMethodException c) { // Constructor.getConstructor +// } catch (IllegalAccessException c) { // Constructor.newInstance +// } catch (IllegalArgumentException c) { // Constructor.newInstance +// } catch (InstantiationException c) { // Constructor.newInstance +// } catch (InvocationTargetException c) { // Constructor.newInstance +// } +// return null; +// } + + + /** This method creates a new child part (via makeChild()), and then + creates an operator for it, and installs it in `kids'. + */ + public MimeObject openChild(String child_type, + InternetHeaders child_headers) { + if (open_child != null) + closeChild(); + open_child = makeChild(child_type, child_headers); + + if (kids == null) + kids = new Vector(); + + if (id == null) + open_child.id = String.valueOf(kids.size() + 1); + else + open_child.id = id + "." + String.valueOf(kids.size() + 1); + + open_child.setOperator(operator.createChild(open_child)); + kids.addElement(open_child); + return open_child; + } + + public void closeChild() { + if (open_child != null) { + open_child.pushEOF(); + open_child = null; + } + } + + public MimeObject openChild(ByteBuf child_type, + InternetHeaders child_headers) { + return openChild((child_type == null + ? null + : child_type.toString()), + child_headers); + } + + public MimeObject openChild(byte child_type[], + InternetHeaders child_headers) { + return openChild((child_type == null + ? null + : new String(child_type)), + child_headers); + } + + public void pushEOF() { + closeChild(); + super.pushEOF(); + } +} diff --git a/mozilla/grendel/mime/parser/MimeDwimText.java b/mozilla/grendel/mime/parser/MimeDwimText.java new file mode 100644 index 00000000000..81038175ece --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeDwimText.java @@ -0,0 +1,256 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; +import javax.mail.internet.InternetHeaders; + +/** This class implements a parser for untyped message contents, that is, it + is the class used for the body of a message/rfc822 object which had + no Content-Type header, as opposed to an unrecognized content-type. + Such a message, technically, does not contain MIME data (it follows only + RFC 822, not RFC 1521.) + +

This is a container class, and the reason for that is that it loosely + parses the body of the message looking for ``sub-parts'' and then creates + appropriate containers for them. + +

More specifically, it looks for uuencoded and BinHexed data. It may + do more than that some day. (DWIM stands for ``Do What I Mean.'') + + @see MimeMultipart + @see MimeXSunAttachment + */ + +class MimeDwimText extends MimeContainer { + + ByteLineBuffer line_buffer; + ByteBuf line_bytes; + + // enumeration of sub-part types + static private final int TEXT = 100; + static private final int UUE = 101; + static private final int BINHEX = 102; + + int type; + + String subpart_name; + String subpart_type; + String subpart_encoding; + + public MimeDwimText(String content_type, InternetHeaders headers) { + super(content_type, headers); + line_buffer = new ByteLineBuffer(); + line_bytes = new ByteBuf(100); + type = TEXT; + } + + /** Buffers the bytes into lines, and calls process_line() on each line. */ + public void pushBytes(ByteBuf bytes) { + line_buffer.pushBytes(bytes); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + } + + /** Flushes the line buffer, and (maybe) calls process_line() one last time. + */ + public void pushEOF() { + line_buffer.pushEOF(); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + line_buffer = null; + line_bytes = null; + super.pushEOF(); + } + + /** Called for each line of this part. */ + void process_line(ByteBuf line_buf) { + + int length = line_buf.length(); + byte line[] = line_buf.toBytes(); + boolean just_started = false; + + if (length < 3) + ; + else if (line[0] == 'b' && uue_begin_line_p(line, length)) { + // Close the old part and open a new one. + just_started = true; + // subpart_name was kludgily set by uue_begin_line_p... I suck. + subpart_type = name_to_content_type(subpart_name); + subpart_encoding = "uuencode"; + open_untyped_child(); + + } else if (line[0] == '(' && line[1] == 'T' && + binhex_begin_line_p(line, length)) { + // Close the old part and open a new one. + just_started = true; + subpart_type = "application/mac-binhex40"; + subpart_encoding = null; + subpart_name = null; + open_untyped_child(); + } + + // Open a text/plain sub-part if there is no sub-part open. + if (open_child == null) { + subpart_type = "text/plain"; + subpart_encoding = null; + subpart_name = null; + open_untyped_child(); + } + + // Hand this line to the currently-open sub-part. + open_child.pushBytes(line_buf); + + // Close this sub-part if this line demands it. + if (length >= 3 && + ((type == UUE && line[0] == 'e' && uue_end_line_p(line, length)) || + (type == BINHEX && binhex_end_line_p(line, length)))) + closeChild(); + } + + void open_untyped_child() { + InternetHeaders hdrs = new InternetHeaders(); + + if (subpart_type == null) + hdrs.addHeaderLine("Content-Type: application/octet-stream\r\n"); + else + hdrs.addHeaderLine("Content-Type: " + subpart_type + "\r\n"); + + if (subpart_encoding != null) + hdrs.addHeaderLine("Content-Transfer-Encoding: " + subpart_encoding + + "\r\n"); + + if (subpart_name != null) + hdrs.addHeaderLine("Content-Disposition: inline; filename=\"" + + subpart_name + "\"\r\n"); + hdrs.addHeaderLine("\r\n"); + + String null_str = null; // weird ambiguous prototype thing... + openChild(null_str, hdrs); + hdrs = null; + } + + boolean uue_begin_line_p(byte line[], int length) { + + if (length < 10 || + line[0] != 'b' || + line[1] != 'e' || + line[2] != 'g' || + line[3] != 'i' || + line[4] != 'n' || + line[5] != ' ') + return false; + + // ...then three or four octal digits. + int i = 6; + if (line[i] < '0' || line[i] > '7') return false; + i++; + if (line[i] < '0' || line[i] > '7') return false; + i++; + if (line[i] < '0' || line[i] > '7') return false; + i++; + + if (line[i] == ' ') + i++; + else { + if (line[i] < '0' || line[i] > '7') return false; + i++; + if (line[i] != ' ') return false; + } + + while (i < length && line[i] <= ' ') + i++; + + while (length >= i && line[length-1] <= ' ') + length--; + + // so sue me. + subpart_name = new String(line, i, length-i); + + return true; + } + + boolean uue_end_line_p(byte line[], int length) { + // accept any line with whitespace at the beginning of the line; + // or any line beginning with the characters "end". + return (length > 1 && + (line[0] == ' ' || + line[0] == '\t' || + (length > 3 && + ((line[0] == 'e' || line[0] == 'E') && + (line[1] == 'n' || line[1] == 'N') && + (line[2] == 'd' || line[2] == 'D'))))); + } + + boolean binhex_begin_line_p(byte line[], int length) { + String s = new String(line, 0, length); + return s.startsWith("(This file must be converted with BinHex 4.0)"); + } + + boolean binhex_end_line_p(byte line[], int length) { + if (length < 64) return false; + if (line[length-1] == '\n') length--; + if (line[length-1] == '\r') length--; + return (length == 64); + } + + String name_to_content_type(String filename) { + // #### need to get at the file-extension-to-mime-type mapping... + + filename = filename.toLowerCase(); + if (filename.endsWith("txt") || filename.endsWith("text")) + return "text/plain"; + else if (filename.endsWith("htm") || filename.endsWith("html")) + return "text/html"; + else if (filename.endsWith("gif")) + return "image/gif"; + else if (filename.endsWith("jpg") || filename.endsWith("jpeg")) + return "image/jpeg"; + else if (filename.endsWith("pjpg") || filename.endsWith("pjpeg")) + return "image/pjpeg"; + else if (filename.endsWith("xbm")) + return "image/x-xbitmap"; + else if (filename.endsWith("xpm")) + return "image/x-xpixmap"; + else if (filename.endsWith("xwd")) + return "image/x-xwindowdump"; + else if (filename.endsWith("bmp")) + return "image/x-MS-bmp"; + else if (filename.endsWith("au")) + return "audio/basic"; + else if (filename.endsWith("aif") || filename.endsWith("aiff") || + filename.endsWith("aifc")) + return "audio/x-aiff"; + else if (filename.endsWith("ps")) + return "application/postscript"; + else if (filename.endsWith("p7m")) + return "application/x-pkcs7-mime"; + else if (filename.endsWith("p7c")) + return "application/x-pkcs7-mime"; + else if (filename.endsWith("p7s")) + return "application/x-pkcs7-signature"; + else + return "application/octet-stream"; + } +} diff --git a/mozilla/grendel/mime/parser/MimeLeaf.java b/mozilla/grendel/mime/parser/MimeLeaf.java new file mode 100644 index 00000000000..77b7089ea43 --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeLeaf.java @@ -0,0 +1,103 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; +import grendel.mime.encoder.MimeEncoder; +import grendel.mime.encoder.MimeBase64Decoder; +import grendel.mime.encoder.MimeQuotedPrintableDecoder; +import grendel.mime.encoder.MimeUUDecoder; + +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.util.Enumeration; + + +/** This abstract class represents the parsers for all MIME objects which are + not containers for other MIME objects. The implication of this is that + they are MIME types which can have Content-Transfer-Encodings applied to + their data. + @see MimeContainer + */ + +class MimeLeaf extends MimeObject { + + MimeEncoder decoder = null; + ByteBuf decoded = null; + + public MimeLeaf(String content_type, InternetHeaders headers) { + super(content_type, headers); + + decoder = null; + if (headers != null) { + String enc[] = headers.getHeader("Content-Transfer-Encoding"); + String encoding = (enc == null || enc.length == 0 ? null : enc[0]); + + if (encoding == null) + ; + else if (encoding.equalsIgnoreCase("base64")) + decoder = new MimeBase64Decoder(); + else if (encoding.equalsIgnoreCase("quoted-printable")) + decoder = new MimeQuotedPrintableDecoder(); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("uue") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + decoder = new MimeUUDecoder(); + } + + if (decoder != null) + decoded = new ByteBuf(); + } + + public Enumeration children() { return null; } + + public void pushBytes(ByteBuf buffer) { + + if (decoder == null) + // If there's no decoder, push the bytes through raw. + operator.pushBytes(buffer); + else { + // If there is a decoder, decode them, and pus the result through. + decoded.setLength(0); + decoder.translate(buffer, decoded); + operator.pushBytes(decoded); + } + } + + public void pushEOF() { + + if (decoder != null) { + // If there is a decoder, tell it we're at EOF, and give it a chance + // to spit out its last batch of buffered bytes. Then feed them + // through to the operator. + decoded.setLength(0); + decoder.eof(decoded); + if (decoded.length() != 0) + operator.pushBytes(decoded); + decoder = null; + decoded = null; + } + + super.pushEOF(); + } +} diff --git a/mozilla/grendel/mime/parser/MimeMessageExternalBody.java b/mozilla/grendel/mime/parser/MimeMessageExternalBody.java new file mode 100644 index 00000000000..8bea87c193c --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeMessageExternalBody.java @@ -0,0 +1,47 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 2 Sep 1997. + */ + +package grendel.mime.parser; + +import javax.mail.internet.InternetHeaders; + + +/** This class implements the parser for the message/external-body MIME + containers, which is essentially a glorified URL. This behaves like + a message, in that it contains interior headers; but the content-type + on those headers refers to remote data, not to the type of a nested + part. So, we parse the interior headers, but always treat the body + of message/external-body as text/plain. + @see MimeLeaf + */ + +class MimeMessageExternalBody extends MimeMessageRFC822 { + + public MimeMessageExternalBody(String content_type, + InternetHeaders headers) { + super(content_type, headers); + } + + public MimeObject makeChild(String child_type, + InternetHeaders child_headers) { + return new MimeLeaf("text/plain", child_headers); + } +} diff --git a/mozilla/grendel/mime/parser/MimeMessageRFC822.java b/mozilla/grendel/mime/parser/MimeMessageRFC822.java new file mode 100644 index 00000000000..9bc1e1f17ab --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeMessageRFC822.java @@ -0,0 +1,111 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +import javax.mail.internet.InternetHeaders; + + +/** This class implements the parser for the message/rfc822 and message/news + MIME containers, which is to say, mail and news messages. This is a + container which holds exactly one child, the body: messages with multiple + parts are those which have a multipart/ as their body. + @see MimeMultipart + @see MimeLeaf + */ + +class MimeMessageRFC822 extends MimeContainer { + + ByteLineBuffer line_buffer; + ByteBuf line_bytes; + InternetHeaders child_headers; + + public MimeMessageRFC822(String content_type, InternetHeaders headers) { + super(content_type, headers); + line_buffer = new ByteLineBuffer(); + line_bytes = new ByteBuf(100); + child_headers = new InternetHeaders(); + } + + /** Buffers the bytes into lines, and calls process_line() on each line. */ + public void pushBytes(ByteBuf bytes) { + line_buffer.pushBytes(bytes); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + } + + /** Flushes the line buffer, and (maybe) calls process_line() one last time. + */ + public void pushEOF() { + line_buffer.pushEOF(); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + line_buffer = null; + + // make sure every message has a body, even if it's a null body. + if (open_child == null) { + line_bytes.setLength(0); + line_bytes.append("\r\n"); + process_line(line_bytes); + } + + line_bytes = null; + super.pushEOF(); + } + + /** Called for each line of this message. */ + void process_line(ByteBuf line) { + + if (open_child == null) { // Still parsing the headers + + child_headers.addHeaderLine(line.toString()); + + // If this is a blank line, create a child. + byte b = line.byteAt(0); + if (b == '\r' || b == '\n') { + + String hh[] = child_headers.getHeader("Content-Type"); + String child_type = null; + + if (hh != null && hh.length != 0) { + child_type = hh[0]; + // #### strip to first token + int i = child_type.indexOf(';'); + if (i > 0) + child_type = child_type.substring(0, i); + child_type = child_type.trim(); + } + hh = null; + openChild(child_type, child_headers); + child_type = null; + child_headers = null; + } + + } else { // Parsing the body + open_child.pushBytes(line); + } + } +} diff --git a/mozilla/grendel/mime/parser/MimeMultipart.java b/mozilla/grendel/mime/parser/MimeMultipart.java new file mode 100644 index 00000000000..164ceb1670b --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeMultipart.java @@ -0,0 +1,292 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +import javax.mail.internet.InternetHeaders; + + +/** This class implements the parser for all "multipart/" MIME subtypes. + @see MimeMultipartDigest + @see MimeDwimText + @see MimeXSunAttachment + */ +class MimeMultipart extends MimeContainer { + + ByteLineBuffer line_buffer; + ByteBuf line_bytes; + ByteBuf boundary; + ByteBuf crlf; + InternetHeaders child_headers; + + int state; + + // enumeration of boundary types + static protected final int BOUNDARY_NONE = 100; + static protected final int BOUNDARY_TERMINATOR = 101; + static protected final int BOUNDARY_SEPARATOR = 102; + + // enumeration of states in the machine + static protected final int MULTIPART_PREAMBLE = 200; + static protected final int MULTIPART_HEADERS = 201; + static protected final int MULTIPART_FIRST_LINE = 202; + static protected final int MULTIPART_LINE = 203; + static protected final int MULTIPART_EPILOGUE = 204; + + public MimeMultipart(String content_type, InternetHeaders headers) { + super(content_type, headers); + + crlf = new ByteBuf("\r\n"); + line_buffer = new ByteLineBuffer(); + line_bytes = new ByteBuf(100); + + computeBoundary(); + } + + protected void computeBoundary() { + // extract the boundary string from the Content-Type header. + if (headers != null) { + state = MULTIPART_PREAMBLE; + ByteBuf ct = new ByteBuf(); + boundary = null; + String hh[] = headers.getHeader("Content-Type"); + if (hh != null && hh.length != 0) { + String ctv = hh[0]; + // #### do this right! + int i = ctv.indexOf("boundary="); + if (i > 0) { + int j; + i += 9; + if (ctv.charAt(i) == '"') { + i++; + j = ctv.indexOf('"', i); + } else { + j = ctv.indexOf(' ', i); + if (j < 0) j = ctv.length()-1; + } + boundary = new ByteBuf(ctv.substring(i, j)); + } + } + } + + if (boundary == null) { + // Gag! No boundary string? Try and display it as plain-text. + openChild("text/plain", null); + state = MULTIPART_FIRST_LINE; + } + } + + /** When a sub-part doesn't have a content-type specified, this method + returns the default type. The default is "text/plain", but, for + example, the "multipart/digest" handler overrides this to + "message/rfc822". Note that this is a class method. + */ + public String defaultSubpartType() { return "text/plain"; } + + /** Buffers the bytes into lines, and calls process_line() on each line. */ + public void pushBytes(ByteBuf bytes) { + line_buffer.pushBytes(bytes); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + } + + /** Flushes the line buffer, and (maybe) calls process_line() one last time. + */ + public void pushEOF() { + line_buffer.pushEOF(); + while (line_buffer.pullLine(line_bytes)) + process_line(line_bytes); + line_buffer = null; + line_bytes = null; + super.pushEOF(); + } + + + /** Called for each line of the body of this multipart. + Maintains a state machine and decides what the line means. + */ + private void process_line(ByteBuf line) { + + int boundary_type; + + if (state == MULTIPART_EPILOGUE) + boundary_type = BOUNDARY_NONE; + else + boundary_type = checkBoundary(line); + + if (boundary_type == BOUNDARY_TERMINATOR || + boundary_type == BOUNDARY_SEPARATOR) { + // Match! Close the currently-open part, move on to the next + // state, and discard this line. + if (state != MULTIPART_PREAMBLE) + closeChild(); + + if (boundary_type == BOUNDARY_TERMINATOR) { + state = MULTIPART_EPILOGUE; + child_headers = null; + } else { + state = MULTIPART_HEADERS; + child_headers = new InternetHeaders(); + } + + } else { + // this line is not a boundary line. + + switch (state) { + case MULTIPART_PREAMBLE: + case MULTIPART_EPILOGUE: + // ignore this line. + break; + + case MULTIPART_HEADERS: + // parse this line as a header for the sub-part. + child_headers.addHeaderLine(line.toString()); + + // If this is a blank line, create a child. + byte b = line.byteAt(0); + if (b == '\r' || b == '\n') { + createMultipartChild(); + state = MULTIPART_FIRST_LINE; + } + break; + + case MULTIPART_FIRST_LINE: + // hand this line off to the sub-part. + push_child_line(line, true); + state = MULTIPART_LINE; + break; + + case MULTIPART_LINE: + // hand this line off to the sub-part. + push_child_line(line, false); + break; + + default: + throw new Error("internal error"); + } + } + } + + + protected int checkBoundary(ByteBuf line_buf) { + + if (boundary == null) + return BOUNDARY_NONE; + + int length = line_buf.length(); + byte line[] = line_buf.toBytes(); + + if (length < 3 || line[0] != '-' || line[1] != '-') + return BOUNDARY_NONE; + + // strip trailing whitespace (including the newline.) + while(length > 2 && (line[length-1] <= ' ')) + length--; + + int blen = boundary.length(); + boolean term_p = false; + + // Could this be a terminating boundary? + if (length == blen + 4 && + line[length-1] == '-' && + line[length-2] == '-') { + term_p = true; + length -= 2; + } + + // If the two don't have the same length (modulo trailing dashes) + // then they can't match. + if (blen != length-2) + return BOUNDARY_NONE; + + // Compare each byte, and bug out if there's a mismatch. + // (Note that if `boundary' is "xxx" then `line' is "--xxx"; + // that's what the +2 is for.) + byte bbytes[] = boundary.toBytes(); + for (int i = 0; i < length-2; i++) + if (line[i+2] != bbytes[i]) + return BOUNDARY_NONE; + + // Success! Return the type of boundary. + if (term_p) + return BOUNDARY_TERMINATOR; + else + return BOUNDARY_SEPARATOR; + } + + + protected void createMultipartChild() { + String hh[] = child_headers.getHeader("Content-Type"); + String child_type = null; + + if (hh != null && hh.length != 0) { + child_type = hh[0]; + // #### strip to first token + int i = child_type.indexOf(';'); + if (i > 0) + child_type = child_type.substring(0, i); + child_type = child_type.trim(); + } + + if (child_type != null) { + openChild(child_type, child_headers); + } else { + // If there was no content-type, assume text/plain -- null as a ct + // would mean that we should do the auto-uudecode-hack, which we don't + // ever want to do for subparts of a multipart (only for untyped + // children of message/rfc822.) + openChild(defaultSubpartType(), child_headers); + } + } + + + private void push_child_line(ByteBuf line_buf, boolean first_line_p) { + + // The newline issues here are tricky, since both the newlines before + // and after the boundary string are to be considered part of the + // boundary: this is so that a part can be specified such that it + // does not end in a trailing newline. + // + // To implement this, we send a newline *before* each line instead + // of after, except for the first line, which is not preceeded by a + // newline. + + int length = line_buf.length(); + byte line[] = line_buf.toBytes(); + + // Remove the trailing newline... + if (length > 0 && line[length-1] == '\n') length--; + if (length > 0 && line[length-1] == '\r') length--; + line_buf.setLength(length); + + if (!first_line_p) + // Push out a preceeding newline... + open_child.pushBytes(crlf); + + // Now push out the line sans trailing newline. + open_child.pushBytes(line_buf); + } +} diff --git a/mozilla/grendel/mime/parser/MimeMultipartDigest.java b/mozilla/grendel/mime/parser/MimeMultipartDigest.java new file mode 100644 index 00000000000..36aad27e5d9 --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeMultipartDigest.java @@ -0,0 +1,40 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; +import javax.mail.internet.InternetHeaders; + +/** This class implements the parser for the multipart/digest MIME container, + which differs from multipart/mixed only in that the default type (for + parts with no type explicitly specified) is message/rfc822 instead of + text/plain. + */ + +class MimeMultipartDigest extends MimeMultipart { + + public MimeMultipartDigest(String content_type, InternetHeaders headers) { + super(content_type, headers); + } + + public String defaultSubpartType() { return "message/rfc822"; } +} diff --git a/mozilla/grendel/mime/parser/MimeObject.java b/mozilla/grendel/mime/parser/MimeObject.java new file mode 100644 index 00000000000..6bc909cc6db --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeObject.java @@ -0,0 +1,129 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeObject; +import grendel.mime.IMimeParser; +import grendel.mime.IMimeOperator; + +import calypso.util.ByteBuf; + +import javax.mail.internet.InternetHeaders; + +import java.io.InputStream; +import java.util.Enumeration; + + + +/** This is the base class for objects implementing parsers for MIME parts. + It implements the IMimeObject interface. + +

The class hierarchy is: +

+

    +
  • MimeObject (abstract) +
      +
    • MimeContainer (abstract) +
        +
      • MimeMultipart +
          +
        • MimeMultipartDigest +
        • MimeXSunAttachment +
        +
      • MimeMessageRFC822 +
          +
        • MimeMessageExternalBody +
        +
      • MimeDwimText +
      +
    • MimeLeaf (abstract) +
    +
+ */ + +abstract class MimeObject implements IMimeObject, IMimeParser { + + String content_type; // definitive content-type (may be null) + InternetHeaders headers; // headers describing this object + String id; // the part-number string, like "1.2.3" + IMimeOperator operator; // the operator to feed information to + + /** Creates a parser for a MIME object. + + @arg content_type The MIME content type of this object. + If this is null, it will be extracted + from the headers argument. + + @arg headers The headers describing this MIME object. + This arg may be null for the outermost + container (but in that case, the + content_type arg must be provided.) + */ + public MimeObject(String content_type, InternetHeaders headers) { + + if (content_type == null && headers != null) { + String hh[] = headers.getHeader("Content-Type"); + if (hh != null && hh.length != 0) { + content_type = hh[0]; + // #### strip to first token + int i = content_type.indexOf(';'); + if (i > 0) + content_type = content_type.substring(0, i); + content_type = content_type.trim(); + } + } + + // Note: content_type may still be null. That's ok. + this.content_type = content_type; + this.headers = headers; + } + + public void setOperator(IMimeOperator op) { + if (operator != null) throw new Error("operator already set"); + operator = op; + } + + public IMimeObject getObject() { return this; } + + public abstract void pushBytes(ByteBuf buffer); + + public void pushEOF() { + operator.pushEOF(); + operator = null; + } + +// public void parseStream(InputStream in) { +// ByteBuf b = new ByteBuf(10240); /* Is there some way to ask the +// stream what it's preferred buffer +// size is? */ +// while (in.read(b) >= 0) +// pushBytes(b); +// b.setLength(0); +// b = null; +// } + + // For IMimeObject + public String contentType() { return content_type; } + public InternetHeaders headers() { return headers; } + public String partID() { return id; } + public abstract Enumeration children(); +} diff --git a/mozilla/grendel/mime/parser/MimeParserFactory.java b/mozilla/grendel/mime/parser/MimeParserFactory.java new file mode 100644 index 00000000000..d9ccbc60446 --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeParserFactory.java @@ -0,0 +1,42 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 29 Aug 1997. + */ + +package grendel.mime.parser; + +import java.lang.NullPointerException; +import grendel.mime.IMimeOperator; +import grendel.mime.IMimeParser; + +public class MimeParserFactory { + public static IMimeParser Make(String toplevel_content_type) { + + if (toplevel_content_type == null) + throw new NullPointerException("content-type must not be null"); + + else if (toplevel_content_type.equalsIgnoreCase("message/rfc822") || + toplevel_content_type.equalsIgnoreCase("message/news")) + return new MimeMessageRFC822(toplevel_content_type, null); + + else + throw new Error("currently, only message/rfc822 and message/news are" + + " valid top-level content types."); + } +} diff --git a/mozilla/grendel/mime/parser/MimeXSunAttachment.java b/mozilla/grendel/mime/parser/MimeXSunAttachment.java new file mode 100644 index 00000000000..531fb30e635 --- /dev/null +++ b/mozilla/grendel/mime/parser/MimeXSunAttachment.java @@ -0,0 +1,268 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 21 Aug 1997. + */ + +package grendel.mime.parser; + +import grendel.mime.IMimeOperator; +import calypso.util.ByteBuf; +import javax.mail.internet.InternetHeaders; +import java.util.StringTokenizer; + + +/** This class represents the X-Sun-Attachment type, which is the Content-Type + assigned by that pile of garbage called MailTool. This is not a MIME type + per se, but it's very similar to multipart/mixed, so it's easy to parse. + Lots of people use MailTool, so we're stuck with having to parse this + idiotic, obsolete format. + @see MimeMultipart + @see MimeUntypedText + */ + +/* The format is this: + + = Content-Type is X-Sun-Attachment + = parts are separated by lines of exactly ten dashes + = just after the dashes comes a block of headers, including: + + X-Sun-Data-Type: (manditory) + Values are Text, Postscript, Scribe, SGML, TeX, Troff, DVI, + and Message. + + X-Sun-Encoding-Info: (optional) + Ordered, comma-separated values, including Compress and Uuencode. + + X-Sun-Data-Name: (optional) + File name, maybe. + + X-Sun-Data-Description: (optional) + Longer text. + + X-Sun-Content-Lines: (manditory, unless Length is present) + Number of lines in the body, not counting headers and the blank + line that follows them. + + X-Sun-Content-Length: (manditory, unless Lines is present) + Bytes, presumably using Unix line terminators. + */ + +class MimeXSunAttachment extends MimeMultipart { + + public MimeXSunAttachment(String content_type, InternetHeaders headers) { + super(content_type, headers); + } + + protected void computeBoundary() { + // handled in checkBoundary(); + } + + protected int checkBoundary(ByteBuf line_buf) { + if (line_buf.length() <= 10) + return BOUNDARY_NONE; + else { + byte line[] = line_buf.toBytes(); + if (line[0] == '-' && line[1] == '-' && line[2] == '-' && + line[3] == '-' && line[4] == '-' && line[5] == '-' && + line[6] == '-' && line[7] == '-' && line[8] == '-' && + line[9] == '-' && + (line[10] == '\r' || line[10] == '\n')) + return BOUNDARY_SEPARATOR; + else + return BOUNDARY_NONE; + } + } + + protected void createMultipartChild() { + child_headers = translateSunHeaders(child_headers); + super.createMultipartChild(); + } + + private String getHeader(InternetHeaders ih, String name) { + String h[] = ih.getHeader(name); + if (h == null || h.length == 0) + return null; + else + return h[0]; + } + + /** Translates x-sun-attachment sub-headers to legal MIME headers. */ + InternetHeaders translateSunHeaders(InternetHeaders in) { + InternetHeaders out = new InternetHeaders(); + String dt = getHeader(in, "X-Sun-Data-Type"); + String cs = getHeader(in, "X-Sun-Charset"); + String ei = getHeader(in, "X-Sun-Encoding-Info"); + String dn = getHeader(in, "X-Sun-Data-Name"); + String dd = getHeader(in, "X-Sun-Data-Description"); + + if (dn != null && dd != null && dn.equalsIgnoreCase(dd)) + dd = null; // you losers. + + if (dt != null && dd != null && dt.equalsIgnoreCase(dd)) + dd = null; // you complete losers. + + if (dt != null && dn != null && dt.equalsIgnoreCase(dn)) + dn = null; // you complete and utter losers. + + String mime_type = null; + String mime_enc = null; + + + /* These are the magic types used by MailTool that I can determine. + The only actual written spec I've found only listed the first few. + The rest were found by inspection (both of real-world messages, + and by running `strings' on the MailTool binary, and on the file + /usr/openwin/lib/cetables/cetables (the "Class Engine", Sun's + equivalent to .mailcap and mime.types.) + */ + String sun_type_table[][] = { + { "default", "text/plain" }, + { "default-doc", "text/plain" }, + { "text", "text/plain" }, + { "scribe", "text/plain" }, + { "sgml", "text/plain" }, + { "tex", "text/plain" }, + { "troff", "text/plain" }, + { "c-file", "text/plain" }, + { "h-file", "text/plain" }, + { "readme-file", "text/plain" }, + { "shell-script", "text/plain" }, + { "cshell-script", "text/plain" }, + { "makefile", "text/plain" }, + { "hidden-docs", "text/plain" }, + { "message", "message/rfc822" }, + { "mail-message", "message/rfc822" }, + { "mail-file", "text/plain" }, + { "gif-file", "image/gif" }, + { "jpeg-file", "image/jpg" }, + { "ppm-file", "image/ppm" }, + { "pgm-file", "image/x-portable-graymap" }, + { "pbm-file", "image/x-portable-bitmap" }, + { "xpm-file", "image/x-xpixmap" }, + { "ilbm-file", "image/ilbm" }, + { "tiff-file", "image/tiff" }, + { "photocd-file", "image/x-photo-cd" }, + { "sun-raster", "image/x-sun-raster" }, + { "audio-file", "audio/basic" }, + { "postscript", "application/postscript" }, + { "postscript-file", "application/postscript" }, + { "framemaker-document", "application/x-framemaker" }, + { "sundraw-document", "application/x-sun-draw" }, + { "sunpaint-document", "application/x-sun-paint" }, + { "sunwrite-document", "application/x-sun-write" }, + { "islanddraw-document", "application/x-island-draw" }, + { "islandpaint-document", "application/x-island-paint" }, + { "islandwrite-document", "application/x-island-write" }, + { "sun-executable", "application/octet-stream" }, + { "default-app", "application/octet-stream" }, + { "compress", "application/x-compress" }, + { "default-compress", "application/x-compress" }, + { "gzip", "application/x-gzip" }, + { "uuencode", "application/x-uuencode" }, + }; + for (int i = 0; i < sun_type_table.length; i++) { + if (dt.equalsIgnoreCase(sun_type_table[i][0])) { + mime_type = sun_type_table[i][1]; + break; + } + } + + + /* Translate x-sun-attachment sub-part encodings to legal MIME encodings. + However, if the X-Sun-Encoding-Info field contains more than one + encoding (that is, contains a comma) then assign it the encoding of + the *rightmost* element in the list; and change its Content-Type to + application/octet-stream. Examples: + + Sun Type: Translates To: + ================== ==================== + type: TEXT type: text/plain + encoding: COMPRESS encoding: x-compress + + type: POSTSCRIPT type: application/x-compress + encoding: COMPRESS,UUENCODE encoding: x-uuencode + + type: TEXT type: application/octet-stream + encoding: UNKNOWN,UUENCODE encoding: x-uuencode + + */ + if (ei != null) { + StringTokenizer st = new StringTokenizer(ei, " ,", false); + String last_enc = null; + String butlast_enc = null; + + while (st.hasMoreTokens()) { + String t = st.nextToken(); + if (t.equalsIgnoreCase("adpcm-compress")) { + // this "adpcm-compress" pseudo-encoding is some random junk that + // MailTool adds to the encoding description of .AU files: we can + // ignore it if it is the leftmost element of the encoding field. + // (It looks like it's created via `audioconvert -f g721'. Why? + // Who knows.) Ignore it. + } else { + butlast_enc = last_enc; + last_enc = t; + } + } + + if (butlast_enc != null) { + if (butlast_enc.equalsIgnoreCase("uuencode")) + mime_type = "application/x-uuencode"; + else if (butlast_enc.equalsIgnoreCase("gzip")) + mime_type = "application/x-gzip"; + else if (butlast_enc.equalsIgnoreCase("compress") || + butlast_enc.equalsIgnoreCase("default-compress")) + mime_type = "application/x-compress"; + else + mime_type = "application/octet-stream"; + } + + if (last_enc != null) { + if (last_enc.equalsIgnoreCase("uuencode")) { + mime_enc = "x-uuencode"; + } else { + mime_type = "application/octet-stream"; + } + } + } + + if (mime_type == null) { + mime_type = "application/octet-stream"; + if (mime_enc == null && dt != null && dt.equalsIgnoreCase("uuencode")) + mime_enc = "x-uuencode"; + } + + if (cs != null && mime_type.startsWith("text/")) + mime_type += "; charset=\"" + cs + "\""; + + out.addHeaderLine("Content-Type: " + mime_type + "\r\n"); + + if (mime_enc != null) + out.addHeaderLine("Content-Transfer-Encoding: " + mime_enc + "\r\n"); + + if (dn != null) + out.addHeaderLine("Content-Disposition: inline; filename=\"" + dn + + "\"\r\n"); + + if (dd != null) + out.addHeaderLine("Content-Description: " + dd + "\r\n"); + + return out; + } +} diff --git a/mozilla/grendel/mime/parser/README b/mozilla/grendel/mime/parser/README new file mode 100644 index 00000000000..3416fa65740 --- /dev/null +++ b/mozilla/grendel/mime/parser/README @@ -0,0 +1,6 @@ +This is Grendel -- a Java mail/news client. + +The grendel.mime.parser package is the implementation of the low-level +logic of the MIME parser. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/prefs/MailServerPrefs.java b/mozilla/grendel/prefs/MailServerPrefs.java new file mode 100644 index 00000000000..96ccc940c67 --- /dev/null +++ b/mozilla/grendel/prefs/MailServerPrefs.java @@ -0,0 +1,67 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 16 Oct 1997. + */ + +package grendel.prefs; + +import javax.mail.URLName; + +public class MailServerPrefs { + String fMailHost; + String fMailUser; + String fMailPassword; + String fMailDirectory; + boolean fLeaveOnServer; + String fSMTPHost; + + URLName fStores[]; + + public void setStores(URLName aStores[]) { + fStores = aStores; + } + + public URLName[] getStores() { + return fStores; + } + + public void setMailDirectory(String aDir) { + fMailDirectory = aDir; + } + + public String getMailDirectory() { + return fMailDirectory; + } + + public void setLeaveOnServer(boolean aLeave) { + fLeaveOnServer = aLeave; + } + + public boolean getLeaveOnServer() { + return fLeaveOnServer; + } + + public void setSMTPHost(String aHost) { + fSMTPHost = aHost; + } + + public String getSMTPHost() { + return fSMTPHost; + } +} diff --git a/mozilla/grendel/prefs/MailServerPrefsEditor.java b/mozilla/grendel/prefs/MailServerPrefsEditor.java new file mode 100644 index 00000000000..0cbce03f1a3 --- /dev/null +++ b/mozilla/grendel/prefs/MailServerPrefsEditor.java @@ -0,0 +1,358 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 16 Oct 1997. + */ + +package grendel.prefs; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.beans.PropertyEditor; +import java.net.URL; +import java.util.EventObject; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.Vector; + +import javax.mail.Store; +import javax.mail.URLName; + +import com.sun.java.swing.AbstractListModel; +import com.sun.java.swing.FileType; +import com.sun.java.swing.JFileChooser; +//import java.awt.FileDialog; +//import java.io.File; +//import java.io.FilenameFilter; +import com.sun.java.swing.JList; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.ListModel; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.event.EventListenerList; +import com.sun.java.swing.event.ListDataEvent; +import com.sun.java.swing.event.ListDataListener; +import com.sun.java.swing.event.ListSelectionEvent; +import com.sun.java.swing.event.ListSelectionListener; +import com.sun.java.swing.table.AbstractTableModel; + +import netscape.orion.dialogs.AbstractCtrl; +import netscape.orion.dialogs.AttrNotFoundException; +import netscape.orion.dialogs.JLISTeditor; +import netscape.orion.dialogs.PageModel; +import netscape.orion.dialogs.PageUI; + +import xml.tree.XMLNode; +import xml.tree.TreeBuilder; + +import grendel.ui.EditHostDialog; +import grendel.ui.Util; + +public class MailServerPrefsEditor implements PropertyEditor +{ + MailServerPrefs fPrefs = new MailServerPrefs(); + PropertyChangeSupport fListeners = new PropertyChangeSupport(this); + + ResourceBundle fLabels = + ResourceBundle.getBundle("grendel.prefs.PrefLabels"); + + final static String kMailDirectoryKey = "mailDirectoryField"; + final static String kLeaveOnServerKey = "leaveOnServerCheck"; + final static String kSMTPHostKey = "smtpHostField"; + final static String kHostListKey = "hostList"; + final static String kNewKey = "newButton"; + final static String kEditKey = "editButton"; + final static String kDeleteKey = "deleteButton"; + final static String kChooseKey = "chooseButton"; + + Hashtable fValues = null; + HostListModel fHostListModel = null; + + PageUI fPanel; + + ServerPrefsModel fModel; + + class ServerPrefsModel extends PageModel { + public ServerPrefsModel() { + fValues = new Hashtable(); + fValues.put(kMailDirectoryKey, ""); + fValues.put(kLeaveOnServerKey, Boolean.TRUE); + fValues.put(kSMTPHostKey, ""); + } + + public Object getAttribute(String aAttrib) throws AttrNotFoundException { + Object res = fValues.get(aAttrib); + if (res == null) { + res = fLabels.getString(aAttrib); + } + if (res == null) { + throw new AttrNotFoundException(aAttrib); + } + return res; + } + + public void setAttribute(String aAttrib, Object aValue) { + if (fValues.containsKey(aAttrib)) { + fValues.put(aAttrib, aValue); + } + } + + public void actionPerformed(ActionEvent aEvent) { + String action = aEvent.getActionCommand(); + if (action.equals(kNewKey)) { + EditHostDialog hostDialog = + new EditHostDialog(Util.GetParentFrame(fPanel), null); + + if (hostDialog.getURLName() != null) { + fHostListModel.add(hostDialog.getURLName()); + fPrefs.setStores(fHostListModel.getStores()); + fListeners.firePropertyChange(null, null, fPrefs); + } + } else if (action.equals(kEditKey)) { + AbstractCtrl c; + c = fPanel.getCtrlByName(kHostListKey); + + URLName value = (URLName) c.getValue(); + if (value != null) { + EditHostDialog hostDialog = + new EditHostDialog(Util.GetParentFrame(fPanel), value); + + if (hostDialog.getURLName() != null) { + fHostListModel.update(value, hostDialog.getURLName()); + fPrefs.setStores(fHostListModel.getStores()); + fListeners.firePropertyChange(null, null, fPrefs); + } + } + } else if (action.equals(kDeleteKey)) { + AbstractCtrl c; + c = fPanel.getCtrlByName(kHostListKey); + + URLName value = (URLName) c.getValue(); + if (value != null) { + fHostListModel.remove(value); + fPrefs.setStores(fHostListModel.getStores()); + fListeners.firePropertyChange(null, null, fPrefs); + } + } else if (action.equals(kChooseKey)) { + JFileChooser chooser = new JFileChooser(fPrefs.getMailDirectory()); + chooser.setChoosableFileTypes(new FileType[] {FileType.SharedFolder}); + chooser.showDialog(fPanel); + } + } + } + + public MailServerPrefsEditor() { + fModel = new ServerPrefsModel(); + fHostListModel = new HostListModel(); + + XMLNode root = null; + URL url = getClass().getResource("PrefDialogs.xml"); + try { + root = xml.tree.TreeBuilder.build(url, getClass()); + XMLNode editHost = root.getChild("dialog", "id", "serverPrefs"); + + fPanel = new PageUI(url, editHost, fModel); + + AbstractCtrl c; + ChangeAction ca = new ChangeAction(); + + c = fPanel.getCtrlByName(kSMTPHostKey); + c.addPropertyChangeListener(ca); + + c = fPanel.getCtrlByName(kMailDirectoryKey); + c.addPropertyChangeListener(ca); + + c = fPanel.getCtrlByName(kHostListKey); + if (c instanceof JLISTeditor) { + ((JLISTeditor) c).setModel(fHostListModel); + } + c.addPropertyChangeListener(new ListListener()); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getAsText() { + return null; + } + + public Component getCustomEditor() { + return fPanel; + } + + public String getJavaInitializationString() { + return ""; + } + + public String[] getTags() { + return null; + } + + public Object getValue() { + fPanel.saveAll(); + + fPrefs.setMailDirectory((String) fValues.get(kMailDirectoryKey)); + fPrefs.setLeaveOnServer(((Boolean) fValues.get(kLeaveOnServerKey)).booleanValue()); + fPrefs.setSMTPHost((String) fValues.get(kSMTPHostKey)); + + fPrefs.setStores(fHostListModel.getStores()); + return fPrefs; + } + + public boolean isPaintable() { + return false; + } + + public void paintValue (Graphics g, Rectangle r) { + } + + public void setAsText(String aValue) { + } + + public void setValue(Object aValue) { + if (aValue instanceof MailServerPrefs) { + MailServerPrefs oldPrefs = fPrefs; + fPrefs = (MailServerPrefs) aValue; + + fValues.put(kMailDirectoryKey, fPrefs.getMailDirectory()); + fValues.put(kLeaveOnServerKey, fPrefs.getLeaveOnServer() ? + Boolean.TRUE : Boolean.FALSE); + fValues.put(kSMTPHostKey, fPrefs.getSMTPHost()); + + fHostListModel.setStores(fPrefs.getStores()); + + fPanel.initAll(); + + fListeners.firePropertyChange(null, oldPrefs, fPrefs); + } + } + + public boolean supportsCustomEditor() { + return true; + } + + public void addPropertyChangeListener(PropertyChangeListener l) { + fListeners.addPropertyChangeListener(l); + } + + public void removePropertyChangeListener(PropertyChangeListener l) { + fListeners.removePropertyChangeListener(l); + } + + class ListListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent e) { + AbstractCtrl c; + c = (AbstractCtrl) e.getSource(); + boolean enabled = c.getValue() != null; + + c = fPanel.getCtrlByName(kDeleteKey); + c.setEnabled(enabled); + + c = fPanel.getCtrlByName(kEditKey); + c.setEnabled(enabled); + } + } + + + class HostListModel extends AbstractListModel { + URLName fStores[]; + Vector fEditableStores = new Vector(); + EventListenerList fListeners = new EventListenerList(); + + HostListModel() { + fStores = new URLName[0]; + } + + HostListModel(URLName aStores[]) { + fStores = aStores; + } + + public void setStores(URLName aStores[]) { + int i; + fStores = aStores; + for (i = 0; i < aStores.length; i++) { + if (aStores[i] != null) { + fEditableStores.addElement(aStores[i]); + } + } + + fireContentsChanged(this, 0, fEditableStores.size() - 1); + } + + public URLName[] getStores() { + URLName res[] = new URLName[fEditableStores.size()]; + fEditableStores.copyInto(res); + return res; + } + + public void update(URLName aOld, URLName aNew) { + if (!aOld.equals(aNew)) { + int idx = fEditableStores.indexOf(aOld); + fEditableStores.removeElementAt(idx); + fEditableStores.insertElementAt(aNew, idx); + + fireContentsChanged(this, idx, idx); + } + } + + public void add(URLName aURLName) { + fEditableStores.addElement(aURLName); + + fireIntervalAdded(this, fEditableStores.size() - 1, + fEditableStores.size() - 1); + + //fScrollPane.validate(); + } + + public void remove(URLName aURLName) { + int idx = fEditableStores.indexOf(aURLName); + if (idx != -1) { + fEditableStores.removeElementAt(idx); + + fireIntervalRemoved(this, idx, idx); + //fScrollPane.validate(); + } + } + + public int getSize() { + return fEditableStores.size(); + } + + public Object getElementAt(int index) { + return fEditableStores.elementAt(index); + } + } + + class ChangeAction implements PropertyChangeListener { + void event(EventObject aEvent) { + getValue(); + fListeners.firePropertyChange(null, null, fPrefs); + } + + public void propertyChange(PropertyChangeEvent aEvent) { + event(aEvent); + } + } +} diff --git a/mozilla/grendel/prefs/Makefile b/mozilla/grendel/prefs/Makefile new file mode 100644 index 00000000000..659354bda7f --- /dev/null +++ b/mozilla/grendel/prefs/Makefile @@ -0,0 +1,30 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + MailServerPrefs.java \ + MailServerPrefsEditor.java \ + Prefs.java \ + PrefsBeanInfo.java \ + UIPrefs.java \ + UIPrefsEditor.java \ + UserPrefs.java \ + UserPrefsEditor.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/prefs/PrefDialogs.xml b/mozilla/grendel/prefs/PrefDialogs.xml new file mode 100644 index 00000000000..3e926986c71 --- /dev/null +++ b/mozilla/grendel/prefs/PrefDialogs.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mozilla/grendel/prefs/PrefLabels.properties b/mozilla/grendel/prefs/PrefLabels.properties new file mode 100644 index 00000000000..12521e172d9 --- /dev/null +++ b/mozilla/grendel/prefs/PrefLabels.properties @@ -0,0 +1,42 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + + +# +# Labels for the preferences panels +# + +userPrefsLabel=User +userNameLabel=Name: +userOrganizationLabel=Organization: +userEmailAddressLabel=E-mail Address: + +mailServerPrefsLabel=Mail Server +mailHostLabel=POP3 Server: +mailUserLabel=Username: +mailPasswordLabel=Password: +mailDirectoryLabel=Mail Directory: +mailLeaveOnServerLabel=Leave Messages on Server +smtpHostLabel=SMTP Host: + +UIPrefsLabel=Appearence +lafLabel=Look and Feel: + +editLabel=Edit +newLabel=New +deleteLabel=Delete +chooseLabel=Choose diff --git a/mozilla/grendel/prefs/Prefs.java b/mozilla/grendel/prefs/Prefs.java new file mode 100644 index 00000000000..bb9515494ca --- /dev/null +++ b/mozilla/grendel/prefs/Prefs.java @@ -0,0 +1,154 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 15 Oct 1997. + */ + +package grendel.prefs; + +import java.util.StringTokenizer; + +import javax.mail.URLName; + +import com.sun.java.swing.UIManager; +import com.sun.java.swing.UnsupportedLookAndFeelException; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import grendel.ui.StoreFactory; + +public class Prefs { + static Preferences fPrefs = PreferencesFactory.Get(); + + static final String kUserName = "user.name"; + static final String kOrganization = "user.organization"; + static final String kEmailAddress = "user.email_address"; + static final String kPopHost = "pop.host"; + static final String kPopUser = "pop.user"; + static final String kPopPassword = "pop.password"; + static final String kPopLeaveOnServer = "pop.leaveMailOnServer"; + static final String kMailDirectory = "mail.directory"; + static final String kSMTPHost = "mail.smtp.host"; + static final String kStoreList = "mail.storelist"; + + static final String kLocalProtocol = "berkeley"; + + public UserPrefs getUserPrefs() { + UserPrefs res = new UserPrefs(); + + res.setUserName(fPrefs.getString(kUserName, "John Doe")); + res.setUserEmailAddress(fPrefs.getString(kEmailAddress, "john@doe.com")); + res.setUserOrganization(fPrefs.getString(kOrganization, "")); + + return res; + } + + public void setUserPrefs(UserPrefs aPrefs) { + fPrefs.putString(kUserName, aPrefs.getUserName()); + fPrefs.putString(kEmailAddress, aPrefs.getUserEmailAddress()); + fPrefs.putString(kOrganization, aPrefs.getUserOrganization()); + } + + public MailServerPrefs getMailServerPrefs() { + MailServerPrefs res = new MailServerPrefs(); + + res.setMailDirectory(fPrefs.getString(kMailDirectory, "")); + res.setLeaveOnServer(fPrefs.getBoolean(kPopLeaveOnServer, false)); + res.setSMTPHost(fPrefs.getString(kSMTPHost, "mail")); + + String storelist = fPrefs.getString(kStoreList, ""); + StringTokenizer st = new StringTokenizer(storelist, " ,;"); + + URLName urlNames[] = new URLName[st.countTokens()]; + int i = 0; + while (st.hasMoreTokens()) { + String storename = st.nextToken().trim(); + URLName urlName = null; + if (storename.indexOf(":") != -1) { + urlName = new URLName(storename); + } else { + urlName = new URLName(storename, null, -1, null, null, null); + } + urlNames[i++] = urlName; + } + + res.setStores(urlNames); + + return res; + } + + public void setMailServerPrefs(MailServerPrefs aPrefs) { + fPrefs.putString(kMailDirectory, aPrefs.getMailDirectory()); + fPrefs.putBoolean(kPopLeaveOnServer, aPrefs.getLeaveOnServer()); + fPrefs.putString(kSMTPHost, aPrefs.getSMTPHost()); + + URLName stores[] = aPrefs.getStores(); + boolean berkeley = false; + int i; + + StringBuffer buffer = new StringBuffer(); + for (i = 0; i < stores.length; i++) { + if (stores[i].getProtocol().equals(kLocalProtocol)) { + berkeley = true; + if (aPrefs.getMailDirectory().equals("")) { + continue; + } + } + if (i > 0) { + buffer.append(","); + } + if (stores[i].getHost() == null) { + buffer.append(stores[i].getProtocol()); + } else { + buffer.append(stores[i].toString()); + } + } + + if (!berkeley && !aPrefs.getMailDirectory().equals("")) { + if (i > 0) { + buffer.append(","); + } + buffer.append(kLocalProtocol); + } + + System.out.println("putting " + kStoreList + ":" + buffer.toString()); + fPrefs.putString(kStoreList, buffer.toString()); + + StoreFactory.Instance().refreshStores(); + } + + public UIPrefs getUIPrefs() { + UIPrefs res = new UIPrefs(); + + res.setLAF(UIManager.getLookAndFeel()); + + return res; + } + + public void setUIPrefs(UIPrefs aUIPrefs) { + if (aUIPrefs.getLAF() != null) { + try { + System.out.println("Setting L&F to " + aUIPrefs.getLAF()); + UIManager.setLookAndFeel(aUIPrefs.getLAF()); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + } + } +} diff --git a/mozilla/grendel/prefs/PrefsBeanInfo.java b/mozilla/grendel/prefs/PrefsBeanInfo.java new file mode 100644 index 00000000000..f14f50ae6cf --- /dev/null +++ b/mozilla/grendel/prefs/PrefsBeanInfo.java @@ -0,0 +1,77 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 15 Oct 1997. + */ + +package grendel.prefs; + +import java.beans.Introspector; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.beans.SimpleBeanInfo; +import java.util.ResourceBundle; +import java.util.MissingResourceException; + +public class PrefsBeanInfo extends SimpleBeanInfo { + + static final String sProps[] = {"UserPrefs", + "MailServerPrefs", + "UIPrefs" + }; + + public PropertyDescriptor[] getPropertyDescriptors() { + PropertyDescriptor res[] = null; + + ResourceBundle labels = + ResourceBundle.getBundle("grendel.prefs.PrefLabels"); + + try { + res = new PropertyDescriptor[sProps.length]; + for (int i = 0; i < sProps.length; i++) { + PropertyDescriptor propDesc = + new PropertyDescriptor(Introspector.decapitalize(sProps[i]), + Prefs.class, + "get" + sProps[i], + "set" + sProps[i]); + String label = sProps[i]; + try { + label = labels.getString(Introspector.decapitalize(sProps[i]) + + "Label"); + } catch (MissingResourceException e) {} + + propDesc.setDisplayName(label); + try { + propDesc.setPropertyEditorClass(Class.forName("grendel.prefs." + + sProps[i] + "Editor")); + } catch (ClassNotFoundException e) {} + + res[i] = propDesc; + } + + return res; + } catch (IntrospectionException e) { + System.err.println("PreferencesBeanInfo: " + e); + res = null; + } + return res; + } +} + + + diff --git a/mozilla/grendel/prefs/UIPrefs.java b/mozilla/grendel/prefs/UIPrefs.java new file mode 100644 index 00000000000..f129e2c56f6 --- /dev/null +++ b/mozilla/grendel/prefs/UIPrefs.java @@ -0,0 +1,36 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 23 Dec 1997. + */ + +package grendel.prefs; + +import com.sun.java.swing.LookAndFeel; + +public class UIPrefs { + LookAndFeel fLAF; + + public LookAndFeel getLAF() { + return fLAF; + } + + public void setLAF(LookAndFeel aLAF) { + fLAF = aLAF; + } +} diff --git a/mozilla/grendel/prefs/UIPrefsEditor.java b/mozilla/grendel/prefs/UIPrefsEditor.java new file mode 100644 index 00000000000..a9d9c7cc7ce --- /dev/null +++ b/mozilla/grendel/prefs/UIPrefsEditor.java @@ -0,0 +1,219 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 23 Dec 1997. + */ + +package grendel.prefs; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.beans.PropertyEditor; +import java.net.URL; +import java.util.EventObject; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import com.sun.java.swing.AbstractListModel; +import com.sun.java.swing.LookAndFeel; +import com.sun.java.swing.UIManager; + +import netscape.orion.dialogs.AbstractCtrl; +import netscape.orion.dialogs.AttrNotFoundException; +import netscape.orion.dialogs.JLISTeditor; +import netscape.orion.dialogs.PageModel; +import netscape.orion.dialogs.PageUI; + +import xml.tree.XMLNode; +import xml.tree.TreeBuilder; + +public class UIPrefsEditor implements PropertyEditor { + UIPrefs fPrefs = new UIPrefs(); + PropertyChangeSupport fListeners = new PropertyChangeSupport(this); + + ResourceBundle fLabels = + ResourceBundle.getBundle("grendel.prefs.PrefLabels"); + + static final String kLAFKey="LAFList"; + + Hashtable fValues = null; + + PageUI fPanel; + + UIPrefsModel fModel; + + class UIPrefsModel extends PageModel { + public UIPrefsModel() { + fValues = new Hashtable(); + } + + public Object getAttribute(String aAttrib) throws AttrNotFoundException { + Object res = fValues.get(aAttrib); + if (res == null) { + res = fLabels.getString(aAttrib); + } + if (res == null) { + throw new AttrNotFoundException(aAttrib); + } + return res; + } + + public void setAttribute(String aAttrib, Object aValue) { + if (fValues.containsKey(aAttrib)) { + fValues.put(aAttrib, aValue); + } + } + + public void actionPerformed(ActionEvent aEvent) { + } + } + + public UIPrefsEditor() { + fModel = new UIPrefsModel(); + + XMLNode root = null; + URL url = getClass().getResource("PrefDialogs.xml"); + try { + root = xml.tree.TreeBuilder.build(url, getClass()); + XMLNode editHost = root.getChild("dialog", "id", "UIPrefs"); + + fPanel = new PageUI(url, editHost, fModel); + + AbstractCtrl c; + ChangeAction ca = new ChangeAction(); + + c = fPanel.getCtrlByName(kLAFKey); + c.addPropertyChangeListener(ca); + if (c instanceof JLISTeditor) { + ((JLISTeditor) c).setModel(new LAFListModel()); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getAsText() { + return null; + } + + public Component getCustomEditor() { + return fPanel; + } + + public String getJavaInitializationString() { + return ""; + } + + public String[] getTags() { + return null; + } + + public Object getValue() { + fPanel.saveAll(); + + AbstractCtrl c = fPanel.getCtrlByName(kLAFKey); + fPrefs.setLAF((LookAndFeel) c.getValue()); + + return fPrefs; + } + + public boolean isPaintable() { + return false; + } + + public void paintValue (Graphics g, Rectangle r) { + } + + public void setAsText(String aValue) { + } + + public void setValue(Object aValue) { + if (aValue instanceof UIPrefs) { + UIPrefs oldPrefs = fPrefs; + fPrefs = (UIPrefs) aValue; + + AbstractCtrl c = fPanel.getCtrlByName(kLAFKey); + c.setValue(fPrefs.getLAF()); + + fListeners.firePropertyChange(null, oldPrefs, fPrefs); + } + } + + public boolean supportsCustomEditor() { + return true; + } + + public void addPropertyChangeListener(PropertyChangeListener l) { + fListeners.addPropertyChangeListener(l); + } + + public void removePropertyChangeListener(PropertyChangeListener l) { + fListeners.removePropertyChangeListener(l); + } + + class ChangeAction implements PropertyChangeListener { + void event(EventObject aEvent) { + getValue(); + fListeners.firePropertyChange(null, null, fPrefs); + } + + public void propertyChange(PropertyChangeEvent aEvent) { + System.out.println("propertyChange" + aEvent); + event(aEvent); + } + } + + class LAFListModel extends AbstractListModel { + LookAndFeel fLAFs[] = null; + + LAFListModel() { + /* Hack until this works + fLAFs = UIManager.getAuxiliaryLookAndFeels(); + */ + fLAFs = new LookAndFeel[] { + new com.sun.java.swing.jlf.JLFLookAndFeel(), + new com.sun.java.swing.motif.MotifLookAndFeel(), + new com.sun.java.swing.windows.WindowsLookAndFeel() + }; + } + + public int getSize() { + if (fLAFs != null) { + return fLAFs.length; + } + return 0; + } + + public Object getElementAt(int index) { + if (fLAFs != null && index < fLAFs.length) { + return fLAFs[index]; + } + return null; + } + } + +} diff --git a/mozilla/grendel/prefs/UserPrefs.java b/mozilla/grendel/prefs/UserPrefs.java new file mode 100644 index 00000000000..6592f041e1e --- /dev/null +++ b/mozilla/grendel/prefs/UserPrefs.java @@ -0,0 +1,62 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 16 Oct 1997. + */ + +package grendel.prefs; + +public class UserPrefs { + String fUserName; + String fUserEmailAddress; + String fUserOrganization; + + public String getUserName() { + return fUserName; + } + + public void setUserName(String aName) { + fUserName = aName; + } + + public String getUserEmailAddress() { + return fUserEmailAddress; + } + + public void setUserEmailAddress(String aAddress) { + fUserEmailAddress = aAddress; + } + + public String getUserOrganization() { + return fUserOrganization; + } + + public void setUserOrganization(String aOrganization) { + fUserOrganization = aOrganization; + } + + public boolean equals(Object aObject) { + if (aObject instanceof UserPrefs) { + UserPrefs prefs = (UserPrefs) aObject; + return prefs.fUserName.equals(fUserName) && + prefs.fUserEmailAddress.equals(fUserEmailAddress) && + prefs.fUserOrganization.equals(fUserOrganization); + } + return false; + } +} diff --git a/mozilla/grendel/prefs/UserPrefsEditor.java b/mozilla/grendel/prefs/UserPrefsEditor.java new file mode 100644 index 00000000000..437e5ee7832 --- /dev/null +++ b/mozilla/grendel/prefs/UserPrefsEditor.java @@ -0,0 +1,196 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 16 Oct 1997. + */ + +package grendel.prefs; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.beans.PropertyEditor; +import java.net.URL; +import java.util.EventObject; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import netscape.orion.dialogs.AbstractCtrl; +import netscape.orion.dialogs.AttrNotFoundException; +import netscape.orion.dialogs.PageModel; +import netscape.orion.dialogs.PageUI; + +import xml.tree.XMLNode; +import xml.tree.TreeBuilder; + +public class UserPrefsEditor implements PropertyEditor { + UserPrefs fPrefs = new UserPrefs(); + PropertyChangeSupport fListeners = new PropertyChangeSupport(this); + + ResourceBundle fLabels = + ResourceBundle.getBundle("grendel.prefs.PrefLabels"); + + static final String kNameKey="userNameField"; + static final String kOrganizationKey="userOrganizationField"; + static final String kEmailAddressKey="userEmailAddressField"; + + Hashtable fValues = null; + + PageUI fPanel; + + UserPrefsModel fModel; + + class UserPrefsModel extends PageModel { + public UserPrefsModel() { + fValues = new Hashtable(); + fValues.put(kNameKey, ""); + fValues.put(kOrganizationKey, ""); + fValues.put(kEmailAddressKey, ""); + } + + public Object getAttribute(String aAttrib) throws AttrNotFoundException { + Object res = fValues.get(aAttrib); + if (res == null) { + res = fLabels.getString(aAttrib); + } + if (res == null) { + throw new AttrNotFoundException(aAttrib); + } + return res; + } + + public void setAttribute(String aAttrib, Object aValue) { + if (fValues.containsKey(aAttrib)) { + fValues.put(aAttrib, aValue); + } + } + + public void actionPerformed(ActionEvent aEvent) { + } + } + + public UserPrefsEditor() { + fModel = new UserPrefsModel(); + XMLNode root = null; + URL url = getClass().getResource("PrefDialogs.xml"); + try { + root = xml.tree.TreeBuilder.build(url, getClass()); + XMLNode editHost = root.getChild("dialog", "id", "userPrefs"); + + fPanel = new PageUI(url, editHost, fModel); + + AbstractCtrl c; + ChangeAction ca = new ChangeAction(); + + c = fPanel.getCtrlByName(kNameKey); + c.addPropertyChangeListener(ca); + + c = fPanel.getCtrlByName(kOrganizationKey); + c.addPropertyChangeListener(ca); + + c = fPanel.getCtrlByName(kEmailAddressKey); + c.addPropertyChangeListener(ca); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getAsText() { + return null; + } + + public Component getCustomEditor() { + return fPanel; + } + + public String getJavaInitializationString() { + return ""; + } + + public String[] getTags() { + return null; + } + + public Object getValue() { + fPanel.saveAll(); + + String name = (String) fValues.get(kNameKey); + String org = (String) fValues.get(kOrganizationKey); + String email = (String) fValues.get(kEmailAddressKey); + + fPrefs.setUserName(name); + fPrefs.setUserOrganization(org); + fPrefs.setUserEmailAddress(email); + + return fPrefs; + } + + public boolean isPaintable() { + return false; + } + + public void paintValue (Graphics g, Rectangle r) { + } + + public void setAsText(String aValue) { + } + + public void setValue(Object aValue) { + if (aValue instanceof UserPrefs) { + UserPrefs oldPrefs = fPrefs; + fPrefs = (UserPrefs) aValue; + + fValues.put(kNameKey, fPrefs.getUserName()); + fValues.put(kOrganizationKey, fPrefs.getUserOrganization()); + fValues.put(kEmailAddressKey, fPrefs.getUserEmailAddress()); + + fPanel.initAll(); + + fListeners.firePropertyChange(null, oldPrefs, fPrefs); + } + } + + public boolean supportsCustomEditor() { + return true; + } + + public void addPropertyChangeListener(PropertyChangeListener l) { + fListeners.addPropertyChangeListener(l); + } + + public void removePropertyChangeListener(PropertyChangeListener l) { + fListeners.removePropertyChangeListener(l); + } + + class ChangeAction implements PropertyChangeListener { + void event(EventObject aEvent) { + getValue(); + fListeners.firePropertyChange(null, null, fPrefs); + } + + public void propertyChange(PropertyChangeEvent aEvent) { + event(aEvent); + } + } +} diff --git a/mozilla/grendel/rules.mk b/mozilla/grendel/rules.mk new file mode 100644 index 00000000000..28ca8a17c58 --- /dev/null +++ b/mozilla/grendel/rules.mk @@ -0,0 +1,58 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +# You need these: +# +# http://java.sun.com/products/javamail/ +# http://java.sun.com/beans/glasgow/jaf.html +# http://java.sun.com/products/jfc/#download-swing +# +# setenv CLASSPATH '' +# setenv CLASSPATH ${CLASSPATH}:/u/jwz/src/mozilla +# setenv CLASSPATH ${CLASSPATH}:/u/jwz/src/mozilla/grendel +# setenv CLASSPATH ${CLASSPATH}:/u/jwz/tmp/grendel/javamail-1.1/mail.jar +# setenv CLASSPATH ${CLASSPATH}:/u/jwz/tmp/grendel/jaf/activation.jar +# setenv CLASSPATH ${CLASSPATH}:/u/jwz/tmp/grendel/swing-1.0.3/swingall.jar + + +JAVAC = javac +RM = rm -f + +OBJS = $(subst .java,.class,$(SRCS)) + +.SUFFIXES: .java .class + +.java.class: + $(JAVAC) $*.java + +all:: $(OBJS) + +#clean:: +# $(RM) $(OBJS) + +clean:: + $(RM) *.class + +distclean:: + $(RM) *.class *~ core + +all clean distclean:: + @sd="$(SUBDIRS)" ; \ + for dir in $$sd; do \ + ( cd $$dir ; $(MAKE) $@ ); \ + done diff --git a/mozilla/grendel/search/AndPanel.java b/mozilla/grendel/search/AndPanel.java new file mode 100644 index 00000000000..2c6fb8bd75a --- /dev/null +++ b/mozilla/grendel/search/AndPanel.java @@ -0,0 +1,56 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Dimension; +import java.awt.LayoutManager2; + +import javax.mail.search.AndTerm; +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.border.TitledBorder; + +import grendel.widgets.SelectionManager; + +class AndPanel extends NaryTermPanel { + public AndPanel(SearchPanel aParent) { + super(aParent, new OperatorLayout()); + if (bStyle2) { + setBorder(new TitledBorder("And")); + } else { + setBorder(new BevelBorder(BevelBorder.RAISED)); + add(OperatorLayout.kOperator, new JLabel("And")); + } + } + + public SearchTerm getSearch() { + int count = getTermCount(); + SearchTerm term = getTerm(0).getSearch(); + for (int i = 1; i < count; i++) { + term = new AndTerm(term, getTerm(i).getSearch()); + } + return term; + } +} diff --git a/mozilla/grendel/search/AttributeLayout.java b/mozilla/grendel/search/AttributeLayout.java new file mode 100644 index 00000000000..64ce6729f99 --- /dev/null +++ b/mozilla/grendel/search/AttributeLayout.java @@ -0,0 +1,126 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager2; +import java.awt.IllegalComponentStateException; + +class AttributeLayout implements LayoutManager2 { + int fMargin = 4; + Component fAttribute; + Component fOperator; + Component fValue; + + public static final String kAttribute = "attribute"; + public static final String kOperator = "operator"; + public static final String kValue = "value"; + + public void addLayoutComponent(Component aComponent, Object aConstraint) { + if (aConstraint.equals(kAttribute) && fAttribute == null) { + fAttribute = aComponent; + } else if (aConstraint.equals(kOperator) && fOperator == null) { + fOperator = aComponent; + } else if (aConstraint.equals(kValue) && fValue == null) { + fValue = aComponent; + } else { + throw new IllegalComponentStateException(); + } + } + + public void addLayoutComponent(String aConstraint, Component aComponent) { + addLayoutComponent(aComponent, aConstraint); + } + + public void removeLayoutComponent(Component aComponent) { + if (aComponent == fAttribute) { + fAttribute = null; + } else if (aComponent == fOperator) { + fOperator = null; + } else if (aComponent == fValue) { + fValue = null; + } + } + + public void layoutContainer(Container aParent) { + int count = aParent.getComponentCount(); + Component c[] = aParent.getComponents(); + Insets insets = aParent.getInsets(); + Dimension size = aParent.getSize(); + int x = insets.top; + int y = insets.left; + int w = size.width - fMargin * (count - 1) - insets.left - insets.right; + + for (int i = 0; i < count; i++) { + Dimension dim = c[i].getPreferredSize(); + if (c[i] == fValue) { + dim.width = w / 2 + w % 4; + } else { + dim.width = w / 4; + } + c[i].setBounds(x, y, dim.width, dim.height); + x += dim.width + fMargin; + } + } + + public Dimension preferredLayoutSize(Container aParent) { + int count = aParent.getComponentCount(); + Component c[] = aParent.getComponents(); + Insets insets = aParent.getInsets(); + int h = 0; + int w = 0; + + for (int i = 0; i < count; i++) { + Dimension dim = c[i].getPreferredSize(); + if (dim.height > h) { + h = dim.height; + } + w += dim.width; + } + w += insets.left + insets.right + (count - 1) * fMargin; + h += insets.top + insets.bottom; + + return new Dimension(w, h); + } + + public Dimension minimumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + + public Dimension maximumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + + public float getLayoutAlignmentX(Container aParent) { + return 0; + } + + public float getLayoutAlignmentY(Container aParent) { + return 0; + } + + public void invalidateLayout(Container aParent) { + } +} diff --git a/mozilla/grendel/search/AttributePanel.java b/mozilla/grendel/search/AttributePanel.java new file mode 100644 index 00000000000..477fef89624 --- /dev/null +++ b/mozilla/grendel/search/AttributePanel.java @@ -0,0 +1,121 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagLayout; +import java.awt.LayoutManager2; +import java.awt.SystemColor; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JComboBox; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JList; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JTextField; +import com.sun.java.swing.ListCellRenderer; +import com.sun.java.swing.border.EmptyBorder; + +import grendel.widgets.SelectionManager; + +class AttributePanel extends TermPanel { + ISearchable fSearchable; + int fMargin = 3; + JComboBox fAttribCombo; + JComboBox fOpCombo; + Component fValueComponent; + + public AttributePanel(SearchPanel aParent, ISearchable aSearchable) { + super(aParent, new AttributeLayout()); + + fSearchable = aSearchable; + + setBorder(new EmptyBorder(fMargin, fMargin, fMargin, fMargin)); + + fAttribCombo = new JComboBox(); + add(fAttribCombo, AttributeLayout.kAttribute); + updateAttribCombo(); + + fOpCombo = new JComboBox(); + add(fOpCombo, AttributeLayout.kOperator); + updateOpCombo(); + + fValueComponent = null; + updateValueComponent(); + + fAttribCombo.addItemListener(new AttribListener()); + } + + public SearchTerm getSearch() { + ISearchAttribute attrib = + (ISearchAttribute) fAttribCombo.getSelectedItem(); + + return attrib.getAttributeTerm(fOpCombo.getSelectedItem(), + attrib.getValue(fValueComponent)); + } + + void updateAttribCombo() { + fAttribCombo.removeAllItems(); + for (int i = 0; i < fSearchable.getSearchAttributeCount(); i++) { + fAttribCombo.addItem(fSearchable.getSearchAttribute(i)); + } + fAttribCombo.setSelectedIndex(0); + } + + void updateOpCombo() { + ISearchAttribute attrib = + (ISearchAttribute) fAttribCombo.getSelectedItem(); + + fOpCombo.removeAllItems(); + for (int i = 0; i < attrib.getOperatorCount(); i++) { + fOpCombo.addItem(attrib.getOperator(i)); + } + fOpCombo.setSelectedIndex(0); + } + + void updateValueComponent() { + ISearchAttribute attrib = + (ISearchAttribute) fAttribCombo.getSelectedItem(); + + if (fValueComponent != null) { + remove(fValueComponent); + } + fValueComponent = attrib.getValueComponent(); + if (fValueComponent != null) { + add(fValueComponent, AttributeLayout.kValue); + } + } + + class AttribListener implements ItemListener { + public void itemStateChanged(ItemEvent aEvent) { + if (aEvent.getStateChange() == ItemEvent.SELECTED) { + updateOpCombo(); + updateValueComponent(); + repaint(); + } + } + } +} diff --git a/mozilla/grendel/search/ISearchAttribute.java b/mozilla/grendel/search/ISearchAttribute.java new file mode 100644 index 00000000000..cc256bfb88f --- /dev/null +++ b/mozilla/grendel/search/ISearchAttribute.java @@ -0,0 +1,78 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +import java.awt.Component; + +import javax.mail.search.SearchTerm; + +public interface ISearchAttribute { + /** + * Returns a localized, user visible attribute string + */ + + public String getName(); + + /** + * Returns a non-localized attribute identifier + */ + + public Object getID(); + + /** + * Returns the number of available operators for this attribute + */ + + public int getOperatorCount(); + + /** + * Returns the localized, user visible operator string + */ + + public Object getOperator(int aIndex); + + /** + * Returns the component to use for selecting a value + */ + + public Component getValueComponent(); + + /** + * Given a component, return the value represented by it. + * The component is one returned by getValueComponent from + * the same object. + */ + + public Object getValue(Component aComponent); + + /** + * Returns an attribute search term. + */ + + public SearchTerm getAttributeTerm(Object aOperatorID, Object aValue); + + /** + * Same as getName(); + */ + + public String toString(); +} diff --git a/mozilla/grendel/search/ISearchResults.java b/mozilla/grendel/search/ISearchResults.java new file mode 100644 index 00000000000..316953410e8 --- /dev/null +++ b/mozilla/grendel/search/ISearchResults.java @@ -0,0 +1,26 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +public interface ISearchResults { + +} diff --git a/mozilla/grendel/search/ISearchable.java b/mozilla/grendel/search/ISearchable.java new file mode 100644 index 00000000000..47e21b1e0a0 --- /dev/null +++ b/mozilla/grendel/search/ISearchable.java @@ -0,0 +1,37 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +import java.awt.Component; + +import javax.mail.search.SearchTerm; + +public interface ISearchable { + public int getSearchAttributeCount(); + public ISearchAttribute getSearchAttribute(int idx); + + public void search(SearchTerm aTerm); + + public ISearchResults getSearchResults(); + public Component getTargetComponent(); + public Component getResultComponent(ISearchResults aResults); +} diff --git a/mozilla/grendel/search/MailSearch.java b/mozilla/grendel/search/MailSearch.java new file mode 100644 index 00000000000..171ea8ce185 --- /dev/null +++ b/mozilla/grendel/search/MailSearch.java @@ -0,0 +1,236 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +import java.awt.Component; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; + +import javax.mail.Folder; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; +import javax.mail.search.DateTerm; +import javax.mail.search.FromTerm; +import javax.mail.search.SentDateTerm; +import javax.mail.search.SubjectTerm; +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JTextField; + +import grendel.ui.FolderCombo; +import grendel.ui.FolderFrame; +import grendel.ui.FolderPanel; +import grendel.ui.MessageDisplayManager; + +import grendel.storage.SearchResultsFolderFactory; +import grendel.view.ViewedFolder; + +public class MailSearch implements ISearchable { + static ISearchAttribute fAttributes[] = { + new SubjectAttribute(), + new AuthorAttribute(), + new SentDateAttribute()}; + + FolderCombo fTarget = null; + + public int getSearchAttributeCount() { + return fAttributes.length; + } + + public ISearchAttribute getSearchAttribute(int idx) { + return fAttributes[idx]; + } + + public void search(SearchTerm aTerm) { + ViewedFolder folder = fTarget.getSelectedFolder(); + + // Set target folder focus and search term in SearchResults singleton. + SearchResultsFolderFactory.SetTarget(folder.getFolder(), aTerm); + + // Now need to force re-opening of the SearchResults folder in + // Master Panel.... Will? + Folder results = SearchResultsFolderFactory.Get(); + // MessageDisplayManager.GetDefaultManager().displayFolder(results); + FolderFrame frame = FolderFrame.FindFolderFrame(results); + if (frame == null) { + frame = new FolderFrame(results); + frame.setVisible(true); + } + frame.toFront(); + frame.requestFocus(); + } + + public ISearchResults getSearchResults() { + return null; + } + + public Component getTargetComponent() { + if (fTarget == null) { + fTarget = new FolderCombo(); + } + + fTarget.populate(Folder.HOLDS_MESSAGES, 0); + + return fTarget; + } + + public Component getResultComponent(ISearchResults aResults) { + Folder results = SearchResultsFolderFactory.Get(); + //FolderPanel resultPanel = new FolderPanel(); + //resultPanel.setFolder(results); + FolderPanel resultPanel = null; + return resultPanel; + } +} + +class SubjectAttribute implements ISearchAttribute { + + static final String kID = "Subject"; + static final String kContains = "Contains"; + + public String getName() { + return "Subject"; + } + + public Object getID() { + return kID; + } + + public int getOperatorCount() { + return 1; + } + + public Object getOperator(int aIndex) { + return kContains; + } + + public Component getValueComponent() { + return new JTextField(30); + } + + public Object getValue(Component aComponent) { + return ((JTextField) aComponent).getText(); + } + + public SearchTerm getAttributeTerm(Object aOperatorID, Object aValue) { + return new SubjectTerm((String) aValue); + } + + public String toString() { + return getName(); + } +} + +class AuthorAttribute implements ISearchAttribute { + + static final String kID = "Author"; + static final String kIs = "Is"; + + public String getName() { + return "Author"; + } + + public Object getID() { + return kID; + } + + public int getOperatorCount() { + return 1; + } + + public Object getOperator(int aIndex) { + return kIs; + } + + public Component getValueComponent() { + return new JTextField(30); + } + + public Object getValue(Component aComponent) { + return ((JTextField) aComponent).getText(); + } + + public SearchTerm getAttributeTerm(Object aOperatorID, Object aValue) { + try { + return new FromTerm(new InternetAddress((String) aValue)); + } catch (AddressException e) { + return null; + } + } + + public String toString() { + return getName(); + } +} + +class SentDateAttribute implements ISearchAttribute { + + static final String kID = "SendDate"; + static final String kLessThan = "is before"; + static final String kIs = "is"; + static final String kGreaterThan = "is after"; + + static String sOperators[] = {kIs, kLessThan, kGreaterThan}; + + public String getName() { + return "Sent Date"; + } + + public Object getID() { + return kID; + } + + public int getOperatorCount() { + return sOperators.length; + } + + public Object getOperator(int aIndex) { + return sOperators[aIndex]; + } + + public Component getValueComponent() { + return new JTextField(15); + // return new DateChooser(DateChooser.DATE_MODE, DateChooser.SMALL); + } + + public Object getValue(Component aComponent) { + Date res = null; + try { + res = + DateFormat.getDateInstance().parse(((JTextField)aComponent).getText()); + } catch (ParseException e) { + } + return res; + } + + public SearchTerm getAttributeTerm(Object aOperatorID, Object aValue) { + int op = aOperatorID.equals(kLessThan) ? DateTerm.LT : + (aOperatorID.equals(kGreaterThan) ? DateTerm.GT : DateTerm.EQ); + return new SentDateTerm(op, (Date) aValue); + } + + public String toString() { + return getName(); + } +} + diff --git a/mozilla/grendel/search/Makefile b/mozilla/grendel/search/Makefile new file mode 100644 index 00000000000..81c1b550e14 --- /dev/null +++ b/mozilla/grendel/search/Makefile @@ -0,0 +1,37 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + AndPanel.java \ + AttributeLayout.java \ + AttributePanel.java \ + ISearchAttribute.java \ + ISearchResults.java \ + ISearchable.java \ + MailSearch.java \ + NaryTermPanel.java \ + NoopPanel.java \ + OperatorLayout.java \ + OrPanel.java \ + ResultsFrame.java \ + SearchFrame.java \ + SearchPanel.java \ + TermPanel.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/search/NaryTermPanel.java b/mozilla/grendel/search/NaryTermPanel.java new file mode 100644 index 00000000000..24ec9d6911b --- /dev/null +++ b/mozilla/grendel/search/NaryTermPanel.java @@ -0,0 +1,69 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 11 Nov 1997. + */ + +package grendel.search; + +import java.awt.Container; +import java.awt.LayoutManager; +import java.util.Vector; + + +public abstract class NaryTermPanel extends TermPanel { + Vector fTerms = new Vector(); + + public NaryTermPanel(SearchPanel aParent, LayoutManager aLayout) { + super(aParent, aLayout); + } + + public void addTerm(TermPanel aPanel) { + add(aPanel); + fTerms.addElement(aPanel); + } + + public void removeTerm(TermPanel aPanel) { + remove(aPanel); + fTerms.removeElement(aPanel); + } + + public int getTermCount() { + return fTerms.size(); + } + + public TermPanel getTerm(int idx) { + return (TermPanel) fTerms.elementAt(idx); + } + + public void replaceTerm(TermPanel aOld, TermPanel aNew) { + for (int i = 0; i < getComponentCount(); i++) { + if (getComponent(i) == aOld) { + remove(i); + add(aNew, i); + break; + } + } + + int idx = fTerms.indexOf(aOld); + if (idx >= 0) { + fTerms.removeElementAt(idx); + fTerms.insertElementAt(aNew, idx); + } + } +} diff --git a/mozilla/grendel/search/NoopPanel.java b/mozilla/grendel/search/NoopPanel.java new file mode 100644 index 00000000000..539ca56b0bd --- /dev/null +++ b/mozilla/grendel/search/NoopPanel.java @@ -0,0 +1,39 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 10 Nov 1997. + */ + +package grendel.search; + +import javax.mail.search.SearchTerm; + +class NoopPanel extends NaryTermPanel { + public NoopPanel(SearchPanel aParent) { + super(aParent, new OperatorLayout()); + } + + public SearchTerm getSearch() { + SearchTerm search = null; + TermPanel term = getTerm(0); + if (term != null) { + search = term.getSearch(); + } + return search; + } +} diff --git a/mozilla/grendel/search/OperatorLayout.java b/mozilla/grendel/search/OperatorLayout.java new file mode 100644 index 00000000000..0a25adfd589 --- /dev/null +++ b/mozilla/grendel/search/OperatorLayout.java @@ -0,0 +1,135 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.IllegalComponentStateException; +import java.awt.Insets; +import java.awt.LayoutManager2; + +class OperatorLayout implements LayoutManager2 { + int fMargin = 4; + Component fOperator; + + public final static String kOperator = "Operator"; + + public void addLayoutComponent(Component aComponent, Object aConstraint) { + addLayoutComponent((String) aConstraint, aComponent); + } + + public void addLayoutComponent(String aConstraint, Component aComponent) { + if (aConstraint != null && aConstraint.equals(kOperator)) { + if (fOperator == null) { + fOperator = aComponent; + } else { + throw new java.awt.IllegalComponentStateException(); + } + } + } + + public void removeLayoutComponent(Component aComponent) { + if (aComponent == fOperator) { + fOperator = null; + } + } + + public void layoutContainer(Container aParent) { + int count = aParent.getComponentCount(); + Component c[] = aParent.getComponents(); + Insets insets = aParent.getInsets(); + Dimension size = aParent.getSize(); + int x = insets.left + fMargin; + int y = insets.top + fMargin; + int w = size.width - insets.left - insets.right - fMargin * 2; + + if (fOperator != null) { + Dimension dim = fOperator.getPreferredSize(); + //x += dim.width + fMargin; + w -= dim.width + fMargin; + } + + for (int i = 0; i < count; i++) { + if (c[i] != fOperator) { + Dimension dim = c[i].getPreferredSize(); + c[i].setBounds(x, y, w, dim.height); + y += dim.height + fMargin; + } + } + + if (fOperator != null) { + Dimension dim = fOperator.getPreferredSize(); + //fOperator.setBounds(insets.left + fMargin, + fOperator.setBounds(x + w + fMargin, + (y - dim.height) / 2, + dim.width, dim.height); + } + } + + public Dimension preferredLayoutSize(Container aParent) { + int count = aParent.getComponentCount(); + Component c[] = aParent.getComponents(); + Insets insets = aParent.getInsets(); + int h = fMargin * 2; + int w = 0; + + for (int i = 0; i < count; i++) { + if (c[i] != fOperator) { + Dimension dim = c[i].getPreferredSize(); + if (dim.width > w) { + w = dim.width; + } + h += dim.height; + } + } + + w += insets.left + insets.right + fMargin * 2; + h += insets.top + insets.bottom + (count - 1) * fMargin; + + if (fOperator != null) { + Dimension dim = fOperator.getPreferredSize(); + w += dim.width; + } + + return new Dimension(w, h); + } + + public Dimension minimumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + + public Dimension maximumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + + public float getLayoutAlignmentX(Container aParent) { + return 0; + } + + public float getLayoutAlignmentY(Container aParent) { + return 0; + } + + public void invalidateLayout(Container aParent) { + } +} diff --git a/mozilla/grendel/search/OrPanel.java b/mozilla/grendel/search/OrPanel.java new file mode 100644 index 00000000000..caa222557fd --- /dev/null +++ b/mozilla/grendel/search/OrPanel.java @@ -0,0 +1,56 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Dimension; +import java.awt.LayoutManager2; + +import javax.mail.search.OrTerm; +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.border.TitledBorder; + +import grendel.widgets.SelectionManager; + +class OrPanel extends NaryTermPanel { + public OrPanel(SearchPanel aParent) { + super(aParent, new OperatorLayout()); + if (bStyle2) { + setBorder(new TitledBorder("Or")); + } else { + setBorder(new BevelBorder(BevelBorder.LOWERED)); + add(OperatorLayout.kOperator, new JLabel("Or")); + } + } + + public SearchTerm getSearch() { + int count = getTermCount(); + SearchTerm term = getTerm(0).getSearch(); + for (int i = 1; i < count; i++) { + term = new OrTerm(term, getTerm(i).getSearch()); + } + return term; + } +} diff --git a/mozilla/grendel/search/ResultsFrame.java b/mozilla/grendel/search/ResultsFrame.java new file mode 100644 index 00000000000..f3c995598ab --- /dev/null +++ b/mozilla/grendel/search/ResultsFrame.java @@ -0,0 +1,43 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 18 Nov 1997. + */ + +package grendel.search; + +import java.awt.Component; + +import grendel.ui.GeneralFrame; + +public class ResultsFrame extends GeneralFrame { + + public ResultsFrame(Component aComponent) { + super("Search Results", "mail.search_results"); + fPanel.add(aComponent); + + restoreBounds(); + setVisible(true); + } + + public void dispose() { + saveBounds(); + + super.dispose(); + } +} diff --git a/mozilla/grendel/search/SearchFrame.java b/mozilla/grendel/search/SearchFrame.java new file mode 100644 index 00000000000..232c392b28e --- /dev/null +++ b/mozilla/grendel/search/SearchFrame.java @@ -0,0 +1,116 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +import java.awt.BorderLayout; +import java.awt.Canvas; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import com.sun.java.swing.Action; +import com.sun.java.swing.AbstractAction; +import com.sun.java.swing.JComponent; +import com.sun.java.swing.JButton; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.KeyStroke; +import com.sun.java.swing.border.EmptyBorder; + +import netscape.orion.toolbars.BarLayout; + +import grendel.ui.GeneralFrame; + +public class SearchFrame extends GeneralFrame { + SearchPanel fSearchPanel; + + JComponent fTargetPanel; + JComponent fActionPanel; + JButton fSearchButton; + JButton fCloseButton; + + public SearchFrame() { + super("Search", "mail.search"); + + fSearchPanel = new SearchPanel(new MailSearch()); + getContentPane().add(fSearchPanel); + + fActionPanel = new JPanel(new BarLayout(BarLayout.kHorizontal, 5), true); + fActionPanel.setBorder(new EmptyBorder(5,5,5,5)); + fActionPanel.add(new Canvas(), BarLayout.kSpring); + + fCloseButton = new JButton("Close"); + fCloseButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent aEvent) { + dispose(); + } + }); + fActionPanel.add(fCloseButton, BarLayout.kSheep); + fSearchButton = new JButton("Search"); + fSearchButton.addActionListener(new SearchAction()); + fActionPanel.add(fSearchButton, BarLayout.kSheep); + + fPanel.add(BorderLayout.SOUTH, fActionPanel); + + fTargetPanel = new JPanel(new BarLayout(BarLayout.kHorizontal), true); + fTargetPanel.setBorder(new EmptyBorder(5,5,5,5)); + + fTargetPanel.add(new Canvas(), BarLayout.kSpring); + fTargetPanel.add(new JLabel("Search In: "), + BarLayout.kVariable); + fTargetPanel.add(fSearchPanel.getSearchable().getTargetComponent(), + BarLayout.kVariable); + fTargetPanel.add(new Canvas(), BarLayout.kSpring); + + fPanel.add(BorderLayout.NORTH, fTargetPanel); + + getRootPane().registerKeyboardAction(new SearchAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + restoreBounds(); + setVisible(true); + } + + public void dispose() { + saveBounds(); + + super.dispose(); + } + + class SearchAction extends AbstractAction { + SearchAction() { + super("search"); + } + + public void actionPerformed(ActionEvent aEvent) { + ISearchable searchable = fSearchPanel.getSearchable(); + searchable.search(fSearchPanel.getSearch()); + ISearchResults results = searchable.getSearchResults(); + Component c = searchable.getResultComponent(results); + if (c != null) { + new ResultsFrame(c); + } + } + } +} diff --git a/mozilla/grendel/search/SearchPanel.java b/mozilla/grendel/search/SearchPanel.java new file mode 100644 index 00000000000..f0b4fc1737e --- /dev/null +++ b/mozilla/grendel/search/SearchPanel.java @@ -0,0 +1,204 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Oct 1997. + */ + +package grendel.search; + +import java.awt.BorderLayout; +import java.awt.Canvas; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Enumeration; + +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JComponent; +import com.sun.java.swing.JButton; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.border.EmptyBorder; + +import netscape.orion.toolbars.BarLayout; + +import grendel.widgets.SelectionManager; +import grendel.widgets.SingleSelectionManager; + +public class SearchPanel extends JPanel { + ISearchable fSearchable; + SearchPanel fThis; + + JComponent fOperatorPanel; + JScrollPane fQueryPanel; + JButton fAndButton; + JButton fOrButton; + JButton fDeleteButton; + NoopPanel fOuterTerm; + + SelectionManager fSelection; + + public SearchPanel(ISearchable aSearchable) { + super(new BorderLayout(), true); + fThis = this; + + fSearchable = aSearchable; + fSelection = new SingleSelectionManager(); + + // Create Query Panel + fQueryPanel = new JScrollPane(); + + fOuterTerm = new NoopPanel(this); + fOuterTerm.addTerm(new AttributePanel(this, fSearchable)); + + fQueryPanel.setViewportView(fOuterTerm); + add(fQueryPanel); + + // Create Operator buttons + fOperatorPanel = new JPanel(new BarLayout(BarLayout.kVertical, 5), true); + fOperatorPanel.setBorder(new EmptyBorder(5,5,5,5)); + + fAndButton = new JButton("And"); + fAndButton.addActionListener(new AndAction()); + fOperatorPanel.add(fAndButton, BarLayout.kSheep); + + fOrButton = new JButton("Or"); + fOrButton.addActionListener(new OrAction()); + fOperatorPanel.add(fOrButton, BarLayout.kSheep); + + fDeleteButton = new JButton("Delete"); + fDeleteButton.addActionListener(new DeleteAction()); + fOperatorPanel.add(fDeleteButton, BarLayout.kSheep); + + add(BorderLayout.EAST, fOperatorPanel); + } + + public void setSearchable(ISearchable aSearchable) { + fSearchable = aSearchable; + } + + public ISearchable getSearchable() { + return fSearchable; + } + + public SearchTerm getSearch() { + return fOuterTerm.getSearch(); + } + + public SelectionManager getSelection() { + return fSelection; + } + + protected void sanitize(NaryTermPanel aTerm) { + int i = 0; + while (i < aTerm.getTermCount()) { + TermPanel term = aTerm.getTerm(i); + if (term instanceof NaryTermPanel) { + NaryTermPanel nary = (NaryTermPanel) term; + sanitize(nary); + if (nary.getTermCount() < 1) { + aTerm.removeTerm(nary); + } else { + if (nary.getTermCount() < 2) { + aTerm.replaceTerm(nary, nary.getTerm(0)); + } + i++; + } + } else { + i++; + } + } + } + + protected TermPanel getSelectedTerm() { + TermPanel term = null; + + Enumeration selection = fSelection.getSelection(); + if (selection.hasMoreElements()) { + term = (TermPanel) selection.nextElement(); + } else if (fOuterTerm.getTermCount() != 0) { + term = fOuterTerm.getTerm(0); + } + + return term; + } + + class OrAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + TermPanel term = getSelectedTerm(); + + if (term instanceof OrPanel) { + OrPanel or = (OrPanel) term; + or.addTerm(new AttributePanel(fThis, fSearchable)); + } else { + NaryTermPanel parent = term != null ? term.getParentTerm() : null; + OrPanel or = new OrPanel(fThis); + if (parent != null) { + parent.replaceTerm(term, or); + or.addTerm(term); + } else { + fOuterTerm.addTerm(or); + } + or.addTerm(new AttributePanel(fThis, fSearchable)); + } + sanitize(fOuterTerm); + validate(); + } + } + + class AndAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + TermPanel term = getSelectedTerm(); + + if (term instanceof AndPanel) { + AndPanel and = (AndPanel) term; + and.addTerm(new AttributePanel(fThis, fSearchable)); + } else { + NaryTermPanel parent = term != null ? term.getParentTerm() : null; + AndPanel and = new AndPanel(fThis); + if (parent != null) { + parent.replaceTerm(term, and); + and.addTerm(term); + } else { + fOuterTerm.addTerm(and); + } + and.addTerm(new AttributePanel(fThis, fSearchable)); + } + validate(); + } + } + + class DeleteAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + TermPanel term = getSelectedTerm(); + + NaryTermPanel parent = term != null ? term.getParentTerm() : null; + if (parent != null) { + parent.removeTerm(term); + } + sanitize(fOuterTerm); + validate(); + } + } +} diff --git a/mozilla/grendel/search/TermPanel.java b/mozilla/grendel/search/TermPanel.java new file mode 100644 index 00000000000..301cc3e62cb --- /dev/null +++ b/mozilla/grendel/search/TermPanel.java @@ -0,0 +1,136 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Nov 1997. + */ + +package grendel.search; + +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.LayoutManager; +import java.awt.SystemColor; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.mail.search.SearchTerm; + +import com.sun.java.swing.JPanel; + +import grendel.widgets.SelectionEvent; +import grendel.widgets.SelectionListener; +import grendel.widgets.SelectionManager; + +public abstract class TermPanel extends JPanel implements SelectionListener { + static boolean bStyle2 = false; + SelectionManager fSelection; + TermPanel fThis; + + public TermPanel(SearchPanel aParent, LayoutManager l) { + super(l, true); + init(aParent.getSelection()); + } + + public TermPanel(SearchPanel aParent) { + super(true); + init(aParent.getSelection()); + } + + public NaryTermPanel getParentTerm() { + Container parent = getParent(); + if (parent instanceof NaryTermPanel) { + return (NaryTermPanel) parent; + } else { + return null; + } + } + + void init(SelectionManager aSelection) { + fSelection = aSelection; + fThis = this; + + fSelection.addSelectionListener(this); + addMouseListener(new TermMouseListener()); + } + + public Dimension getPreferredSize() { + return getLayout().preferredLayoutSize(this); + } + + public abstract SearchTerm getSearch(); + + public void paintComponent(Graphics g) { + // Save foreground + Color fg = getForeground(); + + if (isSelected()) { + g.setColor(SystemColor.textHighlight); + setForeground(SystemColor.textHighlightText); + } else { + g.setColor(SystemColor.control); + setForeground(SystemColor.textText); + } + Dimension size = getSize(); + g.fillRect(0, 0, size.width, size.height); + + // Restore foreground + setForeground(fg); + } + + boolean isSelected() { + return fSelection.isSelected(fThis); + } + + class TermMouseListener extends MouseAdapter { + public void mouseClicked(MouseEvent aEvent) { + fSelection.setSelection(fThis); + } + } + + public void selectionChanged(SelectionEvent aEvent) { + Object added[] = aEvent.getAdded(); + Object removed[] = aEvent.getRemoved(); + int i; + + if (added != null) { + for (i = 0; i < added.length; i++) { + if (added[i] == this) { + repaint(); + } + } + } + if (removed != null) { + for (i = 0; i < removed.length; i++) { + if (removed[i] == this) { + repaint(); + } + } + } + } + + public void selectionDoubleClicked(MouseEvent aEvent) { + } + + public void selectionContextClicked(MouseEvent aEvent) { + } + + public void selectionDragged(MouseEvent aEvent) { + } +} diff --git a/mozilla/grendel/storage/BerkeleyFolder.java b/mozilla/grendel/storage/BerkeleyFolder.java new file mode 100644 index 00000000000..6963661a6c3 --- /dev/null +++ b/mozilla/grendel/storage/BerkeleyFolder.java @@ -0,0 +1,1582 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 27 Aug 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; +import calypso.util.Assert; + +import grendel.storage.intertwingle.Twingle; + +import java.lang.Thread; +import java.lang.SecurityException; +import java.lang.InterruptedException; +import java.io.File; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.text.DecimalFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.GregorianCalendar; +import java.util.Vector; + +import javax.mail.FetchProfile; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.event.MessageChangedEvent; +import javax.mail.event.MessageCountEvent; + +import javax.mail.internet.InternetHeaders; + + +class BerkeleyFolder extends FolderBase implements FilenameFilter { + + File fFile; // the underlying disk file + + Folder parent; + + boolean fLoaded = false; // whether the folder has been parsed + private boolean fLoading = false; // true while in the process of parsing + + private boolean summary_dirty = false; // whether changes have been made that + // would necessitate updating the + // summary file. + + private boolean flags_dirty = false; // whether any of the messages in + // this folder have the FLAG_DIRTY + // bit set (meaning we need to flush + // out some X-Mozilla-Status headers.) + + /* The object that knows how to read and (possibly) write the summary file. + It will be null if there is no summary file, or if the summary file + is in a format we don't understand. If fLoaded is true, this will always + be non-null. If fLoaded is false, this might be null or might not be. + */ + MailSummaryFile mailSummaryFile = null; + + BerkeleyFolder(Store s, File f) { + super(s); + fFile = f; + } + + protected BerkeleyFolder(BerkeleyFolder parent, String name) { + this(parent.getStore(), new File(parent.fFile, name)); + } + + protected BerkeleyFolder(BerkeleyFolder parent, File f) { + super(parent.getStore()); + fFile = f; + } + + public char getSeparator() { + return File.separatorChar; + } + + public int getType() { + return HOLDS_MESSAGES | HOLDS_FOLDERS; + } + + + /** Returns the file name associated with this folder. */ + File getFile() { + return fFile; + } + + public String getName() { + return fFile.getName(); + } + + public String getFullName() { + return fFile.getAbsolutePath(); // ### This isn't quite right! + } + + public Folder getParent() { + return parent; + } + + public boolean accept(File f, String name) { + int L = name.length(); + if (L == 0) return false; + if (name.charAt(0) == '.') return false; + if (name.charAt(L-1) == '~') return false; + if (L < 4) return true; + if (name.regionMatches(true, L-4, ".SNM", 0, 4)) return false; + if (name.regionMatches(true, L-4, ".DAT", 0, 4)) return false; + if (name.regionMatches(true, L-4, ".LOG", 0, 4)) return false; + return true; + } + + public Folder[] list(String pattern) { + if (pattern.equals("%")) { + String list[] = fFile.list(this); + if (list == null || list.length == 0) return null; + Vector result = new Vector(); + for (int i=0 ; i 0; + } + + /** Returns the total number of messages in the folder, or -1 if unknown. + This includes deleted and unread messages. + */ + public int getMessageCount() { + if (fMessages != null) + return fMessages.size(); + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile != null) + return mailSummaryFile.totalMessageCount(); + else if (!fFile.exists() || fFile.length() < 5) + return 0; + else + return -1; + } + + /** Returns the number of non-deleted messages in the folder, or -1 if + unknown. This includes unread messages. + */ + public int getUndeletedMessageCount() { + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile != null) + return mailSummaryFile.undeletedMessageCount(); + else if (!fFile.exists() || fFile.length() < 5) + return 0; + else + return -1; + } + + /** Returns the number of unread messages in the folder, or -1 if unknown. + This does not include unread messages that are also deleted. + */ + public int getUnreadMessageCount() { + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile != null) + return mailSummaryFile.unreadMessageCount(); + else if (!fFile.exists() || fFile.length() < 5) + return 0; + else + return -1; + } + + + /** Returns the number of bytes consumed by deleted but not expunged + messages in the folder, or -1 if unknown. + */ + long deletedMessageBytes() { + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile != null) + return mailSummaryFile.deletedMessageBytes(); + else if (!fFile.exists() || fFile.length() < 5) + return 0; + else + return -1; + } + + + /** If the messages are not known, parse the underlying files. + If the folder file has changed on disk since it was parsed, + re-parse it (replacing the existing message objects with new + ones.) + */ + void ensureLoaded() { + Assert.Assertion(!fLoading); + + if (!fLoaded) { + synchronized (this) { + if (!fLoaded) // double check, to avoid race. + loadMessages(); + } + + } else { // it was already loaded once; see if we need to reload. + + if (verifyFileDate(false)) // file (probably) hasn't changed + return; + + synchronized (this) { + + if (verifyFileDate(true)) // double check, to avoid race. + return; + + System.err.println("warning: file " + fFile + + " has changed; re-loading!"); + + // notify all the old messages as deleted + Message[] oldlist = new Message[fMessages.size()]; + fMessages.copyInto(oldlist); + notifyMessageRemovedListeners(true /*???*/, oldlist); + + fMessages.setSize(0); // nuke the old messages + fLoaded = false; + summary_dirty = false; + flags_dirty = false; + loadMessages(); // read in the new messages + + // notify all the new messages as added + Message[] newlist = new Message[fMessages.size()]; + fMessages.copyInto(newlist); + notifyMessageAddedListeners(newlist); + } + } + } + + private synchronized void loadMessages() { + // Do the timebomb. +// java.util.Date before = new Date(97, 9, 1, 0, 0); +// java.util.Date now = new Date(); +// java.util.Date then = new Date(97, 11, 25, 12, 00); +// if (now.before(before) || now.after(then)) { +// System.err.println("This software has expired"); +// System.exit(-1); +// } + + Assert.Assertion(!fLoading && !fLoaded); + + summary_dirty = false; + try { // make sure unlockFolderFile() gets called. + fLoading = true; + lockFolderFile(); + + ParseBerkeleyFolderWithSummary parser = + new ParseBerkeleyFolderWithSummary(); + RandomAccessFile in = null; + try { + try { + in = new RandomAccessFile(fFile, "r"); + parser.mapOverMessages(in, this); + + // If there is no summary object, or an unwritable one, use Grendel. + // Do this now so that the newly-created summary object gets the + // proper date/size from the folder file (date/size that corresponds + // to the time at which we parsed the folder.) + // + // Also mark it dirty, so that the background thread will write a + // new file in Grendel format (creating a summary where there was + // none, or auto-upgrading the old format to the new one.) + // + if (mailSummaryFile == null || !mailSummaryFile.writable()) { + mailSummaryFile = new MailSummaryFileGrendel(this); + setSummaryDirty(true); + } + + } finally { + if (in != null) + in.close(); + } + } catch (IOException e) { + System.err.println("error parsing folder " + fFile + ": " + e); + e.printStackTrace(); + throw new Error("Argh! Couldn't open mailbox folder!"); + } + } finally { + unlockFolderFile(); + fLoading = false; + } + + Assert.Assertion(!fLoaded); + fLoaded = true; + + if (summary_dirty) { + // Kludge to deal properly with dirtying that occurs during parsing: + // delay the spawning of the background thread until after parsing + // is complete. + // + // #### Perhaps instead of spawning the thread to write the summary, + // we should just write it now? If we have parsed the folder from + // scratch, we've already done a lot of I/O, and a little more won't + // hurt. + summary_dirty = false; + setSummaryDirty(true); + } + } + + + /** Assert whether the folder's summary file should be regenerated. + */ + void setSummaryDirty(boolean summary_dirty) { + if (!summary_dirty || + (this.summary_dirty == summary_dirty)) { + // no change, or being marked as non-dirty. + this.summary_dirty = summary_dirty; + + } else if (fLoading) { + // being marked as dirty while loading. Mark it dirty, but don't spawn. + this.summary_dirty = summary_dirty; + + } else if (!fLoaded) { + // being marked as dirty, but not loaded (or loading.) Ignore it. + + } else { + // being marked as dirty. + // Add this folder to the update-thread's list, but make sure we don't + // add it more than once. + synchronized (this) { + if (this.summary_dirty) return; // double-check that we didn't lose + // a race. + this.summary_dirty = summary_dirty; + SummaryUpdateThread t = SummaryUpdateThread.Get(); + t.addDirtyFolder(this); + } + } + } + + // The per-folder thread that flushes X-Mozilla-Status changes out to the + // file. Reads and writes of this variable must be synchronized. + Thread statusFlagsUpdateThread = null; + + /** Assert whether any messages have the FLAGS_DIRTY bit set in them. + A child message should call this on its parent when any persistent + flag value is changed. If a non-null message is provided, and + flags_dirty is true, then notify any observers that this message + has changed. + + @param flags_dirty Whether the flags should currently be considered + to be dirty. + + @param message If flags_dirty, the Message (BerkeleyMessage) that + has become dirty. + + @param old_flags If message is non-null, the previous value of its + flags (Message.flags should be the new, dirty value.) + */ + void setFlagsDirty(boolean flags_dirty, Message message, long old_flags) { + + if (flags_dirty && fLoaded && !fLoading) + // This is as good a time as any to make sure the disk file hasn't + // changed (note that this is lazy: the disk isn't pegged each time + // ensureLoaded() is called.) + ensureLoaded(); + + // If the deleted-ness or read-ness of a message has changed, flush + // the new counts down into the summary object. + if (flags_dirty && message != null) { + long new_flags = ((MessageBase)message).flags; + long interesting = (MessageBase.FLAG_DELETED | MessageBase.FLAG_READ); + if ((old_flags & interesting) != (new_flags & interesting)) { + int msglen = ((BerkeleyMessage)message).fLength; + updateSummaryMessageCounts(old_flags, new_flags, msglen); + } + } + + if (!flags_dirty || + (this.flags_dirty == flags_dirty)) { + // no change, or being marked as non-dirty. + this.flags_dirty = flags_dirty; + + } else if (fLoading) { + // being marked as dirty while loading. Mark it dirty, but don't spawn. + this.flags_dirty = flags_dirty; + + } else if (!fLoaded) { + // being marked as dirty, but not loaded (or loading.) Ignore it. + + } else { + // being marked as dirty. + // Spawn a thread to update this folder's X-Mozilla-Status headers + // in a few seconds, if there's not already one around. + synchronized (this) { + this.flags_dirty = flags_dirty; + if (statusFlagsUpdateThread == null || + !statusFlagsUpdateThread.isAlive()) { + statusFlagsUpdateThread = new StatusFlagsUpdateThread(this); + statusFlagsUpdateThread.start(); + } + } + } + if (flags_dirty && message != null) { + notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, + message); + } + } + + + /** If the deleted-ness or read-ness of a message has changed, flush + the new counts down into the summary object. + */ + protected void updateSummaryMessageCounts(long old_flags, long new_flags, + int msglen) { + Assert.Assertion(msglen > 0); + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile == null) + return; + + int total = mailSummaryFile.totalMessageCount(); + int undel = mailSummaryFile.undeletedMessageCount(); + int unread = mailSummaryFile.unreadMessageCount(); + long dbytes = mailSummaryFile.deletedMessageBytes(); + + if ((old_flags & MessageBase.FLAG_DELETED) != + (new_flags & MessageBase.FLAG_DELETED)) { + // + // deleted-ness of a message changed. + // + if ((new_flags & MessageBase.FLAG_DELETED) != 0) { // became deleted + Assert.Assertion(undel > 0); + if (undel > 0) { + undel--; + dbytes += msglen; + } + } else { // became undeleted + Assert.Assertion(undel < total); + if (undel < total) { + undel++; + Assert.Assertion(dbytes >= msglen); + if (dbytes >= msglen) + dbytes -= msglen; + } + } + + } else if (((new_flags & MessageBase.FLAG_DELETED) == 0) && + (old_flags & MessageBase.FLAG_READ) != + (new_flags & MessageBase.FLAG_READ)) { + // + // read-ness of a non-deleted message changed. + // + if ((new_flags & MessageBase.FLAG_READ) != 0) { // became read + Assert.Assertion(unread > 0); + if (unread > 0) + unread--; + } else { // became unread + Assert.Assertion(unread < total); + if (unread < total) + unread++; + } + } else { + Assert.Assertion(false); + } + + mailSummaryFile.setFolderMessageCounts(total, undel, unread, dbytes); + } + + + protected void ensureSummaryFileHeaderParsed() { + // If there isn't a summary file object, presumably this is because the + // messages of this folder aren't loaded. Examine the summary file on + // disk, if any, and make a summary object corresponding to it. + // + if (mailSummaryFile == null) { + Assert.Assertion(!fLoaded); + synchronized (this) { + if (mailSummaryFile == null) { // double-check + try { // ignore errors while reading the summary file. + InputStream stream = null; + try { // be sure to close stream + File name = summaryFileName(); + stream = new BufferedInputStream(new FileInputStream(name)); +// System.err.println("opened (unloaded) summary file " + name); + mailSummaryFile = + MailSummaryFileFactory.ParseFileHeader(this, stream); + } finally { + if (stream != null) stream.close(); + } + } catch (IOException e) { + } catch (SecurityException e) { + } + } + } + } + } + + + private long verify_file_tick = 0; + + /** Returns false if the disk file has changed unexpectedly. + + @param force If true, guarentee that the disk will be checked, and + the returned value will be accurate. If false, the + disk will not be checked more frequently than once + a second, so true might be returned without verification. + +

That is, if `force' is false, and this method returns + true, the disk might or might not be in sync. However, + regardless of the value of `force', if false is returned, + the disk is definitely out of sync. + */ + protected boolean verifyFileDate(boolean force) { + if (!fLoaded) return true; + + Assert.Assertion(mailSummaryFile != null); + + long now = System.currentTimeMillis(); + + // If less than a second has passed, assume everything is ok. + if (!force && + verify_file_tick + 1000 > now) + return true; + + synchronized (this) { + verify_file_tick = now; + if (mailSummaryFile.folderDate() != fFile.lastModified() || + mailSummaryFile.folderSize() != fFile.length()) + return false; + else + return true; + } + } + + + /** Call this to inform the summary file module that the folder file + has just been modified. (The code managing the summary file may wish + to act on this information in some way. With our current summary file + format, failing to so notice it will cause the folder to be re-parsed + the next time, since we won't be able to tell the difference between + some *other* program modifying the file instead of us.) + +

Call this with the folder file still locked, to avoid a potential + race. + + @see MailSummaryFile + @see MailSummaryFileGrendel + */ + protected void updateSummaryFile() { + + ensureSummaryFileHeaderParsed(); + + if (mailSummaryFile != null && + mailSummaryFile.writable()) { + synchronized (this) { + try { + mailSummaryFile.updateSummaryFile(); + } catch (IOException e) { + // Ignore it, I guess... + } + } + } else if (fLoaded) { + // If there was no summary file, or if it was in an unknown format, + // or if it was in a format that we don't know how to write, then + // schedule a modern-format summary file to be written. We can + // do this only when the messages are loaded: if the messages aren't + // loaded, we might have a MailSummaryFile object, but we won't + // be able to write anything meaningful into it. + setSummaryDirty(true); + } + } + + + /** Returns the name of the mail summary file used for this folder. + This name is OS-dependent; it is assumed that the data within the + file will contain versioning information. +

+ On all systems, the summary file resides in the same directory + as the folder file. +

+ On Unix, the summary file for FOO would be + .FOO.summary (note leading dot.) +

+ On Windows and Mac, the summary file for FOO.MBX would be + FOO.SNM. + */ + File summaryFileName() { + File file = getFile(); + String dir = file.getParent(); + String name = file.getName(); + + if (Constants.ISUNIX) { + name = "." + name + ".summary"; + } else { + int i = name.lastIndexOf("."); + if (i > 0) + name = name.substring(0, i); + name = name + ".snm"; + } + + return new File(dir, name); + } + + + /** Returns the name of a non-existent file that can be used as a temporary + file for this folder. This name is OS-dependent. +

+ On all systems, the summary file resides in the same directory + as the folder file, and has the same "base" name. +

+ On Unix, the summary file for FOO would be + .FOO.ns_tmp (note leading dot.) +

+ On Windows and Mac, the summary file for FOO.MBX would be + FOO.TMP. + */ + File tempFolderName() { + File file = getFile(); + String dir = file.getParent(); + String name = file.getName(); + + if (Constants.ISUNIX) { + name = "." + name + ".ns_tmp"; + } else { + int i = name.lastIndexOf("."); + if (i > 0) + name = name.substring(0, i); + name = name + ".tmp"; + } + + return new File(dir, name); + } + + + + + + /** Write a summary file for this folder. + @see MailSummaryFile + @see MailSummaryFileGrendel + */ + synchronized void writeSummaryFile() throws IOException { + Assert.Assertion(fLoaded); + if (!fLoaded) return; + if (!summary_dirty) return; + setSummaryDirty(false); // do this first + mailSummaryFile.writeSummaryFile(); + } + + + /** This causes the X-Mozilla-Status headers in the folder file itself + to be updated. (Not to be confused with the summary file: this + modifies the folder.) If a changed message's bits in the + file include an X-Mozilla-Status header, it will be overwritten + (random-access.) If the header is not present, the message will be + unchanged (and in that situation, the flags will only live in the + summary file, not the folder itself; and if the summary file gets + blown away, the flags will be lost. That's why we try to have an + X-Mozilla-Status header on every message.) + */ + synchronized void flushMozillaStatus() throws IOException { + + Assert.Assertion(!fLoading); + if (!fLoaded) return; + if (!flags_dirty) return; + + // check file dates to make sure it hasn't changed from under us. + ensureLoaded(); + + if (!flags_dirty) return; // double-check, in case reloaded + + setFlagsDirty(false, null, 0); + boolean wrote_anything = false; + + try { // make sure unlockFolderFile() gets called. + lockFolderFile(); + + ParseBerkeleyFolder parser = new ParseBerkeleyFolder(); + RandomAccessFile io = null; + try { + io = new RandomAccessFile(fFile, "rw"); + + for (Enumeration e = fMessages.elements(); e.hasMoreElements();) { + BerkeleyMessage m = (BerkeleyMessage) e.nextElement(); + if (m.flagsAreDirty()) { + parser.flushMozillaStatus(io, + m.getStorageFolderIndex(), + m.getSize(), + (short) m.makeMozillaFlags(m)); + m.setFlagsDirty(false); + wrote_anything = true; + } + } + + } finally { + if (io != null) + io.close(); + } + + // At this point, the file has been written, and closed, without error. + // Update the date/size that will be encoded into the summary file the + // next time it is written. + // + // Don't call updateSummaryFile() now, which would update the dates in + // the existing disk summary file. Wait until writeSummaryFile() is + // called before putting the above dates on disk. The reason for this + // is, if we *have* written the X-Mozilla-Status headers into the + // folder, but have *not* written a new summary file, the folder has + // more current flags than the summary, so it would be appropriate to + // discard the summary file and re-parse the folder. + // + // However, this situation (re-parsing) can only occur if the program + // dies after the StatusFlagsUpdateThread runs, but before the + // SummaryUpdateThread runs. + // + mailSummaryFile.setFolderDateAndSize(fFile.lastModified(), + fFile.length()); + + } finally { + unlockFolderFile(); + } + + if (wrote_anything) + // Since flags changed, mark the summary file as needing (eventually) + // to be regenerated as well. + setSummaryDirty(true); + } + + + + + public void appendMessages(Message msgs[]) throws MessagingException { + try { + for (int i=0 ; i> error! truncated!"); + buf.append(Constants.LINEBREAK); + buf.append(Constants.LINEBREAK); + try { + out.write(buf.toBytes(), 0, buf.length()); + } catch (IOException e) { + // just ignore it; we're writing in service of error recovery. + } + + out.close(); + } + } + } + + + // Update the folder's statistics about what is contained in it. + // + ensureSummaryFileHeaderParsed(); + if (mailSummaryFile != null) { + int total = mailSummaryFile.totalMessageCount(); + int undel = mailSummaryFile.undeletedMessageCount(); + int unread = mailSummaryFile.unreadMessageCount(); + long dbytes = mailSummaryFile.deletedMessageBytes(); + + total++; + if (!mextra.isRead()) + unread++; + + if (mextra.isDeleted()) + dbytes += bytes_written; + else if (undel != -1) + undel++; + + // Inform the summary file of the new statistics. + // + mailSummaryFile.setFolderMessageCounts(total, undel, unread, dbytes); + } + + // At this point, the file has been written, and closed, without error. + // Update the date/size encoded into the summary file (without + // rewriting the whole summary file itself.) + // + updateSummaryFile(); + + } finally { + unlockFolderFile(); + } + + // Everything has been written to disk without error (both folder + // and summary file.) Now, create an in-memory message representing + // the new message, and let any observers know that this message has + // been added to the folder. + // + BerkeleyMessage newmessage = new BerkeleyMessage(this, headers); + + // Add this message to the list of messages in this folder, *if* this + // folder knows what messages are in it. If we haven't yet parsed + // the folder, don't make it appear that the folder has only one + // message in it; rather, just wait until it gets parsed for real. + // #### fix me wrt observers + if (fLoaded) { + fMessages.addElement(newmessage); + } + + Twingle twingle = Twingle.GetSingleton(); + if (twingle != null) { + twingle.add(headers, this); + } + + Message list[] = { newmessage }; + notifyMessageAddedListeners(list); + } + + + public void fetch(Message msgs[], FetchProfile fp) {} + + /** Remove all messages that are marked as deleted from the folder's file. + This is done by writing a new file, then renaming it. + + @throws IOException if the file could not be expunged. A likely + cause would be running out of disk space, since + the peak file usage can be up to 2x the size of + the original folder. + */ + public Message[] expunge() throws MessagingException { + Message[] result = null; + try { + result = realExpunge(); + } catch (IOException e) { + throw new MessagingException(e.toString()); + } + return result; + } + + protected synchronized Message[] realExpunge() throws IOException { + + + // To compact a folder, we must have its message list around. + // This (probably) means parsing the summary file, which is important, + // because the summary file might be the only place where the proper + // flags live. One of the things that expunging does is ensure that + // every message in the folder has an X-Mozilla-Status header. The + // absence of that header is what can cause the situation that the + // summary file contains non-recomputable data. + // + // The pessimal case here is where we're compacting a folder that doesn't + // have a summary file: that will cause it to be fully parsed, and then + // read again to compact it. There's not a lot we can do about that. + // + ensureLoaded(); + Assert.Assertion(fLoaded); + Assert.Assertion(mailSummaryFile != null); + + // Before compacting the folder, ensure that any in-memory changes to + // the flags have been flushed out. + if (flags_dirty) + flushMozillaStatus(); + + boolean everything_completed_normally = false; + + Vector old_msgs = fMessages; + Assert.Assertion(old_msgs != null); + + try { // if something goes wrong, restore the value of fMessages + + // We need to keep two copies of the folder's Message objects: + // + // * we need the old messages around in case there is an error, + // and we need to back out our changes; + // + // * we need the new messages so that we can correctly write a + // new summary file. + // + // Since the second *should* be a subset of the first, it would be + // good if we could manage to share the data; but I don't see an easy + // way to do that, and still be able to back out the changes if an error + // occurs (the expunging process wants to record a new file-offset into + // the message objects, among other things.) + // + // Another reason for keeping them is so that, at the end, we can tell + // all the observers that the first set is gone and the new set exists. + // We don't want to tell the observers anything if the attempt to + // compact the disk file is abortive. + // + // This does not mean that we're using twice as much memory, since all + // of these messages will be sharing strings and IDs by virtue of the + // ByteStringTable and MessageIDTable. + // + fMessages = new Vector(old_msgs.size()); + + + try { // make sure unlockFolderFile() gets called. + lockFolderFile(); + + File tmp_file = tempFolderName(); + + try { // make sure the temp file gets deleted, if there's an error + + RandomAccessFile in = null; + RandomAccessFile out = null; + boolean io_completed_normally = false; + + try { // make sure in.close() and out.close() get called. + + in = new RandomAccessFile(fFile, "r"); + + // make sure the tmp file doesn't exist (we have the folder locked, + // so if it does exist, it's probably left over from a crash.) + tmp_file.delete(); + + out = new RandomAccessFile(tmp_file, "rw"); + + ParseBerkeleyFolderAndExpunge expungator = + new ParseBerkeleyFolderAndExpunge(); + expungator.expungeDeletedMessages(this, in, out, // do the deed + old_msgs.elements()); + + int total = expungator.totalMessageCount(); + int undel = expungator.undeletedMessageCount(); + int unread = expungator.unreadMessageCount(); + long dbytes = expungator.deletedMessageBytes(); + Assert.Assertion(total == undel); + Assert.Assertion(dbytes == 0); + mailSummaryFile.setFolderMessageCounts(total, undel, + unread, dbytes); + + io_completed_normally = true; + + } finally { // close in and out, and (if we didn't get an error) + // move the temp-file. + if (in != null) + in.close(); + if (out != null) + out.close(); + + if (io_completed_normally) { + // Move the temp file to the folder file. This might get an + // error; that's ok, it will be caught because tmp_file will + // not be null. + tmp_file.renameTo(fFile); + tmp_file = null; + } + } + + } finally { + // if temp_file is non-null, then an error occurred. + if (tmp_file != null) { + try { + tmp_file.delete(); + } catch (SecurityException e) { + } + } + } + + // At this point, the folder file has been successfully overwritten + // with new data. Write out a new summary file right now (we've + // already done a ton of disk I/O, a little bit more won't hurt.) + // + Assert.Assertion(fLoaded); + summary_dirty = true; + mailSummaryFile.setFolderDateAndSize(fFile.lastModified(), + fFile.length()); + writeSummaryFile(); + + } finally { + unlockFolderFile(); + } + + everything_completed_normally = true; + + } finally { // if something went wrong, restore the value of fMessages + + if (!everything_completed_normally) + // something blew up; put back the old messages and pretend + // we didn't do anything. + fMessages = old_msgs; + } + + // At this point, the new, compacted folder is on disk, and it has a + // summary. All that remains is to let the observers (if any) know + // that the message objects have changed. + // + // #### We could concievably do something clever here, by "zippering" the + // two message lists together. What we have (above) in fMessages and + // old_msgs looks something like + // + // old_msgs: A1 B1 C1 D1 E1 F1 G1 + // fMessages: A2 C2 E2 F2 + // + // A1 and A2 are non-identical BerkeleyMessage objects that represent + // the same message on disk. There are two things we could do: + // + // Option 1: + // * have an observable event that says "A2 replaces A1." + // * have an observable event that says "B1 has been expunged." + // + // Option 2: + // + // * copy the new contents of A2 into A1, and discard A2. + // * have an observable event that says "A1 has changed." + // * have an observable event that say "B1 has been expunged." + // + // I think I lean towards #2. But what we do today is: + // + // * say "A1 has been deleted." + // * say "A2 has been added". + // + // Actually, all of the comment above was written before we went to + // javamail APIs. Those APIs require approach #2 (well, I don't think + // we need the "A1 has changed" part), and so I attempt to + // implement that here... -- Terry + + int oldsize = old_msgs.size(); + int newsize = fMessages.size(); + int j=0; + NEXTMESSAGE: + for (int i=0 ; iFrom ". + if (L > 4 && + b[0] == 'F' && b[1] == 'r' && b[2] == 'o' && b[3] == 'm' && + b[4] == ' ') { + out.write((int) '>'); + content_length++; + } + + out.write(b, 0, L); + content_length += L; + } + } + } + + // We have written out the whole message, and now know the correct + // value for Content-Length. Go back and finish the job, overwriting + // the dummy header we left earlier. + // + if (content_length_header_pos > 0 && + content_length >= 0) { + + // write to the file if CL will fit in ten digits (pretty likely...) + // + if (content_length <= 9999999999L) { + out.seek(content_length_header_pos); + line.setLength(0); + line.append(Long.toString(content_length)); + out.write(line.toBytes(), 0, line.length()); + } + + // Add this new Content-Length value into the Header object. + line.setLength(0); + line.append("Content-Length: "); + line.append(Long.toString(content_length)); + line.append(Constants.LINEBREAK); + headers.addHeaderLine(line.toString()); + } + + } finally { + if (buf != null) ByteBuf.Recycle(buf); + if (line != null) ByteBuf.Recycle(line); + } + return headers; + } + + + private Object disk_lock_object = null; + + /** On those systems which support locking of disk files, this causes + a lock to be acquired on the file associated with this folder. + It is imperative that a balanced unlockFolderFile() call be made: + be sure to only use this within try/finally. + */ + void lockFolderFile() { +// System.err.println("locking file " + fFile); + + if (disk_lock_object != null) { + // Nested locks aren't allowed... + Assert.Assertion(false); + + } else if (false) { // #### change this test to `Constants.ISUNIX' once + // #### the UnixDotLock class actually works. + try { + + disk_lock_object = new UnixDotLock(fFile); + + } catch (IOException e) { + // Ignore it, I guess -- it's ok to try and visit a folder in a + // read-only directory, or in someone else's directory, and in that + // case we won't be able to acquire the lock. + // (Or would that be a SecurityException?) + System.err.println("warning: I/O error when trying to lock folder:" + + e); + + } catch (InterruptedException e) { + // #### GAG, what do I do here?? + // I don't really want the fact that class UnixDotLock calls wait() + // to escape up to the higher levels. What to do... + System.err.println("exception ignored: " + e); + + } + + } else { + // Non-Unix -- put some random non-null object in the lock slot. + disk_lock_object = Boolean.TRUE; + } + } + + /** Releases the lock obtained by lockFolderFile(). */ + void unlockFolderFile() { +// System.err.println("unlocking file " + fFile); + + if (disk_lock_object == null) { + // It should be locked, if we're unlocking it... + Assert.Assertion(false); + } else { + Object o = disk_lock_object; + disk_lock_object = null; + if (o != Boolean.TRUE) // see above + ((UnixDotLock) o).unlock(); + } + } + + + + static private final String MONTHS[] = {"Jan", "Feb", "Mar", "Apr", + "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec"}; + static private final String DAYS[] = {"Mon", "Tue", "Wed", "Thu", + "Fri", "Sat", "Sun"}; + + static DecimalFormat TwoDigitNumFormat = new DecimalFormat("00"); + protected String TwoDigitNum(int num) { + return TwoDigitNumFormat.format(num); + } + + protected void makeDummyEnvelope(ByteBuf buf) { + Calendar c = new GregorianCalendar(); + c.setTime(new Date()); + + int zone = (c.get(Calendar.ZONE_OFFSET) + + c.get(Calendar.DST_OFFSET)) / (1000 * 60); + + boolean zonenegative = (zone < 0); + zone = Math.abs(zone); + int zonehours = zone / 60; + int zoneminutes = zone % 60; + + buf.append("From - "); + buf.append(DAYS[c.get(Calendar.DAY_OF_WEEK) - 1]); + buf.append(", "); + buf.append(c.get(Calendar.DATE)); + buf.append(" "); + buf.append(MONTHS[c.get(Calendar.MONTH)]); + buf.append(" "); + buf.append(c.get(Calendar.YEAR)); + buf.append(" "); + buf.append(TwoDigitNum(c.get(Calendar.HOUR_OF_DAY))); + buf.append(":"); + buf.append(TwoDigitNum(c.get(Calendar.MINUTE))); + buf.append(":"); + buf.append(TwoDigitNum(c.get(Calendar.SECOND))); + buf.append(" "); + buf.append(zonenegative ? "-" : "+"); + buf.append(TwoDigitNum(zonehours)); + buf.append(TwoDigitNum(zoneminutes)); + buf.append(Constants.LINEBREAK); + } + +// ### I'm too lazy to port this test right now to the new world, so I'm +// just commenting it out. - Terry +// +// public static final void main(String av[]) throws Exception { +// +// BerkeleyFolder f3 = new BerkeleyFolder(new File("/u/jwz/tmp/f3"), "f3"); +//// f3.ensureLoaded(); +// f3.expunge(); +// +///* +// BerkeleyFolder f1 = new BerkeleyFolder(new File("/u/jwz/tmp/f1"), "f1"); +// BerkeleyFolder f2 = new BerkeleyFolder(new File("/u/jwz/tmp/f2"), "f2"); +// +// Enumeration e1 = f1.getMessages(); +//// Enumeration e2 = f2.getMessages(); +// BerkeleyMessage m1 = (BerkeleyMessage) e1.nextElement(); +// +// System.err.println("flagged = " + m1.isFlagged() + " (" + m1.flags + ")"); +// m1.setFlagged(!m1.isFlagged()); +// System.err.println("flagged = " + m1.isFlagged() + " (" + m1.flags + ")"); +// +// f2.addMessage(m1); +// System.err.println("sleeping"); +// Thread.sleep(50 * 1000); +// System.err.println("exiting"); +//*/ +// } + + public boolean delete(boolean value) { + Assert.NotYetImplemented("BerkeleyFolder.delete"); + return false; + } + + public boolean renameTo(Folder f) { + Assert.NotYetImplemented("BerkeleyFolder.renameTo"); + return false; + } + + public void open(int mode) { + ensureLoaded(); + } + + public void close(boolean doExpunge) throws MessagingException { + if (doExpunge) expunge(); + // ### Throw away message array and stuff? + } + + public boolean isOpen() { + return true; // ### Wrong! + } + + + +} + + +/** This class implements a thread which runs in the background and writes + out all modified mail summary files. This thread dies once the files + have been written; it will be re-spawned once some folder or folders + become dirty. + +

There is only ever one of these threads: it writes the summaries for + all modified folders, serially, then exits. + */ +class SummaryUpdateThread extends Thread { + + static int delay = 30; // seconds + static SummaryUpdateThread the_thread = null; // singleton + + private Vector dirty_folders = new Vector(); + + static synchronized SummaryUpdateThread Get() { + if (the_thread == null || !the_thread.isAlive()) { + the_thread = new SummaryUpdateThread(); + the_thread.setDaemon(true); + the_thread.setName(the_thread.getClass().getName()); + the_thread.start(); + } + return the_thread; + } + + synchronized void addDirtyFolder(BerkeleyFolder f) { + if (!dirty_folders.contains(f)) + dirty_folders.addElement(f); + } + + public void run() { + + try { + sleep(delay * 1000); + } catch (InterruptedException e) { + return; // is this the right way to do this? + } + + synchronized (this.getClass()) { + // Do this first, so that if someone calls add() just as we're coming + // out of the loop, below, the thing they've added doesn't get lost. + the_thread = null; + } + + for (int i = 0; i < dirty_folders.size(); i++) { + BerkeleyFolder f = (BerkeleyFolder) dirty_folders.elementAt(i); + try { + +System.err.println("background thread writing summary file for " + f.fFile); + + f.writeSummaryFile(); + } catch (Exception e) { + f.setSummaryDirty(true); + } + } + } +} + + +/** This class implements a thread which runs in the background and overwrites + the changed X-Mozilla-Status headers in a particular folder. This thread + dies once the file has been updated; it will be re-spawned once some + folder's messages become dirty again. + +

There can be one of these threads per folder. + */ +class StatusFlagsUpdateThread extends Thread { + + static int delay = 5; // seconds + BerkeleyFolder folder = null; + + StatusFlagsUpdateThread(BerkeleyFolder folder) { + this.folder = folder; + setDaemon(true); + setName(getClass().getName() + " " + folder.fFile); + } + + public void run() { + + try { + sleep(delay * 1000); + } catch (InterruptedException e) { + return; // is this the right way to do this? + } + + System.err.println("background thread updating X-Mozilla-Status headers " + + "in " + folder.fFile); + try { + synchronized (folder) { + folder.statusFlagsUpdateThread = null; + folder.flushMozillaStatus(); + } + } catch (IOException e) { + // nothing to be done? + System.err.println("IOException while writing X-Mozilla-Status: " + e); + } + } +} diff --git a/mozilla/grendel/storage/BerkeleyMessage.java b/mozilla/grendel/storage/BerkeleyMessage.java new file mode 100644 index 00000000000..d38d5af44d4 --- /dev/null +++ b/mozilla/grendel/storage/BerkeleyMessage.java @@ -0,0 +1,227 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 28 Aug 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; + +class BerkeleyMessage extends MessageBase { + int fIndex; + int fLength; + + /* Note: these flag values are compatible with the X-Mozilla-Status + headers generated by Netscape Mail 2.0 through 4.0. Don't change + them. + + Note also: Mozilla 2.0 and 3.0 can't read X-Mozilla-Status headers + that contain other than exactly 4 hex digits (16 bits.) If we + ever need to save more than 16 flags to the file, we will probably + have to do so incompatibly... + + Think twice before adding a new flag here! Do you really need to + write it to X-Mozilla-Status? Is parsing it from another header + good enough? These flags are scarce, and once added, they can't + ever be removed without breaking old files. + */ + protected static final int X_MOZILLA_FLAG_READ = 0x0001; + protected static final int X_MOZILLA_FLAG_REPLIED = 0x0002; // since 4.0 + protected static final int X_MOZILLA_FLAG_MARKED = 0x0004; + protected static final int X_MOZILLA_FLAG_DELETED = 0x0008; + protected static final int X_MOZILLA_FLAG_HAS_RE = 0x0010; + /* 0x0020, 0x0040, 0x0080, and 0x0100 are unused. */ + protected static final int X_MOZILLA_FLAG_SMTP_AUTH = 0x0200; // since 4.0 + protected static final int X_MOZILLA_FLAG_PARTIAL = 0x0400; + protected static final int X_MOZILLA_FLAG_QUEUED = 0x0800; // since 3.0? + protected static final int X_MOZILLA_FLAG_FORWARDED = 0x1000; // since 4.0 + /* Note: this one is 3 bits. */ + protected static final int X_MOZILLA_FLAG_PRIORITY = 0xE000; // since 4.0 + + + BerkeleyMessage(BerkeleyFolder f, InternetHeaders h) { + super(f, h); + parseMozillaStatus(h); + } + + BerkeleyMessage(BerkeleyFolder f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + ByteBuf id, + ByteBuf refs[]) { + super(f, date, flags, author, recipient, subj, id, refs); + } + + BerkeleyMessage(BerkeleyFolder f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + MessageID id, + MessageID refs[]) { + super(f, date, flags, author, recipient, subj, id, refs); + } + + + protected void parseMozillaStatus(InternetHeaders h) { + int xms = 0; + String hh[] = h.getHeader("X-Mozilla-Status"); + if (hh != null && hh.length != 0) { + byte value[] = hh[0].trim().getBytes(); + /* sucks that we can't use Short.parseShort(buf, 16). */ + for (int i = 0; i < value.length; i++) { + byte b = value[i]; + if (b >= '0' && b <= '9') xms = (xms << 4) | (b - '0'); + else if (b >= 'A' && b <= 'F') xms = (xms << 4) | (b - 'A' + 10); + else if (b >= 'a' && b <= 'f') xms = (xms << 4) | (b - 'a' + 10); + else { xms = 0; break; } + } + flags = mozillaFlagsToInternalFlags(xms); + } + } + + /** Given a value read from an X-Mozilla-Status header, returns a + value suitable for storing in MessageBase.flags. + (The two types of flags don't use the same space or range.) + */ + static long mozillaFlagsToInternalFlags(long x_mozilla_status) { + long xms = x_mozilla_status; + long flags = 0; + if ((xms & X_MOZILLA_FLAG_READ) != 0) flags |= FLAG_READ; + if ((xms & X_MOZILLA_FLAG_REPLIED) != 0) flags |= FLAG_REPLIED; + if ((xms & X_MOZILLA_FLAG_MARKED) != 0) flags |= FLAG_MARKED; + if ((xms & X_MOZILLA_FLAG_DELETED) != 0) flags |= FLAG_DELETED; + if ((xms & X_MOZILLA_FLAG_HAS_RE) != 0) flags |= FLAG_HAS_RE; + if ((xms & X_MOZILLA_FLAG_SMTP_AUTH) != 0) flags |= FLAG_SMTP_AUTH; + if ((xms & X_MOZILLA_FLAG_PARTIAL) != 0) flags |= FLAG_PARTIAL; + if ((xms & X_MOZILLA_FLAG_QUEUED) != 0) flags |= FLAG_QUEUED; + if ((xms & X_MOZILLA_FLAG_FORWARDED) != 0) flags |= FLAG_FORWARDED; + /* if ((xms & X_MOZILLA_FLAG_PRIORITY) != 0) ... #### */ + return flags; + } + + /** Given a value of the form found in MessageBase.flags, returns a + value that may be written to an X-Mozilla-Flags header. + (The two types of flags don't use the same space or range.) + */ + static long internalFlagsToMozillaFlags(long internal_flags) { + long f = internal_flags; + long xms = 0; + if ((f & FLAG_READ) != 0) xms |= X_MOZILLA_FLAG_READ; + if ((f & FLAG_REPLIED) != 0) xms |= X_MOZILLA_FLAG_REPLIED; + if ((f & FLAG_MARKED) != 0) xms |= X_MOZILLA_FLAG_MARKED; + if ((f & FLAG_DELETED) != 0) xms |= X_MOZILLA_FLAG_DELETED; + if ((f & FLAG_HAS_RE) != 0) xms |= X_MOZILLA_FLAG_HAS_RE; + if ((f & FLAG_SMTP_AUTH) != 0) xms |= X_MOZILLA_FLAG_SMTP_AUTH; + if ((f & FLAG_PARTIAL) != 0) xms |= X_MOZILLA_FLAG_PARTIAL; + if ((f & FLAG_QUEUED) != 0) xms |= X_MOZILLA_FLAG_QUEUED; + if ((f & FLAG_FORWARDED) != 0) xms |= X_MOZILLA_FLAG_FORWARDED; + /* #### X_MOZILLA_FLAG_PRIORITY */ + return xms; + } + + /** Returns a reasonable value for the X-Mozilla-Status header of the + given Message. If the Message is a subclass of MessageBase, this + value will be based on MessageBase.flags; otherwise, we will go through + the painful string-based javamail API. (The Message may also be null.) + */ + + static long makeMozillaFlags(Message m) { + long xms = 0; + if (m instanceof MessageBase) { + xms = internalFlagsToMozillaFlags(((MessageBase)m).flags); + } else if (m == null) { + xms = X_MOZILLA_FLAG_READ; + } else { + for (int i=0 ; i, 22 Oct 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; + +import java.awt.Component; +import java.io.File; +import java.net.URL; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.URLName; +//import javax.mail.event.ConnectionEvent; +import javax.mail.event.StoreEvent; + +/** Store for Berkeley mail folders. +

+ This class really shouldn't be public, but I haven't figured out how to + tie into javamail's Session class properly. So, instead of using + Session.getStore(String), you instead need to call + BerkeleyStore.GetDefaultStore(Session). + */ + +public class BerkeleyStore extends Store { + protected Folder defaultFolder; + + static protected BerkeleyStore DefaultStore = null; + public static Store GetDefaultStore(Session s) { + if (DefaultStore == null) { + DefaultStore = new BerkeleyStore(s); + } + return DefaultStore; + } + + + public BerkeleyStore(Session s) { + super(s, null); + } + + public BerkeleyStore(Session s, URLName u) { + super(s, u); + } + + public void connect(String host, + String user, + String password) { + notifyStoreListeners(StoreEvent.NOTICE, "opened"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.OPENED); + } + + public void connect() { + notifyStoreListeners(StoreEvent.NOTICE, "opened"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.OPENED); + } + + public void close() { + notifyStoreListeners(StoreEvent.NOTICE, "opened"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.CLOSED); + defaultFolder = null; + } + + + public Folder getDefaultFolder() { + if (defaultFolder == null) { + if (getURLName() == null || getURLName().getFile() == null) { + defaultFolder = + new BerkeleyFolder(this, + new File(session.getProperty("mail.directory"))); + } else { + defaultFolder = + new BerkeleyFolder(this, + new File(getURLName().getFile())); + } + } + return defaultFolder; + } + + public Folder getFolder(String name) throws MessagingException{ + return getDefaultFolder().getFolder(name); + } + + public Folder getFolder(URL url) { + Assert.NotYetImplemented("BerkeleyStore.getFolder(URL)"); + return null; + } + + public Folder getFolder(URLName urlName) { + Assert.NotYetImplemented("BerkeleyStore.getFolder(URLName)"); + return null; + } +} diff --git a/mozilla/grendel/storage/ByteString.java b/mozilla/grendel/storage/ByteString.java new file mode 100644 index 00000000000..eefef379d85 --- /dev/null +++ b/mozilla/grendel/storage/ByteString.java @@ -0,0 +1,114 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +/** Represents a set of bytes. This is a spiritual cousin of String, + but without the misfeature that characters are two bytes each, and + there are 16 bytes of extranious object header. Since we store + message headers internally in their on-the-wire form, and that + is 7-bit ASCII (and occasionally 8-bit if people are being + nonstandard) this is a much more tolerable representation. +

+ This class doesn't provide all of the utilities that String does, + but that's pretty much just because I didn't need them (yet?) +

+ this is used by ByteStringTable to uniqueify the strings, to + further reduce memory usage. + + @see ByteStringTable + @see MessageID + */ +class ByteString { + + protected byte[] bytes; + + // Warning, don't add more instance variables unless you're really + // really sure. There are zillions of these objects, so they should + // be as small as we can make them. + + + ByteString(byte bytes[]) { + this.bytes = bytes; // this bites! ha ha. + } + + ByteString(ByteBuf buf) { + bytes = new byte[buf.length()]; + System.arraycopy(bytes, 0, buf.toBytes(), 0, bytes.length); + } + + public String toString() { + return new String(bytes); + } + + public ByteBuf toByteBuf() { + return new ByteBuf(bytes, 0, bytes.length); + } + + public byte[] toBytes() { + return bytes; + } + + protected int hashBytes(byte b[], int start, int len) { + // Copied from java.lang.String.hashCode() and adapted to work + // on bytes instead of characters. + + int h = 0; + int off = start; + int end = start + len; + + if (len < 16) { + for (int i = len ; i > 0; i--) { + h = (h * 37) + bytes[off++]; + } + } else { + // only sample some characters + int skip = len / 8; + for (int i = len ; i > 0; i -= skip, off += skip) { + h = (h * 39) + bytes[off]; + } + } + return h; + } + + public int hashCode() { + return hashBytes(bytes, 0, bytes.length); + } + + public boolean equals(Object x) { + if (x instanceof ByteString) { + int L = bytes.length; + ByteString ix = (ByteString) x; + if (L != ix.bytes.length) + return false; + else { + for (int i = 0; i < L; i++) { + if (bytes[i] != ix.bytes[i]) + return false; + } + return true; + } + } + else + return false; + } +} diff --git a/mozilla/grendel/storage/ByteStringTable.java b/mozilla/grendel/storage/ByteStringTable.java new file mode 100644 index 00000000000..5a8b7360f74 --- /dev/null +++ b/mozilla/grendel/storage/ByteStringTable.java @@ -0,0 +1,152 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 19 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import java.util.Hashtable; + +/** This is a mechanism for uniqueifying header strings (ByteString objects.) + To save memory, we want all `equal' strings to also be `=='. + This table is how we accomplish that. See intern(). + + @see ByteString + @see MessageIDTable + */ + +class ByteStringTable extends Obarray { + + /** This is the way we check to see if an object is in the table, or rather, + if an object constructed from that sequence of bytes would be in the + table. This is a kludge to get around the lack of function pointers... + */ + protected DummyByteString dummy; + + ByteStringTable(int default_size) { + super(default_size); + dummy = new DummyByteString(); + } + + ByteStringTable() { + this(100); + } + + /** Check whether there is a ByteString representing the given subsequence + of bytes in the table already. Returns null, or an object from the + table (an Integer object.) + */ + synchronized protected Object checkHash(byte bytes[], + int start, int length) { + dummy.bytes = bytes; + dummy.start = start; + dummy.length = length; + Object hashed = hashtable.get(dummy); + dummy.bytes = null; + return hashed; + } + + /** Creates a new ByteString object (which will then be interned.) */ + protected Object newInternable(byte bytes[], int start, int length) { + byte new_bytes[] = new byte[length]; + System.arraycopy(bytes, start, new_bytes, 0, length); + return new ByteString(new_bytes); + } +} + + +/** Kludge used by ByteStringTable. + This behaves like ByteString, but does its dirty work + on an array and offset into it, without copying it. + + @see ByteString + @see ByteStringTable + */ +class DummyByteString extends ByteString { + + int start; + int length; + + // Note that this object is a subclass rather than a special mode of + // operation of ByteString to avoid having to make IBS be any + // larger -- there are a zillion of those, and only one of these per + // ByteStringTable. + + DummyByteString() { + super((byte[]) null); + } + + void setBytes(byte[] data, int start, int length) { + this.bytes = data; + this.start = start; + this.length = length; + } + + public String toString() { + return new String(bytes, start, length); + } + + public ByteBuf toByteBuf() { + return new ByteBuf(bytes, start, length); + } + + public int hashCode() { + return hashBytes(bytes, start, length); + } + + public boolean equals(Object x) { + + byte this_bytes[] = bytes; + int this_start = start; + int this_length = length; + + byte that_bytes[]; + int that_start; + int that_length; + + if (x instanceof DummyByteString) { + DummyByteString that = (DummyByteString) x; + that_length = that.length; + if (this_length != that_length) + return false; + else { + that_start = that.start; + that_bytes = that.bytes; + } + } else if (x instanceof ByteString) { + ByteString that = (ByteString) x; + that_length = that.bytes.length; + if (this_length != that_length) + return false; + else { + that_start = 0; + that_bytes = that.bytes; + } + } else { + return false; + } + + for (int i = 0; i < this_length; i++) { + if (this_bytes[this_start + i] != that_bytes[that_start + i]) + return false; + } + return true; + } +} diff --git a/mozilla/grendel/storage/Constants.java b/mozilla/grendel/storage/Constants.java new file mode 100644 index 00000000000..a7ed8c83922 --- /dev/null +++ b/mozilla/grendel/storage/Constants.java @@ -0,0 +1,74 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 2 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; + +import java.io.File; + +/** + This class contains various constants that are used in various places that + we don't want to recompute all over the place. Mostly, these are system- + dependent things. */ + +class Constants { + /** The string used to separate lines in files on this system. */ + public final static String LINEBREAK = + System.getProperties().getProperty("line.separator"); + + /** The string used to separate lines in files on this system, stored as + a ByteBuf. */ + public final static ByteBuf BYTEBUFLINEBREAK = new ByteBuf(); + static { + BYTEBUFLINEBREAK.append(LINEBREAK); + } + private static boolean unix = false; // Need to use these silly three temp + private static boolean mac = false; // variables just so that I can declare + private static boolean windows = false; // ISUNIX, etc, to be "final". + static { + String osname = System.getProperties().getProperty("os.name"); + if (osname.startsWith("Windows") || + osname.startsWith("Win32") || + osname.startsWith("Win16") || + osname.startsWith("16-bit Windows")) { + windows = true; + } else if (osname.startsWith("Mac OS")) { + mac = true; + } else { + unix = true; + } + } + + /** Whether this is a Unix machine. */ + public final static boolean ISUNIX = unix; + + /** Whether this is a Macintosh machine. */ + public final static boolean ISMAC = mac; + + /** Whether this is a Windows machine. */ + public final static boolean ISWINDOWS = windows; + + + /** File representing the user's home directory. */ + public final static File HOMEDIR = + new File(System.getProperties().getProperty("user.home")); +}; diff --git a/mozilla/grendel/storage/DotTerminatedInputStream.java b/mozilla/grendel/storage/DotTerminatedInputStream.java new file mode 100644 index 00000000000..61ef1126db5 --- /dev/null +++ b/mozilla/grendel/storage/DotTerminatedInputStream.java @@ -0,0 +1,153 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 5 Sep 1997. + */ + +package grendel.storage; + +import java.io.FilterInputStream; +import java.io.InputStream; +import java.io.IOException; + +import calypso.util.Assert; +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +class DotTerminatedInputStream extends FilterInputStream { + + private static final int CAPACITY = 1024; + + ByteBuf curline = new ByteBuf(); + ByteBuf inbuf = new ByteBuf(CAPACITY); + ByteLineBuffer linebuf; + boolean eof = false; + + DotTerminatedInputStream(InputStream s) { + super(s); + linebuf = new ByteLineBuffer(); + ByteBuf inputeol = new ByteBuf(); + inputeol.append((byte) '\r'); + inputeol.append((byte) '\n'); + linebuf.setInputEOL(inputeol); + } + + public int read(byte[] buf, int start, int length) throws IOException { + if (length <= 0) return length; + int curlen = curline.length(); + if (curlen == 0) { + if (eof) return -1; + while (!linebuf.pullLine(curline)) { + inbuf.setLength(CAPACITY); + int inlen = in.read(inbuf.toBytes(), 0, CAPACITY); + if (inlen <= 0) { + eof = true; + linebuf.pushEOF(); + linebuf.pullLine(curline); // Try for the last gasp... + break; + } + inbuf.setLength(inlen); +// System.out.println("<< " + inbuf); + linebuf.pushBytes(inbuf); + } +// System.out.println(">> " + curline); + curlen = curline.length(); + if (curlen >= 2 && curline.byteAt(0) == '.') { + byte c = curline.byteAt(1); + if (c == '\r' || c == '\n') { + eof = true; + curline.setLength(0); + curlen = 0; + } else if (c == '.' && curlen >= 3) { + c = curline.byteAt(2); + if (c == '\r' || c == '\n') { + // This line is a "..", which we need to translate to ".". + curline.remove(0, 1); + curlen--; + } + } + } + if (curlen == 0) return -1; + } + int result = length; + if (result > curlen) result = curlen; + curline.getBytes(0, result, buf, start); + curlen -= result; + curline.remove(0, result); +// for (int i=0 ; i 0); + } + } + + // Suck everything up into a buffer, so that we can use the input socket + // for something else. Right now, we just suck it into memory; we should + // put it on a temp file if it becomes real big. ### + public void bufferUpEverything() { + if (eof) return; + ByteBuf tmp = new ByteBuf(CAPACITY); + int length = 0; + for (;;) { + tmp.ensureCapacity(length + CAPACITY); + int numread = -1; + try { + numread = read(tmp.toBytes(), length, CAPACITY); + } catch (IOException e) { + } + if (numread <= 0) break; + length += numread; + } + Assert.Assertion(eof); + curline = tmp; + } + + +} diff --git a/mozilla/grendel/storage/FilterFolder.java b/mozilla/grendel/storage/FilterFolder.java new file mode 100644 index 00000000000..d98f3a870a5 --- /dev/null +++ b/mozilla/grendel/storage/FilterFolder.java @@ -0,0 +1,182 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterFolder. + * Implements a filtered projection of anything implimenting Folder. + * + * Created: David Williams , 1 Oct 1997. + */ + +package grendel.storage; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Store; +import javax.mail.MessagingException; +import javax.mail.search.SearchTerm; +import javax.mail.event.MessageCountEvent; + +class FilterFolder extends FolderBase { + + private String fName = null; + private Folder fTargetFolder = null; + private SearchTerm fTargetTerm = null; + private Message[] fSearchResult = null; + private Message[] fOld = null; + private boolean fSearchResultsDirty = true; + + FilterFolder(Folder f, String n, SearchTerm term) { + // Set store to null, as the hstore makes no sense to a FilterFolder. + // This seems to work ok in current JavaMail API. Maybe not in future. + // djw Oct/01/1997 + super(null); + fName = n; + fTargetFolder = f; + fTargetTerm = term; + fSearchResultsDirty = true; + fOld = null; + } + FilterFolder(Folder f, SearchTerm term) { + this(f, "Filtered " + f.getName(), term); + } + void setTarget(Folder f, SearchTerm t) { + fOld = fSearchResult; + fSearchResult = null; // conservative, don't bother to check for match + fTargetFolder = f; + fTargetTerm = t; + fSearchResultsDirty = true; + // this can be aggressive or lazy. + // Let's do it aggresive, the user will get the hit when they do + // a search, not when they open the filter folder. + resync(); + } + + // javax.mail.Folder implimentations that make sense in FilterFolder: + public String getName() { + return fName; + } + public String getFullName() { + return getName(); + } + public int getType() { + return HOLDS_MESSAGES; + } + public boolean exists() { + return false; // folder never physically exists + } + public boolean renameTo(Folder f) { + fName = f.getName(); + return true; + } + private void resync() { + Message[] old = fOld; + if (fSearchResultsDirty == true) { // need to make a new fSearchResult. + if (fTargetFolder != null) { + if (fTargetTerm != null) { // do a search + try { + fSearchResult = fTargetFolder.search(fTargetTerm); + } catch (MessagingException e) { + fSearchResult = null; + } + } else { // return target folders messages + try { + fSearchResult = fTargetFolder.getMessages(); + } catch (MessagingException e) { + fSearchResult = null; + } + } + } else { // no target folder yet. + fSearchResult = null; + } + fSearchResultsDirty = false; + } + if (old != fSearchResult) { + // This is pretty hacky. + if (old != null) { + notifyMessageRemovedListeners(true, old); + } + notifyMessageAddedListeners(fSearchResult); + } + } + // Returns the set of messages that match the current search term, + // all messages if there is no term, and null if there is no target folder. + // I think this should propogate the MessagingException up, but FolderBase + // is not doing this, so I need to talk to Terry. + public synchronized Message[] getMessages() { + resync(); + return fSearchResult; + } + + // to do with sub-folders: + public Folder getParent() { + return null; // for now we are always on top + } + public Folder[] list(String notused) { + return null; // no sub-folders + } + public char getSeparator() { + // This should never be called as we don't support sub-folders. + return '/'; + } + + // Things that should never be called: + public Folder getFolder(String notused) throws MessagingException { + throw new MessagingException("FilterFolder.getFolder() not implimented"); + } + public boolean delete(boolean notused) throws MessagingException { + throw new MessagingException("FilterFolder.delete() not implimented"); + } + public Message[] expunge() throws MessagingException { + throw new MessagingException("FilterFolder.expunge() not implimented"); + } + public void appendMessages(Message[] notused) throws MessagingException { + throw new + MessagingException("FilterFolder.appendMessages() not implimented"); + } + public void deleteMessage(Message m) throws MessagingException { + throw new + MessagingException("FilterFolder.deleteMessage() not implimented"); + } + + // FIXME: + // Methods that I don't know what to do for. + public boolean create(int notused) { + return true; // return success, as this method should be a no-op + } + public void open(int mode) { + // noop + } + public void close(boolean expunge) { + // noop + } + public boolean isOpen() { + // noop + return false; + } + public boolean hasNewMessages() { + return fSearchResultsDirty; // has the target set changed? + } + + // FolderExtra stuff.. + public int getUndeletedMessageCount() { + return 0; + } +} + + diff --git a/mozilla/grendel/storage/FilterFolderFactory.java b/mozilla/grendel/storage/FilterFolderFactory.java new file mode 100644 index 00000000000..07721012715 --- /dev/null +++ b/mozilla/grendel/storage/FilterFolderFactory.java @@ -0,0 +1,37 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class FilterFolder. + * Implements a filtered projection of anything implimenting Folder. + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.storage; + +import javax.mail.Folder; +import javax.mail.Store; +import javax.mail.search.SearchTerm; +import javax.mail.MessagingException; + +import grendel.storage.FilterFolder; + +public class FilterFolderFactory { + static public Folder Make(Folder f, SearchTerm t) { + return new FilterFolder(f, t); + } +} diff --git a/mozilla/grendel/storage/FolderBase.java b/mozilla/grendel/storage/FolderBase.java new file mode 100644 index 00000000000..8ac433bafc6 --- /dev/null +++ b/mozilla/grendel/storage/FolderBase.java @@ -0,0 +1,145 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 22 Aug 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.NullJavaEnumeration; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.Vector; // Yes, I want the fully synchronized version. + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Store; + +abstract class FolderBase extends Folder implements FolderExtra { + Vector fMessages; + ByteStringTable fTable; + MessageIDTable id_table; + Vector observers = new Vector(); + + FolderBase(Store s) { + super(s); + fMessages = new Vector(); + } + + public synchronized Message getMessage(int msgnum) { + ensureLoaded(); + return (Message) fMessages.elementAt(msgnum - 1); + } + + public synchronized Message[] getMessages() { + ensureLoaded(); + Message[] result = new Message[fMessages.size()]; + fMessages.copyInto(result); + return result; + } + + public synchronized Message[] getMessages(int start, + int end) { + ensureLoaded(); + Message[] result = new Message[end - start + 1]; + for (int i=start ; i<=end ; i++) { + result[i - start] = (Message) fMessages.elementAt(i - 1); + } + return result; + } + + public synchronized Message[] getMessages(int msgnums[]) { + ensureLoaded(); + Message[] result = new Message[msgnums.length]; + for (int i=msgnums.length - 1 ; i>=0 ; i--) { + result[i] = (Message) fMessages.elementAt(msgnums[i] - 1); + } + return result; + } + + void ensureLoaded() { + } + + /** Returns the total number of messages in the folder, or -1 if unknown. + This includes deleted and unread messages. + */ + public int getMessageCount() { + if (fMessages == null) + return -1; + else + return fMessages.size(); + } + + void noticeInitialMessage(Message m) { + fMessages.addElement(m); +// #### How the hell are we supposed to do this? +// m.setMessageNumber(fMessages.size() - 1); + } + + ByteStringTable getStringTable() { + if (fTable == null) { + synchronized(this) { + // Check again, making sure a different thread didn't just do this. + if (fTable == null) { + fTable = new ByteStringTable(); + } + } + } + return fTable; + } + + MessageIDTable getMessageIDTable() { + if (id_table == null) { + synchronized(this) { + // Check again, making sure a different thread didn't just do this. + if (id_table == null) { + id_table = new MessageIDTable(); + } + } + } + return id_table; + } + + + // This is just silly. javamail won't let me call this directly, because + // they protected the method; I have to put this stupid intermediate call in. + void doNotifyMessageChangedListeners(int i, Message message) { + notifyMessageChangedListeners(i, message); + } + + static Flags permflags = null; + public Flags getPermanentFlags() { + if (permflags == null) { + Flags result = new Flags(); + for (int i=0 ; i < MessageBase.FLAGDEFS.length ; i++) { + if (MessageBase.FLAGDEFS[i].builtin != null) + result.add(MessageBase.FLAGDEFS[i].builtin); + else + result.add(MessageBase.FLAGDEFS[i].non_builtin); + } + permflags = result; + } + return permflags; + } + + + +} diff --git a/mozilla/grendel/storage/FolderExtra.java b/mozilla/grendel/storage/FolderExtra.java new file mode 100644 index 00000000000..9355986006f --- /dev/null +++ b/mozilla/grendel/storage/FolderExtra.java @@ -0,0 +1,35 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 22 Oct 1997. + */ + +package grendel.storage; + +import javax.mail.MessagingException; + +/** These are extra interfaces that our folder objects implement that are not + implemented as part of the standard Folder class. If you want to call + any of these, and you just have a base Folder object, use + FolderExtraFactory to find or create the FolderExtra object for you. */ + +public interface FolderExtra { + /** The number of messages in this folder that have not been deleted. */ + public int getUndeletedMessageCount() throws MessagingException; +}; + diff --git a/mozilla/grendel/storage/FolderExtraFactory.java b/mozilla/grendel/storage/FolderExtraFactory.java new file mode 100644 index 00000000000..4da4f2eca4b --- /dev/null +++ b/mozilla/grendel/storage/FolderExtraFactory.java @@ -0,0 +1,57 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 28 Oct 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.MessagingException; + +/** If you want to call any of the FolderExtra methods on a Folder, you + should get the FolderExtra object using this call. Some Folder objects + will implement that interface directly; the rest need to have them + painfully computed. */ + +public class FolderExtraFactory { + static public FolderExtra Get(Folder f) { + if (f instanceof FolderExtra) return (FolderExtra) f; + return new FolderExtraWrapper(f); + } +} + +class FolderExtraWrapper implements FolderExtra { + Folder f; + protected FolderExtraWrapper(Folder fold) { + f = fold; + } + + public int getUndeletedMessageCount() { + try { + return f.getMessageCount(); // ### WRONG WRONG WRONG! + } catch (MessagingException e) { + } catch (IllegalStateException e) { + } + return -1; + } + +} diff --git a/mozilla/grendel/storage/MailDrop.java b/mozilla/grendel/storage/MailDrop.java new file mode 100644 index 00000000000..5aadb703c21 --- /dev/null +++ b/mozilla/grendel/storage/MailDrop.java @@ -0,0 +1,55 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 28 Aug 1997. + */ + +package grendel.storage; + +import java.io.IOException; + +/** Interface to a maildrop: a place that we get new mail from. */ + +public interface MailDrop { + /** Returned by getBiffState() if there are new messages waiting. */ + public static final int NEW = 1; + + /** Returned by getBiffState() if there are no new messages waiting. */ + public static final int NONE = 2; + + /** Returned by getBiffState() if we can't tell if there are new messages + waiting. */ + public static final int UNKNOWN = 3; + + /** Actually go to maildrop, grab any messages, and stuff them into + folders. */ + public void fetchNewMail() throws IOException; + + /** Go to the maildrop and update the info on how many messages out there + are waiting. */ + public void doBiff() throws IOException; + + /** Find out whether we actually know anything about new messages waiting + on the maildrop. */ + public int getBiffState(); + + /** Return how many new messages are out there waiting. If + getBiffState() returns NONE, returns zero. If getBiffState() returns + UNKNOWN, or if we just can't tell with this maildrop, returns -1. */ + public int getNumMessagesWaiting(); +}; diff --git a/mozilla/grendel/storage/MailDropException.java b/mozilla/grendel/storage/MailDropException.java new file mode 100644 index 00000000000..6cfa0cf3440 --- /dev/null +++ b/mozilla/grendel/storage/MailDropException.java @@ -0,0 +1,37 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 28 Aug 1997. + */ + +package grendel.storage; + +import java.io.IOException; + +/** An exception while trying to process a maildrop. Usually means a network + problem or a configuration problem. */ + +public class MailDropException extends IOException { + public MailDropException() { + } + + public MailDropException(String msg) { + super(msg); + } +} + diff --git a/mozilla/grendel/storage/MailSummaryFile.java b/mozilla/grendel/storage/MailSummaryFile.java new file mode 100644 index 00000000000..7b9fc7e54ae --- /dev/null +++ b/mozilla/grendel/storage/MailSummaryFile.java @@ -0,0 +1,356 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.io.EOFException; +import java.util.Hashtable; +import java.util.Enumeration; + +abstract class MailSummaryFile { + + /** The folder and associated disk file. The file might not exist. */ + protected BerkeleyFolder folder; + protected File file; // note this is the *folder*, not the *summary*. + + /* This holds the date of the file when it was parsed. + We need to save this (older) values so that we can tell when + the summary file is out of date. This is measured in the units + of File.lastModified(). */ + protected long file_date; + + /* This holds the size of the file when it was parsed. + We need to save this (older) values so that we can tell when + the summary file is out of date. */ + protected long file_size; + + /* The total number of messages, non-deleted messages, and unread messages + in the folder, assuming that file_date and file_size match reality. */ + protected int total_message_count = -1; + protected int undeleted_message_count = -1; + protected int unread_message_count = -1; + protected long deleted_message_bytes = -1; + + /** For salvageMessages(). If the table is non-null, its keys will be + MessageID objects, and the values will be opaque objects intended + to be passed to salvageMessageSummary(). Subclasses are responsible + for populating this table, if they are able. + */ + protected Hashtable salvage = null; + + + MailSummaryFile(BerkeleyFolder f) { + folder = f; + file = folder.getFile(); + file_size = file.length(); + file_date = file.lastModified(); + } + + /** Parses a summary file for the associated folder, and adds the described + message objects to the folder. The given stream is assumed to have + been opened, and positioned past any version-identifying header info. + Returns the byte position at which parsing should resume (that is, the + last byte position which the summary file summarizes.) + */ + abstract long readSummaryFile(InputStream sum) throws IOException; + + /** Whether this class knows how to write a summary file. */ + boolean writable() { return false; } + + /** Write a summary file for the associated folder. If a subclass provides + an implementation for this method, it should also override writable(). + */ + void writeSummaryFile() throws IOException { + throw new Error("unimplemented"); + } + + + /** Returns the date that the summary file expects to find on the + folder's file. This is measured in the units of File.lastModified(). + Set this expectation with setFolderDateAndSize(). + */ + long folderDate() { + return file_date; + } + + /** Returns the size in bytes that the summary file expects of the + folder's file. Set this expectation with setFolderDateAndSize(). + */ + long folderSize() { + return file_size; + } + + /** Returns the total number of messages in the folder. + This includes deleted and unread messages. + */ + int totalMessageCount() { + if (total_message_count == -1) + synchronized (this) { + if (total_message_count == -1) // double check + getMessageCounts(); + } + return total_message_count; + } + + /** Returns the number of non-deleted messages in the folder. + This includes unread messages. + */ + int undeletedMessageCount() { + if (undeleted_message_count == -1) + synchronized (this) { + if (undeleted_message_count == -1) // double check + getMessageCounts(); + } + return undeleted_message_count; + } + + /** Returns the number of unread messages in the folder. + This does not include unread messages that are also deleted. + */ + int unreadMessageCount() { + if (unread_message_count == -1) + synchronized (this) { + if (unread_message_count == -1) // double check + getMessageCounts(); + } + return unread_message_count; + } + + + /** Returns the number of bytes consumed by deleted but not expunged + messages in the folder. */ + long deletedMessageBytes() { + if (deleted_message_bytes == -1) + synchronized (this) { + if (deleted_message_bytes == -1) // double check + getMessageCounts(); + } + return deleted_message_bytes; + } + + /** Read only enough of the file to fill in the values of + total_message_count, undeleted_message_count, unread_message_count, + and deleted_message_bytes. */ + abstract protected void getMessageCounts(); + + + /** Set the date and size of the corresponding folder-file. + This date/time pair should correspond to the file at the time at + which it was parsed, not the current time. + The next time writeSummaryFile() is called, the new summary + file will encode this date and size expectation. + */ + void setFolderDateAndSize(long file_date, long file_size) { + this.file_date = file_date; + this.file_size = file_size; + } + + + /** Set the message counts (total, undeleted, and unread) of the + corresponding folder-file. The next time writeSummaryFile() is + called, the new summary file will include these numbers. + + @param total_count The total number of messages in the folder. + This must match the number of elements returned + by getMessages() at the time that + writeSummaryFile() is called. + + @param undeleted_count The total number of messages in the folder + that are not marked as deleted. This also + must match reality when writeSummaryFile() is + called. + + @param unread_count The total number of messages in the folder + that are not marked as either read or deleted. + Ditto writeSummaryFile(). + + @param deleted_bytes The total number of bytes consumed by messages + that are marked as deleted. + Ditto writeSummaryFile(). + */ + void setFolderMessageCounts(int total_count, int undeleted_count, + int unread_count, long deleted_bytes) { + Assert.Assertion(total_count >= 0); + Assert.Assertion(undeleted_count >= 0); + Assert.Assertion(unread_count >= 0); + Assert.Assertion(deleted_bytes >= 0); + Assert.Assertion(undeleted_count <= total_count); + Assert.Assertion(unread_count <= total_count); + + this.total_message_count = total_count; + this.undeleted_message_count = undeleted_count; + this.unread_message_count = unread_count; + this.deleted_message_bytes = deleted_bytes; + } + + + /** Called when the folder's disk file has been appended to by *this* + program. (As opposed to, an unexpected, unknown change by some other + program.) This calls setFolderDateAndSize() with the current date + and size of the folder file (as it appears on disk right now.) + +

If the number of messages has changed, you should have called + setFolderMessageCounts() before calling this. + */ + void updateSummaryFile() throws IOException { + setFolderDateAndSize(file.lastModified(), file.length()); + // by default, do nothing to the disk file. + } + + /** Compare the date of the folder disk file to a date (presumably) read + from the summary file, and return true of they are equal. + */ + protected boolean checkFolderDate(long summary_date) { + long folder_date = file.lastModified(); + + // #### System dependency: to be compatible with the Cheddar file format, + // we need to be able to compare lastModified() with the time_t + // value in the file (which came from `stat.st_mtime'.) + // + // Note that, to be compatible with this, MailSummaryFileGrendel divides + // the date by 1000 when saving it to the file. + // + summary_date *= 1000L; + + return (folder_date == summary_date); + } + + /** Compare the size of the folder disk file to a size (presumably) read + from the summary file, and return true of they are equal. + */ + protected boolean checkFolderSize(long summary_size) { + return (summary_size == file.length()); + } + + + /** Call this if something has gone wrong when parsing the summary file, + but after the folder file has been parsed. This might cause + some additional information to be scavenged from the summary file + and stored into the messages. + +

The idea here is that an out-of-date summary file cannot be + trusted as far as byte-offsets and such goes, but if the summary + file had stored some property of a message (such as whether it was + deleted) then we might as well believe that, lacking any other + source of information. + +

Note that one folder can contain multiple messages with the same + ID, so this process is not totally foolproof. It is, however, + better than nothing. + */ + void salvageMessages() { + if (salvage == null) return; + Hashtable t = salvage; + salvage = null; + if (folder.fMessages != null) { + for (Enumeration e = folder.fMessages.elements(); e.hasMoreElements();) { + BerkeleyMessage m = (BerkeleyMessage) e.nextElement(); + MessageID id = (MessageID) m.getMessageID(); + if (id == null) continue; + Object value = t.get(id); + if (value != null) + salvageMessage(m, value); + } + } + } + + protected void salvageMessage(BerkeleyMessage m, Object salvage_object) { + // default is to do nothing. Subclasses should implement this if + // they can. + } + + + /* Some random I/O utilities... */ + + + protected synchronized int readUnsignedShort(InputStream s) + throws IOException { + int b0 = s.read(); if (b0 == -1) throw new EOFException(); + int b1 = s.read(); if (b1 == -1) throw new EOFException(); + return ((b0 << 8) | b1); + } + + protected synchronized long readUnsignedInteger(InputStream s) + throws IOException { + int b0 = s.read(); if (b0 == -1) throw new EOFException(); + int b1 = s.read(); if (b1 == -1) throw new EOFException(); + int b2 = s.read(); if (b2 == -1) throw new EOFException(); + int b3 = s.read(); if (b3 == -1) throw new EOFException(); + return ((((long)b0) << 24) | (b1 << 16) | (b2 << 8) | b3); + } + + protected synchronized void writeUnsignedShort(OutputStream s, int i) + throws IOException { + s.write((i >> 8) & 0xFF); + s.write(i & 0xFF); + } + + protected synchronized void writeUnsignedInteger(OutputStream s, long i) + throws IOException { + s.write((int) (i >> 24) & 0xFF); + s.write((int) (i >> 16) & 0xFF); + s.write((int) (i >> 8) & 0xFF); + s.write((int) (i & 0xFF)); + } + + /* Java sucks! I had to duplicate these because, as far as I can tell, + there is no class or interface which FileInputStream and RandomAccessFile + have in common, nor that FileOutputStream and RandomAccessFile have in + common. What a total crock! + */ + + protected synchronized int readUnsignedShort(RandomAccessFile s) + throws IOException { + int b0 = s.read(); if (b0 == -1) throw new EOFException(); + int b1 = s.read(); if (b1 == -1) throw new EOFException(); + return ((b0 << 8) | b1); + } + + protected synchronized long readUnsignedInteger(RandomAccessFile s) + throws IOException { + int b0 = s.read(); if (b0 == -1) throw new EOFException(); + int b1 = s.read(); if (b1 == -1) throw new EOFException(); + int b2 = s.read(); if (b2 == -1) throw new EOFException(); + int b3 = s.read(); if (b3 == -1) throw new EOFException(); + return ((((long)b0) << 24) | (b1 << 16) | (b2 << 8) | b3); + } + + protected synchronized void writeUnsignedShort(RandomAccessFile s, int i) + throws IOException { + s.write((i >> 8) & 0xFF); + s.write(i & 0xFF); + } + + protected synchronized void writeUnsignedInteger(RandomAccessFile s, long i) + throws IOException { + s.write((int) (i >> 24) & 0xFF); + s.write((int) (i >> 16) & 0xFF); + s.write((int) (i >> 8) & 0xFF); + s.write((int) (i & 0xFF)); + } + +} diff --git a/mozilla/grendel/storage/MailSummaryFileCheddar.java b/mozilla/grendel/storage/MailSummaryFileCheddar.java new file mode 100644 index 00000000000..26fe3764cd0 --- /dev/null +++ b/mozilla/grendel/storage/MailSummaryFileCheddar.java @@ -0,0 +1,295 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import java.util.Vector; +import java.util.Enumeration; +import java.util.Hashtable; +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; + +/** This class knows how to read a Netscape 2.0 / 3.0 mail summary file. + It does not know how to write them, because our new internal representation + of messages doesn't keep around enough information to regenerate the file + compatibly (doing so would consume a lot more memory.) + */ + +/* The summary format, as documented in ns/lib/libmsg/mailsum.h: + + MAGIC_NUMBER VERSION FOLDER_SIZE FOLDER_DATE PARSED_THRU NMSGS NVALIDMSGS + NUNREADMSGS DELETED_BYTES STRING_TABLE [ MSG_DESC ]* + + MAGIC_NUMBER := '# Netscape folder cache\r\n' + VERSION := uint32 (file format version number; this is version 4) + + FOLDER_SIZE := uint32 (the size of the folder itself, so that we can + easily detect an out-of-date summary file) + FOLDER_DATE := uint32 (time_t of the folder; same purpose as above) + PARSED_THRU := uint32 (the last position in the folder that we have + parsed data for. Normally the same as + FOLDER_SIZE, but if we append messages to a file, + we can just update FOLDER_SIZE and FOLDER_SIZE and + use this field to tell us what part of the + folder needs to be parsed.) + NMSGS := uint32 (total number of messages in this folder, including + deleted messages) + NVALIDMSGS := uint32 (total number of non-deleted messages in this + folder) + NUNREADMSGS := uint32 (total number of unread messages in this folder) + DELETED_BYTES := uint32 (number of bytes of messages which have been + deleted from the summary, but not yet compacted out + of the folder file itself. The messages in question + have the DELETED bit set in their flags.) + + STRING_TABLE := ST_LENGTH [ ST_STRING ]+ + ST_LENGTH := uint16 + ST_STRING := null terminated string + + MSG_DESC := SENDER RECIPIENT SUBJECT DATE LINES ID REFERENCES + FLAGS POS LENGTH STATUSINDEX + SENDER := uint16 (index into STRING_TABLE) + RECIPIENT := uint16 (index into STRING_TABLE) + SUBJECT := uint16 (index into STRING_TABLE) + DATE := uint32 (time_t) + FLAGS := uint32 (bitfield of other attributes of the message; + this is in `X-Mozilla-Status' format.) + POS := uint32 (byte index into file) + LENGTH := uint32 (length in bytes from envelope to end of last line) + STATUSOFFSET := uint16 (Offset from beginning of message envelope for + X-Mozilla-Status header) + LINES := uint16 (length in lines; but I think this was unused...) + ID := uint16 (index into STRING_TABLE) + REFERENCES := RLENGTH [ ID ]* + RLENGTH := uint16 + */ + +class MailSummaryFileCheddar extends MailSummaryFile { + + MailSummaryFileCheddar(BerkeleyFolder folder) { + super(folder); + } + + synchronized long readSummaryFile(InputStream sum) throws IOException { + + // At the point at which this method is called, MAGIC_NUMBER and VERSION + // have already been read. + + long folder_size = readUnsignedInteger(sum); + long folder_date = readUnsignedInteger(sum); + long old_thru = readUnsignedInteger(sum); // old parsed_thru + long nmsgs = readUnsignedInteger(sum); + long nvalidmsgs = readUnsignedInteger(sum); + long nunreadmsgs = readUnsignedInteger(sum); + long deletedbytes = readUnsignedInteger(sum); + int table_length = readUnsignedShort(sum); + + boolean pessimistic = false; + + if (!checkFolderDate(folder_date)) { + System.err.println("warning: folder " + folder.getName() + + " is newer than summary file" + +// " (" + this.file_date + ", " + +// folder_date*1000L + ")" + + "; attempting salvage."); + pessimistic = true; + + } else if (!checkFolderSize(folder_size)) { + System.err.println("warning: folder " + folder.getName() + " is not " + + folder_size + " bytes, as summary file expected; " + + "\n\t attempting salvage."); + pessimistic = true; + } + + ByteBuf table[] = new ByteBuf[table_length]; + // Read N null-terminated strings. + for (int i = 0; i < table_length; i++) { + ByteBuf bb = new ByteBuf(); + for (int j = 0;; j++) { + int b = sum.read(); + if (b == 0) break; + if (b == -1) throw new IOException(); + bb.append((byte) b); + } + + table[i] = bb; + } + + Vector msgs = (pessimistic + ? null + : new Vector((int) nmsgs)); + + if (pessimistic) + salvage = new Hashtable((int) nmsgs); + + boolean success = false; + try { // if we fail for some reason, set up the `salvage' table + + // Read N message descriptors. + for (int i = 0; i < nmsgs; i++) { + int sender = readUnsignedShort(sum); + int recipient = readUnsignedShort(sum); + int subject = readUnsignedShort(sum); + long date = readUnsignedInteger(sum); + long flags = readUnsignedInteger(sum); + long pos = readUnsignedInteger(sum); + long length = readUnsignedInteger(sum); + int statusoff = readUnsignedShort(sum); + int lines = readUnsignedShort(sum); + int id = readUnsignedShort(sum); + int rlength = readUnsignedShort(sum); + + ByteBuf refs[] = null; + if (rlength > 0) { + refs = new ByteBuf[rlength]; + for (int j = 0; j < rlength; j++) { + refs[j] = table[readUnsignedShort(sum)]; + } + } + + if (pessimistic) { + refs = null; + ByteBuf bid = table[id]; + MessageID mid = new MessageID(bid.toBytes(), 0, bid.length()); + salvage.put(mid, new Long(flags)); + } else { + flags = BerkeleyMessage.mozillaFlagsToInternalFlags(flags); + BerkeleyMessage m = new BerkeleyMessage(folder, + date * 1000L, + flags, + table[sender], + table[recipient], + table[subject], + table[id], + refs); + refs = null; + m.setSize((int) length); + msgs.addElement(m); + } + } + + success = true; + + } finally { + table = null; + + if (!success && + msgs != null) { + // We got some kind of error while reading the message data. + // The `msgs' vector contains the messages we were able to parse, + // but since we got an error, we're not going to actually add them + // to the folder. But, let's pull the flags out of the messages + // and populate `salvage' with it (see doc of salvageMessages() + // for rationale.) + // + // Note that if we realized very early that this file was out of + // date, salvage will have already been populated, and `msgs' + // will be null. This branch is taken only if the file at first + // appears to be up to date, but later turns out to be corrupted + // or truncated. + // + if (salvage == null) + salvage = new Hashtable((int) nmsgs); + for (Enumeration e = msgs.elements(); e.hasMoreElements();) { + BerkeleyMessage m = (BerkeleyMessage) e.nextElement(); + salvage.put(m.getMessageID(), + new Long(m.internalFlagsToMozillaFlags(m.flags))); + } + } + } + + if (pessimistic) + // Ok, we got what we needed. Tell the caller to go and reparse + // the folder. It will then call salvageMessages() which will + // pull stuff out of the hash table we just populated. + return 0; + + this.total_message_count = (int) nmsgs; + this.undeleted_message_count = (int) nvalidmsgs; + this.unread_message_count = (int) nunreadmsgs; + this.deleted_message_bytes = deletedbytes; + + // Successfully parsed it all; feed the messages into the folder. + for (Enumeration e = msgs.elements(); e.hasMoreElements();) + folder.noticeInitialMessage((BerkeleyMessage) e.nextElement()); + msgs = null; + + // Return the byte at which parsing in the folder should begin + // (the last byte covered by this summary file.) + return old_thru; + } + + + /** Assumes the salvage table contains Long values which are flags + in X-Mozilla-Status form. */ + protected void salvageMessage(BerkeleyMessage m, Object salvage_object) { + long flags = ((Long)salvage_object).longValue(); + m.flags |= m.mozillaFlagsToInternalFlags(flags); + } + + + /** Read only enough of the file to fill in the values of + total_message_count, undeleted_message_count, unread_message_count, + and deleted_message_bytes. */ + synchronized protected void getMessageCounts() { + InputStream in = null; + try { + File sum = folder.summaryFileName(); + in = new BufferedInputStream(new FileInputStream(sum)); + + // header + for (int i = 0; + i < MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER.length; + i++) { + if (in.read() != MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER[i]) + return; + } + + // version number + if (readUnsignedInteger(in) != MailSummaryFileFactory.VERSION_GRENDEL) + return; + + readUnsignedInteger(in); // FOLDER_SIZE + readUnsignedInteger(in); // FOLDER_DATE + readUnsignedInteger(in); // PARSED_THRU + + total_message_count = (int) readUnsignedInteger(in); // NMSGS + undeleted_message_count = (int) readUnsignedInteger(in); // NVALIDMSGS + unread_message_count = (int) readUnsignedInteger(in); // NUNREADMSGS + deleted_message_bytes = readUnsignedInteger(in); // DELETED_BYTES + + } catch (IOException e) { + // ignore it. + } finally { + if (in != null) + try { + in.close(); + } catch (IOException e) { + } + } + } + +} diff --git a/mozilla/grendel/storage/MailSummaryFileFactory.java b/mozilla/grendel/storage/MailSummaryFileFactory.java new file mode 100644 index 00000000000..462240d5c11 --- /dev/null +++ b/mozilla/grendel/storage/MailSummaryFileFactory.java @@ -0,0 +1,105 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 2 Oct 1997. + */ + +package grendel.storage; + +import java.io.InputStream; +import java.io.IOException; + +class MailSummaryFileFactory { + + static final byte SUMMARY_MAGIC_NUMBER[] = + "# Netscape folder cache\r\n".getBytes(); + static final int NEOACCESS_MAGIC_NUMBER[] = + { 0000, 0036, 0204, 0212 }; + + static final int VERSION_CHEDDAR = 4; + static final int VERSION_DOGBERT = 5; + static final int VERSION_GRENDEL = 6; + + + private MailSummaryFileFactory() {} // all methods are static + + /** Given a folder and a file stream to that folder's summary file, + this static method parses the header of that file, and returns + an appropriate MailSummaryFile object (or null if the file is + not one we recognise.) + +

To parse the body of the summary file, use the readSummaryFile() + on the returned MailSummaryFile object. + + @see MailSummaryFile + @see MailSummaryFileCheddar + @see MailSummaryFileGrendel + */ + static MailSummaryFile ParseFileHeader(BerkeleyFolder folder, + InputStream summary_stream) { + MailSummaryFile result = null; + + LOST: { + try { + int b0 = summary_stream.read(); if (b0 == -1) break LOST; + int b1 = summary_stream.read(); if (b1 == -1) break LOST; + int b2 = summary_stream.read(); if (b2 == -1) break LOST; + int b3 = summary_stream.read(); if (b3 == -1) break LOST; + + long version; + if (b0 == NEOACCESS_MAGIC_NUMBER[0] && + b1 == NEOACCESS_MAGIC_NUMBER[1] && + b2 == NEOACCESS_MAGIC_NUMBER[2] && + b3 == NEOACCESS_MAGIC_NUMBER[3]) + version = VERSION_DOGBERT; + else { + if (b0 != SUMMARY_MAGIC_NUMBER[0] || + b1 != SUMMARY_MAGIC_NUMBER[1] || + b2 != SUMMARY_MAGIC_NUMBER[2] || + b3 != SUMMARY_MAGIC_NUMBER[3]) + return null; + + for (int i = 4; i < SUMMARY_MAGIC_NUMBER.length; i++) + if (summary_stream.read() != SUMMARY_MAGIC_NUMBER[i]) + return null; + + b0 = summary_stream.read(); if (b0 == -1) break LOST; + b1 = summary_stream.read(); if (b1 == -1) break LOST; + b2 = summary_stream.read(); if (b2 == -1) break LOST; + b3 = summary_stream.read(); if (b3 == -1) break LOST; + version = ((((long)b0) << 24) | (b1 << 16) | (b2 << 8) | b3); + } + + if (version == VERSION_CHEDDAR) + result = new MailSummaryFileCheddar(folder); + else if (version == VERSION_DOGBERT) + result = null; // we don' know how to read no stinkink NeoAccess... + else if (version == VERSION_GRENDEL) + result = new MailSummaryFileGrendel(folder); + + } catch (IOException e) { + } + } + +// System.err.println("summary file reader: " + +// (result == null ? "null" : +// result.getClass().getName())); + + return result; + } +} diff --git a/mozilla/grendel/storage/MailSummaryFileGrendel.java b/mozilla/grendel/storage/MailSummaryFileGrendel.java new file mode 100644 index 00000000000..7d65b795e47 --- /dev/null +++ b/mozilla/grendel/storage/MailSummaryFileGrendel.java @@ -0,0 +1,534 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 28 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import calypso.util.Assert; +import java.util.Vector; +import java.util.Enumeration; +import java.util.Hashtable; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.BufferedOutputStream; +import java.io.RandomAccessFile; +import java.io.IOException; + +/** This class knows how to read and write a mail folder summary file. + This is a different format than that used by any previous version of + Mozilla; it's similar to the 2.0/3.0 format, and totally unlike the + 4.0/5.0 format. + */ + +/* The format is as follows: + + MAGIC_NUMBER VERSION FOLDER_SIZE FOLDER_DATE PARSED_THRU NMSGS NVALIDMSGS + NUNREADMSGS DELETED_BYTES STRING_TABLE ID_TABLE [ MSG_DESC ]* + + MAGIC_NUMBER := '# Netscape folder cache\r\n' + VERSION := uint32 (file format version number; this is version 6) + + FOLDER_SIZE := uint32 (the size of the folder itself, so that we can + easily detect an out-of-date summary file) + FOLDER_DATE := uint32 (time_t of the folder; same purpose as above) + PARSED_THRU := uint32 (the last position in the folder that we have + parsed data for. Normally the same as + FOLDER_SIZE, but if we append messages to a file, + we can just update FOLDER_SIZE and FOLDER_SIZE and + use this field to tell us what part of the + folder needs to be parsed.) + NMSGS := uint32 (total number of messages in this folder, including + expunged messages) + NVALIDMSGS := uint32 (total number of non-expunged messages in this + folder) + NUNREADMSGS := uint32 (total number of unread messages in this folder) + DELETED_BYTES := uint32 (number of bytes of messages which have been + deleted from the summary, but not yet compacted out + of the folder file itself. The messages in question + have the DELETED bit set in their flags.) + + STRING_TABLE := ST_LENGTH [ ST_STRING ]+ + ST_LENGTH := uint32 + ST_STRING := null terminated string + + ID_TABLE := ID_LENGTH [ MSG_ID ]+ + ID_LENGTH := uint32 + MSG_ID := uint64 (64-bit hash of a Message-ID string) + + MSG_DESC := AUTHOR RECIPIENT SUBJECT DATE ID REFERENCES + FLAGS POS LENGTH + AUTHOR := SINDEX (index into STRING_TABLE) + RECIPIENT := SINDEX (index into STRING_TABLE) + SUBJECT := SINDEX (index into STRING_TABLE) + DATE := uint32 (time_t) + FLAGS := uint32 (bitfield of other attributes of the message; + this is in `X-Mozilla-Status' format.) + POS := uint32 (byte index into file) + LENGTH := uint32 (length in bytes from envelope to end of last line) + ID := IINDEX (index into ID_TABLE) + REFERENCES := RLENGTH [ ID ]* + RLENGTH := uint16 + + SINDEX := uint16 -or- uint32 (depending on whether ST_LENGTH is + able to be represented with 16 bits.) + IINDEX := uint16 -or- uint32 (same, but for ID_LENGTH.) + */ + +class MailSummaryFileGrendel extends MailSummaryFile { + + MailSummaryFileGrendel(BerkeleyFolder folder) { + super(folder); + } + + /** Parses a Grendel (version 6) mail summary file, and adds the described + messages to the associated Folder object. + */ + synchronized long readSummaryFile(InputStream sum) throws IOException { + + // At the point at which this method is called, MAGIC_NUMBER and VERSION + // have already been read. + + long folder_size = readUnsignedInteger(sum); + long folder_date = readUnsignedInteger(sum); + long old_thru = readUnsignedInteger(sum); // old parsed_thru + long nmsgs = readUnsignedInteger(sum); + long nvalidmsgs = readUnsignedInteger(sum); + long nunreadmsgs = readUnsignedInteger(sum); + long deletedbytes = readUnsignedInteger(sum); + + boolean pessimistic = false; + + if (!checkFolderDate(folder_date)) { + System.err.println("warning: folder " + folder.getName() + + " is newer than summary file" + +// " (" + this.file_date + ", " + +// folder_date*1000L + ")" + + "; attempting salvage."); + pessimistic = true; + + } else if (!checkFolderSize(folder_size)) { + System.err.println("warning: folder " + folder.getName() + " is not " + + folder_size + " bytes, as summary file expected; " + + "\n\t attempting salvage."); + pessimistic = true; + } + + + long string_table_length = readUnsignedInteger(sum); + ByteBuf string_table[] = (pessimistic + ? null + : new ByteBuf[(int) string_table_length]); + + // Read N null-terminated strings. + for (int i = 0; i < string_table_length; i++) { + ByteBuf bb = new ByteBuf(); + for (int j = 0;; j++) { + int b = sum.read(); + if (b == 0) break; + if (b == -1) throw new IOException(); + bb.append((byte) b); + } + if (!pessimistic) + string_table[i] = bb; + } + + long id_table_length = readUnsignedInteger(sum); + MessageID id_table[] = new MessageID[(int) id_table_length]; + + // Read N 64-bit numbers (hashes of the Message-IDs.) + for (int i = 0; i < id_table_length; i++) { + long b0 = readUnsignedInteger(sum); + long b1 = readUnsignedInteger(sum); + id_table[i] = new MessageID((b0 << 32) | b1); + } + + Vector msgs = (pessimistic + ? null + : new Vector((int) nmsgs)); + + if (pessimistic) + salvage = new Hashtable((int) nmsgs); + + boolean success = false; + try { // if we fail for some reason, set up the `salvage' table + + // Read N message descriptors. + for (int i = 0; i < nmsgs; i++) { + + long author, recipient, subject, id; + if (string_table_length <= 0xFFFF) { + author = readUnsignedShort(sum); + recipient = readUnsignedShort(sum); + subject = readUnsignedShort(sum); + } else { + author = readUnsignedInteger(sum); + recipient = readUnsignedInteger(sum); + subject = readUnsignedInteger(sum); + } + long date = readUnsignedInteger(sum); + long flags = readUnsignedInteger(sum); + long pos = readUnsignedInteger(sum); + long length = readUnsignedInteger(sum); + if (id_table_length <= 0xFFFF) { + id = readUnsignedShort(sum); + } else { + id = readUnsignedInteger(sum); + } + + int rlength = readUnsignedShort(sum); + MessageID refs[] = null; + + if (rlength > 0) { + refs = new MessageID[rlength]; + if (id_table_length <= 0xFFFF) { + for (int j = 0; j < rlength; j++) + refs[j] = id_table[readUnsignedShort(sum)]; + } else { + for (int j = 0; j < rlength; j++) + refs[j] = id_table[(int) readUnsignedInteger(sum)]; + } + } + + if (pessimistic) { + refs = null; + salvage.put(id_table[(int) id], new Long(flags)); + } else { + flags = BerkeleyMessage.mozillaFlagsToInternalFlags(flags); + BerkeleyMessage m = + new BerkeleyMessage(folder, + date * 1000L, + flags, + string_table[(int) author], + string_table[(int) recipient], + string_table[(int) subject], + id_table[(int) id], + refs); + refs = null; + m.setStorageFolderIndex((int) pos); + m.setSize((int) length); + msgs.addElement(m); + } + } + + success = true; + + } finally { + string_table = null; + id_table = null; + + if (!success && + msgs != null) { + // We got some kind of error while reading the message data. + // The `msgs' vector contains the messages we were able to parse, + // but since we got an error, we're not going to actually add them + // to the folder. But, let's pull the flags out of the messages + // and populate `salvage' with it (see doc of salvageMessages() + // for rationale.) + // + // Note that if we realized very early that this file was out of + // date, salvage will have already been populated, and `msgs' + // will be null. This branch is taken only if the file at first + // appears to be up to date, but later turns out to be corrupted + // or truncated. + // + if (salvage == null) + salvage = new Hashtable((int) nmsgs); + for (Enumeration e = msgs.elements(); e.hasMoreElements();) { + BerkeleyMessage m = (BerkeleyMessage) e.nextElement(); + salvage.put(m.getMessageID(), + new Long(m.internalFlagsToMozillaFlags(m.flags))); + } + } + } + + if (pessimistic) + // Ok, we got what we needed. Tell the caller to go and reparse + // the folder. It will then call salvageMessages() which will + // pull stuff out of the hash table we just populated. + return 0; + + this.total_message_count = (int) nmsgs; + this.undeleted_message_count = (int) nvalidmsgs; + this.unread_message_count = (int) nunreadmsgs; + this.deleted_message_bytes = deletedbytes; + + // Successfully parsed it all; feed the messages into the folder. + for (Enumeration e = msgs.elements(); e.hasMoreElements();) + folder.noticeInitialMessage((BerkeleyMessage) e.nextElement()); + msgs = null; + + // Return the byte at which parsing in the folder should begin + // (the last byte covered by this summary file.) + return old_thru; + } + + + /** Assumes the salvage table contains Long values which are flags + in X-Mozilla-Status form. */ + protected void salvageMessage(BerkeleyMessage m, Object salvage_object) { + long flags = ((Long)salvage_object).longValue(); + m.flags |= m.mozillaFlagsToInternalFlags(flags); + } + + + /** Read only enough of the file to fill in the values of + total_message_count, undeleted_message_count, unread_message_count, + and deleted_message_bytes. */ + synchronized protected void getMessageCounts() { + InputStream in = null; + try { + File sum = folder.summaryFileName(); + in = new BufferedInputStream(new FileInputStream(sum)); + + // header + for (int i = 0; + i < MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER.length; + i++) { + if (in.read() != MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER[i]) + return; + } + + // version number + if (readUnsignedInteger(in) != MailSummaryFileFactory.VERSION_GRENDEL) + return; + + readUnsignedInteger(in); // FOLDER_SIZE + readUnsignedInteger(in); // FOLDER_DATE + readUnsignedInteger(in); // PARSED_THRU + + total_message_count = (int) readUnsignedInteger(in); // NMSGS + undeleted_message_count = (int) readUnsignedInteger(in); // NVALIDMSGS + unread_message_count = (int) readUnsignedInteger(in); // NUNREADMSGS + deleted_message_bytes = readUnsignedInteger(in); // DELETED_BYTES + + } catch (IOException e) { + // ignore it. + } finally { + if (in != null) + try { + in.close(); + } catch (IOException e) { + } + } + } + + + + /** Returns true, since this class implements writeSummaryFile(). */ + boolean writable() { return true; } + + /** Write a summary file for the associated folder, in the Grendel format + (version 6.) + */ + void writeSummaryFile() throws IOException { + File sum = folder.summaryFileName(); + OutputStream out = null; + boolean success = false; + try { + out = new BufferedOutputStream(new FileOutputStream(sum)); + writeSummaryData(out); + success = true; + } finally { + // always close the file. + // if there was an error, delete the partial file we just wrote. + if (out != null) { + out.close(); + if (!success) + sum.delete(); + } + } + } + + /* subroutine of writeSummaryFile, broken out for clarity. */ + protected void writeSummaryData(OutputStream out) + throws IOException, SecurityException { + + Vector msgs = folder.fMessages; + int nmsgs = msgs.size(); + int nvalidmsgs = 0; + int nunreadmsgs = 0; + int deletedbytes = 0; + + // Walk over the messages to count up the unread, deleted, and bytes. + for (int i = 0; i < nmsgs; i++) { + BerkeleyMessage m = (BerkeleyMessage) msgs.elementAt(i); + if (m.isDeleted()) { + deletedbytes += m.fLength; + } else { + nvalidmsgs++; + if (!m.isRead()) + nunreadmsgs++; + } + } + + // These *should* be in sync, but if they're not, correct them. +/* + Assert.Assertion(nmsgs == this.total_message_count); + Assert.Assertion(nvalidmsgs == this.undeleted_message_count); + Assert.Assertion(nunreadmsgs == this.unread_message_count); + Assert.Assertion(deletedbytes == this.deleted_message_bytes); +*/ + this.total_message_count = nmsgs; + this.undeleted_message_count = nvalidmsgs; + this.unread_message_count = nunreadmsgs; + this.deleted_message_bytes = deletedbytes; + + + ByteStringTable string_table = folder.getStringTable(); + MessageIDTable id_table = folder.getMessageIDTable(); + + int string_table_length = string_table.size(); + int id_table_length = id_table.size(); + + out.write(MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER); + writeUnsignedInteger(out, MailSummaryFileFactory.VERSION_GRENDEL); + + writeUnsignedInteger(out, this.file_size); + writeUnsignedInteger(out, (this.file_date / 1000L)); + writeUnsignedInteger(out, this.file_size); // parsed_thru + writeUnsignedInteger(out, nmsgs); + writeUnsignedInteger(out, nvalidmsgs); + writeUnsignedInteger(out, nunreadmsgs); + writeUnsignedInteger(out, deletedbytes); + + writeUnsignedInteger(out, string_table_length + 1); + out.write(0); // first elt in table is a null string, for simplicity + for (int i = 0; i < string_table_length; i++) { + ByteString s = (ByteString) string_table.getObject(i); + out.write(s.toBytes()); + out.write(0); + } + + writeUnsignedInteger(out, id_table_length); + for (int i = 0; i < id_table_length; i++) { + MessageID id = (MessageID) id_table.getObject(i); + long h = id.hash; + writeUnsignedInteger(out, h >> 32); + writeUnsignedInteger(out, h & 0xFFFFFFFF); + } + + for (int i = 0; i < nmsgs; i++) { + + BerkeleyMessage m = (BerkeleyMessage) msgs.elementAt(i); + int author_name = m.author_name + 1; + int recipient_name = m.recipient_name + 1; + int subject = m.subject + 1; + + if (string_table_length <= 0xFFFF) { + writeUnsignedShort(out, author_name); + writeUnsignedShort(out, recipient_name); + writeUnsignedShort(out, subject); + } else { + writeUnsignedInteger(out, author_name); + writeUnsignedInteger(out, recipient_name); + writeUnsignedInteger(out, subject); + } + + writeUnsignedInteger(out, (m.sentDate / 1000L)); + writeUnsignedInteger(out, m.internalFlagsToMozillaFlags(m.flags)); + writeUnsignedInteger(out, m.getStorageFolderIndex()); + writeUnsignedInteger(out, m.getSize()); + + if (id_table_length <= 0xFFFF) { + writeUnsignedShort(out, m.message_id); + } else { + writeUnsignedInteger(out, m.message_id); + } + + int refs[] = m.references; + if (refs == null) { + writeUnsignedShort(out, 0); + } else { + int rlength = refs.length; + writeUnsignedShort(out, rlength); + if (id_table_length <= 0xFFFF) { + for (int j = 0; j < rlength; j++) + writeUnsignedShort(out, refs[j]); + } else { + for (int j = 0; j < rlength; j++) + writeUnsignedInteger(out, refs[j]); + } + } + } + } + + /** Called when the folder's disk file has been appended to by this + program. (As opposed to, an unexpected, unknown change by some other + program.) This overwrites certain fields in a Grendel-format summary + file. Those fields are: +

    +
  • FOLDER_SIZE +
  • FOLDER_DATE +
  • NMSGS +
  • NVALIDMSGS +
  • NUNREADMSGS +
  • DELETED_BYTES +
+ */ + synchronized void updateSummaryFile() throws IOException { + super.updateSummaryFile(); + + RandomAccessFile io = null; + try { + io = new RandomAccessFile(folder.summaryFileName(), "rw"); + boolean ok = true; + + // First, ensure the file is in the format we expect it to be in. + // + for (int i = 0; + i < MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER.length; + i++) + if (io.read() != MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER[i]) { + ok = false; + break; + } + + if (ok && (readUnsignedInteger(io) != + MailSummaryFileFactory.VERSION_GRENDEL)) + ok = false; + + if (ok) { + // We are now positioned just before the FOLDER_SIZE and FOLDER_DATE + // slots. Overwrite them with the current size/date of the file. + // (Note that super.updateSummaryFile() called setFolderDateAndSize().) + writeUnsignedInteger(io, this.file_size); + writeUnsignedInteger(io, (this.file_date / 1000L)); + + // Skip over the PARSED_THRU token + io.seek(io.getFilePointer() + 4); + + // Now overwrite the NMSGS, NVALIDMSGS and NUNREADMSGS tokens. + writeUnsignedInteger(io, this.total_message_count); + writeUnsignedInteger(io, this.undeleted_message_count); + writeUnsignedInteger(io, this.unread_message_count); + writeUnsignedInteger(io, this.deleted_message_bytes); + } + + } finally { + if (io != null) + io.close(); + } + } +} diff --git a/mozilla/grendel/storage/Makefile b/mozilla/grendel/storage/Makefile new file mode 100644 index 00000000000..722071317d5 --- /dev/null +++ b/mozilla/grendel/storage/Makefile @@ -0,0 +1,78 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SUBDIRS= \ + addressparser \ + intertwingle \ + $(NULL) + +SRCS= \ + BerkeleyFolder.java \ + BerkeleyMessage.java \ + BerkeleyStore.java \ + ByteString.java \ + ByteStringTable.java \ + Constants.java \ + DotTerminatedInputStream.java \ + FilterFolder.java \ + FilterFolderFactory.java \ + FolderBase.java \ + FolderExtra.java \ + FolderExtraFactory.java \ + MailDrop.java \ + MailDropException.java \ + MailSummaryFile.java \ + MailSummaryFileCheddar.java \ + MailSummaryFileFactory.java \ + MailSummaryFileGrendel.java \ + MasterBase.java \ + MessageBase.java \ + MessageExtra.java \ + MessageExtraFactory.java \ + MessageExtraWrapper.java \ + MessageID.java \ + MessageIDTable.java \ + MessageReadOnly.java \ + NNTPConnection.java \ + NNTPException.java \ + NewsFolder.java \ + NewsFolderRoot.java \ + NewsMessage.java \ + NewsRC.java \ + NewsRCLine.java \ + NewsSet.java \ + NewsStore.java \ + Obarray.java \ + PackedIntArray.java \ + ParseBerkeleyFolder.java \ + ParseBerkeleyFolderAndExpunge.java \ + ParseBerkeleyFolderWithSummary.java \ + PartialFileInputStream.java \ + PopFolder.java \ + PopMailDrop.java \ + PopMessage.java \ + PopStore.java \ + SearchResultsFolderFactory.java \ + TestBerkeley.java \ + UnixDotLock.java \ + $(NULL) + +# SelfTest.java \ + + +include ../rules.mk diff --git a/mozilla/grendel/storage/MasterBase.java b/mozilla/grendel/storage/MasterBase.java new file mode 100644 index 00000000000..871b92b7a12 --- /dev/null +++ b/mozilla/grendel/storage/MasterBase.java @@ -0,0 +1,95 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 27 Aug 1997. + */ + +package grendel.storage; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.Enumeration; +import java.util.Vector; + +import javax.mail.Folder; + +/** This whole thing is obsolete. I'm just keeping it around until I figure + out how to really do maildrops. */ + + +class MasterBase { + Vector fFolders = null; + Vector fMailDrops = null; + + MasterBase() { + // Do the timebomb. +// java.util.Date before = new java.util.Date(97, 9, 1, 0, 0); +// java.util.Date now = new java.util.Date(); +// java.util.Date then = new java.util.Date(97, 11, 25, 12, 00); +// if (now.before(before) || now.after(then)) { +// System.err.println("This software has expired"); +// System.exit(-1); +// } + } + + + public Enumeration getMailDrops() { +// if (fMailDrops == null) { +// synchronized(this) { +// if (fMailDrops == null) { +// fMailDrops = new Vector(); +// PopMailDrop drop = new PopMailDrop(); +// fMailDrops.addElement(drop); +// Preferences prefs = PreferencesFactory.Get(); +// drop.setHost(prefs.getString("pop.host", "mail")); +// drop.setUser(prefs.getString("pop.user", +// System.getProperties().getProperty("user.name"))); +// drop.setPassword(prefs.getString("pop.password", null)); +// +// for (Enumeration e = getFolders() ; e.hasMoreElements() ; ) { +// Folder f = (Folder) e.nextElement(); +// if (f.getName().equalsIgnoreCase("inbox")) { +// drop.setDestinationFolder(f); +// break; +// } +// } +// +// File popstate; +// if (Constants.ISUNIX) { +// popstate = new File(Constants.HOMEDIR, ".popstate"); +// // ### This is the wrong place; should be in .netscape dir or +// // some such. +// } else { +// popstate = new File(prefs.getFile("mail.directory", null), +// "popstate.dat"); +// } +// drop.setPopStateFile(popstate); +// drop.setLeaveMessagesOnServer +// (prefs.getBoolean("pop.leaveMailOnServer", true)); +// } +// } +// } + return fMailDrops.elements(); + } + +} + + diff --git a/mozilla/grendel/storage/MessageBase.java b/mozilla/grendel/storage/MessageBase.java new file mode 100644 index 00000000000..ebd2149e038 --- /dev/null +++ b/mozilla/grendel/storage/MessageBase.java @@ -0,0 +1,727 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + + +package grendel.storage; + +import calypso.util.NetworkDate; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.DateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Vector; + +import javax.activation.DataHandler; +import javax.mail.Address; +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import javax.mail.Multipart; +import javax.mail.event.MessageChangedEvent; +import javax.mail.IllegalWriteException; +import javax.mail.Part; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.InternetHeaders; + + +abstract class MessageBase extends MessageReadOnly implements MessageExtra { + + /* ********************************************************** + Class variables + ********************************************************** + */ + + + /* Don't confuse these flags with the values used in X-Mozilla-Status + headers: they occupy a different space, and range. There is a + difference between the in-memory format, and the storage format. + This is partially because there are potentially in-memory flags + that don't get saved to disk. + + Parsing of the X-Mozilla-Status header (and conversion to this form) + happens in the BerkeleyMessage class. + + There can be up to 64 flag-bits (the in-memory flags.) + There can only be up to 16 X-Mozilla-Status flags (the persistent flags.) + */ + public static final long FLAG_READ = 0x00000001; + public static final long FLAG_REPLIED = 0x00000002; + public static final long FLAG_FORWARDED = 0x00000004; + public static final long FLAG_MARKED = 0x00000008; + public static final long FLAG_DELETED = 0x00000010; + public static final long FLAG_HAS_RE = 0x00000020; // Subject + public static final long FLAG_SIGNED = 0x00000040; // S/MIME + public static final long FLAG_ENCRYPTED = 0x00000080; // S/MIME + public static final long FLAG_SMTP_AUTH = 0x00000100; // Gag + public static final long FLAG_PARTIAL = 0x00000200; // POP3 + public static final long FLAG_QUEUED = 0x00000400; // Offline + + + // The mapping between our internal flag bits and javamail's flag strings. + // The editable entry defines whether this flag can be changed from the + // outside. + + static class FlagMap { + long flag; + Flags.Flag builtin; + String non_builtin; + boolean editable; + FlagMap(long f, String n, boolean e) { + flag = f; + non_builtin = n; + editable = e; + } + FlagMap(long f, Flags.Flag n, boolean e) { + flag = f; + builtin = n; + editable = e; + } + }; + static FlagMap[] FLAGDEFS = new FlagMap[11]; + static { + int i = 0; + FLAGDEFS[i++] = new FlagMap(FLAG_READ, Flags.Flag.SEEN, true); + FLAGDEFS[i++] = new FlagMap(FLAG_REPLIED, Flags.Flag.ANSWERED, true); + FLAGDEFS[i++] = new FlagMap(FLAG_FORWARDED, "Forwarded", true); + FLAGDEFS[i++] = new FlagMap(FLAG_MARKED, "Marked", true); + FLAGDEFS[i++] = new FlagMap(FLAG_DELETED, Flags.Flag.DELETED, true); + FLAGDEFS[i++] = new FlagMap(FLAG_HAS_RE, "HasRe", false); + FLAGDEFS[i++] = new FlagMap(FLAG_SIGNED, "Signed", false); + FLAGDEFS[i++] = new FlagMap(FLAG_ENCRYPTED, "Encrypted", false); + FLAGDEFS[i++] = new FlagMap(FLAG_SMTP_AUTH, "SmtpAuth", false); + FLAGDEFS[i++] = new FlagMap(FLAG_PARTIAL, "Partial", false); + FLAGDEFS[i++] = new FlagMap(FLAG_QUEUED, "Queued", false); + Assert.Assertion(i == FLAGDEFS.length); + }; + + // This bit, when set, means that some change has been made to the flags + // which should be written out to the X-Mozilla-Status header in the + // folder's disk file. This is done so that we can lazily update the + // file, rather than writing it every time the flags change. + public static final long FLAG_DIRTY = 0x00000800; + + + /* Some string-constants in bytebuf form that we use for interrogating + headers during parsing. + */ + protected static final ByteBuf FROM = new ByteBuf("from"); + protected static final ByteBuf TO = new ByteBuf("to"); + protected static final ByteBuf CC = new ByteBuf("cc"); + protected static final ByteBuf NEWSGROUPS = new ByteBuf("newsgroups"); + protected static final ByteBuf DATE = new ByteBuf("date"); + protected static final ByteBuf SUBJECT = new ByteBuf("subject"); + protected static final ByteBuf MESSAGE_ID = new ByteBuf("message-id"); + protected static final ByteBuf REFERENCES = new ByteBuf("references"); + protected static final ByteBuf IN_REPLY_TO = new ByteBuf("in-reply-to"); + + /* For simplifiedDate() */ + private static Date scratch_date = new Date(); + private static DateFormat date_format = null; + + + /* *********************************************************** + Instance variables -- add them sparingly, memory is scarce. + *********************************************************** + */ + long flags; // see `FLAG_READ', etc, above. + long sentDate; // milliseconds since the Epoch. + + // These slots are ints but really represent ByteStrings: they are indexes + // into a ByteStringTable. It's quite likely that we could live with these + // being of type `short' instead of `int'. Should memory usage be a problem, + // we should consider that. + // + int author_name; // name (not address) of the From or Sender. + int recipient_name; // name of first To, or CC, or newsgroup. + int subject; // subject minus "Re:" (see `FLAG_HAS_RE'). + + // These slots are as above, but represent MessageID objects instead of + // ByteString objects. These also could stand to be of type `short'. + // + int message_id; // will never be -1 (meaning null). + int references[]; // may be null; else length > 0. + + + /* ********************************************************** + Methods + ********************************************************** + */ + + MessageBase(FolderBase f) { + super(); + this.folder = f; + } + + MessageBase(FolderBase f, InternetHeaders h) { + this(f); + initialize(f, h); + } + + MessageBase(FolderBase f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + ByteBuf id, + ByteBuf refs[]) { + this(f); + ByteStringTable string_table = f.getStringTable(); + MessageIDTable id_table = f.getMessageIDTable(); + + if (id == null || id.length() == 0) { + // #### In previous versions, we did this by getting the MD5 hash + // #### of the whole header block. We should do that here too... + if (id == null) id = new ByteBuf(); + id.append(grendel.util.MessageIDGenerator.generate("missing-id")); + } + + this.folder = f; + this.flags = flags; + this.sentDate = date; + this.author_name = string_table.intern(author); + this.recipient_name = string_table.intern(recipient); + this.subject = string_table.intern(subj); + this.message_id = id_table.intern(id); + + if (refs == null || refs.length == 0) + this.references = null; + else { + int L = refs.length; + references = new int[L]; + for (int i = 0; i < L; i++) + references[i] = id_table.intern(refs[i]); + } + } + + MessageBase(FolderBase f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + MessageID id, + MessageID refs[]) { + this(f); + ByteStringTable string_table = f.getStringTable(); + MessageIDTable id_table = f.getMessageIDTable(); + + if (id != null) { + this.message_id = id_table.intern(id); + } else { + // #### In previous versions, we did this by getting the MD5 hash + // #### of the whole header block. We should do that here too... + ByteBuf b = + new ByteBuf(grendel.util.MessageIDGenerator.generate("missing-id")); + this.message_id = id_table.intern(b); + } + + this.folder = f; + this.flags = flags; + this.sentDate = date; + this.author_name = string_table.intern(author); + this.recipient_name = string_table.intern(recipient); + this.subject = string_table.intern(subj); + + if (refs == null || refs.length == 0) + this.references = null; + else { + int L = refs.length; + references = new int[L]; + for (int i = 0; i < L; i++) + references[i] = id_table.intern(refs[i]); + } + } + + protected void initialize(Folder f, InternetHeaders h) { + folder = f; + FolderBase fb = (FolderBase) f; + ByteStringTable string_table = fb.getStringTable(); + MessageIDTable id_table = fb.getMessageIDTable(); + String hh[]; + + hh = h.getHeader("From"); + author_name = (hh == null || hh.length == 0 ? -1 : + string_table.intern(hh[0].trim())); + + // #### need an address parser here... + recipient_name = -1; +/* + hh = h.getHeader("To"); + // #### deal with multiple to fields + recipient_name = (hh == null || hh.length == 0 ? -1 : + string_table.intern(hh[0].trim())); + + if (recipient == -1) { + // #### deal with multiple cc fields + hh = h.getHeader("CC"); + recipient_name = (hh == null || hh.length == 0 ? -1 : + string_table.intern(hh[0].trim())); + } + + if (recipient == -1) { + hh = h.getHeader("Newsgroups"); + recipient_name = (hh == null || hh.length == 0 ? -1 : + string_table.intern(hh[0].trim())); + } + */ + + hh = h.getHeader("Subject"); + if (hh != null && hh.length != 0) { + // Much of this code is duplicated in MessageExtraFactory. Sigh. ### + ByteBuf value = new ByteBuf(hh[0]); + if (value.length() > 2 && + (value.byteAt(0) == 'r' || value.byteAt(0) == 'R') && + (value.byteAt(1) == 'e' || value.byteAt(1) == 'E')) { + byte c = value.byteAt(2); + if (c == ':') { + value.remove(0, 3); // Skip over "Re:" + value.trim(); // Remove any whitespace after colon + flags |= FLAG_HAS_RE; // yes, we found it. + } else if (c == '[' || c == '(') { + int i = 3; // skip over "Re[" or "Re(" + + // Skip forward over digits after the "[" or "(". + int length = value.length(); + while (i < length && + value.byteAt(i) >= '0' && + value.byteAt(i) <= '9') { + i++; + } + // Now ensure that the following thing is "]:" or "):" + // Only if it is do we treat this all as a "Re"-ish thing. + if (i < (length-1) && + (value.byteAt(i) == ']' || + value.byteAt(i) == ')') && + value.byteAt(i+1) == ':') { + value.remove(0, i+2); // Skip the whole thing. + value.trim(); // Remove any whitespace after colon + flags |= FLAG_HAS_RE; // yes, we found it. + } + } + } + subject = string_table.intern(value); + } + + hh = h.getHeader("Date"); + if (hh != null && hh.length != 0) + sentDate = NetworkDate.parseLong(new ByteBuf(hh[0]), true); + + hh = h.getHeader("Message-ID"); + if (hh != null && hh.length != 0) { + ByteBuf value = new ByteBuf(hh[0]); + value.trim(); + int length = value.length(); + if (length > 0 && + value.byteAt(0) == '<' && + value.byteAt(length-1) == '>') { + value.remove(length-1, length); + value.remove(0, 1); + } + message_id = id_table.intern(value.trim()); + } + + // There must be a message ID on every message. + if (message_id == -1) { + // #### In previous versions, we did this by getting the MD5 hash + // #### of the whole header block. We should do that here too... + String id = grendel.util.MessageIDGenerator.generate("missing-id"); + message_id = id_table.intern(new ByteBuf(id)); + } + + hh = h.getHeader("References"); + if (hh != null && hh.length != 0) { + ByteBuf value = new ByteBuf(hh[0]); + references = internReferences(id_table, value.trim()); + } + + // Only examine the In-Reply-To header if there is no References header. + if (references == null) { + + hh = h.getHeader("In-Reply-To"); + if (hh != null && hh.length != 0) { + ByteBuf value = new ByteBuf(hh[0]); + references = internReferences(id_table, value.trim()); + } + } + } + + + // Ported from akbar's "msg_intern_references", mailsum.c. + protected int[] internReferences(MessageIDTable id_table, ByteBuf refs) { + byte data[] = refs.toBytes(); + int length = refs.length(); + int s; + int n_refs = 0; + for (s=0 ; s') { + n_refs++; + } + } + + if (n_refs == 0) + return null; + + int result[] = new int[n_refs]; + int start = 0; + int cur = 0; + + s = 0; + while (s < length) { + + // The old way was to skip over whitespace, then skip an optional "<". + // The new way is to skip over everything up to and including "<". + // This lets us deal better with In-Reply-To headers, in addition to + // References headers: we can cope with + // + // In-Reply-To: NAME's message of TIME + // In-Reply-To: article of TIME + // In-Reply-To: , from NAME + // + // In the latter case, we're going to fuck up and think that both + // of them are IDs, but headers like appear to be extremely rare. + // In a survey of 22,950 mail messages with In-Reply-To headers: + // + // 18,396 had at least one occurence of <>-bracketed text. + // 4,554 had no <>-bracketed text at all (just names and dates.) + // 714 contained one <>-bracketed addr-spec and no message IDs. + // 4 contained multiple message IDs. + // 1 contained one message ID and one <>-bracketed addr-spec. + // + // The most common forms of In-Reply-To seem to be + // + // 31% NAME's message of TIME + // 22% + // 9% from NAME at "TIME" + // 8% USER's message of TIME + // 7% USER's message of TIME + // 6% Your message of "TIME" + // 17% hundreds of other variants (average 0.4% each?) + // + // jwz, 17 Sep 1997. + // + while (start < length && data[start] != '<') + start++; + + // skip over consecutive "<" -- I've seen "<>". + while (start < length && data[start] == '<') + start++; + + s = start; + while (s < length && data[s] != '>') + s++; + + if (s > start && + s < length && + data[s] == '>') { + result[cur++] = id_table.intern(data, start, s - start); + start = s + 1; + + // skip over consecutive ">" -- I've seen "<>". + while (start < length && data[start] == '>') + start++; + } else { + s++; + } + } + + if (cur != n_refs) { + // Whoops! Something's funny about this line, and the number of + // ">" characters didn't equal the number of IDs we extracted. + // This will be an extremely rare situation, so when it happens, + // just make a new array. + if (cur == 0) + result = null; + else { + int r2[] = new int[cur]; + System.arraycopy(result, 0, r2, 0, cur); + result = r2; + } + } + + return result; + } + + + + public Object getMessageID() { + MessageIDTable id_table = ((FolderBase)folder).getMessageIDTable(); + return (MessageID) id_table.getObject(message_id); + } + + public String getSubject() { + String result = simplifiedSubject(); + if (subjectIsReply()) result = "Re: " + result; + return result; + } + + public String getAuthor() { + ByteStringTable string_table = ((FolderBase)folder).getStringTable(); + ByteString a = (ByteString) + string_table.getObject(author_name); + if (a == null) return ""; + else return a.toString(); + } + + public String getRecipient() { + ByteStringTable string_table = ((FolderBase)folder).getStringTable(); + ByteString r = (ByteString) string_table.getObject(recipient_name); + if (r == null) return ""; + else return r.toString(); + } + + public Object[] messageThreadReferences() { + if (references == null) return null; + int count = references.length; + if (count == 0) return null; + + // Note: this conses. + MessageIDTable id_table = ((FolderBase)folder).getMessageIDTable(); + Object result[] = new Object[count]; + for (int i = 0; i < result.length; i++) + result[i] = id_table.getObject(references[i]); + + return result; + } + + public long getSentDateAsLong() { + return sentDate; + } + + public Date getSentDate() { + return new Date(sentDate); + } + + public Date getReceivedDate() { + // ### We don't currently remember this info. Should we? + return getSentDate(); + } + + + public Folder getFolder() { + return folder; + } + + + // #### Warning, this is untested -- the "javax.mail.Flags" class changed + // around a bunch since the last time we tried to use this code, and I had + // to beat on this. + + public Flags getFlags() { + Flags result = new Flags(); + for (int i=0 ; i, 22 Oct 1997. + */ + +package grendel.storage; + +import java.io.InputStream; +import javax.mail.MessagingException; + +/** These are extra interfaces that our message objects implement that are not + implemented as part of the standard Message class. If you want to call + any of these, and you just have a base Message object, use + MessageExtraFactory to find or create the MessageExtra object for you. */ + +public interface MessageExtra { + /** The name of the author of this message (not his email address). */ + public String getAuthor() throws MessagingException; + + /** The name of the recipient of this message (not his email address). */ + public String getRecipient() throws MessagingException; + + /** The subject, minus any "Re:" part. */ + public String simplifiedSubject() throws MessagingException; + + /** Whether the subject has a "Re:" part." */ + public boolean subjectIsReply() throws MessagingException; + + /** A short rendition of the sent date. */ + public String simplifiedDate() throws MessagingException; + + /** A unique object representing the message-id for this message. */ + public Object getMessageID() throws MessagingException; + + /** A list of the above messageid objects that this message has references + to. */ + public Object[] messageThreadReferences() throws MessagingException; + + public boolean isRead() throws MessagingException; + public void setIsRead(boolean value) throws MessagingException; + public boolean isReplied() throws MessagingException; + public void setReplied(boolean value) throws MessagingException; + public boolean isForwarded() throws MessagingException; + public void setForwarded(boolean value) throws MessagingException; + public boolean isFlagged() throws MessagingException; + public void setFlagged(boolean value) throws MessagingException; + public boolean isDeleted() throws MessagingException; + public void setDeleted(boolean value) throws MessagingException; + + /** Gets the input stream for the message, in RFC822 format: a bunch of + headers, and then a blank line, and then the message itself. */ + public InputStream getInputStreamWithHeaders() throws MessagingException; + +}; + diff --git a/mozilla/grendel/storage/MessageExtraFactory.java b/mozilla/grendel/storage/MessageExtraFactory.java new file mode 100644 index 00000000000..42a5baa974a --- /dev/null +++ b/mozilla/grendel/storage/MessageExtraFactory.java @@ -0,0 +1,37 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 27 Oct 1997. + */ + +package grendel.storage; + +import javax.mail.Message; + +/** If you want to call any of the MessageExtra methods on a Message, you + should get the MessageExtra object using this call. Some Message objects + will implement that interface directly; the rest need to have them + painfully computed. */ + +public class MessageExtraFactory { + static public MessageExtra Get(Message m) { + if (m instanceof MessageExtra) return (MessageExtra) m; + return new MessageExtraWrapper(m); + } +} + diff --git a/mozilla/grendel/storage/MessageExtraWrapper.java b/mozilla/grendel/storage/MessageExtraWrapper.java new file mode 100644 index 00000000000..06b2e8ba316 --- /dev/null +++ b/mozilla/grendel/storage/MessageExtraWrapper.java @@ -0,0 +1,269 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 24 Nov 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import calypso.util.Assert; + +import grendel.storage.addressparser.RFC822Mailbox; +import grendel.storage.addressparser.RFC822MailboxList; + +import java.io.InputStream; +import java.io.IOException; +import java.io.SequenceInputStream; +import java.io.StringBufferInputStream; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.util.Enumeration; + +import javax.mail.Flags; +import javax.mail.Header; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; +import javax.mail.internet.MimeUtility; + +class MessageExtraWrapper implements MessageExtra { + + Message m; + protected MessageExtraWrapper(Message mess) { + m = mess; + } + + protected String getSingleAddressName(String headername) { + String[] list = null; + try { + list = m.getHeader("From"); + } catch (MessagingException e) { + } + if (list == null || list.length < 1) return null; + RFC822Mailbox boxes[] = new RFC822MailboxList(list[0]).getMailboxArray(); + if (boxes == null || boxes.length < 1) return null; + String result = boxes[0].getName(); + if (result == null || result.length() == 0) result = boxes[0].getAddress(); + return result; + } + + + + public String getAuthor() { + return getSingleAddressName("From"); + } + + public String getRecipient() { + return getSingleAddressName("To"); + } + + + // Removes leading "Re:" or similar from the given StringBuffer. Returns + // true if it found such a string to remove; false otherwise. + protected boolean stripRe(StringBuffer buf) { + // Much of this code is duplicated in MessageBase. Sigh. ### + if (buf == null) return false; + int numToTrim = 0; + int length = buf.length(); + if (length > 2 && + (buf.charAt(0) == 'r' || buf.charAt(0) == 'R') && + (buf.charAt(1) == 'e' || buf.charAt(1) == 'E')) { + char c = buf.charAt(2); + if (c == ':') { + numToTrim = 3; // Skip over "Re:" + } else if (c == '[' || c == '(') { + int i = 3; // skip over "Re[" or "Re(" + + // Skip forward over digits after the "[" or "(". + while (i < length && + buf.charAt(i) >= '0' && + buf.charAt(i) <= '9') { + i++; + } + // Now ensure that the following thing is "]:" or "):" + // Only if it is do we treat this all as a "Re"-ish thing. + if (i < (length-1) && + (buf.charAt(i) == ']' || + buf.charAt(i) == ')') && + buf.charAt(i+1) == ':') { + numToTrim = i+2; // Skip the whole thing. + } + } + } + if (numToTrim > 0) { + int i = numToTrim; + while (i < length - 1 && Character.isWhitespace(buf.charAt(i))) { + i++; + } + for (int j=i ; j, 19 Sep 1997. + */ + +package grendel.storage; + +/** This class represents a Message-ID. + It does not contain the ID itself: there is no way to reconstruct the + original string from this object; however, it contains a hash of that + string which should be sufficiently unique so that the odds of a + collision are 1 in 2^32 (since it is a 64-bit hash.) +

+ We store the hash instead of the original string because it takes up + significantly less memory, and in order to do threading, we + tend to need a huge number of Message-IDs in memory at once. + + @see MessageIDTable + */ + +class MessageID { + + long hash; // not protected for the benfit of a MessageIDTable kludge. + +// String debug_id_string; + + + /** Constructs a MessageID object with a null hash. */ + MessageID() { + } + + /** Constructs a MessageID object with the given hash. */ + MessageID(long hash) { + this.hash = hash; + } + + /** Constructs a MessageID object for the given sub-sequence of bytes. + A pointer to the bytes is not retained. + */ + MessageID(byte bytes[], int start, int length) { + hash = hashBytes(bytes, start, length); +// debug_id_string = new String(bytes, start, length); + } + + MessageID(String chars) { + byte bytes[] = chars.getBytes(); + hash = hashBytes(bytes, 0, bytes.length); +// debug_id_string = chars; + } + + /** Converts it to a string, for debugging. */ + public String toString() { + return "<" + this.getClass() + " hash=" + Long.toHexString(hash) + +// " string=" + debug_id_string + + ">"; + } + + public int hashCode() { + // Note that this is a 32-bit hash, not the 64-bit internal hash; + // it's good enough for hash tables, but not for equality checks. + return (int) ((hash >> 32) ^ (hash & 0xFFFFFFFF)); + } + + public boolean equals(Object x) { + if (x instanceof MessageID) + return (hash == ((MessageID) x).hash); + else + return false; + } + + /** Computes a 64-bit hash of the given subsequence of bytes. +

+ Note: this hash value is written out to mail summary files by the + MailSummaryFileGrendel class. If the hash algorithm is changed, + it will invalidate all existing mail summary files, and necessitate + a change of the file format version number. + @see MailSummaryFileGrendel + */ + protected long hashBytes(byte bytes[], int start, int length) { + long h = 0; + int end = start + length; + int i = start; + + // Just in case, don't count a leading < or trailing > in the hash. + // All callers should strip those first, but be safe... + while (i < end && (bytes[i] == '<' || bytes[i] <= ' ')) i++; + while (end > i && (bytes[end-1] == '>' || bytes[end-1] <= ' ')) end--; + + while (i < end) { + long hh = 0; + for (int j = 0; i < end && j < 16; j++) { + hh = (hh * 37) + bytes[i++]; // the guts of String.hashCode(). + } + h = (h << 8) ^ hh; // fill in high-high-order bits too. + } + return h; + } +} diff --git a/mozilla/grendel/storage/MessageIDTable.java b/mozilla/grendel/storage/MessageIDTable.java new file mode 100644 index 00000000000..6b1dfbd9ffc --- /dev/null +++ b/mozilla/grendel/storage/MessageIDTable.java @@ -0,0 +1,84 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 19 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import java.util.Hashtable; + +/** This is a mechanism for uniqueifying MessageID objects. + To save memory, we want all `equal' IDs to also be `=='. + This table is how we accomplish that. See intern(). + + @see MessageID + @see ByteStringTable + */ + +class MessageIDTable extends Obarray { + + /** This is the way we check to see if an object is in the table, or rather, + if an object constructed from that sequence of bytes would be in the + table. This is a kludge to get around the lack of function pointers... + */ + protected MessageID dummy; + + MessageIDTable(int default_size) { + super(default_size); + dummy = new MessageID(); + } + + MessageIDTable() { + this(100); + } + + /** Check whether there is a MessageID representing the given subsequence + of bytes in the table already. Returns null, or an object from the + table (an Integer object.) + */ + synchronized protected Object checkHash(byte bytes[], + int start, int length) { + dummy.hash = dummy.hashBytes(bytes, start, length); + return hashtable.get(dummy); + } + + /** Creates a new MessageID object (which will then be interned.) */ + protected Object newInternable(byte bytes[], int start, int length) { + return new MessageID(bytes, start, length); + } + + /** Ensures that an equivalent MessageID is present in the table, adding + it if necessary. Returns the index in the table of that object. + The given MessageID object may be stored in the table, + without being copied. + */ + public int intern(MessageID id) { + Object hashed = hashtable.get(id); + if (hashed != null) { + return ((Integer) hashed).intValue(); + } else { + ensureCapacity(count+1); + hashtable.put(id, new Integer(count)); + array[count] = id; + count++; + return count-1; + } + } +} diff --git a/mozilla/grendel/storage/MessageReadOnly.java b/mozilla/grendel/storage/MessageReadOnly.java new file mode 100644 index 00000000000..6b9c00e17da --- /dev/null +++ b/mozilla/grendel/storage/MessageReadOnly.java @@ -0,0 +1,301 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 24 Nov 1997. + */ + +package grendel.storage; + +import calypso.util.NetworkDate; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +import java.io.InputStream; +import java.io.IOException; +import java.util.Date; +import java.util.Enumeration; +import java.util.Vector; + +import javax.activation.DataHandler; + +import javax.mail.Address; +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.IllegalWriteException; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import javax.mail.Multipart; +import javax.mail.Part; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.InternetHeaders; + +/** This implements those methods on Message which are commonly used in all + of our read-only message implementations. It also provides an + implementation for the header-reading routines, all of which assume that + the subclass implements getHeadersObj, a method to get an InternetHeaders + object for this message. */ + +abstract class MessageReadOnly extends Message { + + MessageReadOnly() { + super(); + } + + MessageReadOnly(Folder f, int num) { + super(f, num); + } + + protected void readonly() throws MessagingException { + throw new IllegalWriteException("Can't edit this message."); + } + + public void addFrom(Address addresses[]) throws MessagingException { + readonly(); + } + + public void setSubject(String s)throws MessagingException { + readonly(); + } + + public void setRecipients(Message.RecipientType type, Address addresses[]) + throws MessagingException + { + readonly(); + } + + public void addRecipients(Message.RecipientType type, Address addresses[]) + throws MessagingException + { + readonly(); + } + + public void setFrom() throws MessagingException { + readonly(); + } + + public void setFrom(Address address) throws MessagingException { + readonly(); + } + + public void setSentDate(Date d) throws MessagingException { + readonly(); + } + + public String getContentType() { + return "message/rfc822"; + } + + public String getDisposition() { + return null; + } + + public String getDescription() { + return null; + } + + public String getFileName() { + return null; + } + + public void setDisposition(String d) throws MessagingException { + readonly(); + } + + public void setDescription(String d) throws MessagingException { + readonly(); + } + + public void setFileName(String f) throws MessagingException { + readonly(); + } + + public void setDataHandler(DataHandler d) { + Assert.NotYetImplemented("Can't set a datahandler on our readonly messages!"); + } + + public void setText(String s) throws MessagingException { + readonly(); + } + + public void setContent(String s) throws MessagingException { + readonly(); + } + + public void setContent(Object o, String s) { + Assert.NotYetImplemented("Can't set content on our readonly messages!"); + } + + public void setContent(Multipart m) { + Assert.NotYetImplemented("Can't set content on our readonly messages!"); + } + + public void addHeader(String s1, String s2) throws MessagingException { + readonly(); + } + + public void setHeader(String s1, String s2) throws MessagingException { + readonly(); + } + + public void removeHeader(String s) throws MessagingException { + readonly(); + } + + + /** Get the InternetHeaders object. */ + abstract protected InternetHeaders getHeadersObj() throws MessagingException; + + public String[] getHeader(String name) throws MessagingException { + return getHeadersObj().getHeader(name); + } + + protected String getOneHeader(String name) throws MessagingException { + String[] list = getHeader(name); + if (list == null || list.length < 1) return null; + return list[0]; + } + + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + String[] list; + if (type.equals(Message.RecipientType.TO)) { + list = getHeader("To"); + } else if (type.equals(Message.RecipientType.CC)) { + list = getHeader("Cc"); + } else if (type.equals(Message.RecipientType.BCC)) { + list = getHeader("Bcc"); + } else { + throw new IllegalArgumentException("Bad type " + type); + } + if (list == null || list.length == 0) { + return new Address[0]; + } + if (list.length == 1) { + return InternetAddress.parse(list[0]); + } + Vector result = new Vector(); + for (int i=0 ; i= 0) { + if (b1 == '\r') { + if (b2 == '\r') { + // Two CR's in a row; done. + break; + } + } else if (b1 == '\n') { + if (b2 == '\n') { + // Two LF's in a row; done. + break; + } else if (b2 == '\r') { + // We have a LFCR. It's gotta be the middle of a CRLFCRLF. So, + // we need to read one more character and then we're done. + b1 = b2; + b2 = in.read(); + if (b2 == '\n') break; + } + } + b1 = b2; + } + } catch (IOException e) { + throw new MessagingException("Can't skip headers in stream", e); + } + return in; + } + + public InputStream getInputStreamWithHeaders() throws MessagingException { + throw new MethodNotSupportedException("MessageBase.getInputStreamWithHeaders"); + + } +} diff --git a/mozilla/grendel/storage/NNTPConnection.java b/mozilla/grendel/storage/NNTPConnection.java new file mode 100644 index 00000000000..c7ccc63d22f --- /dev/null +++ b/mozilla/grendel/storage/NNTPConnection.java @@ -0,0 +1,910 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 20 Nov 1997. + */ + +package grendel.storage; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.IOException; + +import java.net.Socket; +import java.net.UnknownHostException; + +import java.text.DecimalFormat; + +import java.util.Enumeration; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.Date; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import java.util.NoSuchElementException; + +import grendel.storage.DotTerminatedInputStream; +import calypso.util.LineBufferingInputStream; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +import javax.mail.internet.InternetHeaders; + + +class NNTPConnection { + + static final boolean DEBUG = false; + + Socket socket = null; + LineBufferingInputStream input; + OutputStream output; + String selected_group = null; + String host = null; + int port = -1; + String user = null; + String pass = null; + + /* If we have returned to a caller a stream which points to the NNTP + connection (for example, for reading BODY or XOVER data) we hold on + to it here. If a caller tries to interact with the NNTP connection + before this stream has been drained, we fully drain it first. This + means two things: + + 1: if the caller gets a stream, then tries to do some other NNTP + thing while the stream is pending, all streaming-ness is shot, + since the second NNTP request will block waiting for all of + the stream data to come in and be buffered first. + + 2: if thread A is reading from a stream, and thread B tries to + do some short-running NNTP action, thread A will block, and + thread B will fill up the whole buffer A is waiting for + before doing its own action. + + There's no way around #1; within a thread, that's how things need + to work. But, in the #2 case, it might be nicer to have thread B + block until thread A had fully drained its stream. But not only + do I not know how to detect that situation, it's rife with other + problems too; what if thread A is dead, or reading really slowly, + or... + */ + protected NNTPDotTerminatedInputStream pending_dot_stream = null; + + static public final int DEFAULT_NNTP_PORT = 119; + + protected boolean hasSetGetExtension = false; + protected boolean hasOverExtension = false; + protected boolean hasXpatTextExtension = false; + protected boolean hasListSubscrExtension = false; + + NNTPConnection() { + } + + synchronized boolean connect(String host, int port, + String user, String password) + throws UnknownHostException, IOException { + + if (socket != null) { + close(); + } + + this.host = host; + this.port = port; + + if (port == -1) port = DEFAULT_NNTP_PORT; + + if (DEBUG) { + System.err.println("NNTP: connect(\"" + host + "\", " + port + ")"); + } + + socket = new Socket(host, port); + input = new LineBufferingInputStream(socket.getInputStream()); + output = socket.getOutputStream(); + + // This would be technically legal; by why tempt fate. + // input.setInputEOL(new ByteBuf("\r\n")); + + if (DEBUG) { + System.err.println("NNTP: connected."); + } + + int code = readResponse(null); // 200 foo NNRP ready (posting ok). + // #### check "posting allowed" code + + + // #### do async auth -- return false if it fails + // #### or throw AuthenticationFailedException? + this.user = user; + this.pass = password; + + // Do some initial setup. + MODE_READER(); + LIST_EXTENSIONS(); + return true; + } + + synchronized void close() { + if (DEBUG) { + System.err.println("NNTP: close()"); + } + + if (output != null) { // try to shut down cleanly; ignore errors. + try { + write("QUIT\r\n"); + } catch (IOException e) { + } + } + + user = null; + pass = null; + selected_group = null; + try { input.close(); } catch (IOException e) {} + try { output.close(); } catch (IOException e) {} + try { socket.close(); } catch (IOException e) {} + input = null; + output = null; + socket = null; + } + + protected synchronized void write(byte b[], int start, int length) + throws IOException { + + if (DEBUG) { + String s = "NNTP: ==> "; + for (int i = start; i < start+length; i++) + s += (b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1)); + System.err.print(s + "\n"); + } + + output.write(b, start, length); + } + + protected synchronized void write(String buf) throws IOException { + write(buf.getBytes(), 0, buf.length()); + } + + protected synchronized void readLine(ByteBuf into_buf) throws IOException { + + flushDotStream(); + + // Note that `input' is a LineBufferingInputStream, which means that + // `read' will only return chunks consisting of a single line (or + // max_bytes, whichever is smaller.) + // + into_buf.setLength(0); + into_buf.read(input, 10240); // NNTP spec limits lines to 1000 characters + + if (DEBUG) { + String s = "NNTP: <== "; + byte b[] = into_buf.toBytes(); + for (int i = 0; i < into_buf.length(); i++) + s += (b[i] == '\r' ? "\\r" : + b[i] == '\n' ? "\\n" : + b[i] == '\t' ? "\\t" : + b[i] == '\\' ? "\\\\" : + new String(b, i, 1)); + System.err.print(s + "\n"); + } + + // strip off trailing newlines (really, all ctl chars) + int i = into_buf.length(); + int j = i; + while (j > 0 && + (into_buf.byteAt(j-1) == '\r' || + into_buf.byteAt(j-1) == '\n')) + j--; + if (i != j) + into_buf.setLength(j); + } + + protected synchronized String readLine() throws IOException { + ByteBuf b = new ByteBuf(); + readLine(b); + return b.toString(); + } + + protected void flushDotStream() { + if (pending_dot_stream != null) { + synchronized (this) { + if (pending_dot_stream != null) { + if (DEBUG) + System.err.println("NNTP: Yikes! Reentrant call!\n" + + "NNTP: Buffering content of outstanding " + + "dot-terminated input stream..."); + // new Exception().printStackTrace(System.err); + pending_dot_stream.bufferUpEverything(); + // dotStreamFinished should now have been called. + Assert.Assertion(pending_dot_stream == null); + } + } + } + } + + protected synchronized NNTPDotTerminatedInputStream newDotStream() { + flushDotStream(); + Assert.Assertion(pending_dot_stream == null); + if (DEBUG) { + System.err.println("NNTP: returning dot-terminated input stream."); + // new Exception().printStackTrace(System.err); + } + pending_dot_stream = new NNTPDotTerminatedInputStream(this, input); + return pending_dot_stream; + } + + protected void checkString(String s) throws NNTPException { + int L = s.length(); + for (int i = 0; i < L; i++) + if (s.charAt(i) <= ' ') + throw new NNTPException("bad character in string: " + s); + } + + protected synchronized int readResponse(ByteBuf into_buf) throws IOException{ + ByteBuf b = (into_buf != null ? into_buf : new ByteBuf()); + readLine(b); + if (b.length() < 4) + throw new NNTPException("improper NNTP response: \"" + b + "\""); + + byte b0 = b.byteAt(0); + byte b1 = b.byteAt(1); + byte b2 = b.byteAt(2); + if (b0 != '1' && b0 != '2' && b0 != '3') + throw new NNTPException("NNTP error: \"" + b + "\""); + + return (((b0-'0') * 100) + ((b1-'0') * 10) + (b2-'0')); + } + + + protected synchronized void MODE_READER() throws IOException { + write("MODE READER\r\n"); + try { + readResponse(null); + } catch (NNTPException e) { + // ignore errors (probably meaning "mode reader not supported.") + } + } + + + /** Returns an array of Strings, the names of extensions supported by + this server. If no extensions are supported, returns null. + */ + protected synchronized String[] LIST_EXTENSIONS() throws IOException { + write("LIST EXTENSIONS\r\n"); + try { + readResponse(null); // 215 Extensions supported by server. + Vector v = new Vector(); + ByteBuf b = new ByteBuf(); + + hasSetGetExtension = false; + hasOverExtension = false; + hasXpatTextExtension = false; + hasListSubscrExtension = false; + + while (true) { // dot-terminated list of extensions + readLine(b); + if (b.equals(".")) + break; + else if (b.byteAt(0) == '.') + b.remove(0, 1); + if (b.equals("SETGET")) hasSetGetExtension = true; + else if (b.equals("OVER")) hasOverExtension = true; + else if (b.equals("XPATTEXT")) hasXpatTextExtension = true; + else if (b.equals("LISTSUBSCR")) hasListSubscrExtension = true; + // #### etc + } + + String ss[] = new String[v.size()]; + Enumeration e = v.elements(); + for (int i = 0; e.hasMoreElements(); i++) + ss[i] = (String) e.nextElement(); + return ss; + + } catch (NNTPException e) { + // ignore errors (probably meaning "list extensions not supported".) + return new String[0]; + } + } + + synchronized void LIST_SEARCHES() { + // #### if SEARCH + } + + synchronized void LIST_SRCHFIELDS() { + // #### if SEARCH + } + + synchronized String GET(String prop) throws IOException { + checkString(prop); + if (!hasSetGetExtension) + return null; + else { + write("GET " + prop + "\r\n"); + readResponse(null); // 209 values follow + ByteBuf b = new ByteBuf(); + String target = prop + " "; + int tl = target.length(); + String result = null; + + while (true) { // dot-terminated list of "KEY value" + readLine(b); + if (b.equals(".")) + break; + else if (b.byteAt(0) == '.') + b.remove(0, 1); + + if (b.regionMatches(true, 0, target, 0, tl)) { + String s = b.toString().substring(tl); + if (result == null) + result = s; + else + result = result + "\n" + s; + } + } + return result; + } + } + + + /** Returns an array of strings, the names of newsgroups to which new users + of this server should be subscribed by default. + */ + synchronized String[] LIST_SUBSCRIPTIONS() throws IOException { + if (!hasListSubscrExtension) + return new String[0]; + else { + Vector v = new Vector(); + write("LIST SUBSCRIPTIONS\r\n"); + readResponse(null); // 215 default newsgroups. + ByteBuf b = new ByteBuf(); + while (true) { // dot-terminated list of group names + readLine(b); + if (b.equals(".")) + break; + else if (b.byteAt(0) == '.') + b.remove(0, 1); + + v.addElement(b.toString()); + } + + String ss[] = new String[v.size()]; + Enumeration e = v.elements(); + for (int i = 0; e.hasMoreElements(); i++) + ss[i] = (String) e.nextElement(); + return ss; + } + } + + + /** Returns array of int: [ nmessages low hi ] + */ + synchronized int[] GROUP(String group_name) throws IOException { + checkString(group_name); + write("GROUP " + group_name + "\r\n"); + ByteBuf b = new ByteBuf(); + + try { + readResponse(b); + + } catch (NNTPException e) { + // If we got an error selecting this group, reconnect and try again, + // just once more. If we get an error this time, give up. + if (DEBUG) + System.err.println("NNTP: rebooting connection..."); + connect(host, port, user, pass); + write("GROUP " + group_name + "\r\n"); + readResponse(b); + } + + // parse response: 211 2553 609025 611724 alt.test + StringTokenizer st = new StringTokenizer(b.toString(), " \t", false); + int result[] = new int[3]; + for (int i = 0; i < (1+result.length); i++) { + int x; + try { + x = Integer.parseInt((String) st.nextToken()); + } catch (NoSuchElementException e) { + x = -1; + } + if (i != 0) + result[i-1] = x; + } + selected_group = group_name; + return result; + } + + private synchronized InternetHeaders readHeaders(String terminator) + throws IOException { + ByteBuf b = new ByteBuf(); + InternetHeaders h = new InternetHeaders(); + while (true) { + readLine(b); + if (b.equals(terminator)) + break; + else if (b.byteAt(0) == '.') + b.remove(0, 1); + h.addHeaderLine(b.toString()); + } + return h; + } + + synchronized NewsMessage HEAD(NewsFolder folder, String id) + throws IOException { + if (folder == null) + throw new NullPointerException(); + checkString(id); + write("HEAD " + id + "\r\n"); + readResponse(null); // 221 0 head <346D5A48.399F@host> + InternetHeaders h = readHeaders("."); + return new NewsMessage(folder, h); + } + + synchronized NewsMessage HEAD(NewsFolder folder, long article) + throws IOException { + String group = folder.getFullName(); + GROUP(group); + NewsMessage m = HEAD(folder, Long.toString(article)); + m.setStorageFolderIndex((int) article); + return m; + } + + /** Returns a stream of the message's body. This takes care of the + dot termination for you. You must drain this stream + before issuing another NNTP command. + */ + synchronized InputStream BODY(String id) throws IOException { + checkString(id); + write("BODY " + id + "\r\n"); + readResponse(null); // 221 609025 <346D5A48.399F@grid.lock> head + return newDotStream(); + } + + synchronized InputStream BODY(String group, long article) + throws IOException { + checkString(group); + GROUP(group); + return BODY(Long.toString(article)); + } + + /** Returns a stream of the full message, including headers and body. + This takes care of the dot termination for you. You must + drain this stream before issuing another NNTP command. + */ + synchronized InputStream ARTICLE(String id) throws IOException { + checkString(id); + write("ARTICLE " + id + "\r\n"); + readResponse(null); // 221 609025 <346D5A48.399F@grid.lock> head + return newDotStream(); + } + + synchronized InputStream ARTICLE(String group, long article) + throws IOException { + GROUP(group); + return ARTICLE(Long.toString(article)); + } + + /** Returns a stream listing the new newsgroups added since the given date. + If the date is null, lists all of them. The stream lists the group + names one per line, in no particular order. + */ + synchronized InputStream NEWGROUPS(Date since) throws IOException { + String cmd; + if (since == null) { + cmd = "NEWGROUPS\r\n"; + } else { + DecimalFormat twod = new DecimalFormat("00"); + Calendar c = new GregorianCalendar(); + c.setTime(since); + c.setTimeZone(TimeZone.getTimeZone("GMT")); + cmd = ("NEWGROUPS " + + twod.format(c.get(c.YEAR)) + + twod.format(c.get(c.MONTH)) + + twod.format(c.get(c.DAY_OF_MONTH)) + + " " + + twod.format(c.get(c.HOUR_OF_DAY)) + + twod.format(c.get(c.MINUTE)) + + twod.format(c.get(c.SECOND)) + + " GMT" + + "\r\n"); + } + + write(cmd); + + try { + readResponse(null); // 231 New newsgroups follow. + } catch (NNTPException e) { + // If we got an error, reconnect and try again, just once more. + // If we get an error this time, give up. + if (DEBUG) + System.err.println("NNTP: rebooting connection..."); + connect(host, port, user, pass); + write(cmd); + readResponse(null); + } + + return newDotStream(); + } + + + /** Sends the OVER or XOVER command, as appropriate, and returns a stream + of the overview data. If neither OVER nor XOVER is supported, + throws an NNTPException. + */ + synchronized InputStream OVER(String group, long from, long to) + throws IOException { + if (to < from) + throw new NNTPException("arguments out of order: " + from + ", " + to); + GROUP(group); +// String cmd = (hasOverExtension ? "OVER" : "XOVER"); + // #### wait, OVER is weird... + String cmd = "XOVER"; + if (from <= 0) + write(cmd + "\r\n"); + else if (from == to) + write(cmd + " " + from + "\r\n"); + else + write(cmd + " " + from + "-" + to + "\r\n"); + + readResponse(null); // 224 data follows + return newDotStream(); + } + + + synchronized void dotStreamFinished(NNTPDotTerminatedInputStream stream) { + Assert.Assertion(pending_dot_stream == null || + pending_dot_stream == stream); + if (stream == pending_dot_stream) + pending_dot_stream = null; + } + + + NewsMessage parseOverviewLine(NewsFolder folder, + byte line[], int start, int length) { + + // We can't use StringTokenizer for this, because "a\t\tb" is supposed to + // be interpreted as the three fields "a", "", and "b". StringTokenizer + // would interpret it as two fields, "a" and "b". + + // Field order is always: + // article subject from date id references bytes lines + + long article = 0; + String subject; + String from; + String date; + String id; + String references; + long bytes = 0; + long lines = 0; + + int i = start; + int j = i; + + if (length > 0 && line[length-1] == '\n') + length--; + if (length > 0 && line[length-1] == '\r') + length--; + + while (j < length && line[j] != '\t') + article = (article * 10) + line[j++] - '0'; + + i = ++j; + while (j < length && line[j] != '\t') j++; + subject = new String(line, i, j-i); + + i = ++j; + while (j < length && line[j] != '\t') j++; + from = new String(line, i, j-i); + + i = ++j; + while (j < length && line[j] != '\t') j++; + date = new String(line, i, j-i); + + i = ++j; + while (j < length && line[j] != '\t') j++; + id = new String(line, i, j-i); + + i = ++j; + while (j < length && line[j] != '\t') j++; + references = new String(line, i, j-i); + + i = ++j; + while (j < length && line[j] != '\t') + bytes = (bytes * 10) + line[j++] - '0'; + + i = ++j; + while (j < length && line[j] != '\t') + lines = (lines * 10) + line[j++] - '0'; + + InternetHeaders h = new InternetHeaders(); + h.addHeaderLine("Subject: " + subject + "\r\n"); + h.addHeaderLine("From: " + from + "\r\n"); + h.addHeaderLine("Date: " + date + "\r\n"); + h.addHeaderLine("Message-ID: " + id + "\r\n"); + h.addHeaderLine("References: " + references + "\r\n"); + h.addHeaderLine("Content-Length: " + bytes + "\r\n"); + h.addHeaderLine("Lines: " + lines + "\r\n"); + + NewsMessage m = new NewsMessage(folder, h); + m.setStorageFolderIndex((int) article); + return m; + } + + Enumeration getMessages(NewsFolder folder, long from, long to) + throws IOException { + try { + NNTPDotTerminatedInputStream s = (NNTPDotTerminatedInputStream) + OVER(folder.getFullName(), from, to); + return new XOVERMessagesEnumeration(folder, this, s); + + } catch (NNTPException e) { // XOVER not supported; do HEAD. + if (DEBUG) + System.err.println("NNTP: XOVER didn't work: " + e + "; trying HEAD."); + return new HEADEnumeration(folder, this, from, to); + } + } + + +// begin_authorize +// list +// FIGURE_NEXT_CHUNK +// profile add +// profile delete +// read group +// post +// check for message +// display newsrc +// cancel +// xpat +// search +// list pretty names +// list xactive + + +} + + +class NNTPDotTerminatedInputStream extends DotTerminatedInputStream { + + static private final boolean DEBUG = false; + + // debugging kludge: if the number printed in the stderr output doesn't + // increase monotonically, there's a synchronization problem. + private int debug_count = 0; + + private NNTPConnection nntp; + + NNTPDotTerminatedInputStream(NNTPConnection nntp, InputStream s) { + super(s); + this.nntp = nntp; + } + + public int read(byte[] buf, int start, int length) throws IOException { + Assert.Assertion(nntp != null); + synchronized (nntp) { + int i = super.read(buf, start, length); + Assert.Assertion(i > 0 || i == -1); + + if (DEBUG && NNTPConnection.DEBUG) { + String s = "NNTP/DOT #" + (debug_count++) + ": <== "; + int k = (i > 50 ? 50 : i); + for (int j = start; j < start+k; j++) + s += (buf[j] == '\r' ? "\\r" : + buf[j] == '\n' ? "\\n" : + buf[j] == '\t' ? "\\t" : + buf[j] == '\\' ? "\\\\" : + new String(buf, j, 1)); + System.err.print(s + "\n"); + } + + if (i <= 0) { + finished(); + Assert.Assertion(i < 0); + } + + return i; + } + } + + public void close() { + super.close(); + finished(); + } + + public void bufferUpEverything() { + super.bufferUpEverything(); + finished(); + } + + private void finished() { + NNTPConnection n2 = nntp; // avoid double sync and possible deadlock + nntp = null; + if (n2 != null) { + synchronized (n2) { + n2.dotStreamFinished(this); + if (NNTPConnection.DEBUG) { + if (DEBUG) + System.err.println("NNTP/DOT #" + debug_count + + ": dot-terminated input stream finished."); + else + System.err.println("NNTP: dot-terminated input stream finished."); + } + } + } + } + +} + + + +/** An enumeration object that reads XOVER data from a stream, and returns + successive NewsMessage objects. It streams; the enumeration will block + when there is no data ready on the stream. + */ +class XOVERMessagesEnumeration implements Enumeration { + + static private final boolean DEBUG = false; + + // debugging kludge: if the number printed in the stderr output doesn't + // increase monotonically, there's a synchronization problem. + private int debug_count = 0; + + private NNTPDotTerminatedInputStream s; + private NewsFolder folder; + private NNTPConnection nntp; + + private int max_bytes = 10240; + private byte buf[] = new byte[max_bytes]; + private NewsMessage next_value = null; + + XOVERMessagesEnumeration(NewsFolder folder, + NNTPConnection nntp, + NNTPDotTerminatedInputStream s) { + this.folder = folder; + this.nntp = nntp; + this.s = s; + this.next_value = read_one(); + } + + private NewsMessage read_one() { + try { + // Note that `s' is a DotTerminatedInputStream, which means that + // `read' will only return chunks consisting of a single line (or + // max_bytes, whichever is smaller.) + int i = s.read(buf, 0, max_bytes); + + if (DEBUG && NNTPConnection.DEBUG) { + String s = "NNTP/XOVER #" + (debug_count++) + ": <== "; + int k = (i > 50 ? 50 : i); + for (int j = 0; j < k; j++) + s += (buf[j] == '\r' ? "\\r" : + buf[j] == '\n' ? "\\n" : + buf[j] == '\t' ? "\\t" : + buf[j] == '\\' ? "\\\\" : + new String(buf, j, 1)); + System.err.print(s + "\n"); + } + + if (i <= 0) { + return null; + } else { + return nntp.parseOverviewLine(folder, buf, 0, i); + } + } catch (IOException e) { + return null; + } + } + + public Object nextElement() { + if (next_value == null) { + throw new NoSuchElementException(); + } else { + NewsMessage m = next_value; + next_value = read_one(); + return m; + } + } + + public boolean hasMoreElements() { + return (next_value != null); + } + +} + + +/** An enumeration object that sends and interprets successive HEAD commands + to the NNTP server, and returns successive NewsMessage objects. + */ +class HEADEnumeration implements Enumeration { + + static private final boolean DEBUG = false; + + // debugging kludge: if the number printed in the stderr output doesn't + // increase monotonically, there's a synchronization problem. + private int debug_count = 0; + + private NewsFolder folder; + private NNTPConnection nntp; + private long from; + private long to; + private NewsMessage next_value = null; + + /* #### Possible optimization: + GNUS used to send many HEAD requests all at once, then parse the + responses as they came in, rather than doing a round-trip / + synchronization between each message. The trick here is, you have + to be careful not to blow the network buffer (pipe) or you'll + deadlock. I think GNUS only sent 50 or 100 at a time, under the + assumption that the returned data would tend to be of some size + that made that safe. + + This probably isn't that big a deal in the case of HEAD (since almost + nobody has servers that don't support XOVER these days) but it would + be a huge win to be able to do this with GROUP (for listing the message + counts at startup.) + */ + + HEADEnumeration(NewsFolder folder, NNTPConnection nntp, long from, long to) { + this.folder = folder; + this.nntp = nntp; + this.from = from; + this.to = to; + this.next_value = read_one(); + } + + private NewsMessage read_one() { + try { + if (from > to) { + return null; + } else { + NewsMessage m = nntp.HEAD(folder, from++); + if (NNTPConnection.DEBUG && DEBUG) { + System.err.println("NNTP/HEAD #" + (debug_count++) + ": " + m); + } + return m; + } + } catch (IOException e) { + if (NNTPConnection.DEBUG && DEBUG) { + System.err.println("NNTP/HEAD: caught I/O error: " + e); + } + return null; + } + } + + public Object nextElement() { + if (next_value == null) { + throw new NoSuchElementException(); + } else { + NewsMessage m = next_value; + next_value = read_one(); + return m; + } + } + + public boolean hasMoreElements() { + return (next_value != null); + } + +} diff --git a/mozilla/grendel/storage/NNTPException.java b/mozilla/grendel/storage/NNTPException.java new file mode 100644 index 00000000000..d8eb40af01e --- /dev/null +++ b/mozilla/grendel/storage/NNTPException.java @@ -0,0 +1,35 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 25 Nov 1997. + */ + +package grendel.storage; +import java.io.IOException; + +/** An exception while talking to an NNTP server. Usually means a network + problem or a configuration problem. */ +class NNTPException extends IOException { + public NNTPException() { + } + + public NNTPException(String msg) { + super(msg); + } +} + diff --git a/mozilla/grendel/storage/NewsFolder.java b/mozilla/grendel/storage/NewsFolder.java new file mode 100644 index 00000000000..47dde34d18d --- /dev/null +++ b/mozilla/grendel/storage/NewsFolder.java @@ -0,0 +1,540 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 1 Dec 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; +import calypso.util.Assert; + +import java.util.Vector; + +import java.io.InputStream; +import java.io.IOException; + +import javax.mail.FetchProfile; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.event.MessageChangedEvent; +import javax.mail.event.MessageCountEvent; + + +/** This class implements a Folder representing a newsgroup. + */ +class NewsFolder extends FolderBase { + + static final boolean DEBUG = false; + + private Folder parent; + private String name; + private NewsStore store; + + // Whether the fMessages vector has been initialized. + private boolean loaded = false; + + // Cached responses of the GROUP command. + private int high_message = -1; + private int low_message = -1; + private int estimated_message_count = -1; + + // If true, GROUP gave us an error; this group is probably nonexistant. + private boolean bogus_group_p = false; + + + /** The maximum portion of the elements in an NNTP XOVER request which + are allowed to be superfluous. (Some amount of superfluity is good, + since it reduces the number of client-server round-trips, at the cost + of sending slightly more data per request.) + */ + static final double ALLOWABLE_XOVER_WASTE = 0.25; + + /** The maximum number of articles which may be requested per NNTP XOVER + request. Larger values reduce the number of client-server round-trips; + smaller values increase client responsiveness on slow connections. + */ + static final int MAX_XOVER_REQUEST_SIZE = 50; + + + NewsFolder(Store s, Folder parent, String name) { + super(s); + this.parent = parent; + this.name = name; + this.store = (NewsStore) s; + } + + public char getSeparator() { + // #### If we're in "all groups" mode, this will be '.'. + // Otherwise, there is no hierarchy, and therefore no separator. + return '\000'; + } + + public int getType() { + return HOLDS_MESSAGES; + } + + public String getName() { + return name; + } + + public String getFullName() { + return name; + } + + public Folder getParent() { + return parent; + } + + public Folder[] list(String pattern) { + return null; + } + + public Folder getFolder(String subfolder) { + return null; + } + + public boolean create(int type) { + return false; + } + + public boolean exists() { + return !bogus_group_p; + } + + protected void getMessageCounts() { + int counts[] = store.getGroupCounts(this); + if (counts == null) { + bogus_group_p = true; + } else { + estimated_message_count = counts[0]; + low_message = counts[1]; + high_message = counts[2]; + } + } + + // #### Is "unread" the right sense of "new" for NNTP? + public boolean hasNewMessages() { + return (getUnreadMessageCount() > 0); + } + + /** Returns the total number of messages in the folder, or -1 if unknown. + This includes deleted and unread messages. + */ + public int getMessageCount() { + if (bogus_group_p) + return -1; + if (estimated_message_count == -1) + getMessageCounts(); + return estimated_message_count; + } + + /** Returns the number of non-deleted messages in the folder, or -1 if + unknown. This includes unread messages. + */ + public int getUndeletedMessageCount() { + return getMessageCount(); + } + + /** Returns the number of unread messages in the folder, or -1 if unknown. + This does not include unread messages that are also deleted. + */ + public int getUnreadMessageCount() { + if (bogus_group_p) + return -1; + if (estimated_message_count == -1) + getMessageCounts(); + + if (low_message == -1 || high_message == -1) + return -1; + + NewsRC rc = store.getNewsRC(); + NewsRCLine nl = rc.getNewsgroup(getFullName()); + long unread = nl.countMissingInRange(low_message, high_message+1); + return (int) unread; + } + + + /** Returns the number of bytes consumed by deleted but not expunged + messages in the folder, or -1 if unknown. + */ + long deletedMessageBytes() { + return 0; + } + + + public void appendMessages(Message msgs[]) throws MessagingException { + throw new MessagingException("can't append messages to newsgroups"); + } + + public void fetch(Message msgs[], FetchProfile fp) { + } + + public Message[] expunge() throws MessagingException { + return null; + } + + public boolean delete(boolean value) { + // #### this should do cancel + Assert.NotYetImplemented("NewsFolder.delete()"); + return false; + } + + public boolean renameTo(Folder f) { + // Can't rename newsgroups. + return false; + } + + void ensureLoaded() { + if (DEBUG) + System.err.println("NF.ensureLoaded " + name); + + if (bogus_group_p) + return; + + if (loaded) + return; + + synchronized (this) { + if (loaded) // double check + return; + + if (estimated_message_count == -1) + getMessageCounts(); + + NewsRC rc = store.getNewsRC(); + NewsRCLine nl = rc.getNewsgroup(getFullName()); + + long start = (int) nl.firstNonMember(); + long end = high_message; + + Assert.Assertion(start > 0); + + if (start < low_message) { + // There is a message marked as unread in the newsrc file which + // has expired from the news server (it is below the low water mark.) + // Mark all messages up to (but not including) the low water mark as + // read, to reduce the newsrc file size. + // + if (DEBUG) + System.err.println("NEWSRC: low water mark is " + low_message + + " but first-unread is " + start + ".\n" + + " marking 1-" + (low_message-1) + + " as read."); + nl.insert(1, low_message); + start = (int) nl.firstNonMember(); + } + + if (end < start) + return; + + + /* We want to get the data for *certain* message between `start' + and `end' inclusive; but not necessarily *all* of them (there + might be holes.) + + We can only do this by requesting ranges. + + * The fewer unneeded articles we request, the better. + * The fewer range requests we make, the better. + * But, keeping the length of the range requested relatively + short is also good, to avoid hogging the pipe, and giving + other users of the NNTP connection a chance to have their + say. In other words, there is a preferred range length. + + We can request a range that includes articles we don't + specifically want (the holes.) + + Algorithm: + + - Move forward from start to end, accumulating ranges of + articles to request. + + - Keep track of how many needed and unneeded articles we + are requesting. + + - If adding a needed range (along with its preceeding + unneeded range) to the set would push the ratio of + needed/unneeded in the current request past a certain + threshold, then break the request into two before this + range: flush out what we've got, skip over the unneeded + leading range (the unneeded messages before this range of + needed messages), and start building a new request starting + with the beginning of this range of needed messages. + + - Also, if adding a needed range would push the number of + articles in the request past a certain number, break + the request at that point: that is, break the current + range in two. + */ + + long req_start = start; + long total_in = 0; + long total_out = 0; + + if (DEBUG) + System.err.println("NEWSRC: computing XOVER range for " + + start + "-" + end); + + long i = start; + long last_read = nl.max(); + + while (i <= end) { + long range_start, range_end; // this block of unread messages + + // Count up and skip over the following range of already-read + // messages. That is, the already-read messages that lie before + // this range, and after the previous range. + // + range_start = (i >= last_read + ? i + : nl.firstNonMember(i, last_read+1)); + if (range_start == -1) + range_start = i; + + if (range_start > end) + break; + + // Count up and skip over the following range of unread messages. + // That is, the unread messages lying after the read messages we + // skipped above. + // + range_end = nl.nextMember(range_start) - 1; + if (range_end < 0 || range_end > end) + range_end = end; + + if ((range_end - req_start) >= MAX_XOVER_REQUEST_SIZE) { + // + // Adding this range, plus its leading junk, would push the + // cumulative request over its maximum allowable size. + + long reduced_end = req_start + MAX_XOVER_REQUEST_SIZE - 1; + Assert.Assertion(reduced_end < range_end); + + if (i == range_start) { + // + // This range has no leading junk: it is adjascent to the + // previously-scheduled range. Request only part of this + // range, and start accumulating the next batch at the + // point where we divided this range. + // + if (DEBUG) + System.err.println("NEWSRC: range " + + range_start + "-" + range_end + " (" + + (range_end - range_start + 1) + " + " + + (range_start-i) + ") is large (+" + + (i - req_start) + " = " + + (range_end - req_start + 1) + + ");\n cont: " + + req_start + "-" + reduced_end + " (" + + (reduced_end - req_start + 1) + ")"); + + store.openNewsgroup(this, req_start, reduced_end); + i = reduced_end+1; + req_start = i; + total_in = 0; + total_out = 0; + // (we know that req_start is an unread message.) + Assert.Assertion(!nl.member(req_start)); + + } else { + // + // Adding this range, plus its leading junk, would make the + // cumulative request size be too large. So ask for the + // already-accumulated batch (not including this range); + // and start accumulating the next batch at the beginning of + // this range (after its leading junk.) + // + if (DEBUG) + System.err.println("NEWSRC: range " + + range_start + "-" + range_end + " (" + + (range_end - range_start + 1) + " + " + + (range_start-i) + ") is large (+" + + (i - req_start) + " = " + + (range_end - req_start + 1) + + ");\n load: " + + req_start + "-" + (i-1) + " (" + + (i - req_start) + ")"); + + store.openNewsgroup(this, req_start, (i-1)); + Assert.Assertion(i < range_start); + i = range_start; + req_start = i; + total_in = 0; + total_out = 0; + // (we know that req_start is an unread message, because + // it points to the begining of this range.) + Assert.Assertion(!nl.member(req_start)); + } + + } else { + // + // The range, plus its leading junk, satisfied the size constraint. + // Now see if it satisfies the allowable-waste constraint. + + long range_in = (range_end - range_start) + 1; + long range_out = range_start - i; + double waste; + { + long in = (total_in + range_in); + long out = (total_out + range_out); + waste = ((double) out) / ((double) (in + out)); + Assert.Assertion(waste >= 0.0 && waste <= 1.0); + } + + if (waste > ALLOWABLE_XOVER_WASTE) { + // + // Adding this range, plus its leading junk, would push the + // cumulative request over its "acceptable junk" ratio. + // So ask for the already-accumulated batch (not including + // this range); and start accumulating the next batch at the + // beginning of this range (after its leading junk.) + // + if (DEBUG) + System.err.println("NEWSRC: range " + + range_start + "-" + range_end + + " plus leading waste (" + + i + "-" + (range_start-1) + + ") would mean " + + (((int) (waste * 1000)) / 10.0) + + "% waste; requesting " + + req_start + "-" + i + " (" + + (i - range_start + 1) + ")"); + + store.openNewsgroup(this, req_start, i); + req_start = range_start; + total_in = 0; + total_out = 0; + // (we know that req_start is an unread message.) + Assert.Assertion(!nl.member(req_start)); + + } else { + // + // Adding this range, plus its leading junk, is ok! + // + if (DEBUG) + System.err.println("NEWSRC: range " + + range_start + "-" + range_end + + " (" + (range_end - range_start + 1) + " + " + + (total_out + range_out) + ") added; total " + + (range_end - req_start + 1) + + "; wasted " + + (((int) (waste * 1000)) / 10.0) + "%"); + + total_in += range_in; + total_out += range_out; + + i = range_end + 1; + // If we're not done, then `i' points at a read message + // (that is, it points at leading junk.) + Assert.Assertion(i > end || nl.member(i)); + } + } + } + + // Got to end. Is there still stuff in the request buffer? + if (req_start <= end) { + + if (DEBUG) + System.err.println("NEWSRC: done: " + + req_start + "-" + end + " (" + + (end - req_start + 1) + ")"); + + store.openNewsgroup(this, req_start, end); + } + } + } + + + public void open(int mode) { + if (DEBUG) + System.err.println("NF.open " + name); + ensureLoaded(); + } + + public void close(boolean doExpunge) throws MessagingException { + NewsRC rc = store.getNewsRC(); + if (rc != null) { + try { + rc.save(); + } catch (IOException e) { + throw new MessagingException("error saving newsrc file", e); + } + } + fMessages = new Vector(); + } + + public boolean isOpen() { + return true; // #### Wrong! + } + + InputStream getMessageStream(NewsMessage message, boolean headers_too) + throws IOException { + Assert.Assertion(this == message.getFolder()); + return store.getMessageStream(message, headers_too); + } + + + /* #### combine this with BerkeleyFolder via MessageBase? + */ + /** Assert whether any messages have had their flags changed. + A child message should call this on its parent when any persistent + flag value is changed. If a non-null message is provided, and + flags_dirty is true, then notify any observers that this message + has changed. + + @param flags_dirty Whether the flags should currently be considered + to be dirty. + + @param message If flags_dirty, the Message (BerkeleyMessage) that + has become dirty. + + @param old_flags If message is non-null, the previous value of its + flags (Message.flags should be the new, dirty value.) + */ + void setFlagsDirty(boolean flags_dirty, Message message, long old_flags) { + + // If the read-ness of a message has changed, flush the new counts down + // into the newsrc object. + // + if (flags_dirty && message != null) { + long new_flags = ((MessageBase)message).flags; + long interesting = (MessageBase.FLAG_READ); + if ((old_flags & interesting) != (new_flags & interesting)) { + NewsRC rc = store.getNewsRC(); + NewsRCLine nl = rc.getNewsgroup(getFullName()); + int n = ((NewsMessage)message).getStorageFolderIndex(); + if (n < 0) + ; // Eh? + else if ((new_flags & MessageBase.FLAG_READ) != 0) + nl.insert(n); + else + nl.delete(n); + } + } + + if (flags_dirty && message != null) { + notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, + message); + } + } + +} diff --git a/mozilla/grendel/storage/NewsFolderRoot.java b/mozilla/grendel/storage/NewsFolderRoot.java new file mode 100644 index 00000000000..db2238b1a72 --- /dev/null +++ b/mozilla/grendel/storage/NewsFolderRoot.java @@ -0,0 +1,207 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 1 Dec 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; + +import java.io.IOException; + +import java.util.Vector; +import java.util.Enumeration; + +import javax.mail.FetchProfile; +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.event.MessageChangedEvent; +import javax.mail.event.MessageCountEvent; + + +/** This class implements a Folder representing the root-level of a news + server. That is, this is the folder that holds all of the user's + subscribed newsgroups. This folder has children and not messages; + its children hold messages and (probably) not folders. + */ +class NewsFolderRoot extends Folder { + + NewsStore store = null; + + NewsFolderRoot(Store s) { + super(s); + store = (NewsStore) s; + } + + public char getSeparator() { + // #### If we're in "all groups" mode, this will be '.'. + // Otherwise, there is no hierarchy, and therefore no separator. + return '\000'; + } + + public int getType() { + return HOLDS_FOLDERS; + } + + public String getName() { + return ""; + } + + public String getFullName() { + return ""; + } + + public Folder getParent() { + return null; + } + + public Folder[] list(String pattern) { + NewsRC newsrc = store.getNewsRC(); + Assert.Assertion(newsrc != null); + Vector v = new Vector(); + for (Enumeration e = newsrc.elements(); e.hasMoreElements(); ) { + NewsRCLine n = (NewsRCLine) e.nextElement(); + boolean match; + + if (!n.subscribed()) { + match = false; + } else if (pattern.equals("%") || pattern.equals("*")) { + match = true; + } else { + // #### I'm not really expected to write a regexp matcher, am I? + Assert.NotYetImplemented("NewsFolderRoot.list(String pattern)"); + match = false; + } + + if (match) { + NewsFolder f = new NewsFolder(store, this, n.name()); + v.addElement(f); + } + } + + Folder ff[] = new Folder[v.size()]; + Enumeration e = v.elements(); + for (int i = 0; e.hasMoreElements(); i++) + ff[i] = (Folder) e.nextElement(); + return ff; + } + + + public Folder getFolder(String subfolder) { + NewsRC newsrc = store.getNewsRC(); + for (Enumeration e = newsrc.elements(); e.hasMoreElements(); ) { + NewsRCLine n = (NewsRCLine) e.nextElement(); + if (subfolder.equals(n.name())) + return new NewsFolder(store, this, n.name()); + } + return null; + } + + public boolean create(int type) { + return false; + } + + public boolean exists() { + return true; + } + + public boolean hasNewMessages() { + // #### descend into sub-folders? + return false; + } + + /** Returns the total number of messages in the folder, or -1 if unknown. + This includes deleted and unread messages. + */ + public int getMessageCount() { + // #### descend into sub-folders? + return -1; + } + + /** Returns the number of non-deleted messages in the folder, or -1 if + unknown. This includes unread messages. + */ + public int getUndeletedMessageCount() { + return getMessageCount(); + } + + /** Returns the number of unread messages in the folder, or -1 if unknown. + This does not include unread messages that are also deleted. + */ + public int getUnreadMessageCount() { + // #### descend into sub-folders? + return -1; + } + + + /** Returns the number of bytes consumed by deleted but not expunged + messages in the folder, or -1 if unknown. + */ + long deletedMessageBytes() { + return -1; + } + + + public void appendMessages(Message msgs[]) throws MessagingException { + throw new + MessagingException("can't append messages to the root news folder."); + } + + + public void fetch(Message msgs[], FetchProfile fp) { + } + + public Message[] expunge() throws MessagingException { + return null; + } + + public boolean delete(boolean recurse) { + // #### signal error? + return false; + } + + public boolean renameTo(Folder f) { + // #### signal error? + return false; + } + + public void open(int mode) { + // Not called on folders that don't hold messages. + } + + public void close(boolean doExpunge) throws MessagingException { + } + + public boolean isOpen() { + return false; + } + + public Flags getPermanentFlags() { + // No messages, therefore, no flags. + return new Flags(); + } + + public Message getMessage(int msgnum) { + return null; + } + +} diff --git a/mozilla/grendel/storage/NewsMessage.java b/mozilla/grendel/storage/NewsMessage.java new file mode 100644 index 00000000000..bea34ba73ee --- /dev/null +++ b/mozilla/grendel/storage/NewsMessage.java @@ -0,0 +1,125 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 1 Dec 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; + + +class NewsMessage extends MessageBase { + + int article_number = -1; + int byte_length = -1; + int line_length = -1; + + NewsMessage(NewsFolder f, InternetHeaders h) { + super(f, h); + } + + NewsMessage(NewsFolder f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + ByteBuf id, + ByteBuf refs[]) { + super(f, date, flags, author, recipient, subj, id, refs); + } + + NewsMessage(NewsFolder f, + long date, + long flags, + ByteBuf author, + ByteBuf recipient, + ByteBuf subj, + MessageID id, + MessageID refs[]) { + super(f, date, flags, author, recipient, subj, id, refs); + } + + + void setSize(int l) { + byte_length = l; + } + + public int getSize() { + return byte_length; + } + + void setLines(int l) { + line_length = l; + } + + public int getLineCount() { + return line_length; + } + + void setStorageFolderIndex(int p) { + article_number = p; + } + + int getStorageFolderIndex() { + return article_number; + } + + public InputStream getInputStreamWithHeaders() throws MessagingException { + NewsFolder f = (NewsFolder) getFolder(); + try { + return f.getMessageStream(this, true); + } catch (IOException e) { + throw new MessagingException("I/O Error", e); + } + } + + public InputStream getInputStream() throws MessagingException { + NewsFolder f = (NewsFolder) getFolder(); + try { + return f.getMessageStream(this, false); + } catch (IOException e) { + throw new MessagingException("I/O Error", e); + } + } + + protected void setFlagBit(long flag, boolean value) { + long oflags = flags; + super.setFlagBit(flag, value); + if (oflags != flags) { + setFlagsDirty(true); + Folder f = getFolder(); + ((NewsFolder) f).setFlagsDirty(true, this, oflags); + } + } + + public void writeTo(OutputStream aStream) { + Assert.NotYetImplemented("NewsFolder.writeTo(IOStream)"); + } +} diff --git a/mozilla/grendel/storage/NewsRC.java b/mozilla/grendel/storage/NewsRC.java new file mode 100644 index 00000000000..c3c67d3fc82 --- /dev/null +++ b/mozilla/grendel/storage/NewsRC.java @@ -0,0 +1,457 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 10 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; + +import java.lang.Thread; +import java.lang.SecurityException; +import java.lang.InterruptedException; + +import java.util.Vector; +import java.util.Enumeration; + +import java.io.File; +import java.io.OutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.FileNotFoundException; + + +/** This class represents a newsrc-format disk file, and the newsgroup + descriptions within it. + */ +public class NewsRC { + + static final boolean DEBUG = true; + + protected File file = null; // disk file; might not exist, but never null. + + /* This holds the date of the newsrc file when it was parsed. + We need to save this (older) values so that we can tell when + the summary file is out of date. This is measured in the units + of File.lastModified(). */ + protected long file_date = -1; + + /* This holds the size of the file when it was parsed. + We need to save this (older) values so that we can tell when + the summary file is out of date. */ + protected long file_size = -1; + + /* Any "options" lines that appeared at the head of the newsrc file. + We don't use them, but we preserve them if they exist. */ + protected ByteBuf options_lines = null; + + protected Vector lines = new Vector(); // holds NewsRCLines + + protected boolean dirty = false; // needs to be saved to disk? + + Thread updateThread = null; // background save thread. + + + /** Reads and parses the file, and returns a NewsRC object describing it. + If the file doesn't exist, the newsrc object will be empty. + */ + NewsRC(File file) throws IOException { + this.file = file; + load(); + } + + private synchronized void load() throws IOException { + try { + FileInputStream in = null; + try { + in = new FileInputStream(file); + parse_file(in); + file_size = file.length(); + file_date = file.lastModified(); + dirty = false; + + } finally { + if (in != null) + in.close(); + } + } catch (FileNotFoundException e) { + // do nothing -- it's ok for the file to not exist. + } + } + + + /** Returns the file associated with this newsrc object. + Note that the file may not actually exist on disk yet. + */ + public File file() { + Assert.Assertion(file != null); + return file; + } + + protected void parse_file(FileInputStream in) throws IOException { + + ByteLineBuffer b = new ByteLineBuffer(); + ByteBuf inbuf = new ByteBuf(1024); + ByteBuf outbuf = new ByteBuf(100); + boolean eof = false; + + lines.removeAllElements(); + + while (!eof) { + inbuf.setLength(0); + int r = inbuf.read(in, 1024); + if (r == -1) { + eof = true; + b.pushEOF(); + } else { + b.pushBytes(inbuf); + } + + while (b.pullLine(outbuf)) { + NewsRCLine line = makeNewsRCLine(outbuf); + if (line == null) { + if (options_lines == null) + options_lines = new ByteBuf(outbuf.length()); + options_lines.append(outbuf); + } else { + lines.addElement(line); + } + } + } + } + + /** Call this to indicate that the file needs to be flushed to disk. + */ + public synchronized void markDirty() { + + if (dirty) return; + + if (DEBUG) + System.err.println("Marking newsrc file " + file + " dirty."); + + boolean was_dirty = dirty; + dirty = true; + verifyFileDate(was_dirty); + + if (!dirty) return; // verifyFileDate() might have saved or reloaded. + + if (updateThread == null || !updateThread.isAlive()) { + + if (DEBUG) + System.err.println("spawning newsrc background saver for " + file); + + updateThread = new NewsRCUpdateThread(this); + updateThread.start(); + } + } + + /** Parses the given line and creates a NewsRCLine object representing it. + Returns null if the line is unparsable. + */ + protected NewsRCLine makeNewsRCLine(ByteBuf line_buf) { + byte line[] = line_buf.toBytes(); + int length = line_buf.length(); + boolean subscribed = false; + int i = 0; + while (i < length && line[i] <= ' ') i++; // skip whitespace + + int start = i; + while (i < length && // skip non-whitespace, but + line[i] > ' ' && // not the two magic chars + line[i] != ':' && + line[i] != '!') + i++; + + if (i == start) return null; + + String group_name = new String(line, start, i); + + while (i < length && line[i] <= ' ') i++; // skip whitespace + + if (i < length && line[i] == ':') + subscribed = true; + else if (i < length && line[i] == '!') + subscribed = false; + else + return null; + + i++; + while (i < length && line[i] <= ' ') i++; // skip whitespace + return new NewsRCLine(this, group_name, subscribed, line, i, length); + } + + + /* Returns an Enumeration of the NewsRCLines in this NewsRC. */ + public Enumeration elements() { + return lines.elements(); + } + + /** Returns a NewsRCLine describing the named newsgroup. + If there isn't one, creates one. + */ + public NewsRCLine getNewsgroup(String group_name) { + for (Enumeration e = lines.elements(); e.hasMoreElements(); ) { + NewsRCLine n = (NewsRCLine) e.nextElement(); + if (group_name.equals(n.name())) + return n; + } + + NewsRCLine n = new NewsRCLine(this, group_name, true); + lines.addElement(n); + return n; + } + + public void write(OutputStream stream) throws IOException { + + synchronized (stream) { + + if (options_lines != null) { + stream.write(options_lines.toBytes(), 0, + options_lines.length()); + } + ByteBuf b = new ByteBuf(); + + final byte[] CR = { '\r' }; + final byte[] LF = { '\n' }; + final byte[] CRLF = { '\r', '\n' }; + byte linebreak[] = (Constants.ISUNIX ? LF : Constants.ISMAC ? CR : CRLF); + + for (Enumeration e = lines.elements(); e.hasMoreElements(); ) { + NewsRCLine n = (NewsRCLine) e.nextElement(); + b.setLength(0); + n.write(b); + b.append(linebreak); + stream.write(b.toBytes(), 0, b.length()); + } + } + } + + + /** Returns the name of a non-existent file that can be used as a temporary + file while writing this newsrc. This name is OS-dependent. +

+ On all systems, the summary file resides in the same directory + as the folder file, and has the same "base" name. +

+ On Unix, the summary file for .newsrc-HOSTNAME would be + .newsrc-HOSTNAME.ns_tmp. +

+ On Windows and Mac, the summary file for news-HOSTNAME.rc + would be news-HOSTNAME.TMP. + */ + protected File tempFileName() { + File file = file(); + String dir = file.getParent(); + String name = file.getName(); + + if (Constants.ISUNIX) { + name = name + ".ns_tmp"; + } else { + int i = name.lastIndexOf("."); + if (i > 0) + name = name.substring(0, i); + name = name + ".tmp"; + } + + return new File(dir, name); + } + + + /** Writes the file associated with this NewsRC object, if there have + been any changes to it. + */ + public void save() throws IOException { + if (!dirty) + return; + synchronized (this) { + if (!dirty) + return; + verifyFileDate(true); + save_internal(); + } + } + + protected void save_internal() throws IOException { + + if (!dirty) + return; + synchronized (this) { + if (!dirty) // double check + return; + + File file = file(); + File tmp = tempFileName(); + OutputStream out = null; + + try { // make sure the temp file gets deleted. + + try { // make sure the stream gets closed. + out = new FileOutputStream(tmp); + out = new BufferedOutputStream(out); + + if (DEBUG) { + System.err.println("Writing tmp newsrc file " + tmp); + } + + write(out); + } finally { // make sure the stream gets closed. + if (out != null) { + out.close(); + out = null; + } + } + + tmp.renameTo(file); + + file_size = file.length(); + file_date = file.lastModified(); + + dirty = false; + + if (DEBUG) { + System.err.println("Renaming tmp newsrc file " + tmp + " to " + + file); + } + + tmp = null; + + } finally { // make sure the temp file gets deleted. + if (tmp != null) + try { + tmp.delete(); + } catch (SecurityException e) { + } + } + } + } + + + /** Compare the current date/size of the disk file with the date/size at + the time we read it, and if the disk version has changed, do something + sensible. + */ + protected synchronized void verifyFileDate(boolean was_dirty) { + long cur_size = -1; + long cur_date = -1; + + if (file.exists()) { + cur_size = file.length(); + cur_date = file.lastModified(); + } + + if (cur_date != file_date || cur_size != file_size) { + + if (file_date != -1 && cur_date == -1) { + // file used to exist, and has been deleted... + System.err.println("WARNING: " + file + + " was deleted out from under us! Saving now..."); + dirty = true; + + try { + save_internal(); + } catch (IOException e) { + System.err.println("ERROR! Saving " + file + ": " + e); + } + + } else { + // either: the file didn't exist and now it does; + // or it already existed, but has changed since we read it. + + // #### dialog box or something + synchronized(System.err) { + System.err.println("WARNING: " + file + " has changed!"); + if (was_dirty) + System.err.println(" Discarding our changes and " + + "reloading the disk version..."); + else + System.err.println(" Reloading the disk version..."); + } + + try { + load(); + } catch (IOException e) { + System.err.println("ERROR! Reloading " + file + ": " + e); + } + + // #### Perhaps here we should generate some kind of listener event, + // to let the folder list know that the unread message counts + // may have changed? + } + } + } + + + public static void main(String argv[]) { + try { + NewsRC rc = new NewsRC(new File(argv[0])); + if (rc.options_lines != null) + System.out.print(rc.options_lines); + for (Enumeration e = rc.lines.elements(); e.hasMoreElements(); ) + System.out.println(e.nextElement()); + } catch (IOException e) { + System.err.println(e); + } + } +} + + + +/** This class implements a thread which runs in the background and saves a + modified newsrc file. It dies once the file has been updated; it will be + re-spawned if that newsrc file becomes dirty again. + +

There can be one of these threads per NewsRC object. + */ +class NewsRCUpdateThread extends Thread { + + static int delay = 30; // seconds + NewsRC rc = null; + + NewsRCUpdateThread(NewsRC rc) { + this.rc = rc; + setDaemon(true); + setName(getClass().getName() + " " + rc.file()); + } + + public void run() { + + try { + sleep(delay * 1000); + } catch (InterruptedException e) { + return; // is this the right way to do this? + } + + if (NewsRC.DEBUG) + System.err.println("background thread saving newsrc file " + rc.file()); + + try { + synchronized (rc) { + rc.updateThread = null; + rc.save(); + } + } catch (IOException e) { + // nothing to be done? + System.err.println("IOException while saving newsrc file " + rc.file()); + } + } +} diff --git a/mozilla/grendel/storage/NewsRCLine.java b/mozilla/grendel/storage/NewsRCLine.java new file mode 100644 index 00000000000..1e68cf60203 --- /dev/null +++ b/mozilla/grendel/storage/NewsRCLine.java @@ -0,0 +1,97 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 10 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; + + +/** This class represents a single line from a newsrc file: + a group name, whether it is subscribed, and a set of message numbers. + */ +class NewsRCLine extends NewsSet { + private NewsRC rc; + private String group_name; + private boolean subscribed; + + /** Creates a NewsRCLine with no messages marked as read. */ + NewsRCLine(NewsRC parent, String group, boolean subbed) { + super(); + rc = parent; + subscribed = subbed; + } + + /** Creates a NewsRCLine with the indicated messages marked as read. + */ + NewsRCLine(NewsRC parent, String group, boolean subbed, ByteBuf numbers) { + this(parent, group, subbed, numbers.toBytes(), 0, numbers.length()); + } + + /** As above, but takes a byte array and a subsequence into it, instead + of a ByteBuf object. */ + NewsRCLine(NewsRC parent, String name, boolean subbed, + byte[] line, int start, int end) { + super(line, start, end); + rc = parent; + group_name = name; + subscribed = subbed; + } + + /** Returns the name of this newsgroup. + */ + String name() { + return group_name; + } + + /** Returns whether this newsgroup is subscribed. + */ + boolean subscribed() { + return subscribed; + } + + /** Change whether this newsgroup is subscribed. + */ + void setSubscribed(boolean subscribed) { + if (this.subscribed != subscribed) { + this.subscribed = subscribed; + markDirty(); + } + } + + /** Converts the object back into a printed representation for the file. + This will be something like "alt.group: 1-29627,32861-32863". + */ + public void write(ByteBuf out) { + if (group_name == null) + return; + out.append(group_name); + out.append(subscribed ? ":" : "!"); + if (!isEmpty()) out.append(" "); + super.write(out); + } + + /** Called when a change is made to the set. + This just invokes the markDirty() method on the parent NewsRC object. + */ + public void markDirty() { + rc.markDirty(); + } +} diff --git a/mozilla/grendel/storage/NewsSet.java b/mozilla/grendel/storage/NewsSet.java new file mode 100644 index 00000000000..e3d8c54a76e --- /dev/null +++ b/mozilla/grendel/storage/NewsSet.java @@ -0,0 +1,1338 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 10 May 1995. + * Ported to Java, 4 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.ByteBuf; +import calypso.util.Assert; + +/** This class represents a set of integers. It uses a highly compressed + encoding, taking advantage of the assumption that many of the integers + are consecutive. This is intended for representing lines from the + .newsrc file, which have lists of message-numbers lists like +

+       1-29627,29635,29658,32861-32863
+

so the data has these properties: +

    +
  • strictly increasing; +
  • large subsequences of monotonically increasing ranges; +
  • gaps in the set are usually small, but not always; +
  • consecutive ranges tend to be large. +
+ */ + +/* The biggest win is to run-length encode the data, storing ranges as two + numbers (start+length or start,end). We could also store each number as a + delta from the previous number for further compression, but that gets kind + of tricky, since there are no guarentees about the sizes of the gaps, and + we'd have to store variable-length words. + + Current data format: + + DATA := SIZE [ CHUNK ]* + CHUNK := [ RANGE | VALUE ] + RANGE := -LENGTH START + START := VALUE + LENGTH := int32 + VALUE := a literal positive integer, for now + it could also be an offset from the previous value. + + LENGTH could also perhaps be a less-than-32-bit quantity, + at least most of the time. + + Lengths of CHUNKs are stored negative to distinguish the beginning of + a chunk from a literal: negative means two-word sequence, positive + means one-word sequence. + + 0 represents a literal 0, but should not occur, and should never occur + except in the first position. + + A length of -1 won't occur either, except temporarily - a sequence of + two elements is represented as two literals, since they take up the same + space. + + Another optimization we make is to notice that we typically ask the + question ``is N a member of the set'' for increasing values of N. So the + set holds a cache of the last value asked for, and can simply resume the + search from there. + */ + +public class NewsSet { + + private long cached_value = -1; + private int cached_value_index = 0; + + private int data_length = 0; + private int data_size = 0; + private long data[] = null; + + public NewsSet() { + data_size = 10; + data = new long[data_size]; + } + + public NewsSet(String chars) { + this(chars.getBytes(), 0, chars.length()); + } + + public NewsSet(ByteBuf chars) { + this(chars.toBytes(), 0, chars.length()); + } + + public NewsSet(byte chars[], int start, int end) { + data_size = 10; + data = new long[data_size]; + if (end == 0) return; + + int in = start; + while (in < end) { + long from = 0; + long to; + + if (data_length > data_size - 4) + // out of room! + grow(); + + while (in < end && chars[in] <= ' ') + in++; + + if (in < end && (chars[in] < '0' || chars[in] > '9')) + break; // illegal character + + while (in < end && + chars[in] >= '0' && chars[in] <= '9') + from = (from * 10) + (chars[in++] - '0'); + + while (in < end && chars[in] <= ' ') + in++; + + if (in < end && chars[in] != '-') { + to = from; + } else { + to = 0; + in++; + while (in < end && chars[in] >= '0' && chars[in] <= '9') + to = (to * 10) + (chars[in++] - '0'); + while (in < end && chars[in] <= ' ') + in++; + } + + if (to < from) to = from; /* illegal */ + + // This is a total kludge - if the newsrc file specifies a range 1-x as + // being read, we internally pretend that article 0 is read as well. + // (But if only 2-x are read, then 0 is not read.) This is needed + // because some servers think that article 0 is an article (I think) + // but some news readers (including Netscape 1.1) choke if the .newsrc + // file has lines beginning with 0... + // + if (from == 1) from = 0; + + if (to == from) { + // Write it as a literal + data[data_length++] = from; + } else { + // Write it as a range. + data[data_length++] = -(to - from); + data[data_length++] = from; + } + + while (in < end && (chars[in] == ',' || chars[in] <= ' ')) + in++; + } + } + + private void grow() { + int new_size = data_size * 2; + long new_data[] = new long[new_size]; + System.arraycopy(data, 0, new_data, 0, data_length); + data_size = new_size; + data = new_data; + } + + + /** Returns the lowest non-member of the set greater than 0. + Note that this never returns -1, since a NewsSet can't + hold the set of positive integers. + */ + public long firstNonMember() { + if (data_length <= 0) { + return 1; + } else if (data[0] < 0 && data[1] != 1 && data[1] != 0) { + // first range not equal to 0 or 1, always return 1 + return 1; + } else if (data[0] < 0) { + // it's a range + // If there is a range [N-M] we can presume that M+1 is not in the set. + return (data[1] - data[0] + 1); + } else { + // it's a literal + if (data[0] == 1) { + // handle "1,..." + if (data_length > 1 && data[1] == 2) { + // This is "1,2,M-N,..." or "1,2,M,..." where M >= 4. Note + // that M will never be 3, because in that case we would have + // started with a range: "1-3,..." */ + return 3; + } else { + return 2; // handle "1,M-N,.." or "1,M,..." where M >= 3. + } + } + else if (data[0] == 0) { + // handle "0,..." + if (data_length > 1 && data[1] == 1) { + // this is 0,1, (see above) + return 2; + } + else { + return 1; + } + + } else { + // handle "M,..." where M >= 2. + return 1; + } + } + } + + /** Returns the smallest element of the set. + Returns -1 if the set is empty. + */ + public long min() { + if (data_length > 1) { + long first = data[0]; + if (first < 0) { // is range at start? + long second = data[1]; + return (second); + } else { // no, so first number must be first member + return data[0]; + } + } else if (data_length == 1) + return data[0]; // must be only 1 read. + else + return -1; + } + + /** Returns the largest element of the set. + Returns -1 if the set is empty. + */ + public long max() { + if (data_length > 1) { + long nextToLast = data[data_length - 2]; + if (nextToLast < 0) { // is range at end? + long last = data[data_length - 1]; + return (-nextToLast + last - 1); + } else { + // no, so last number must be last member + return data[data_length - 1]; + } + } else if (data_length == 1) + return data[0]; // must be only 1 read. + else + return -1; + } + + /** Returns whether the number is a member of the set. */ + public boolean member(long number) { + + boolean value = false; + + int i = 0; + + // If there is a value cached, and that value is smaller than the + // value we're looking for, skip forward that far. + if (cached_value > 0 && cached_value < number) + i = cached_value_index; + + while (i < data_length) { + if (data[i] < 0) { + // it's a range + long from = data[i+1]; + long to = from + (-(data[i])); + + if (from > number) { + // This range begins after the number - we've passed it. + value = false; + break; + + } else if (to >= number) { + // In range. + value = true; + break; + } else { + i += 2; + } + + } else { + // it's a literal + if (data[i] == number) { + // bang + value = true; + break; + } else if (data[i] > number) { + // This literal is after the number - we've passed it. + value = false; + break; + } else { + i++; + } + } + } + + // Store the position of this chunk for next time. + cached_value = number; + cached_value_index = i; + return value; + } + + /** Cause the number to be a member of the set. + Returns false if the number was already a member of the set, + true otherwise. + */ + public boolean insert(long number) { + + int i = 0; + + if (number < 0) + throw new Error("number must be non-negative"); + + // We're going to modify the set, so invalidate the cache. + cached_value = -1; + + while (i < data_length) { + if (data[i] < 0) { + // it's a range + long from = data[i+1]; + long to = from + (-(data[i])); + + if (from <= number && to >= number) { + // This number is already present - we don't need to do anything. + return false; + } + + if (to > number) { + // We have found the point before which the new number should be + // inserted. + break; + } + + i += 2; + + } else { + // it's a literal + if (data[i] == number) { + // This number is already present - we don't need to do anything. + return false; + } + + if (data[i] > number) { + // We have found the point before which the new number should be + // inserted. + break; + } + i++; + } + } + + // At this point, `i' points to a position in the set which represents + // a value greater than `number'; or it is at `data_length'. In the + // interest of avoiding massive duplication of code, simply insert a + // literal here and then run the optimizer. + // + + if (data_size <= data_length + 1) + grow(); + + if (i >= data_length) { + // at the end: Add a literal to the end. + data[data_length++] = number; + + } else { + // need to insert (or edit) in the middle. + for (int j = data_length; j > i; j--) // open up a space; + data[j] = data[j-1]; + data[i] = number; // insert. + data_length++; + } + + optimize(); + markDirty(); + return true; + } + + /** Cause the number to not be a member of the set. + Returns true if the number had been a member of the set, + false otherwise. + */ + public boolean delete(long number) { + + if (number < 0) + throw new Error("number must be non-negative"); + + // We're going to modify the set, so invalidate the cache. + cached_value = -1; + + int i = 0; + while (i < data_length) { + int mid = i; + + if (data[i] < 0) { + // it's a range + long from = data[i+1]; + long to = from + (-(data[i])); + + if (number < from || number > to) { + // Not this range + i += 2; + continue; + } + + if (to == from + 1) { + // If this is a range [N - N+1] and we are removing M + // (which must be either N or N+1) replace it with a + // literal. This reduces the length by 1. + // + data[mid] = (number == from ? to : from); + while (++mid < data_length) { + data[mid] = data[mid+1]; + } + data_length--; + optimize(); + markDirty(); + return true; + + } else if (to == from + 2) { + // If this is a range [N - N+2] and we are removing M, + // replace it with the literals L,M (that is, either + // (N, N+1), (N, N+2), or (N+1, N+2). The overall + // length remains the same. + // + data[mid] = from; + data[mid+1] = to; + if (from == number) { + data[mid] = from+1; + } else if (to == number) { + data[mid+1] = to-1; + } + optimize(); + markDirty(); + return true; + + } else if (from == number) { + // This number is at the beginning of a long range (meaning a + // range which will still be long enough to remain a range.) + // Increase start and reduce length of the range. + // + data[mid]++; + data[mid+1]++; + optimize(); + markDirty(); + return true; + + } else if (to == number) { + // This number is at the end of a long range (meaning a range + // which will still be long enough to remain a range.) + // Just decrease the length of the range. + // + data[mid]++; + optimize(); + markDirty(); + return true; + + } else { + // The number being deleted is in the middle of a range which + // must be split. This increases overall length by 2. + // + if (data_size - data_length <= 2) + grow(); + + for (int j = data_length + 2; j > mid + 2; j--) { + data[j] = data[j-2]; + } + + data[mid] = (- (number - from - 1)); + data[mid+1] = from; + data[mid+2] = (- (to - number - 1)); + data[mid+3] = number + 1; + data_length += 2; + + // Oops, if we've ended up with a range with a 0 length, + // which is illegal, convert it to a literal, which reduces + // the overall length by 1. + // + if (data[mid] == 0) { + // first range + data[mid] = data[mid+1]; + for (int j = mid + 1; j < data_length; j++) { + data[j] = data[j+1]; + } + data_length--; + } + + if (data[mid+2] == 0) { + // second range + data[mid+2] = data[mid+3]; + for (int j = mid + 3; j < data_length; j++) { + data[j] = data[j+1]; + } + data_length--; + } + optimize(); + markDirty(); + return true; + } + + } else { + // it's a literal + if (data[i] != number) { + // Not this literal + i++; + continue; + } + + // Excise this literal. + data_length--; + while (mid < data_length) { + data[mid] = data[mid+1]; + mid++; + } + optimize(); + markDirty(); + return true; + } + } + + // It wasn't here at all. + return false; + } + + + private final int emit_range(long out[], int i, long a, long b) { + if (a == b) { + out[i++] = a; + } else { + Assert.Assertion(a < b && a >= 0); + if (a >= b || a < 0) throw new Error("emitting out of order"); + out[i++] = -(b - a); + out[i++] = a; + } + return i; + } + + + /** Cause the numbers in the range [start, end) to be members of the set. + Returns false if all of the numbers were already members of the set, + true otherwise. + */ + public boolean insert(long start, long end) { + + if (start >= end) + throw new Error("start must be < end"); + + end--; // the code here operates on [start, end], not [start, end). + + if (start == end) + return insert(start); + + boolean didit = false; + + // We're going to modify the set, so invalidate the cache. + cached_value = -1; + + int new_data_length = data_length + 2; + long new_data[] = new long[new_data_length]; + + int in = 0; + int out = 0; + + while (in < data_length) { + long a; + long b; + // Set [a,b] to be this range. + if (data[in] < 0) { + b = - data[in++]; + a = data[in++]; + b += a; + } else { + a = b = data[in++]; + } + + if (a <= start && b >= end) { + // We already have the entire range marked. + return false; + } + + if (start > b + 1) { + // No overlap yet. + out = emit_range(new_data, out, a, b); + } else if (end < a - 1) { + // No overlap, and we passed it. + out = emit_range(new_data, out, start, end); + out = emit_range(new_data, out, a, b); + didit = true; + break; + } else { + // The ranges overlap. Suck this range into our new range, and + // keep looking for other ranges that might overlap. + start = (start < a ? start : a); + end = (end > b ? end : b); + } + } + if (!didit) + out = emit_range(new_data, out, start, end); + + while (in < data_length) { + new_data[out++] = data[in++]; + } + + data = new_data; + data_length = out; + data_size = data_length; + markDirty(); + return true; + } + + + /** Cause the numbers in the range [start, end) to not be members of the set. + Returns true if any of the numbers had been members of the set, false + otherwise. + */ + public boolean delete(long start, long end) { + + if (start >= end) + throw new Error("start must be < end"); + + boolean any = false; + // #### OPTIMIZE THIS! + // #### ADD A SELF-TEST! + while (start < end) { + boolean b = delete(start++); + if (b) any = true; + } + return any; + } + + /** Returns the number of elements in the range [start, end) + which are not members of the set. + */ + public long countMissingInRange(long range_start, long range_end) { + + if (range_start >= range_end) + throw new Error("range_start must be < range_end"); + + // #### ADD A SELF-TEST! + + range_end--; // the code operates on [start, end], not [start, end). + + long count = range_end - range_start + 1; + + int i = 0; + while (i < data_length) { + if (data[i] < 0) { + // it's a range + long from = data[i+1]; + long to = from + (-(data[i])); + if (from < range_start) from = range_start; + if (to > range_end) to = range_end; + + if (to >= from) + count -= (to - from + 1); + + i += 2; + } else { + // it's a literal + if (data[i] >= range_start && data[i]<= range_end) + count--; + i++; + } + } + return count; + } + + /** Returns the first number which is not a member of the set, + which falls in the range [min, max). Returns -1 if all numbers + in the range are members. + */ + public long firstNonMember(long min, long max) { + + if (min >= max) + throw new Error("min must be < max"); + + // #### OPTIMIZE THIS! + while (min < max) { + if (!member(min)) + return min; + else + min++; + } + return -1; + } + + /** Returns the last number which is not a member of the set, + which falls in the range [min, max). If all numbers in + that range are members of the set, returns -1. + */ + public long lastNonMember(long min, long max) { + + if (min >= max) + throw new Error("min must be < max"); + + // #### OPTIMIZE THIS! + max--; + while (max >= min) { + if (!member(max)) + return max; + else + max--; + } + return -1; + } + + /** Returns the first number which is in the set, and which + is greater than the given value. Returns -1 if no number + greater than the given value is in the set. + */ + public long nextMember(long after) { + // #### OPTIMIZE THIS! + long end = max(); + for (long start = after+1; start < end; start++) { + if (member(start)) + return start; + } + return -1; + } + + /** Returns the last number which is in the set, and which + is less than the given value. Returns -1 if the smallest + member of the set is greater than or equal to the given + value. + */ + public long previousMember(long after) { + // #### OPTIMIZE THIS! + for (long i = after-1; i >= 0; i--) { + if (member(i)) + return i; + } + return -1; + } + + /** Re-compresses the data in the set. +

+ The assumption is made that the data in the set is syntactically + correct (all ranges have a length of at least 1, and all values + are non-decreasing) but we can optimize the compression, for + example, merging consecutive literals or ranges into one range. + */ + private void optimize() { + + int input_size = data_size; + int input_length = data_length; + long input[] = data; + int in = 0; + + int output_size = data_size + 1; + int output_length = 0; + long output[] = new long[output_size]; + int out = 0; + + // We're going to modify the set, so invalidate the cache. + cached_value = -1; + + while (in < input_length) { + long from, to; + boolean range_p = (input[in] < 0); + + if (range_p) { + // it's a range + from = input[in+1]; + to = from + (-(input[in+0])); + + // Copy it over + output[out++] = input[in++]; + output[out++] = input[in++]; + } else { + // it's a literal + from = input[in]; + to = from; + + // Copy it over + output[out++] = input[in++]; + } + + // As long as this chunk is followed by consecutive chunks, + // keep extending it. + while (in < input_length && + ((input[in] > 0 && // literal... + input[in] == to + 1) || // ...and consecutive, or + (input[in] <= 0 && // range... + input[in+1] == to + 1)) // ...and consecutive. + ) { + if (! range_p) { + // convert the literal to a range. + out++; + output[out-2] = 0; + output[out-1] = from; + range_p = true; + } + + if (input[in] > 0) { // literal + output[out-2]--; // increase length by 1 + to++; + in++; + } else { + long L2 = (- input[in]) + 1; + output[out-2] -= L2; // increase length by N + to += L2; + in += 2; + } + } + } + + data = output; + data_length = out; + data_size = output_size; + + // One last pass to turn [N - N+1] into [N, N+1]. + out = 0; + while (out < output_length) { + if (output[out] < 0) { + // it's a range + if (output[out] == -1) { + output[out] = output[out+1]; + output[out+1]++; + } + out += 2; + } else { + // it's a literal + out++; + } + } + + markDirty(); + } + + /** True if there are no elements in the set. */ + protected boolean isEmpty() { + return (data_length == 0); + } + + /** Called when a change is made to the set. + This method does nothing, but is provided for the benefit of + subclasses. + */ + public void markDirty() { + } + + /** Converts a printed representation of the numbers in the set. + This will be something like "1-29627,29635,29658,32861-32863", + which is the same representation that new NewsSet() expects. + */ + public void write(ByteBuf out) { + int i = 0; + while (i < data_length) { + long from; + long to; + + if (i != 0) + out.append(","); + + if (data[i] < 0) { + // it's a range + from = data[i+1]; + to = from + (-(data[i])); + i += 2; + } else { + // it's a literal + from = data[i]; + to = from; + i++; + } + + if (from == 0) + // See 'kludge' comment above + from = 1; + + out.append(String.valueOf(from)); + if (from < to) { + out.append("-"); + out.append(String.valueOf(to)); + } + } + } + + public String toString() { + ByteBuf b = new ByteBuf(data_length * 4); + write(b); + return b.toString(); + } + + + + /************************************************************************* + Self tests + *************************************************************************/ + + + protected static void self_test_decoder(String s, String target) { + String r = new NewsSet(s).toString(); + if (!target.equals(r)) + System.err.println("failed decoder test:\n\t" + + s + " =\n\t" + r + " instead of\n\t" + target); + } + + protected static void self_test_decoder() { + self_test_decoder ("", ""); + self_test_decoder (" ", "1"); + self_test_decoder ("0", "1"); + self_test_decoder ("1", "1"); + self_test_decoder ("123", "123"); + self_test_decoder (" 123 ", "123"); + self_test_decoder (" 123 4", "123,4"); + self_test_decoder (" 1,2, 3, 4", "1,2,3,4"); + self_test_decoder ("0-70,72-99,100,101", "1-70,72-99,100,101"); + self_test_decoder (" 0-70 , 72 - 99 ,100,101 ", "1-70,72,99,100,101"); + self_test_decoder ("0 - 268435455", "1,268435455"); + self_test_decoder ("0 - 4294967295", "1,4294967295"); + self_test_decoder ("0 - 9223372036854775807", "1,9223372036854775807"); + // This one overflows - we can't help it. + // self_test_decoder ("0 - 9223372036854775808", "0-9223372036854775808"); + } + + + protected static void self_test_adder(NewsSet set, + boolean add_p, + long value, + String target) { + String old = set.toString(); + if (add_p) + set.insert(value); + else + set.delete(value); + String s = set.toString(); + if (!s.equals(target)) + System.err.println("failed adder test:\n\t" + + old + (add_p ? " + " : " - ") + value + + " =\n\t" + s + " instead of\n\t" + target); + } + + protected static void self_test_adder() { + NewsSet set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_adder(set, true, 205, + "1-70,72-99,105,107,110-111,117-200,205"); + self_test_adder(set, true, 206, + "1-70,72-99,105,107,110-111,117-200,205-206"); + self_test_adder(set, true, 207, + "1-70,72-99,105,107,110-111,117-200,205-207"); + self_test_adder(set, true, 208, + "1-70,72-99,105,107,110-111,117-200,205-208"); + self_test_adder(set, true, 208, + "1-70,72-99,105,107,110-111,117-200,205-208"); + self_test_adder(set, true, 109, + "1-70,72-99,105,107,109-111,117-200,205-208"); + self_test_adder(set, true, 72, + "1-70,72-99,105,107,109-111,117-200,205-208"); + + self_test_adder(set, false, 205, + "1-70,72-99,105,107,109-111,117-200,206-208"); + self_test_adder(set, false, 206, + "1-70,72-99,105,107,109-111,117-200,207-208"); + self_test_adder(set, false, 207, + "1-70,72-99,105,107,109-111,117-200,208"); + self_test_adder(set, false, 208, + "1-70,72-99,105,107,109-111,117-200"); + self_test_adder(set, false, 208, + "1-70,72-99,105,107,109-111,117-200"); + self_test_adder(set, false, 109, + "1-70,72-99,105,107,110-111,117-200"); + self_test_adder(set, false, 72, + "1-70,73-99,105,107,110-111,117-200"); + + self_test_adder(set, true, 72, + "1-70,72-99,105,107,110-111,117-200"); + self_test_adder(set, true, 109, + "1-70,72-99,105,107,109-111,117-200"); + self_test_adder(set, true, 208, + "1-70,72-99,105,107,109-111,117-200,208"); + self_test_adder(set, true, 208, + "1-70,72-99,105,107,109-111,117-200,208"); + self_test_adder(set, true, 207, + "1-70,72-99,105,107,109-111,117-200,207-208"); + self_test_adder(set, true, 206, + "1-70,72-99,105,107,109-111,117-200,206-208"); + self_test_adder(set, true, 205, + "1-70,72-99,105,107,109-111,117-200,205-208"); + + self_test_adder(set, false, 205, + "1-70,72-99,105,107,109-111,117-200,206-208"); + self_test_adder(set, false, 206, + "1-70,72-99,105,107,109-111,117-200,207-208"); + self_test_adder(set, false, 207, + "1-70,72-99,105,107,109-111,117-200,208"); + self_test_adder(set, false, 208, + "1-70,72-99,105,107,109-111,117-200"); + self_test_adder(set, false, 208, + "1-70,72-99,105,107,109-111,117-200"); + self_test_adder(set, false, 109, + "1-70,72-99,105,107,110-111,117-200"); + self_test_adder(set, false, 72, + "1-70,73-99,105,107,110-111,117-200"); + + self_test_adder(set, true, 100, + "1-70,73-100,105,107,110-111,117-200"); + self_test_adder(set, true, 101, + "1-70,73-101,105,107,110-111,117-200"); + self_test_adder(set, true, 102, + "1-70,73-102,105,107,110-111,117-200"); + self_test_adder(set, true, 103, + "1-70,73-103,105,107,110-111,117-200"); + self_test_adder(set, true, 106, + "1-70,73-103,105-107,110-111,117-200"); + self_test_adder(set, true, 104, + "1-70,73-107,110-111,117-200"); + self_test_adder(set, true, 109, + "1-70,73-107,109-111,117-200"); + self_test_adder(set, true, 108, + "1-70,73-111,117-200"); + } + + protected static void self_test_ranges(NewsSet set, long start, long end, + String target) { + String old = set.toString(); + set.insert(start, end); + String s = set.toString(); + if (!s.equals(target)) + System.err.println("failed range test:\n\t" + + old + " + " + start + "-" + end + + " =\n\t" + s + " instead of\n\t" + target); + } + + + protected static void self_test_ranges() { + NewsSet set = new NewsSet("20-40,72-99,105,107,110-111,117-200"); + self_test_ranges(set, 205, 209, + "20-40,72-99,105,107,110-111,117-200,205-208"); + self_test_ranges(set, 50, 71, + "20-40,50-70,72-99,105,107,110-111,117-200,205-208"); + self_test_ranges(set, 0, 11, + "1-10,20-40,50-70,72-99,105,107,110-111,117-200,205-208"); + self_test_ranges(set, 112, 114, + "1-10,20-40,50-70,72-99,105,107,110-113,117-200,205-208"); + self_test_ranges(set, 101, 102, + "1-10,20-40,50-70,72-99,101,105,107,110-113,117-200,205-208"); + self_test_ranges(set, 5, 76, + "1-99,101,105,107,110-113,117-200,205-208"); + self_test_ranges(set, 103, 110, + "1-99,101,103-113,117-200,205-208"); + self_test_ranges(set, 2, 21, + "1-99,101,103-113,117-200,205-208"); + self_test_ranges(set, 1, 10000, + "1-9999"); + } + + + protected static void self_test_member(NewsSet set, boolean cache, long elt, + boolean target) { + String old = set.toString(); + if (!cache) + set.cached_value = -1; + boolean result = set.member(elt); + if (target != result) + System.err.println("failed " + (cache ? "" : "non-") + + "cache member test:\n\t" + elt + " in " + old + + " = " + result + ", not " + target); + } + + protected static void self_test_first_nonmember(NewsSet set, boolean cache, + long start, long end, + long target) { + String old = set.toString(); + if (!cache) + set.cached_value = -1; + long result = set.firstNonMember(start, end); + if (target != result) + System.err.println("failed " + (cache ? "" : "non-") + + "cache firstNonMember test:\n\t" + + start + "-" + end + " in " + + old + " = " + result + ", not " + target); + } + + protected static void self_test_last_nonmember(NewsSet set, boolean cache, + long start, long end, + long target) { + String old = set.toString(); + if (!cache) + set.cached_value = -1; + long result = set.lastNonMember(start, end); + if (target != result) + System.err.println("failed " + (cache ? "" : "non-") + + "cache lastNonMember test:\n\t" + + start + "-" + end + " in " + + old + " = " + result + ", not " + target); + } + + protected static void self_test_next_member(NewsSet set, boolean cache, + long elt, long target) { + String old = set.toString(); + if (!cache) + set.cached_value = -1; + long result = set.nextMember(elt); + if (target != result) + System.err.println("failed " + (cache ? "" : "non-") + + "cache nextMember test:\n\t" + elt + " in " + + old + " = " + result + ", not " + target); + } + + protected static void self_test_prev_member(NewsSet set, boolean cache, + long elt, long target) { + String old = set.toString(); + if (!cache) + set.cached_value = -1; + long result = set.previousMember(elt); + if (target != result) + System.err.println("failed " + (cache ? "" : "non-") + + "cache previousMember test:\n\t" + elt + " in " + + old + " = " + result + ", not " + target); + } + + protected static void self_test_member(boolean cache) { + NewsSet set; + + set = new NewsSet("1-70,72-99,105,107,110-111,117-200"); + self_test_member(set, cache, -1, false); + self_test_member(set, cache, 0, true); + self_test_member(set, cache, 1, true); + self_test_member(set, cache, 20, true); + + set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_member(set, cache, -1, false); + self_test_member(set, cache, 0, true); + self_test_member(set, cache, 1, true); + self_test_member(set, cache, 20, true); + self_test_member(set, cache, 69, true); + self_test_member(set, cache, 70, true); + self_test_member(set, cache, 71, false); + self_test_member(set, cache, 72, true); + self_test_member(set, cache, 73, true); + self_test_member(set, cache, 74, true); + self_test_member(set, cache, 104, false); + self_test_member(set, cache, 105, true); + self_test_member(set, cache, 106, false); + self_test_member(set, cache, 107, true); + self_test_member(set, cache, 108, false); + self_test_member(set, cache, 109, false); + self_test_member(set, cache, 110, true); + self_test_member(set, cache, 111, true); + self_test_member(set, cache, 112, false); + self_test_member(set, cache, 116, false); + self_test_member(set, cache, 117, true); + self_test_member(set, cache, 118, true); + self_test_member(set, cache, 119, true); + self_test_member(set, cache, 200, true); + self_test_member(set, cache, 201, false); + self_test_member(set, cache, 65535, false); + } + + protected static void self_test_first_nonmember(boolean cache) { + NewsSet set; + + set = new NewsSet("1-70,72-99,105,107,110-111,117-200"); + self_test_first_nonmember(set, cache, -1, 75, -1); + self_test_first_nonmember(set, cache, 0, 32, -1); + self_test_first_nonmember(set, cache, 1, 88, 71); + self_test_first_nonmember(set, cache, 20, 70, -1); + self_test_first_nonmember(set, cache, 20, 71, -1); + self_test_first_nonmember(set, cache, 20, 72, 71); + self_test_first_nonmember(set, cache, 20, 500, 71); + self_test_first_nonmember(set, cache, 71, 90, 71); + self_test_first_nonmember(set, cache, 72, 90, -1); + + set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_first_nonmember(set, cache, -1, 75, -1); + self_test_first_nonmember(set, cache, 0, 32, -1); + self_test_first_nonmember(set, cache, 1, 88, 71); + self_test_first_nonmember(set, cache, 20, 200, 71); + self_test_first_nonmember(set, cache, 69, 100, 71); + self_test_first_nonmember(set, cache, 70, 100, 71); + self_test_first_nonmember(set, cache, 71, 100, 71); + self_test_first_nonmember(set, cache, 72, 100, -1); + self_test_first_nonmember(set, cache, 73, 100, -1); + self_test_first_nonmember(set, cache, 74, 100, -1); + self_test_first_nonmember(set, cache, 104, 300, 104); + self_test_first_nonmember(set, cache, 105, 300, 106); + self_test_first_nonmember(set, cache, 106, 200, 106); + self_test_first_nonmember(set, cache, 107, 300, 108); + self_test_first_nonmember(set, cache, 108, 200, 108); + self_test_first_nonmember(set, cache, 109, 200, 109); + self_test_first_nonmember(set, cache, 110, 200, 112); + self_test_first_nonmember(set, cache, 111, 300, 112); + self_test_first_nonmember(set, cache, 112, 300, 112); + self_test_first_nonmember(set, cache, 116, 300, 116); + self_test_first_nonmember(set, cache, 117, 300, 201); + self_test_first_nonmember(set, cache, 118, 300, 201); + self_test_first_nonmember(set, cache, 119, 300, 201); + self_test_first_nonmember(set, cache, 200, 300, 201); + self_test_first_nonmember(set, cache, 201, 300, 201); + self_test_first_nonmember(set, cache, 65535, 99999, 65535); + } + + protected static void self_test_last_nonmember(boolean cache) { + NewsSet set; + + set = new NewsSet("1-70,72-99,105,107,110-111,117-200"); + self_test_last_nonmember(set, cache, -1, 75, 71); + self_test_last_nonmember(set, cache, 0, 32, -1); + self_test_last_nonmember(set, cache, 1, 88, 71); + self_test_last_nonmember(set, cache, 20, 70, -1); + self_test_last_nonmember(set, cache, 20, 71, -1); + self_test_last_nonmember(set, cache, 20, 72, 71); + self_test_last_nonmember(set, cache, 20, 500, 499); + self_test_last_nonmember(set, cache, 71, 90, 71); + self_test_last_nonmember(set, cache, 72, 90, -1); + + set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_last_nonmember(set, cache, -1, 75, 71); + self_test_last_nonmember(set, cache, 0, 32, -1); + self_test_last_nonmember(set, cache, 1, 88, 71); + self_test_last_nonmember(set, cache, 20, 200, 116); + self_test_last_nonmember(set, cache, 69, 100, 71); + self_test_last_nonmember(set, cache, 70, 100, 71); + self_test_last_nonmember(set, cache, 71, 100, 71); + self_test_last_nonmember(set, cache, 72, 100, -1); + self_test_last_nonmember(set, cache, 73, 100, -1); + self_test_last_nonmember(set, cache, 74, 100, -1); + self_test_last_nonmember(set, cache, 104, 300, 299); + self_test_last_nonmember(set, cache, 105, 300, 299); + self_test_last_nonmember(set, cache, 106, 200, 116); + self_test_last_nonmember(set, cache, 107, 300, 299); + self_test_last_nonmember(set, cache, 108, 200, 116); + self_test_last_nonmember(set, cache, 109, 200, 116); + self_test_last_nonmember(set, cache, 110, 200, 116); + self_test_last_nonmember(set, cache, 111, 300, 299); + self_test_last_nonmember(set, cache, 112, 300, 299); + self_test_last_nonmember(set, cache, 116, 300, 299); + self_test_last_nonmember(set, cache, 117, 300, 299); + self_test_last_nonmember(set, cache, 118, 300, 299); + self_test_last_nonmember(set, cache, 119, 300, 299); + self_test_last_nonmember(set, cache, 200, 300, 299); + self_test_last_nonmember(set, cache, 201, 300, 299); + self_test_last_nonmember(set, cache, 65535, 99999, 99998); + } + + protected static void self_test_next_member(boolean cache) { + NewsSet set; + + set = new NewsSet("1-70,72-99,105,107,110-111,117-200"); + self_test_next_member(set, cache, -1, 0); + self_test_next_member(set, cache, 0, 1); + self_test_next_member(set, cache, 1, 2); + self_test_next_member(set, cache, 20, 21); + + set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_next_member(set, cache, -1, 0); + self_test_next_member(set, cache, 0, 1); + self_test_next_member(set, cache, 1, 2); + self_test_next_member(set, cache, 20, 21); + self_test_next_member(set, cache, 69, 70); + self_test_next_member(set, cache, 70, 72); + self_test_next_member(set, cache, 71, 72); + self_test_next_member(set, cache, 72, 73); + self_test_next_member(set, cache, 73, 74); + self_test_next_member(set, cache, 74, 75); + self_test_next_member(set, cache, 104, 105); + self_test_next_member(set, cache, 105, 107); + self_test_next_member(set, cache, 106, 107); + self_test_next_member(set, cache, 107, 110); + self_test_next_member(set, cache, 108, 110); + self_test_next_member(set, cache, 109, 110); + self_test_next_member(set, cache, 110, 111); + self_test_next_member(set, cache, 111, 117); + self_test_next_member(set, cache, 112, 117); + self_test_next_member(set, cache, 116, 117); + self_test_next_member(set, cache, 117, 118); + self_test_next_member(set, cache, 118, 119); + self_test_next_member(set, cache, 119, 120); + self_test_next_member(set, cache, 200, -1); + self_test_next_member(set, cache, 201, -1); + self_test_next_member(set, cache, 65535, -1); + } + + protected static void self_test_prev_member(boolean cache) { + NewsSet set; + + set = new NewsSet("1-70,72-99,105,107,110-111,117-200"); + self_test_prev_member(set, cache, -1, -1); + self_test_prev_member(set, cache, 0, -1); + self_test_prev_member(set, cache, 1, 0); + self_test_prev_member(set, cache, 20, 19); + + set = new NewsSet("0-70,72-99,105,107,110-111,117-200"); + self_test_prev_member(set, cache, -1, -1); + self_test_prev_member(set, cache, 0, -1); + self_test_prev_member(set, cache, 1, 0); + self_test_prev_member(set, cache, 20, 19); + self_test_prev_member(set, cache, 69, 68); + self_test_prev_member(set, cache, 70, 69); + self_test_prev_member(set, cache, 71, 70); + self_test_prev_member(set, cache, 72, 70); + self_test_prev_member(set, cache, 73, 72); + self_test_prev_member(set, cache, 74, 73); + self_test_prev_member(set, cache, 104, 99); + self_test_prev_member(set, cache, 105, 99); + self_test_prev_member(set, cache, 106, 105); + self_test_prev_member(set, cache, 107, 105); + self_test_prev_member(set, cache, 108, 107); + self_test_prev_member(set, cache, 109, 107); + self_test_prev_member(set, cache, 110, 107); + self_test_prev_member(set, cache, 111, 110); + self_test_prev_member(set, cache, 112, 111); + self_test_prev_member(set, cache, 116, 111); + self_test_prev_member(set, cache, 117, 111); + self_test_prev_member(set, cache, 118, 117); + self_test_prev_member(set, cache, 119, 118); + self_test_prev_member(set, cache, 200, 199); + self_test_prev_member(set, cache, 201, 200); + self_test_prev_member(set, cache, 202, 200); + self_test_prev_member(set, cache, 203, 200); + self_test_prev_member(set, cache, 300, 200); + self_test_prev_member(set, cache, 65535, 200); + } + + protected static void self_test() { + self_test_decoder(); + self_test_adder(); + self_test_ranges(); + self_test_member(false); + self_test_member(true); + self_test_first_nonmember(true); + self_test_first_nonmember(false); + self_test_last_nonmember(true); + self_test_last_nonmember(false); + self_test_next_member(true); + self_test_next_member(false); + self_test_prev_member(true); + self_test_prev_member(false); + } + + public static void main(String args[]) { + self_test(); + } +} diff --git a/mozilla/grendel/storage/NewsStore.java b/mozilla/grendel/storage/NewsStore.java new file mode 100644 index 00000000000..357e24a2311 --- /dev/null +++ b/mozilla/grendel/storage/NewsStore.java @@ -0,0 +1,297 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 20 Nov 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import java.util.Enumeration; + +import java.awt.Component; +import java.net.URL; +import java.net.UnknownHostException; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.URLName; +//import javax.mail.event.ConnectionEvent; +import javax.mail.event.StoreEvent; + + +/** Store for News (NNTP) folders. +

+ This class really shouldn't be public, but I haven't figured out how to + tie into javamail's Session class properly. So, instead of using + Session.getStore(String), you instead need to call + NewsStore.GetDefaultStore(Session). + */ + +public class NewsStore extends Store { + + protected NNTPConnection nntp = null; + protected NewsRC newsrc = null; + protected NewsFolderRoot root_folder = null; + + static protected NewsStore DefaultStore = null; + + public static Store GetDefaultStore(Session s) { + if (DefaultStore == null) { + DefaultStore = new NewsStore(s); + } + return DefaultStore; + } + + + public NewsStore(Session s) { + super(s, null); + } + + public NewsStore(Session s, URLName u) { + super(s, u); + } + + NewsRC getNewsRC() { + return newsrc; + } + + private void loadNewsRC(String host) throws IOException { + Assert.Assertion(newsrc == null); + String dir = System.getProperty("user.home"); + boolean secure = false; // #### + String name = null; + + if (Constants.ISUNIX) { + + String name1 = (secure ? ".snewsrc" : ".newsrc"); + String name2 = name1 + "-" + host; + if (new File(dir, name2).exists()) + name = name2; + else if (new File(dir, name1).exists()) + name = name1; + else + name = name2; + + } else if (Constants.ISWINDOWS) { + + String name1 = (secure ? "snews" : "news"); + String name2 = name1 + "-" + host + ".rc"; + String name3 = name1 + ".rc"; + if (new File(dir, name2).exists()) + name = name2; + else if (new File(dir, name3).exists()) + name = name3; + else + name = name2; + + } else { + Assert.NotYetImplemented("newsrc names only implemented for windows 'n' unix..."); + } + + File f = new File(dir, name); + newsrc = new NewsRC(f); + + // If the newsrc file didn't exist, subscribe to some default newsgroups. + if (!f.exists()) { + String subs[] = getDefaultSubscriptions(); + for (int i = 0; i < subs.length; i++) { + NewsRCLine ng = newsrc.getNewsgroup(subs[i]); + ng.setSubscribed(true); + } + } + + } + + + protected boolean protocolConnect(String host, + String user, + String password) + throws MessagingException { + + if (nntp != null) + return true; // Already connected. + + if (host == null) { + // #### better name? + host = session.getProperty("mail.default_nntp_server"); + if (host == null || host == "") { + // #### pop up a dialog box instead? + host = "news"; + } + } + + try { + loadNewsRC(host); + } catch (IOException e) { + throw new MessagingException("loading newsrc file", e); + } + + nntp = new NNTPConnection(); + + int port = -1; // #### override this from the url or something. + + try { + boolean status = nntp.connect(host, port, user, password); + if (!status) return false; + + } catch (UnknownHostException e) { // This sucks, Beavis! + throw new MessagingException("Unknown host", e); + } catch (IOException e) { + throw new MessagingException("I/O Error", e); + } + + if (!newsrc.file().exists()) { + // #### subscribe to the server's default groups + } + + notifyStoreListeners(StoreEvent.NOTICE, "opened"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.OPENED); + return true; + } + + public void close() { + + if (nntp == null) { + // already closed. + Assert.Assertion(newsrc == null); + return; + } + + nntp.close(); + nntp = null; + + Assert.Assertion(newsrc != null); + if (newsrc != null) { + try { + newsrc.save(); + } catch (IOException e) { + // Sun doesn't allow us to signal here. Bastards... + // Just ignore it, I guess... + } + } + + notifyStoreListeners(StoreEvent.NOTICE, "closed"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.CLOSED); + root_folder = null; + } + + + public Folder getDefaultFolder() { + if (root_folder == null) { + synchronized (this) { + if (root_folder == null) { // double check + root_folder = new NewsFolderRoot(this); + } + } + } + return root_folder; + } + + public Folder getFolder(String name) throws MessagingException { + return getDefaultFolder().getFolder(name); + } + + public Folder getFolder(URL url) { + Assert.NotYetImplemented("NewsStore.getFolder(URL)"); + return null; + } + + public Folder getFolder(URLName urlName) { + Assert.NotYetImplemented("NewsStore.getFolder(URLName)"); + return null; + } + + InputStream getMessageStream(NewsMessage message, boolean headers_too) + throws NNTPException, IOException { + Folder f = message.getFolder(); + String group = (f == null ? null : f.getFullName()); + int n = (group == null ? -1 : message.getStorageFolderIndex()); + if (n == -1) { +/* + String id = #### ; + if (headers_too) + return nntp.ARTICLE(id); + else + return nntp.BODY(id); + */ + Assert.NotYetImplemented("NewsStore.getMessageStream via Message-ID"); + return null; + } else { + if (headers_too) + return nntp.ARTICLE(group, n); + else + return nntp.BODY(group, n); + } + } + + /** Returns array of int: [ nmessages low hi ] + */ + int[] getGroupCounts(NewsFolder folder) { + String group = folder.getFullName(); + try { + return nntp.GROUP(group); + } catch (IOException e) { + return null; + } + } + + /** Returns a list of newsgroups to which new users should be subscribed, + if they don't have a newsrc file. Tries to ask the news server for + this list; otherwise, uses some builtin defaults. + */ + String[] getDefaultSubscriptions() { + String s[] = null; + try { + s = nntp.LIST_SUBSCRIPTIONS(); + } catch (NNTPException e) { // command unsupported + } catch (IOException e) { // something else + } + if (s == null || s.length == 0) + s = new String[] { "news.announce.newusers", + "news.newusers.questions", + "news.groups.questions", + "alt.fan.mozilla" + }; + return s; + } + + void openNewsgroup(NewsFolder folder, long from, long to) { + Enumeration e; + try { + e = nntp.getMessages(folder, from, to); + } catch (NNTPException ex) { + return; + } catch (IOException ex) { + return; + } + + while (e.hasMoreElements()) { + Message m = (Message) e.nextElement(); + folder.noticeInitialMessage(m); + } + } +} diff --git a/mozilla/grendel/storage/Obarray.java b/mozilla/grendel/storage/Obarray.java new file mode 100644 index 00000000000..ec3fd2beefd --- /dev/null +++ b/mozilla/grendel/storage/Obarray.java @@ -0,0 +1,155 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 20 Sep 1997. + */ + +package grendel.storage; + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import calypso.util.ByteBuf; +import java.util.Hashtable; + + +/** Base class for objects which enable interning (uniquification) of + objects. + */ +abstract class Obarray { + + /** Holds unique (non-equal) objects. + */ + protected Object array[]; + + /** How much of the array is in use; we don't use Vector for this because + Vector likes to expand itself by doubling its size, and we will have + enough entries that that would suck. + */ + protected int count; + + /** Hashes from object keys to index-in-the-array values. (Which are + unfortunately `Integer' objects instead of ints, because the Java + object model is hopelessly stupid about such things.) + */ + protected Hashtable hashtable; + + Obarray(int default_size) { + hashtable = new Hashtable(default_size); + array = new Object[default_size]; + count = 0; + } + + Obarray() { + this(100); + } + + /** Check whether there is an object representing the given subsequence + of bytes in the table already. This should end up doing hashtable.get() + and returning null, or an object from the table (an Integer object.) + */ + abstract protected Object checkHash(byte bytes[], int start, int length); + + /** Creates a new object (which will then be interned.) + */ + abstract protected Object newInternable(byte bytes[], int start, int length); + + + protected synchronized void ensureCapacity(int count) { + // we're adding an item; grow the array if necessary. + if (count >= array.length) { + int new_size = ((array.length * 13) / 10) + 10; // grow by 30% + 10. + Object new_array[] = new Object[new_size]; + System.arraycopy(array, 0, new_array, 0, array.length); + array = new_array; + } + } + + + /** Returns the numeric ID of an object containing the given bytes. + This will be reused if possible; otherwise, a new one will be made + and remembered in the table. +

+ The ID-number may be turned into an interned object with getObject(). + */ + public synchronized int intern(byte bytes[], int start, int length) { + + if (length <= 0) return -1; + + Object hashed = checkHash(bytes, start, length); + + if (hashed != null) { + return ((Integer) hashed).intValue(); + } else { + ensureCapacity(count+1); + Object b = newInternable(bytes, start, length); + hashtable.put(b, new Integer(count)); + array[count] = b; + count++; + return count-1; + } + } + + public int intern(ByteBuf buf) { + return intern(buf.toBytes(), 0, buf.length()); + } + + public int intern(String s) { + byte b[] = s.getBytes(); + return intern(b, 0, b.length); + } + + /** Given a numberic ID number, converts it to the corresponding + object in this table. This ID number should be one that + was previously returned by the table's `intern()' method. + */ + public Object getObject(int id) { + if (id == -1 || count <= id) + return null; + else + return array[id]; + } + + public boolean isEmpty() { + return (count == 0); + } + + public int size() { + return count; + } + + public void clear() { + hashtable.clear(); + while (count > 0) + array[--count] = null; + } + + public Enumeration elements() { + return new ObarrayEnumeration(this); + } +} + +class ObarrayEnumeration implements Enumeration { + int i = 0; + Obarray a; + public ObarrayEnumeration(Obarray a) { this.a = a; } + public Object nextElement() { + if (i < a.count) return a.array[i++]; + else throw new NoSuchElementException(); + } + public boolean hasMoreElements() { return (a != null && i < a.count); } +} diff --git a/mozilla/grendel/storage/PackedIntArray.java b/mozilla/grendel/storage/PackedIntArray.java new file mode 100644 index 00000000000..f294050a818 --- /dev/null +++ b/mozilla/grendel/storage/PackedIntArray.java @@ -0,0 +1,227 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 16 Sep 1997. + */ + +package grendel.storage; + +/** This class implements a fixed-size array capable of holding `long' + values in the range [0, 1<<60). It packs the data efficiently, + so that if the array actually only holds values less than 1<<15, + it will take up no more space than a short[] array. (The overhead + is 1 bit per 16.) + */ +class PackedIntArray { + private short data[] = null; + + /** Make a new, empty PackedIntArray. Use setCapacity() to initialize it. + */ + PackedIntArray() { + } + + /** Make a new PackedIntArray capable of holding at most size + elements. */ + PackedIntArray(int size) { + setCapacity(size); + } + + /** Set the number of elements this object is capable of holding. + If the size is reduced, some elements will be discarded. + If the size is increased, the new elements will default to 0. + */ + void setCapacity(int size) { + if (data == null) + data = new short[size]; + else { + long old_data[] = new long[data.length]; + int old_size = 0; + try { + for (old_size = 0; old_size < data.length; old_size++) + old_data[old_size] = elementAt(old_size); + } catch (ArrayIndexOutOfBoundsException c) { + } + + data = new short[size]; + for (int i = 0; i < old_size; i++) + setElementAt(i, old_data[i]); + } + } + + /** Returns the nth element as a long. */ + long elementAt(int index) { + + int i = 0; + // find the index-th entry in the set + for (int j = 0; j < index; j++) { + while ((data[i] & 0x8000) != 0) + i++; + i++; + } + + // pack together the bytes composing that entry + long result = 0; + int n; + do { + n = ((int) data[i++]) & 0xFFFF; + result = (result << 15) | (n & 0x7FFF); + } while ((n & 0x8000) != 0); + + return result; + } + + /** Stores the given value into the nth slot. + The value must be in the range [0, 1<<60), or + ArrayIndexOutOfBoundsException is thrown. + */ + void setElementAt(int index, long value) { + + if (value < 0 || value >= (1L << 60)) // 15 bits per short; 15*4=60. + throw new ArrayIndexOutOfBoundsException(); + + int i = 0; + // find the index-th entry in the set + for (int j = 0; j < index; j++) { + while ((data[i] & 0x8000) != 0) + i++; + i++; + } + + // find the length of that entry + int old_length = 0; + while ((data[i + old_length] & 0x8000) != 0) + old_length++; + old_length++; + + // find the length of the new value + int new_length = ((value < (1L << 15)) ? 1 : + (value < (1L << 30)) ? 2 : + (value < (1L << 45)) ? 3 : + 4); + + if (new_length != old_length) { + // change the size used by this number in the middle of the array. + short new_data[] = new short[data.length + (new_length - old_length)]; + System.arraycopy(data, 0, new_data, 0, i+1); + System.arraycopy(data, i + old_length, + new_data, i + new_length, + data.length - (i + old_length)); + data = new_data; + } + + // write the packed bytes into the array + + if (new_length >= 4) + data[i++] = (short) (((value >> 45) & 0x7FFF) | 0x8000); + if (new_length >= 3) + data[i++] = (short) (((value >> 30) & 0x7FFF) | 0x8000); + if (new_length >= 2) + data[i++] = (short) (((value >> 15) & 0x7FFF) | 0x8000); + data[i] = (short) (value & 0x7FFF); + } + + + public String toString() { + String r = "["; +/* + for (int i = 0; i < data.length; i++) { + String s = Integer.toHexString(((int) data[i]) & 0xFFFF); + if (s.length() == 1) s = "000" + s; + else if (s.length() == 2) s = "00" + s; + else if (s.length() == 3) s = "0" + s; + r += " 0x" + s; + } + r += " /"; + */ + try { + int i = 0; + while (true) + r += " " + elementAt(i++); + } catch (ArrayIndexOutOfBoundsException c) { + } + + r += " ]"; + return r; + } + + + private void check(long i0, long i1, long i2, long i3, long i4) { + if (elementAt(0) != i0 || elementAt(1) != i1 || elementAt(2) != i2 || + elementAt(3) != i3 || elementAt(4) != i4) + throw new Error("self test failed"); + } + + public static void selfTest() { + PackedIntArray p = new PackedIntArray(5); + p.check(0, 0, 0, 0, 0); + + p.setElementAt(0, 0); + p.setElementAt(1, 1); + p.setElementAt(2, 2); + p.setElementAt(3, 3); + p.setElementAt(4, 4); + p.check(0, 1, 2, 3, 4); + + p.setElementAt(0, 0x7FFF); + p.check(0x7FFF, 1, 2, 3, 4); + p.setElementAt(0, 0x8000); + p.check(0x8000, 1, 2, 3, 4); + p.setElementAt(0, 0x8001); + p.check(0x8001, 1, 2, 3, 4); + + p.setElementAt(0, 0x7FFFFFFFL); + p.check(0x7FFFFFFFL, 1, 2, 3, 4); + p.setElementAt(0, 0x80000000L); + p.check(0x80000000L, 1, 2, 3, 4); + p.setElementAt(0, 0x80000001L); + p.check(0x80000001L, 1, 2, 3, 4); + + p.setElementAt(0, 0x7FFFFFFFFFFFL); + p.check(0x7FFFFFFFFFFFL, 1, 2, 3, 4); + p.setElementAt(0, 0x800000000000L); + p.check(0x800000000000L, 1, 2, 3, 4); + p.setElementAt(0, 0x800000000001L); + p.check(0x800000000001L, 1, 2, 3, 4); + + p.setElementAt(0, 0x800000000000000L); + p.check(0x800000000000000L, 1, 2, 3, 4); + + p.setElementAt(1, 0xFFFFFFFFFFFFFFFL); + p.check(0x800000000000000L, 0xFFFFFFFFFFFFFFFL, 2, 3, 4); + + p.setElementAt(4, 0xFFFFFFFFFFFFFFFL); + p.check(0x800000000000000L, 0xFFFFFFFFFFFFFFFL, 2, 3, 0xFFFFFFFFFFFFFFFL); + + p.setElementAt(0, 0); + p.check(0, 0xFFFFFFFFFFFFFFFL, 2, 3, 0xFFFFFFFFFFFFFFFL); + + p.setElementAt(2, 2); + p.check(0, 0xFFFFFFFFFFFFFFFL, 2, 3, 0xFFFFFFFFFFFFFFFL); + + p.setElementAt(1, 1); + p.check(0, 1, 2, 3, 0xFFFFFFFFFFFFFFFL); + + p.setElementAt(3, 3); + p.check(0, 1, 2, 3, 0xFFFFFFFFFFFFFFFL); + + p.setElementAt(4, 4); + p.check(0, 1, 2, 3, 4); + } + + public static final void main(String arg[]) { selfTest(); } +} diff --git a/mozilla/grendel/storage/ParseBerkeleyFolder.java b/mozilla/grendel/storage/ParseBerkeleyFolder.java new file mode 100644 index 00000000000..50ed80f2b47 --- /dev/null +++ b/mozilla/grendel/storage/ParseBerkeleyFolder.java @@ -0,0 +1,485 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * ParseBerkeleyFolder.java + * Created: Terry Weissman , 8 Aug 1997. + * Shamelessly based on mbox.c which was + * Created: Jamie Zawinski , 22 Jul 1997. + */ + +/** + This sectionizes a mailbox file into its component messages; it understands + both the BSD and Solaris formats ("From " and "Content-Length".) See also + http://www.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html + for related ranting. + */ + + +package grendel.storage; + +import grendel.storage.intertwingle.Twingle; + +import calypso.util.Assert; +import calypso.util.ByteBuf; +import calypso.util.ByteLineBuffer; +import calypso.util.RandomAccessFileWithByteLines; + +import javax.mail.internet.InternetHeaders; + +import java.io.RandomAccessFile; +import java.io.InputStream; +import java.io.IOException; + + +class ParseBerkeleyFolder { + + static final boolean DEBUG = false; + + static final int CONTENT_LENGTH_SLACK = 100; + + Twingle twingle = Twingle.GetSingleton(); + + + /* Returns the file-byte-index of the end of the message, if it can be + determined from the given Content-Length, and if it checks out. + If the Content-Length is obvious nonsense, or if any read/seek errors + occur, returns IT_SEEK_ERROR. + + The file-read-pointer is expected to be just past the headers of the + message in question (poised to read the first byte of the body.) + It will be left in that same place upon return. + + The Content-Length is checked for sanity (by examining the file at + the indicated position) and is adjusted if it seems to be off by a + small amount. It can be off by up to N bytes in either direction; + the correct value is assumed to be the closer of the two nearest + message-delimiters within that range (either earlier or later.) + */ + + BerkeleyFolder fFolder; + + byte buf[] = new byte [CONTENT_LENGTH_SLACK * 2 + 1]; + + protected int total_message_count = 0; + protected int undeleted_message_count = 0; + protected int unread_message_count = 0; + protected long deleted_message_bytes = 0; + + int totalMessageCount() { return total_message_count; } + int undeletedMessageCount() { return undeleted_message_count; } + int unreadMessageCount() { return unread_message_count; } + long deletedMessageBytes() { return deleted_message_bytes; } + + + int find_content_end(RandomAccessFileWithByteLines in, int content_length, + int file_length) + throws IOException { + + int start_pos = (int) in.getFilePointer(); + boolean eof = false; + int read_start, read_end, target; + int early_match, late_match; + int bytes; + int result = -1; + + Assert.Assertion(content_length >= 0 && start_pos >= 0); + if (content_length < 0 || start_pos < 0) return -1; + + target = start_pos + content_length; + read_start = target - ((buf.length - 1) / 2); + + /* Read a block centered on the position indicated by Content-Length + (with `slack' bytes in either direction.) */ + + int b = 0; + int bytes_left = buf.length - 1; + + if (read_start < start_pos) { + bytes_left -= (start_pos - read_start); + read_start = start_pos; + } + + if (read_start + bytes_left > file_length) { + bytes_left = file_length - read_start; + eof = true; + } + + in.seek(read_start); + in.readFully(buf, b, bytes_left); + +// System.err.println(new String(buf, b, bytes_left)); + + b += bytes_left; + + read_end = read_start + b; + + /* Find the first mailbox delimiter at or before the position indicated + by Content-Length. + */ + + early_match = -1; + int s = (target - read_start) + 6; + if (s > (read_end - read_start)) + s = (read_end - read_start); + s -= 6; + for (; s > 0; s--) { + if (buf[s] == '\n' && + buf[s+1] == 'F' && + buf[s+2] == 'r' && + buf[s+3] == 'o' && + buf[s+4] == 'm' && + buf[s+5] == ' ') { + early_match = read_start + s; + } + } + + /* Find the first mailbox delimiter at or past the position indicated + by Content-Length. */ + + late_match = -1; + if (read_end > target) { + + s = (target - read_start) - 6; + if (s < 0) s = 0; + for ( ; + s < (read_end - read_start) - 6; + s++) { + if (buf[s] == '\n' && + buf[s+1] == 'F' && + buf[s+2] == 'r' && + buf[s+3] == 'o' && + buf[s+4] == 'm' && + buf[s+5] == ' ') { + late_match = read_start + s; + break; + } + } + } + + if (late_match == -1 && eof) { + /* EOF counts as a match. */ + late_match = file_length - 1; + } + + if (early_match == late_match) { + if (late_match < target) { + late_match = -1; + } else { + early_match = -1; + } + } else if (early_match == -1 && late_match < target) { + early_match = late_match; + late_match = -1; + } + + if (early_match >= 0 && late_match >= 0) { + /* Found two -- pick the closest one. */ + if ((target - early_match) < (late_match - target)) { + result = early_match; + } else { + result = late_match; + } + } + else if (early_match >= 0) { + result = early_match; + } else { + result = late_match; + } + + if (DEBUG) { + if (result == -1) + System.err.println("content-length of " + content_length + " is nonsense (or off by more than " + CONTENT_LENGTH_SLACK + " bytes)"); + else if (result != target) { + if (result == late_match) + System.err.println("content-length of " + content_length + + " was " + + (late_match - target) + + " bytes too small (pos " + + result + + ")"); + else + System.err.println("content-length of " + content_length + + " was " + + (target - early_match) + + " bytes too large (pos " + + result + + ")"); + } else { + System.err.println("*** Hey; Content-Length worked perfectly!"); + } + } + + in.seek(start_pos); + + return result; + } + + + void recordOne(int envelope_start, int message_start, + int headers_end, int message_end, + InternetHeaders headers) + throws IOException { + + Assert.Assertion(envelope_start >= 0 && + message_start > envelope_start && + headers_end >= message_start && + message_end >= headers_end); + + if (DEBUG) { + System.out.println("Found one: start: " + message_start + + "; end: " + message_end); + // headers.dump(); + } + BerkeleyMessage m = new BerkeleyMessage(fFolder, headers); + + total_message_count++; + if (m.isDeleted()) { + deleted_message_bytes += (message_end - message_start); + } else { + undeleted_message_count++; + if (!m.isRead()) + unread_message_count++; + } + + m.setStorageFolderIndex(message_start); + m.setSize(message_end - message_start); + fFolder.noticeInitialMessage(m); + if (twingle != null) { + if (DEBUG) System.out.println("Calling twingle.add"); + twingle.add(headers, fFolder); + } + } + + + void mapOverMessages(RandomAccessFile infile, BerkeleyFolder f) + throws IOException { + mapOverMessages(infile, f, 0); + } + + protected synchronized void mapOverMessages(RandomAccessFile infile, + BerkeleyFolder f, + long starting_file_pos) + throws IOException { + fFolder = f; + InternetHeaders hdrs = new InternetHeaders(); + + int file_length = (int) infile.length(); + infile.seek(starting_file_pos); + + RandomAccessFileWithByteLines in = + new RandomAccessFileWithByteLines(infile); + + ByteBuf line = ByteBuf.Alloc(); + + InternetHeaders headers = new InternetHeaders(); + + int last_end = 0; + int start = -1; + int headers_end = -1; + boolean inheader = false; + + ByteBuf contentlengthname = new ByteBuf("Content-Length"); + ByteBuf lenstr = new ByteBuf(); + + + int last_position = 0; // file byte index of start of prev line + int this_position = 0; // file byte index of start of this line + int next_position = 0; // file byte index of start of next line + + boolean last_blank_p = false; // whether the prev line was blank + boolean this_blank_p = false; // whether this line is blank + + while (in.readLine(line)) { + last_position = this_position; + this_position = next_position; + next_position = (int) in.getFilePointer(); + + byte[] bytes = line.toBytes(); // doesn't cons + + last_blank_p = this_blank_p; + this_blank_p = (line.length() == 0); + + if (line.length() >= 5 && + bytes[0] == 'F' && + bytes[1] == 'r' && + bytes[2] == 'o' && + bytes[3] == 'm' && + bytes[4] == ' ') { + if (start >= 0) { + + // The last byte of the previous message is the byte before the + // first character on the current "From " line; unless the + // previous line was blank, in which case, the last byte of the + // previous message is the byte before the first byte on that + // line. This seems to do the right thing most of the time, in + // the face of missing or slightly-off Content-Length headers. + // + int end = (last_blank_p + ? last_position + : this_position); + recordOne(last_end, start, headers_end, end, headers); + last_end = end; + } + + // The following message begins at the first byte of the next line + // following this "From " line (that is, the byte after this line's + // linebreak character(s).) + // + start = next_position; + headers = new InternetHeaders(); + inheader = true; + continue; + } + + if (inheader) { + if (line.length() == 0 || + line.byteAt(0) == '\n' || line.byteAt(0) == '\r') { + inheader = false; + headers_end = this_position; + String hh[] = headers.getHeader("Content-Length"); + if (hh != null && hh.length != 0) { + int contentLength; + try { + contentLength = Integer.parseInt(hh[0]); + } catch (NumberFormatException e) { + contentLength = -1; + } + if (contentLength > 0) { + int loc = find_content_end(in, contentLength, file_length); + if (loc > 0) { + this_position = loc; + next_position = loc; + this_blank_p = false; + in.seek(loc); + } + } + } + } else { + headers.addHeaderLine(line.toString()); + } + } + } + if (start >= 0 && !inheader) { + if (headers_end <= start) + headers_end = file_length; + recordOne(last_end, start, headers_end, file_length, headers); + } + } + + + /** Overwrite the X-Mozilla-Status header near the given file position. + + @param message_start The position in the file of the start of a + message. A header block is assumed to start at + this position. The file is scanned forward for + an X-Mozilla-Status header in this block. + @param message_length The length of the message in bytes. + @param mozilla_flags The flags to overwrite into the existing + X-Mozilla-Status header, if any. + + @return true if the header was overwritten, false + otherwise. + */ + boolean flushMozillaStatus(RandomAccessFile io, + long message_start, + int message_length, + short mozilla_flags) throws IOException { + + long message_end = message_start + message_length; + + io.seek(message_start); + ByteBuf buf = null; + ByteBuf line = null; + + try { + buf = ByteBuf.Alloc(); + line = ByteBuf.Alloc(); + ByteLineBuffer linebuf = new ByteLineBuffer(); + + boolean eof = false; + + // `pos' will point at the file position of the beginning of the + // line we are processing. + long pos = message_start; + + while (!eof) { + buf.setLength(0); + eof = (buf.read(io, 1024) < 0); + if (eof) { + linebuf.pushEOF(); + } else { + linebuf.pushBytes(buf); + } + + while (linebuf.pullLine(line)) { + + byte first = line.byteAt(0); + if (first == '\r' || first == '\n') + // end of headers -- and we never saw X-Mozilla-Status! + return false; + else if ((first == 'X' || first == 'x') && + (line.regionMatches(true, 0, "X-Mozilla-Status:", 0, 17))) { + // Woo hoo. Make sure it has exactly the right number of bytes. + int start = 17; + while (line.byteAt(start) == ' ' || line.byteAt(start) == '\t') + start++; + + for (int digits = 0; digits < 4; digits++) { + byte b = line.byteAt(start + digits); + if (! ((b >= '0' && b <= '9') || + (b >= 'a' && b <= 'f') || + (b >= 'A' && b <= 'F'))) + // if there aren't 4 consecutive hex digits, bug out. + return false; + } + // If there isn't whitespace or a newline after the digits, + // bug out. + if (line.byteAt(start + 4) > ' ') + return false; + + // Ok! At this point, `start' points to the position in the + // line of the first hex digit. So the position in the file + // is `pos+start'. Seek there, and overwrite the bytes with + // the new flag. + + String s = Integer.toHexString(((int) mozilla_flags) & 0xFFFF); + if (s.length() == 3) s = "0" + s; + else if (s.length() == 2) s = "00" + s; + else if (s.length() == 1) s = "000" + s; + io.seek(pos + start); + io.write(s.getBytes()); + return true; // all done! + } + + pos += line.length(); + + if (pos > message_end) + // oops, reached end of message before end of headers? + return false; + } + } + } finally { + if (buf != null) ByteBuf.Recycle(buf); + if (line != null) ByteBuf.Recycle(line); + } + + // reached eof before end of message headers. + return false; + } + +} diff --git a/mozilla/grendel/storage/ParseBerkeleyFolderAndExpunge.java b/mozilla/grendel/storage/ParseBerkeleyFolderAndExpunge.java new file mode 100644 index 00000000000..489442433ee --- /dev/null +++ b/mozilla/grendel/storage/ParseBerkeleyFolderAndExpunge.java @@ -0,0 +1,284 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 2 Oct 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +import javax.mail.internet.InternetHeaders; +import javax.mail.Header; + +import java.io.RandomAccessFile; +import java.io.IOException; +import java.io.EOFException; +import java.util.Enumeration; + +class ParseBerkeleyFolderAndExpunge extends ParseBerkeleyFolder { + + private static final boolean DEBUG = true; + + protected RandomAccessFile in = null; + protected RandomAccessFile out = null; + protected byte bytes[] = null; + protected ByteBuf bytebuf = null; + protected byte blank_mozilla_status[] = null; + protected byte[] linebreak = null; + Enumeration msgs = null; + + synchronized void expungeDeletedMessages(BerkeleyFolder f, + RandomAccessFile in, + RandomAccessFile out, + Enumeration old_msgs) + throws IOException { + + Assert.Assertion(old_msgs != null); + + total_message_count = 0; // recordOne() updates these. + undeleted_message_count = 0; + unread_message_count = 0; + deleted_message_bytes = 0; + + this.in = in; + this.out = out; + bytes = new byte[1024 * 4]; + bytebuf = new ByteBuf(); + blank_mozilla_status = ("X-Mozilla-Status: 0000" + + Constants.LINEBREAK).getBytes(); + + // Figure out what line terminator is used in this file by searching + // forward for the first "\n", "\r", or "\r\n". + { + long pos = in.getFilePointer(); + int b; + do { + b = in.read(); + } while (b != -1 && b != '\r' && b != '\n'); + + if (b == -1 || (b == '\r' && (in.read() == '\n'))) + linebreak = "\r\n".getBytes(); + else if (b == '\n') + linebreak = "\n".getBytes(); + else + linebreak = "\r".getBytes(); + in.seek(pos); + } + + msgs = old_msgs; + mapOverMessages(in, f); + + bytes = null; + bytebuf = null; + linebreak = null; + } + + + + void recordOne(int envelope_start, int message_start, + int headers_end, int message_end, + InternetHeaders headers) + throws IOException { + +// System.err.println(" ** " + +// envelope_start + " " + message_start + " " + +// headers_end + " " + message_end); + + Assert.Assertion(envelope_start >= 0 && + message_start > envelope_start && + headers_end >= message_start && + message_end >= headers_end); + + BerkeleyMessage old_msg = (BerkeleyMessage) msgs.nextElement(); + Assert.Assertion(old_msg != null); + + if (DEBUG) { + // Make sure the sequence of messages in the file is in step with the + // sequence of messages in the folder object. + String idh = null; + String hh[] = headers.getHeader("Message-ID"); + if (hh != null && hh.length != 0) + idh = hh[0]; + + if (idh != null) { + MessageID id = new MessageID(idh); + if (bytebuf.length() > 4 && + !id.equals(old_msg.getMessageID())) { + System.err.println("ids differ!"); + System.err.println(" new id = " + id + " (" + idh + ")"); + System.err.println(" old id = " + old_msg.getMessageID()); + } + } + } + + if ((old_msg.flags & MessageBase.FLAG_DELETED) != 0) + // This message is deleted; don't write it out. + return; + + + // Create a new message. We can't reuse the old message, because the + // caller is going to want to back out of any changes if an error occurs; + // so it needs to hold on to a copy of the original data until we're done. + // + BerkeleyMessage new_msg = new BerkeleyMessage(fFolder, headers); + + + // First, copy the envelope line from one file to the other. + // Then remember the position in the output file of (what will be) the + // beginning of the headers of this message. + // + long in_pos = in.getFilePointer(); + + in.seek(envelope_start); + copyBytes(message_start - envelope_start, false); + new_msg.setStorageFolderIndex((int) out.getFilePointer()); + + + int total_msg_bytes_written = 0; // doesn't include envelope + + + // Now, write out the headers. Rather than just copying the bytes of + // the headers from one file to another, we reconstruct the output + // headers from the parsed headers object. We do this because it's + // the easiest way to skip over (don't copy) certain headers, which + // we want to recompute the values of. + // + // Note that this could possibly (but not probably) change the linebreaks + // of the headers during their passage from one file to another. Note + // also that we *don't* change the linebreaks of the bodies, just the + // headers. Since all linebreaks in one file should match, this + // shouldn't happen (or matter.) + // + for (Enumeration e = headers.getAllHeaders(); e.hasMoreElements();) { + Header hh = (Header) e.nextElement(); + String h = hh.getName(); + String v = hh.getValue(); + + // Don't copy these headers from input to output. + if (h.equalsIgnoreCase("X-Mozilla-Status") || + h.equalsIgnoreCase("Content-Length") || + h.equalsIgnoreCase("X-UIDL")) + continue; + + bytebuf.setLength(0); + bytebuf.append(h); + bytebuf.append(": "); + bytebuf.append(v); + bytebuf.append(linebreak); + out.write(bytebuf.toBytes(), 0, bytebuf.length()); + total_msg_bytes_written += bytebuf.length(); + } + + // Now, write out the recomputed values of the X-Mozilla-Status, + // Content-Length, and (maybe) X-UIDL headers. + // + bytebuf.setLength(0); + + if (false) { + bytebuf.append("X-UIDL: "); + bytebuf.append("####"); + bytebuf.append(linebreak); + } + +// if (old_msg.flags != new_msg.flags) { +// System.err.println("flags diff at " + message_start + ": " + +// old_msg.flags + " " + new_msg.flags); +// } + + long xms = BerkeleyMessage.makeMozillaFlags(old_msg); + bytebuf.append("X-Mozilla-Status: "); + String s = Long.toHexString(xms); + int i = s.length(); + if (i == 1) bytebuf.append("000"); + else if (i == 2) bytebuf.append("00"); + else if (i == 3) bytebuf.append("0"); + bytebuf.append(s); + bytebuf.append(linebreak); + + bytebuf.append("Content-Length: "); + int cl = (message_end - headers_end - linebreak.length); + if (cl < 0) cl = 0; + bytebuf.append(cl); + bytebuf.append(linebreak); + + out.write(bytebuf.toBytes(), 0, bytebuf.length()); + total_msg_bytes_written += bytebuf.length(); + + + // We've written the headers. Now copy the body bytes. + // + in.seek(headers_end); + copyBytes(message_end - headers_end, true); + total_msg_bytes_written += (message_end - headers_end); + in.seek(in_pos); + + // Now that we've written the data, update the message with the true + // byte-length (this will be the value of the Content-Length header, + // plus the number of bytes we wrote for the headers.) + // + new_msg.setSize(total_msg_bytes_written); + + // Update the summary counts... + Assert.Assertion(!new_msg.isDeleted()); + total_message_count++; + undeleted_message_count++; + if (!new_msg.isRead()) + unread_message_count++; + + // Now hand this new message object back to the folder, and we're done. + // + fFolder.noticeInitialMessage(new_msg); + } + + + /** Copy N bytes from `in' to `out'. + If `blank_line' is true, and the data copied didn't end with a + linebreak, insert a linebreak. + */ + private void copyBytes(int n, boolean blank_line) throws IOException { + int buf_length = bytes.length; + int bytes_left = n; + boolean broken = false; + + while (bytes_left > 0) { + int to_read = bytes_left; + if (to_read > buf_length) + to_read = buf_length; + int bytes_read = in.read(bytes, 0, to_read); +// System.err.println(" read " + bytes_read + +// " (with " + bytes_left + " left)"); + if (bytes_read < 0) + throw new EOFException(); + else + bytes_left -= bytes_read; + + out.write(bytes, 0, bytes_read); + + if (blank_line) + broken = (bytes[bytes_read-1] == '\r' || + bytes[bytes_read-1] == '\n'); + } + + if (blank_line && !broken) { +// System.err.println("inventing linebreak at " + in.getFilePointer()); + out.write(linebreak, 0, linebreak.length); + } + } +} diff --git a/mozilla/grendel/storage/ParseBerkeleyFolderWithSummary.java b/mozilla/grendel/storage/ParseBerkeleyFolderWithSummary.java new file mode 100644 index 00000000000..d6a1f30808e --- /dev/null +++ b/mozilla/grendel/storage/ParseBerkeleyFolderWithSummary.java @@ -0,0 +1,153 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 27 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import calypso.util.ByteBuf; + +import java.io.File; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.BufferedInputStream; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.io.FileNotFoundException; + + +class ParseBerkeleyFolderWithSummary extends ParseBerkeleyFolder { + + synchronized void mapOverMessages(RandomAccessFile infile, BerkeleyFolder f) + throws IOException { + fFolder = f; + + long file_date = f.fFile.lastModified(); + long file_size = f.fFile.length(); + + total_message_count = 0; // super.recordOne() updates these. + undeleted_message_count = 0; + unread_message_count = 0; + deleted_message_bytes = 0; + + long summarized_length = readSummaryFile(f); + + if (summarized_length != file_size) { + mapOverMessages(infile, f, summarized_length); + + if (summarized_length == 0) + System.err.println("summary file was missing or out of sync; " + + "marking folder \"" + f.getName() + "\" dirty."); + else + System.err.println("more messages in folder than summary file; " + + "marking folder \"" + f.getName() + "\" dirty."); + + if (f.mailSummaryFile != null) { + // We actually read a summary file: since we also read some messages, + // update the dates in the summary file *now*, so that when this + // summary object is *later* used to rewrite the summary file (and + // include the new messages) the date/size that get written into the + // new summary file will correspond to the time we actually parsed + // the folder (as opposed to the time at which we wrote the summary.) + f.mailSummaryFile.setFolderDateAndSize(file_date, file_size); + + // Also update the message counts, in case we parsed some more messages + // beyond what the summary file told us about. + f.mailSummaryFile.setFolderMessageCounts(total_message_count, + undeleted_message_count, + unread_message_count, + deleted_message_bytes); + } + f.setSummaryDirty(true); + } + + // If something went wrong while parsing the summary file, it might want + // to tweak some of the messages that were actually parsed from the file, + // based on whatever attrocities it saw in the (broken or out of date) + // summary, before parsing failed for whatever reason. + if (f.mailSummaryFile != null) + f.mailSummaryFile.salvageMessages(); + } + + + + /** Reads opens the summary file, if any, associated with the folder, + and if it is in the proper format and otherwise up to date, + parses it (by calling MailSummaryFile.readSummaryFile().) + Stores a MailSummaryFile into the folder. Returns the number of + bytes which the summary file had summarized. + + @see MailSummaryFileFactory + @see MailSummaryFile + @see MailSummaryFileCheddar + @see MailSummaryFileGrendel + */ + protected synchronized long readSummaryFile(BerkeleyFolder f) { + long result = 0; + InputStream sum = null; + f.mailSummaryFile = null; + + try { // make sure sum is closed. + sum = openSummaryFile(f); + + if (sum != null) { + try { // catch IOException + MailSummaryFile reader = + MailSummaryFileFactory.ParseFileHeader(f, sum); + + if (reader != null) { + result = reader.readSummaryFile(sum); + f.mailSummaryFile = reader; + + total_message_count = reader.totalMessageCount(); + undeleted_message_count = reader.undeletedMessageCount(); + unread_message_count = reader.unreadMessageCount(); + deleted_message_bytes = reader.deletedMessageBytes(); + } + } catch (IOException e) { +// System.err.println("exception while reading summary file: " + e); + } + } + + } finally { + if (sum != null) { + try { sum.close(); } + catch (IOException e) { } + } + } + + return result; + } + + + protected InputStream openSummaryFile(BerkeleyFolder folder) { + try { + File sum = folder.summaryFileName(); + InputStream result = new BufferedInputStream(new FileInputStream(sum)); +System.err.println("opened summary file " + sum); + return result; + } catch (SecurityException e) { + return null; + } catch (FileNotFoundException e) { + return null; + } + } + +} diff --git a/mozilla/grendel/storage/PartialFileInputStream.java b/mozilla/grendel/storage/PartialFileInputStream.java new file mode 100644 index 00000000000..1270f45bccd --- /dev/null +++ b/mozilla/grendel/storage/PartialFileInputStream.java @@ -0,0 +1,75 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 28 Aug 1997. + */ + +package grendel.storage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.IOException; + +/** Implements an input stream that comes from just a portion of a file. */ + +class PartialFileInputStream extends FileInputStream { + int remaining = 1 << 30; + + public PartialFileInputStream(File f, int start, int length) + throws IOException, FileNotFoundException + { + super(f); + skip(start); + remaining = length; + } + + public int read() throws IOException { + if (remaining <= 0) return -1; + remaining--; + return super.read(); + } + + public int read(byte b[]) throws IOException { + if (remaining <= 0) return -1; + int result = super.read(b); + if (result > 0) { + if (result > remaining) result = remaining; + remaining -= result; + } + return result; + } + + public int read(byte b[], int off, int length) throws IOException { + if (remaining <= 0) return -1; + if (length > remaining) length = remaining; + int result = super.read(b, off, length); + if (result > 0) { + if (result > remaining) result = remaining; + remaining -= result; + } + return result; + } + + +} + + + + diff --git a/mozilla/grendel/storage/PopFolder.java b/mozilla/grendel/storage/PopFolder.java new file mode 100644 index 00000000000..5c136e272e7 --- /dev/null +++ b/mozilla/grendel/storage/PopFolder.java @@ -0,0 +1,292 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 30 Oct 1997. + */ + +package grendel.storage; + +import java.io.IOException; + +import java.util.Hashtable; +import java.util.StringTokenizer; + +import javax.mail.Folder; +import javax.mail.FolderNotFoundException; +import javax.mail.Flags; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import javax.mail.event.ConnectionEvent; + +class PopFolder extends Folder implements FolderExtra { + + static public final String DELETED = "d"; + static public final String KEEP = "k"; + + Hashtable leave; + PopMessage msgs[]; + + + + + PopFolder(PopStore store) { + super(store); + } + + + public String getName() { + return "INBOX"; + } + + public String getFullName() { + return "INBOX"; + } + + public Folder getParent() { + return null; + } + + public boolean exists() { + return true; + } + + public Folder[] list(String string) { + return null; + } + + public char getSeparator() { + return '/'; + } + + public int getType() { + return HOLDS_MESSAGES; + } + + public boolean create(int i) { + return true; + } + + public boolean hasNewMessages() { + return false; // ### Is this right? Or should it really + // go do the work and see? + } + + public Folder getFolder(String string) throws MessagingException { + throw new FolderNotFoundException(); + } + + public boolean delete(boolean flag) throws MessagingException { + throw new MethodNotSupportedException(); + } + + public boolean renameTo(Folder folder) throws MessagingException { + throw new MethodNotSupportedException(); + } + + public void open(int mode) throws MessagingException { + if (isOpen()) return; + PopStore store = (PopStore) this.store; + store.connect(); + store.writeln("STAT"); + String line = store.readln(); + store.check(line, "Stat failed"); + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); //skip +OK + int nummsgs = Integer.parseInt(st.nextToken()); + // stat_octetcount = Integer.parseInt(st.nextToken()); + msgs = new PopMessage[nummsgs]; + for (int i=0 ; i, 28 Aug 1997. + */ + +package grendel.storage; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.StringTokenizer; +import java.util.Vector; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; + +//import netscape.pop3.POP3Client; +//import netscape.pop3.POP3Status; +//import netscape.pop3.POP3MailDropStats; + +class PopMailDrop implements MailDrop { + static final String KEEP = "k"; + static final String DELETE = "d"; + + String fHost; + int fPort = 110; + String fUser; + String fPass; + Folder fFolder; + int fNumWaiting = -1; + Socket fSocket; + InputStream fInput; + DataOutputStream fOutput; + int stat_msgcount; + int stat_octetcount; + File popstatefile; + boolean uidlUnimplemented; + boolean xtndUnimplemented; + boolean leaveMessagesOnServer; + + static final boolean DEBUG = false; + static void Spew(String s) { + if (DEBUG) System.out.println("PopMailDrop: " + s); + } + + PopMailDrop() { + } + + protected void clearAssumptionsAboutServer() { + uidlUnimplemented = false; + xtndUnimplemented = false; + } + + void setHost(String hostname) { + fHost = hostname; + clearAssumptionsAboutServer(); + } + + void setPort(int port) { + fPort = port; + clearAssumptionsAboutServer(); + } + + void setUser(String username) { + fUser = username; + clearAssumptionsAboutServer(); + } + + void setPassword(String password) { + fPass = password; + clearAssumptionsAboutServer(); + } + + void setDestinationFolder(Folder dest) { + fFolder = dest; + } + + Folder getDestinationFolder() { + return fFolder; + } + + void setPopStateFile(File f) { + popstatefile = f; + Spew("popstatefile set to " + f); + } + + File getPopStateFile() { + return popstatefile; + } + + void setLeaveMessagesOnServer(boolean value) { + leaveMessagesOnServer = value; + } + + boolean getLeaveMessagesOnServer() { + return leaveMessagesOnServer; + } + + /** All the real work is done here. Go to the POP server, determine + what messages need downloading, and optionally actually download them. + (The only reason not to go ahead and download them is if we're doing a + biff check.) */ + protected void fetchOrCheck(boolean dofetch) throws IOException { + try { + fNumWaiting = -1; + String response; + int bytesToLoad = 0; // How many bytes total we're going to + // download. Someday, this will be used for + // progress indication. ### + login(); + Hashtable leave = loadLeaveOnServerList(); + Hashtable newleave = null; + stat(); + Vector getlist = new Vector(); + if (leave == null && !leaveMessagesOnServer) { + for (int id=1 ; id<=stat_msgcount ; id++) { + getlist.addElement(new Integer(id)); + } + bytesToLoad = stat_octetcount; + } else { + int sizes[] = new int[stat_msgcount + 1]; + if (dofetch) { // Only bother getting sizes if we're going + // to be downloading some of these. + writeln("LIST"); + check(readln(), "LIST command failed"); + for (;;) { + String line = readln(); + if (line.charAt(0) == '.') break; + StringTokenizer st = new StringTokenizer(line); + int id = Integer.parseInt(st.nextToken()); + int count = Integer.parseInt(st.nextToken()); + if (id >= 1 && id <= stat_msgcount) { + sizes[id] = count; + } + } + } + boolean processingXtnd = false; + for (int id=1 ; id<=stat_msgcount ; id++) { + String uidl = null; + if (!uidlUnimplemented) { + writeln("UIDL " + id); + response = readln(); + if (!resultOK(response)) { + uidlUnimplemented = true; + } else { + StringTokenizer st = new StringTokenizer(response); + st.nextToken(); + st.nextToken(); + uidl = st.nextToken(); + } + } + if (uidl == null && !xtndUnimplemented && id == 1) { + writeln("XTND XLST Message-Id"); + response = readln(); + if (!resultOK(response)) { + xtndUnimplemented = true; + } else { + processingXtnd = true; + } + } + if (processingXtnd) { + response = readln(); + if (response.startsWith(".")) { + processingXtnd = false; + } else { + StringTokenizer st = new StringTokenizer(response); + int i = Integer.parseInt(st.nextToken()); + if (i != id) { + // Huh? xtnd skipped a message or something. Fine, throw + // away all the xtnd data, go use TOP instead. + do { + response = readln(); + } while (!response.startsWith(".")); + processingXtnd = false; + } else { + st.nextToken(); + uidl = st.nextToken(); + } + } + } + if (uidl == null) { + writeln("TOP " + id + " 0"); + check(readln(), "The POP3 server (%s) does not\n" + + "support UIDL, which Netscape Mail needs to implement\n" + + "the ``Leave on Server'' and ``Maximum Message Size''\n" + + "options.\n\n" + + "To download your mail, turn off these options in the\n" + + "Servers panel of the ``Mail and News'' Preferences."); + + do { + response = readln(); + if (uidl == null && + response.regionMatches(true, 0, "Message-Id:", 0, 11)) { + uidl = response.substring(11).trim(); + Spew("Top found message-id: " + uidl); + } + } while (!response.startsWith(".")); + if (uidl == null) { + // Gack. No message-id in this message. Um, well, choke. + uidl = "unknown"; // ### Any better ideas??? + Spew("Top couldn't find message-id."); + } + } + if (uidl.charAt(0) == '<' && uidl.endsWith(">")) { + uidl = uidl.substring(1, uidl.length() - 1); + } + + String code = null; + if (leave != null) { + code = (String) leave.get(uidl); + if (code != null && !leaveMessagesOnServer && code.equals(KEEP)) { + // ### This causes us to redownload anything that we were + // keeping on the server if you turn off "keep messages on + // server". This is arguably wrong, because it floods peoples + // mailboxes with messages they've probably already seen. On + // the other hand, just deleting it could potentially lose + // mail for people. Bleah! + code = null; + } + } + if (code == null) { + getlist.addElement(new Integer(id)); + bytesToLoad += sizes[id]; + if (leaveMessagesOnServer) { + code = KEEP; + } + } + if (code != null) { + if (newleave == null) { + newleave = new Hashtable(); + } + newleave.put(uidl, code); + } + } + if (processingXtnd) { + do { + response = readln(); + } while (!response.startsWith(".")); + processingXtnd = false; + } + } + + fNumWaiting = getlist.size(); + +// if (dofetch && fNumWaiting > 0) { +// for (Enumeration e = getlist.elements() ; e.hasMoreElements() ; ) { +// int id = ((Integer) e.nextElement()).intValue(); +// // ### Insert code here to do partial messages... +// writeln("RETR " + id); +// check(readln(), "Requesting message from server"); +// InputStream in = new DotTerminatedInputStream(fInput); +// Message m = new MessageFromStream(in); +// Message[] stupidlist = { m }; +// try { +// fFolder.appendMessages(stupidlist); +// } catch (MessagingException err) { +// throw new IOException(err.toString()); +// } +// +// // Do filters, something like: +// FilterMaster.Get().applyFilters(m); +// // though it probably makes more sense to do this before the +// // fFolder.appendMessages() and not do that if the filter +// // has already saved the message...djw +// +// if (!leaveMessagesOnServer) { +// writeln("DELE " + id); +// check(readln(), "Deleting retrieved message from server"); +// } +// } +// fNumWaiting = 0; +// if (leave != null || newleave != null) { +// saveLeaveOnServerList(newleave); +// } +// } + logout(); + } finally { + cleanup(); + } + } + + protected Hashtable loadLeaveOnServerList() { + if (popstatefile == null) return null; + RandomAccessFile f = null; + Hashtable result = null; + try { + f = new RandomAccessFile(popstatefile, "r"); + boolean found = false; + for (String line = f.readLine() ; line != null ; line = f.readLine()) { + if (line.length() == 0) continue; + if (!found) { + if (line.charAt(0) == '*') { + StringTokenizer st = new StringTokenizer(line.substring(1)); + if (fHost.equals(st.nextToken())) { + if (fUser.equals(st.nextToken())) { + found = true; + } + } + } + } else { + if (line.charAt(0) == '*') break; // Start of next host; we're done. + if (result == null) { + result = new Hashtable(); + } + result.put(line.substring(2).trim(), line.substring(0, 1)); + } + } + } catch (IOException e) { + } finally { + if (f != null) { + try { + f.close(); + } catch (IOException e) { + } + } + } + return result; + } + + protected void saveLeaveOnServerList(Hashtable table) throws IOException { + synchronized(popstatefile) { + // This is all done synchronized via the popstatefile, so that if we + // have multiple pop servers they will not attempt to write to the file + // at the same time. This assumes that every pop server was given the + // same File object to represent the popstate file. + + RandomAccessFile in = null; + RandomAccessFile out = null; + File outfile = new File(popstatefile.getPath() + "-temp"); + try { + out = new RandomAccessFile(outfile, "rw"); + out.writeBytes("# Netscape POP3 State File" + Constants.LINEBREAK + + "# This is a generated file! Do not edit." + + Constants.LINEBREAK + Constants.LINEBREAK); + + if (table != null) { + out.writeBytes("*" + fHost + " " + fUser + Constants.LINEBREAK); + for (Enumeration e = table.keys() ; e.hasMoreElements() ; ) { + String key = (String) e.nextElement(); + out.writeBytes((String) (table.get(key)) + " " + key + + Constants.LINEBREAK); + } + } + if (popstatefile.exists()) { + in = new RandomAccessFile(popstatefile, "r"); + boolean copyit = false; + String line; + for (line = in.readLine() ; line != null ; line = in.readLine()) { + if (line.length() == 0) continue; + if (line.charAt(0) == '*') { + copyit = true; + StringTokenizer st = new StringTokenizer(line.substring(1)); + if (fHost.equals(st.nextToken())) { + if (fUser.equals(st.nextToken())) { + copyit = false; + } + } + } + if (copyit) { + out.writeBytes(line.trim() + Constants.LINEBREAK); + } + } + in.close(); + in = null; + } + out.close(); + out = null; + outfile.renameTo(popstatefile); + } finally { + if (in != null) in.close(); + if (out != null) out.close(); + } + } + } + + + synchronized public void fetchNewMail() throws IOException { + fetchOrCheck(true); + } + + synchronized public void doBiff() throws IOException { + fetchOrCheck(false); + } + + protected void login() throws IOException { + // ### I18N -- all these exceptions need to be internationalized. + if (fHost == null) { + throw new MailDropException("No host specified for POP3 maildrop"); + } + if (fUser == null) { + throw new MailDropException("No user specified for POP3 maildrop"); + } + try { + fSocket = new Socket(fHost, fPort); + } catch (UnknownHostException e) { + throw new MailDropException("Unknown host: " + e.getMessage()); + } + + fInput = fSocket.getInputStream(); + fOutput = new DataOutputStream(fSocket.getOutputStream()); + + check(readln(), "Connecting to server"); + writeln("USER " + fUser); + check(readln(), "USER command not accepted"); + writeln("PASS " + fPass); + check(readln(), "Login failed"); + // ### Should probably do GURL at this point, if necessary, to remember + // the URL used to manage this account. + } + + protected void cleanup() { + if (fSocket != null) { + try { + fSocket.close(); + } catch (IOException e) { + } + } + fSocket = null; + fInput = null; + fOutput = null; + } + + protected void logout() throws IOException { + writeln("QUIT"); + check(readln(), "Logout failed"); + } + + protected void stat() throws IOException { + writeln("STAT"); + String line = readln(); + check(line, "Stat failed"); + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); //skip +OK + stat_msgcount = Integer.parseInt(st.nextToken()); + stat_octetcount = Integer.parseInt(st.nextToken()); + } + + protected void writeln(String line) throws IOException { + Spew(">> " + line); + fOutput.writeBytes(line + "\n"); + } + + StringBuffer inbuffer = new StringBuffer(); + protected String readln() throws IOException { + inbuffer.setLength(0); + int c; + do { + c = fInput.read(); + if (c >= 0) inbuffer.append((char) c); + } while (c != '\n'); + Spew("<< " + inbuffer); + return inbuffer.toString(); + } + + protected void check(String line, String err) throws MailDropException { + if (!resultOK(line)) { + throw new MailDropException(err + " (Server response: " + line + ")"); + // ### I18N + } + } + + protected boolean resultOK(String line) { + return line.startsWith("+OK"); + } + + + + public int getBiffState() { + if (fNumWaiting < 0) return UNKNOWN; + if (fNumWaiting == 0) return NONE; + return NEW; + } + + public int getNumMessagesWaiting() { + return fNumWaiting; + } + +// protected void check(POP3Status status) throws MailDropException { +// check(status.getStatus(), status.getServerResponse()); +// } + + protected void check(boolean status, String response) + throws MailDropException + { + if (!status) { + throw new MailDropException("Pop server reports: " + response); + } + } + + +} diff --git a/mozilla/grendel/storage/PopMessage.java b/mozilla/grendel/storage/PopMessage.java new file mode 100644 index 00000000000..66f376c083d --- /dev/null +++ b/mozilla/grendel/storage/PopMessage.java @@ -0,0 +1,265 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 24 Nov 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.util.Date; + +import javax.activation.DataHandler; + +import javax.mail.Flags; +import javax.mail.IllegalWriteException; +import javax.mail.MessagingException; +import javax.mail.internet.InternetHeaders; + +class PopMessage extends MessageReadOnly implements MessageExtra { + InternetHeaders headers; + String uidl; + boolean deletedValid; + boolean deleted; + boolean recentValid; + boolean recent = true; + MessageExtraWrapper extra = null; + + + PopMessage(PopFolder f, int num) { + super(f, num); + } + + protected InternetHeaders getHeadersObj() + throws MessagingException + { + if (headers == null) { + synchronized(this) { + if (headers == null) { + PopStore store = (PopStore) folder.getStore(); + InputStream stream = store.sendTopOrRetr(msgnum, 0); + headers = new InternetHeaders(stream); + try { + while (stream.read() >= 0) { + // Spin until we've sucked out all data, so that we know + // the socket is back into a known state. + } + } catch (IOException e) { + } + } + } + } + return headers; + } + + protected MessageExtra getExtra() { + if (extra == null) { + synchronized(this) { + if (extra == null) { + extra = new MessageExtraWrapper(this); + } + } + } + return extra; + } + + + public Date getReceivedDate() throws MessagingException { + return getSentDate(); // What else can we do??? + } + + public Flags getFlags() throws MessagingException { + Flags result = new Flags(); + if (isRecent()) result.add(new Flags(Flags.Flag.RECENT)); + if (isDeleted()) result.add(new Flags(Flags.Flag.DELETED)); + return result; + } + + public boolean isSet(Flags.Flag flag) throws MessagingException { + if (flag == Flags.Flag.RECENT) { + return (isRecent()); + } else if (flag == Flags.Flag.DELETED) { + return (isDeleted()); + } else { + return false; + } + } + + public void setFlags(Flags flags, boolean flag) throws MessagingException { + loadFlags(); + + Flags.Flag list[] = flags.getSystemFlags(); + + for (int i = 0; i < list.length; i++) { + if (list[i] == Flags.Flag.RECENT) { + recent = flag; + recentValid = true; + } else if (list[i] == Flags.Flag.DELETED) { + deleted = flag; + deletedValid = true; + } else { + throw new IllegalWriteException("Can't change flag " + list[i] + + " on message " + this); + } + } + + if (flags.getUserFlags().length > 0) { + throw new IllegalWriteException("Can't change user flags on message " + + this); + } + } + + public void writeTo(OutputStream outputStream) throws MessagingException { + Assert.NotYetImplemented("PopMessage.writeTo"); + } + + public void saveChanges() throws MessagingException { + } + + public int getSize() throws MessagingException { + return -1; + } + + public int getLineCount() throws MessagingException { + return -1; + } + + public InputStream getInputStreamWithHeaders() throws MessagingException { + PopStore store = (PopStore) folder.getStore(); + return store.sendRetr(msgnum); + } + + + + public DataHandler getDataHandler() { + return null; + } + + public Object getContent() { + return null; + } + + + public String getAuthor() throws MessagingException { + return getExtra().getAuthor(); + } + + /** The name of the recipient of this message (not this email address). */ + public String getRecipient() throws MessagingException { + return getExtra().getRecipient(); + } + + /** The subject, minus any "Re:" part. */ + public String simplifiedSubject() throws MessagingException { + return getExtra().simplifiedSubject(); + } + + /** Whether the subject has a "Re:" part." */ + public boolean subjectIsReply() throws MessagingException { + return getExtra().subjectIsReply(); + } + + /** A short rendition of the sent date. */ + public String simplifiedDate() throws MessagingException { + return getExtra().simplifiedDate(); + } + + /** A unique object representing the message-id for this message. */ + public Object getMessageID() throws MessagingException { + return getExtra().getMessageID(); + } + + /** A list of the above messageid objects that this message has references + to. */ + public Object[] messageThreadReferences() throws MessagingException { + return getExtra().messageThreadReferences(); + } + + protected void loadFlags() { + // ### Write me!!! + } + + public boolean isRead() { + return false; + } + public void setIsRead(boolean value) { + } + public boolean isReplied() { + return false; + } + public void setReplied(boolean value) { + } + public boolean isForwarded() { + return false; + } + public void setForwarded(boolean value) { + } + public boolean isFlagged() { + return false; + } + public void setFlagged(boolean value) { + } + public boolean isDeleted() throws MessagingException { + if (!deletedValid) { + String state = getStateValue(); + deleted = state != null && state.equals(PopFolder.DELETED); + deletedValid = true; + } + return deleted; + } + public void setDeleted(boolean value) { + deletedValid = true; + deleted = value; + } + + public boolean isRecent() throws MessagingException { + if (!recentValid) { + recent = (getStateValue() == null); + recentValid = true; + } + return recent; + } + public void setRecent(boolean value) { + recentValid = true; + recent = value; + } + + protected String getStateValue() throws MessagingException { + return ((PopFolder)folder).getStateValueForMessage(this); + } + + public String getUidl() throws MessagingException { + if (uidl == null) { + synchronized(this) { + if (uidl == null) { + ((PopFolder)folder).setUidlForMessage(this); + } + } + } + return uidl; + } + + synchronized public void setUidl(String u) { + uidl = u; + } +} diff --git a/mozilla/grendel/storage/PopStore.java b/mozilla/grendel/storage/PopStore.java new file mode 100644 index 00000000000..d1a2e9530cd --- /dev/null +++ b/mozilla/grendel/storage/PopStore.java @@ -0,0 +1,339 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 29 Oct 1997. + */ + +package grendel.storage; + +/** Store for Berkeley mail folders. +

+ This class really shouldn't be public, but I haven't figured out how to + tie into javamail's Session class properly. So, instead of using + Session.getStore(String), you instead need to call + PopStore.GetDefaultStore(Session). + */ + +import calypso.util.Assert; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +import java.net.Socket; +import java.net.UnknownHostException; +import java.net.URL; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.StringTokenizer; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.URLName; +//import javax.mail.event.ConnectionEvent; +import javax.mail.event.StoreEvent; + +public class PopStore extends Store { + static final boolean DEBUG = true; + static void Spew(String s) { + if (DEBUG) System.out.println("PopStore: " + s); + } + + + protected PopFolder defaultFolder; + + String fHost; + int fPort = 110; + String fUser; + Folder fFolder; + int fNumWaiting = -1; + Socket fSocket; + InputStream fInput; + DataOutputStream fOutput; + DotTerminatedInputStream pendingstream; + File popstatefile; + + public boolean uidlUnimplemented; + public boolean xtndUnimplemented; + public boolean topUnimplemented; + + static protected PopStore DefaultStore = null; + public static Store GetDefaultStore(Session s) { + if (DefaultStore == null) { + DefaultStore = new PopStore(s); + } + return DefaultStore; + } + + public PopStore(Session s) { + this(s, null); + } + + public PopStore(Session s, URLName u) { + super(s, u); + defaultFolder = new PopFolder(this); + String popfilename = s.getProperty("mail.popstate"); + if (popfilename == null) { + // ### Gotta do better than System.out.println! + System.out.println("No mail.popstate property specified; won't be able"); + System.out.println("to remember state for messages left on server."); + } else { + popstatefile = new File(popfilename); + } + } + + synchronized public boolean protocolConnect(String host, + String user, + String password) + throws MessagingException + { + + if (fSocket != null) return true; // Already connected. + try { + fSocket = new Socket(host, fPort); + fInput = fSocket.getInputStream(); + fOutput = new DataOutputStream(fSocket.getOutputStream()); + check(readln(), "Connecting to server"); + writeln("USER " + user); + check(readln(), "USER command not accepted"); + writeln("PASS " + password); + if (!resultOK(readln())) { + return false; // Authentication failed. + } + } catch (UnknownHostException e) { + throw new MessagingException("Unknown host: " + e.getMessage()); + } catch (IOException e) { + throw new MessagingException("Can't open socket: " + e.getMessage()); + } + // ### Should probably do GURL at this point, if necessary, to remember + // the URL used to manage this account. + + + fHost = host; + fUser = user; + + notifyStoreListeners(StoreEvent.NOTICE, "opened"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.OPENED); + return true; + } + + synchronized public void close() { + dealWithPendingStream(); + if (fSocket != null) { + try { + writeln("QUIT"); + check(readln(), "Logout failed"); + fSocket.close(); + } catch (IOException e) { + } catch (MessagingException e) { + } + fSocket = null; + fInput = null; + fOutput = null; + } + notifyStoreListeners(StoreEvent.NOTICE, "closed"); // #### ??? +// notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + public Folder getDefaultFolder() { + return defaultFolder; + } + + public Folder getFolder(String name) throws MessagingException { + if (name.equalsIgnoreCase("inbox")) return defaultFolder; + throw new MessagingException("Pop has only one folder, named 'inbox'"); + } + + public Folder getFolder(URLName url) { + Assert.NotYetImplemented("PopStore.getFolder(URLName)"); + return null; + } + + + synchronized protected void writeln(String line) throws MessagingException { + dealWithPendingStream(); + Spew(">> " + line); + try { + fOutput.writeBytes(line + "\n"); + } catch (IOException e) { + throw new MessagingException("Failed writing to pop server: " + e); + } + } + + StringBuffer inbuffer = new StringBuffer(); + synchronized protected String readln() throws MessagingException { + dealWithPendingStream(); + inbuffer.setLength(0); + int c; + do { + try { + c = fInput.read(); + } catch (IOException e) { + throw new MessagingException("Failed reading from pop server: " + e); + } + if (c >= 0) inbuffer.append((char) c); + } while (c != '\n'); + Spew("<< " + inbuffer); + return inbuffer.toString(); + } + + protected void check(String line, String err) throws MessagingException { + if (!resultOK(line)) { + throw new MessagingException(err + " (Server response: " + line + ")"); + // ### I18N + } + } + + protected boolean resultOK(String line) { + return line.startsWith("+OK"); + } + + protected void dealWithPendingStream() { + if (pendingstream != null) { + pendingstream.bufferUpEverything(); + pendingstream = null; + } + } + + synchronized InputStream sendTopOrRetr(int msgnum, int numlines) + throws MessagingException + { + dealWithPendingStream(); + if (!topUnimplemented) { + writeln("TOP " + msgnum + " " + numlines); + String result = readln(); + if (resultOK(result)) { + pendingstream = new DotTerminatedInputStream(fInput); + return pendingstream; + } + topUnimplemented = true; + } + return sendRetr(msgnum); + } + + synchronized InputStream sendRetr(int msgnum) throws MessagingException { + dealWithPendingStream(); + writeln("RETR " + msgnum); + check(readln(), "RETR failed"); + pendingstream = new DotTerminatedInputStream(fInput); + return pendingstream; + } + + + protected Hashtable loadLeaveOnServerList() { + if (popstatefile == null) return null; + RandomAccessFile f = null; + Hashtable result = null; + try { + f = new RandomAccessFile(popstatefile, "r"); + boolean found = false; + for (String line = f.readLine() ; line != null ; line = f.readLine()) { + if (line.length() == 0) continue; + if (!found) { + if (line.charAt(0) == '*') { + StringTokenizer st = new StringTokenizer(line.substring(1)); + if (fHost.equals(st.nextToken())) { + if (fUser.equals(st.nextToken())) { + found = true; + } + } + } + } else { + if (line.charAt(0) == '*') break; // Start of next host; we're done. + if (result == null) { + result = new Hashtable(); + } + result.put(line.substring(2).trim(), line.substring(0, 1)); + } + } + } catch (IOException e) { + } finally { + if (f != null) { + try { + f.close(); + } catch (IOException e) { + } + } + } + return result; + } + + protected void saveLeaveOnServerList(Hashtable table) throws IOException { + if (popstatefile == null) return; + synchronized(popstatefile) { + // This is all done synchronized via the popstatefile, so that if we + // have multiple pop servers they will not attempt to write to the file + // at the same time. This assumes that every pop server was given the + // same File object to represent the popstate file. + // ### Right now, that's not gonna happen, because every PopStore object + // creates its own popstatefile. Gotta fix that! + + RandomAccessFile in = null; + RandomAccessFile out = null; + File outfile = new File(popstatefile.getPath() + "-temp"); + try { + out = new RandomAccessFile(outfile, "rw"); + out.writeBytes("# Netscape POP3 State File" + Constants.LINEBREAK + + "# This is a generated file! Do not edit." + + Constants.LINEBREAK + Constants.LINEBREAK); + + if (table != null) { + out.writeBytes("*" + fHost + " " + fUser + Constants.LINEBREAK); + for (Enumeration e = table.keys() ; e.hasMoreElements() ; ) { + String key = (String) e.nextElement(); + out.writeBytes((String) (table.get(key)) + " " + key + + Constants.LINEBREAK); + } + } + if (popstatefile.exists()) { + in = new RandomAccessFile(popstatefile, "r"); + boolean copyit = false; + String line; + for (line = in.readLine() ; line != null ; line = in.readLine()) { + if (line.length() == 0) continue; + if (line.charAt(0) == '*') { + copyit = true; + StringTokenizer st = new StringTokenizer(line.substring(1)); + if (fHost.equals(st.nextToken())) { + if (fUser.equals(st.nextToken())) { + copyit = false; + } + } + } + if (copyit) { + out.writeBytes(line.trim() + Constants.LINEBREAK); + } + } + in.close(); + in = null; + } + out.close(); + out = null; + outfile.renameTo(popstatefile); + } finally { + if (in != null) in.close(); + if (out != null) out.close(); + } + } + } +} diff --git a/mozilla/grendel/storage/README b/mozilla/grendel/storage/README new file mode 100644 index 00000000000..7cb1324b2a2 --- /dev/null +++ b/mozilla/grendel/storage/README @@ -0,0 +1,9 @@ +This is Grendel -- a Java mail/news client. + +The grendel.storage package is is where we implement most (all?) of the +javamail API. Private to this package are any details about particular +protocol implementations. All the differences between NNTP, IMAP, POP, +etc. are packaged away in here and hidden. This also implements the RCFdata +interfaces; that is, this is the stuff that supports RDF queries. + +See http://www.mozilla.org/projects/grendel/index.html for more info. diff --git a/mozilla/grendel/storage/SearchResultsFolderFactory.java b/mozilla/grendel/storage/SearchResultsFolderFactory.java new file mode 100644 index 00000000000..3b186e1cb8f --- /dev/null +++ b/mozilla/grendel/storage/SearchResultsFolderFactory.java @@ -0,0 +1,44 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Class SearchResultsFolderFactory. + * + * Factory for SearchResultsFolder, which at the moment is just a special + * instance of FilterFolder. + * + * Created: David Williams , 1 Oct 1997. + */ +package grendel.storage; + +import javax.mail.Folder; +import javax.mail.search.SearchTerm; + +import grendel.storage.FilterFolder; + +public class SearchResultsFolderFactory { + static private FilterFolder fSearchResults = null; + static public Folder Get() { + if (fSearchResults == null) + fSearchResults = new FilterFolder(null, "Search Results", null); + return fSearchResults; + } + static public void SetTarget(Folder folder, SearchTerm term) { + FilterFolder ff = (FilterFolder)Get(); + ff.setTarget(folder, term); + } +} diff --git a/mozilla/grendel/storage/SelfTest.java b/mozilla/grendel/storage/SelfTest.java new file mode 100644 index 00000000000..78d0a6a874b --- /dev/null +++ b/mozilla/grendel/storage/SelfTest.java @@ -0,0 +1,250 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 2 Dec 1997. + */ + +package grendel.storage; + + + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Store; + +public class SelfTest extends grendel.SelfTest { + + + public static void main(String args[]) { + SelfTest st = new SelfTest(); + st.startTests(args); + st.runTests(); + st.endTests(); + } + + + public void startTests(String args[]) { + super.startTests(args); + } + + public void runTests() { + try { + writeMessage(null, "berkeley", "Starting test of Berkeley folders..."); + berkeley(); + writeMessage(null, "berkeley", "... finished test of Berkeley folders."); + writeMessage(null, "pop3", "Starting test of Pop3 folders..."); + pop3(); + writeMessage(null, "pop3", "... finished test of Pop3 folders."); + } catch (Throwable e) { + writeFailure(e, "punt", "Can't continue SelfTest because of fatal " + + "problem: " + getStackTrace(e)); + } + } + + + public void endTests() { + super.endTests(); + } + + + public void checkFolder_1(String mname, + Folder f, + int numTotal, + int numUndeleted, + int numUnread, + int numNew, + String[] authors, + String[] subjects, + long[] sentdates) + throws MessagingException + { + FolderExtra fextra = FolderExtraFactory.Get(f); + if (fextra.getUndeletedMessageCount() != numUndeleted) { + if (fextra.getUndeletedMessageCount() == -1) { + // Bug97114 + writeKnownBug(f, mname, 97114, + "Got -1 undeleted messages; kinda rude and weird."); + } else { + writeFailure(f, mname, + "Expected " + numUndeleted + " undeleted messages; got " + + fextra.getUndeletedMessageCount()); + } + } + if (f.getUnreadMessageCount() != numUnread) { + if (f.getUnreadMessageCount() == -1) { + // Bug97114 + writeKnownBug(f, mname, 97114, + "Got -1 unread messages; kinda rude and weird."); + } else { + writeFailure(f, mname, + "Expected " + numUnread + " unread messages; got " + + f.getUnreadMessageCount()); + } + } + if (f.getNewMessageCount() != numNew) { + writeFailure(f, mname, + "Expected " + numNew + " new messages; got " + + f.getNewMessageCount()); + } + if (f.getMessageCount() != numTotal) { + writeFailure(f, mname, + "Expected " + numTotal + " total messages; got " + + f.getMessageCount()); + return; // No use proceeding if this one fails; + // we'll just get array subscript errors or + // other confusing stuff. + } + Message msgs[] = f.getMessages(); + if (msgs.length != numTotal) { + writeFailure(f, mname, + "getMessages() returned " + + msgs.length + + " messages; expected " + numTotal); + return; + } + for (int i=0 ; i", + "Eric Mader ", + "dcasados@netscape.com (Debbie Casados)"}, + new String[] {"Re: Email bug was [Fwd: Apologies and complaints]", + "[Fwd: Luggage]", + "SITN REGISTRATION INFO"}, + new long[] {834197921000L, + 879964342000L, + 881090639000L}); + } + + void pop3() throws MessagingException { +// props.put("pop.host", "dome"); +// props.put("pop.user", "poptest"); +// props.put("pop.password", "poptest"); + props.put("mail.host", "dome"); + setUserAndPassword("poptest", "poptest"); + PopStore store = new PopStore(session); + store.connect(); + Folder f = store.getDefaultFolder(); + if (!f.getName().equalsIgnoreCase("INBOX")) { + writeFailure(f, "pop3", + "Folder should be named INBOX; is instead named " + + f.getName()); + } + Folder flist[] = f.list(); + if (flist != null && flist.length != 0) { + writeFailure(f, "pop3", f.getName() + " claims to have " + flist.length + + "subfolders, not possible in pop3."); + } + + checkFolder("pop3", f, 2, 2, 0, 2, + new String [] {"Accounts@dome.mcom.com", + "\"Terry Weissman\""}, + new String [] {"Form: Greeting Pop Test", + "Welcome, oh pop test account."}, + new long [] {881356029000L, + 881356087000L}); + } + +} diff --git a/mozilla/grendel/storage/TestBerkeley.java b/mozilla/grendel/storage/TestBerkeley.java new file mode 100644 index 00000000000..308bddef4da --- /dev/null +++ b/mozilla/grendel/storage/TestBerkeley.java @@ -0,0 +1,54 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 22 Aug 1997. + */ + +package grendel.storage; + +import java.io.File; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.util.Properties; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Folder; +import javax.mail.Session; + +class TestBerkeley { + public static void main(String args[]) + throws IOException, MessagingException + { + Properties props = new Properties(); + String dir = "/u/jwz/tmp/.netscape/nsmail/"; + props.put("mail.directory", dir); + Session session = Session.getDefaultInstance(props, null); + BerkeleyStore store = new BerkeleyStore(session); + if (args.length == 0) { + String def[] = { "Inbox" }; + args = def; + } + for (int i = 0; i < args.length; i++) { + System.out.println("Folder " + dir + args[i]); + Folder folder = store.getDefaultFolder().getFolder(args[i]); + Message list[] = folder.getMessages(); + System.out.println("Found " + list.length + " messages."); + } + } +} diff --git a/mozilla/grendel/storage/UnixDotLock.java b/mozilla/grendel/storage/UnixDotLock.java new file mode 100644 index 00000000000..b0e3f6d47f1 --- /dev/null +++ b/mozilla/grendel/storage/UnixDotLock.java @@ -0,0 +1,452 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 20 Sep 1997. + */ + +package grendel.storage; + +import calypso.util.Assert; +import java.lang.Thread; +import java.lang.SecurityException; +import java.lang.InterruptedException; +import java.util.Vector; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** Implements Unix-style dot-locking (locking file "FOO" by using an + atomically-created file in the same directory named "FOO.lock".) + +

Use it like this: + +

    + UnixDotLock lock = new UnixDotLock(file);  // this blocks +
    // ... long-running manipulations of file +
    lock.unlock(); +
+ +

All the lock-retrying and lock-date-maintenance happens under + the covers. + +

See also the + + description of movemail. + +

Implementation details: +

+

The protocol for getting a lock on some file "FOO" is + as follows: + +

    +
  1. Create a file "FOO.1234" (random unused name.)

    + +

  2. Hard-link "FOO.1234" to "FOO.lock".
    + (this is the trick, because link() happens to be + one of the few atomic, synchronized operations over NFS.)

    + +

  3. Unlink "FOO.1234"
    + (regardless of whether step 2 succeeded; now either we have + a "FOO.lock" file or we don't.)

    + +

  4. If we obtained the lock (the link() call in + step #2 succeeded), then we're done.

    + +

  5. Else if the creation-time of "FOO.lock" is more + than 60 seconds in the past, then smash the lock (unlink + "FOO.lock") and goto step #1.)

    + +

  6. Else, the lock is held and current. Wait a second, then + goto step #1 and try again. +
+

+ One thing implied by this is that if one wants to hold a lock for longer + than 60 seconds (which we do in some cases) then one must maintain the + lock file: its modification-date must be updated every less-than-60 + seconds. We do this by having a ``heartbeat'' thread that wakes up + periodically and updates all held locks. +

+ */ + +public class UnixDotLock { + + /** Turn this on to cause activity to be logged to System.err. */ + static private final boolean debug = true; + + /** Holds the (one) thread that updates the lock date every <60 seconds. */ + static private Thread lock_heartbeat_thread = null; + + /** List of locked locks. The vector holds UnixDotLock objects. */ + /* #### We really want this vector to hold weak pointers, so that the locks + get deleted if the lock object is GCed. But, failing to explicitly + unlock the lock doesn't have that bad a failure mode, since the + protocol is to smash the lock after 60 seconds anyway. + */ + static private Vector active_locks = null; + + /** After someone else's lock is 60 seconds old, we assume it has been left + dangling, and smash it. This is a part of the de-facto dot-locking + protocol, folks: I wouldn't make this stuff up. The way you hold a + lock for longer than 60 seconds is by periodically updating the + modification date of the lock file to prove that you're still alive. + */ + static private final int maximum_lock_age = 60; + + /** How often to update the write-date on a lock file. If we want to + hold a lock for longer than 60 seconds, we need to update its + write date, and this is how often we do that. This should be + less than `maximum_lock_age' by enough to be comfortable that + system load and thread starvation won't cause the heartbeat + thread to fail to update the lock file date in time. + */ + static final int heart_rate = 30; + + /** System dependency: multiply the result of File.lastModified() by + this to convert it to seconds. Java doesn't specify the units in which + File.lastModified() measures time, but we need to be able to add N + seconds to it, to tell when a file is more than N seconds old. It + happens that, with JDK 1.1.3 on Irix, this scale is 1000. We must + assume that all other Unixen behave the same. If this changes in some + future Java implementation, we're fucked. + */ + static private final int File_lastModified_scale = 1000; + + /** The name of the file for which this lock is being held. */ + private File locked_file = null; + + /** Lock the named file. Unlock it by calling the unlock() method. + If you do not call unlock() before discarding the UnixDotLock object, + the file will remain locked! + + @exception SecurityException the lock file could not be created + (file permission problems?) + @exception IOException a disk I/O error occurred. + @exception InterruptedException this thread was killed while waiting + for the lock. + */ + public UnixDotLock(File file) + throws SecurityException, IOException, InterruptedException { + if (!file.isAbsolute()) + file = new File(file.getAbsolutePath()); + + // We want to work in the real directory of the file, not in the + // directory of a symlink to the file. + file = new File(file.getCanonicalPath()); + + createDiskLock(file); + setLocked(file); + } + + /** Unlock the file. This must be called before discarding the UnixDotLock + object, and may be called only once. The UnixDotLock object must not + be used again after calling this. + + @exception InterruptedException this thread was killed while waiting + for the lock. + */ + public void unlock() { + if (locked_file == null) + throw new Error("not locked"); + removeDiskLock(locked_file); + try { + setLocked(null); + } catch (InterruptedException e) { + Assert.Assertion(false); // shouldn't be thrown when only unlocking. + } + } + + /** Create a lock file name for the given file (append ".lock" to it.) */ + private File makeLockName(File file) { + return new File(file.toString() + ".lock"); + } + + /** Create the name of a new file in the same directory as the given file. + This picks a random name and then checks to make sure it doesn't + already exist. It doesn't actually create the file (so there's a + very slight race here.) */ + private File gentemp(File prefix) { + File f; + do { + int r = (int) Double.doubleToLongBits(Math.random()); + f = new File(prefix.toString() + "." + Integer.toHexString(r)); + } while (f.exists()); + return f; + } + + /** Attempt to lock the file, using the hairy dot-locking protocol. + Does not return until the lock has been obtained. + @exception SecurityException the lock file could not be created + (file permission problems?). + @exception IOException a disk I/O error occurred. + @exception InterruptedException this thread was killed while waiting + for the lock. + */ + private void createDiskLock(File file) + throws SecurityException, IOException, InterruptedException { + + File dir = new File(file.getParent()); + File lock = makeLockName(file); + + if (debug) + System.err.println("LOCK " + file + " (with " + lock + ")"); + + while (true) { + + if (!dir.canWrite()) + throw new SecurityException("unwritable directory " + dir); + + // 1: create file "FOO.1234" (random name) + // + File tmp = gentemp(lock); + if (debug) System.err.println("\n 1: create " + tmp); + FileOutputStream stream = new FileOutputStream(tmp); + stream.close(); + stream = null; + + // Get the current time as the *disk* sees it, not as the *system* + // sees it -- this avoids lossage when this machine and the file + // server do not have synchronized clocks. + long current_time = tmp.lastModified(); + + // 2: hard-link "FOO.1234" to "FOO.lock" + // (this is the trick, because link() is an atomic, synchronized + // operation over NFS) + // + if (debug) System.err.println(" 2: link " + tmp + " " + lock); + // #### this part is wrong -- I don't know how make link() syscall + boolean link_succeeded = false; + if (!lock.exists()) { + if (debug) System.err.println(" 2: (#### but not really)"); + stream = new FileOutputStream(lock); + stream.close(); + stream = null; + link_succeeded = true; + } + + // 3: unlink "FOO.1234" + // (regardless of whether #2 succeeded; now either we have an + // "FOO.lock" file or we don't) + // + if (debug) System.err.println(" 3: delete " + tmp); + if (!tmp.delete()) + throw new IOException("unable to delete " + tmp); + + // 4: if we obtained the lock (the link() call succeeded), we're done + // + if (link_succeeded) { + if (debug) System.err.println(" 4: locked " + lock); + break; + } + + // 5: else if creation-time of "FOO.lock" is > 60 seconds old, + // smash the lock (unlink "FOO.lock") and goto 1. + // + else if ((lock.lastModified() + + (maximum_lock_age * File_lastModified_scale)) + <= current_time) { + if (debug) + System.err.println(" 5: smash lock " + lock + " (" + + ((current_time - lock.lastModified()) / + File_lastModified_scale) + + " seconds old)"); + lock.delete(); + } + + // 6: else, the lock is current; wait a second, then goto 1 + // and try again. + // + else { + if (debug) + System.err.println(" 6: wait for " + lock + " (" + + ((current_time - lock.lastModified()) / + File_lastModified_scale) + + " seconds old)"); + Thread.sleep(1000); + } + } + } + + /** Assumes that this process had at some point obtained a lock on the + given file, and removes that lock. Calling this without having + obtained the lock will smash someone else's lock, and you don't + want to do that. + */ + private void removeDiskLock(File file) { + File lock = makeLockName(file); + if (debug) System.err.println("UNLOCK " + lock); + lock.delete(); // this had better do the Unix unlink() syscall. + } + + /** Marks the object as locked or unlocked. Manages the heartbeat + thread (creating or killing it, as appropriate.) + + @exception InterruptedException this thread was killed while waiting + for the lock. This can only be thrown + when locking, not when unlocking. + */ + private synchronized void setLocked(File file) throws InterruptedException { + + // This method is synchronized to protect access to the `locked_file' + // instance variable. This method will only be called from the locking + // thread, but the heartbeat() method may be called from the heartbeat + // thread, and we must avoid contention between those two threads. + + if (debug) System.err.println("LOCK = " + file); + this.locked_file = file; + updateLockList(this); + } + + /** If we currently own a lock file, update its modification time. + This is a way of informing other processes that this process is + still alive, and still desires to hold the lock. + + @exception IOException a disk I/O error occurred. + */ + private synchronized void heartbeat() throws IOException { + + // This method is synchronized to protect access to the `locked_file' + // instance variable. This method will only be called from the heartbeat + // thread, but the setLocked() method will be called from the locking + // thread, and we must avoid contention between those two threads. + + if (locked_file != null) { + File lock = makeLockName(locked_file); + if (lock.exists()) { + if (debug) System.err.println(" heartbeat touch " + lock); + FileOutputStream stream = new FileOutputStream(lock); + stream.close(); + } + } + } + + /** Call the heartbeat() method on every UnixDotLock in `active_locks'. + */ + synchronized static void globalHeartbeat() { + // This class-method is synchronized because all manipulations of the + // active_locks class-variable must be protected by a lock on the class. + // (The updateLockList() class-method also uses active_locks.) + + if (debug) System.err.println("heartbeat awake"); + if (active_locks != null) { + for (int i = 0; i < active_locks.size(); i++) { + UnixDotLock d = (UnixDotLock) active_locks.elementAt(i); + if (d != null) { + try { + d.heartbeat(); + } catch (IOException e) { + // ignore errors. + if (debug) System.err.println("ignoring " + e); + } + } + } + } + } + + private synchronized static void updateLockList(UnixDotLock lock) { + // This class-method is synchronized because all manipulations of the + // class-variables active_locks and lock_heartbeat_thread must be + // protected by a lock on the class. (It happens that this is the + // only method to touch lock_heartbeat_thread, but active_locks is + // also used by globalHeartbeat().) + + if (lock.locked_file != null) { // locking + + if (active_locks == null) + active_locks = new Vector(); + active_locks.addElement(lock); + + if (lock_heartbeat_thread == null || + !lock_heartbeat_thread.isAlive()) { + Thread t = new UnixDotLockHeartbeatThread(); + lock_heartbeat_thread = t; + t.setDaemon(true); + t.setName(t.getClass().getName()); + if (debug) System.err.println("LAUNCHING " + t); + t.start(); + } + + } else { // unlocking + active_locks.removeElement(lock); + if (active_locks.isEmpty() && + lock_heartbeat_thread != null) { + if (debug) System.err.println("KILLING " + lock_heartbeat_thread); + Thread h = lock_heartbeat_thread; + lock_heartbeat_thread = null; + h.stop(); + try { + h.join(); + } catch (InterruptedException e) { + // ignore it -- not important. + } + } + } + } + + /** If this object becomes reclaimed without unlock() having been called, + then call it (thus unlocking the underlying disk file.) + @exception Throwable + */ + protected synchronized void finalize() throws Throwable { + if (locked_file != null) { + if (debug) + System.err.println("unlock " + locked_file + " due to finalization!"); + unlock(); + } + super.finalize(); + } + + public static final void main(String arg[]) + throws SecurityException, IOException, InterruptedException { + + System.runFinalizersOnExit(true); + + File file1 = new File("/tmp/a"); + File file2 = new File("/tmp/b"); + UnixDotLock lock1 = new UnixDotLock(file1); + Thread.sleep(3 * 1000); + UnixDotLock lock2 = new UnixDotLock(file2); + Thread.sleep(100 * 1000); + lock1.unlock(); + Thread.sleep(40 * 1000); + lock2.unlock(); + Thread.sleep(3 * 1000); + } +} + + +/** This is the thread that runs as a daemon and periodically updates + the write-dates on long-lived file locks which any user of UnixDotLock + process holds. This thread is launched as soon as there are any + outstanding locks, and is killed when there are none; but if there + is more than one lock, there is still only one heartbeat thread which + manages them all. + + @see UnixDotLock + */ +class UnixDotLockHeartbeatThread extends Thread { + public void run() { + while (true) { + try { + Thread.sleep(UnixDotLock.heart_rate * 1000); + } catch (InterruptedException e) { + return; // is this the right way to do this? + } + UnixDotLock.globalHeartbeat(); + } + } +} diff --git a/mozilla/grendel/storage/addressparser/AddressCorrector.java b/mozilla/grendel/storage/addressparser/AddressCorrector.java new file mode 100644 index 00000000000..6d05654b70c --- /dev/null +++ b/mozilla/grendel/storage/addressparser/AddressCorrector.java @@ -0,0 +1,91 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +/** + * A subclass of RFC822AddressList. Its purpose is to + * catch exceptions created by known correctable RFC822 parsing + * error. It will respond to those exceptions by modifying the + * input str until it can be parsed into a valid list of addresses. + *
A WORK IN PROGRESS + * + * @see RFC822AddressList + * @author Eric Bina + */ +public class AddressCorrector extends RFC822AddressList +{ + private Vector mailbox_list; + private RFC822Mailbox mailboxes[]; + + public AddressCorrector(String str) + { + RFC822Tokenizer tokenize = new RFC822Tokenizer(str); + boolean retry = true; + + this.all_tokens = tokenize.getTokens(); + + while (retry) + { + retry = false; + try + { + this.parser = new RFC822AddressParser(this.all_tokens); + } + catch (RouteAddrNoPhraseException ex) + { + if (ex.hasRoute() == false) + { + int start, end; + + System.out.println("Convert route-addr to addr-spec."); + + start = ex.getOrigRouteAddrStart(this.all_tokens); + end = ex.getOrigRouteAddrEnd(this.all_tokens); + this.all_tokens.removeElementAt(end); + this.all_tokens.removeElementAt(start); + + retry = true; + } + else + { + System.out.println("Exception caught! " + ex.getMessage()); + } + } + catch (RFC822ParserException ex) + { + System.out.println("Other exception caught! " + ex.getMessage()); + } + } + } + + + /*********************** + *********************** + ** PRIVATE METHODS ** + *********************** + ***********************/ +} + diff --git a/mozilla/grendel/storage/addressparser/AddressToken.java b/mozilla/grendel/storage/addressparser/AddressToken.java new file mode 100644 index 00000000000..9185edacb2d --- /dev/null +++ b/mozilla/grendel/storage/addressparser/AddressToken.java @@ -0,0 +1,135 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class AddressToken +{ + static final int UNKNOWN = 0; + static final int SPECIAL_CHAR = 1; + static final int QUOTED_STRING = 2; + static final int DOMAIN_LITERAL = 3; + static final int COMMENT = 4; + static final int ATOM = 5; + static final int WORD = 6; + static final int DOMAIN_REF = 7; + static final int SUB_DOMAIN = 8; + static final int ADDR_SPEC = 9; + static final int ROUTE = 10; + static final int ROUTE_ADDR = 11; + static final int PHRASE = 12; + static final int MAILBOX = 13; + static final int GROUP = 14; + static final int MAIL_ADDRESS = 15; + + public int indx1, indx2; + public int token_type; + + public AddressToken() + { + this.indx1 = 0; + this.indx2 = 0; + this.token_type = UNKNOWN; + } + + public AddressToken(int indx1, int indx2, int type) + { + this.indx1 = indx1; + this.indx2 = indx2; + if (this.indx1 > this.indx2) + { + this.indx1 = indx2; + this.indx2 = indx1; + } + + if ((type < SPECIAL_CHAR)||(type > ATOM)) + { + this.token_type = UNKNOWN; + } + else + { + this.token_type = type; + } + } + + public int getTokenStart() + { + return(this.indx1); + } + + public int getTokenEnd() + { + return(this.indx2); + } + + public int getType() + { + return(this.token_type); + } + + public int length() + { + return(this.indx2 - this.indx1 + 1); + } + + + public String getTokenString(Vector tokens) + { + String token_str; + + if ((this.indx1 < 0)||(this.indx2 < 0)) + { + return(null); + } + + token_str = new String(""); + for (int i=this.indx1; i <= this.indx2; i++) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + String str = (String)token.getObject(); + + token_str = token_str + str; + } + return(token_str); + } + + + public void printToken(Vector tokens) + { + if ((this.indx1 < 0)||(this.indx2 < 0)) + { + return; + } + + for (int i=this.indx1; i <= this.indx2; i++) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + String str = (String)token.getObject(); + System.out.print(str); + } + System.out.flush(); + } +} + diff --git a/mozilla/grendel/storage/addressparser/GroupToken.java b/mozilla/grendel/storage/addressparser/GroupToken.java new file mode 100644 index 00000000000..392c45dac76 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/GroupToken.java @@ -0,0 +1,48 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class GroupToken extends AddressToken +{ + private AddressToken phrase; + private Vector mailboxes; + + public GroupToken(AddressToken phrase, Vector mailboxes, int end) + { + this.phrase = phrase; + this.mailboxes = mailboxes; + this.indx1 = phrase.getTokenStart(); + this.indx2 = end; + this.token_type = AddressToken.GROUP; + } + + + public Vector getMailboxList() + { + return(this.mailboxes); + } +} + diff --git a/mozilla/grendel/storage/addressparser/MailAddressToken.java b/mozilla/grendel/storage/addressparser/MailAddressToken.java new file mode 100644 index 00000000000..4d604baa93b --- /dev/null +++ b/mozilla/grendel/storage/addressparser/MailAddressToken.java @@ -0,0 +1,60 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; + + +class MailAddressToken extends AddressToken +{ + private MailboxToken mailbox; + private GroupToken group; + + public MailAddressToken(MailboxToken mailbox) + { + this.mailbox = mailbox; + this.group = null; + this.indx1 = mailbox.getTokenStart(); + this.indx2 = mailbox.getTokenEnd(); + this.token_type = AddressToken.MAIL_ADDRESS; + } + + public MailAddressToken(GroupToken group) + { + this.mailbox = null; + this.group = group; + this.indx1 = group.getTokenStart(); + this.indx2 = group.getTokenEnd(); + this.token_type = AddressToken.MAIL_ADDRESS; + } + + public MailboxToken getMailbox() + { + return(this.mailbox); + } + + public GroupToken getGroup() + { + return(this.group); + } +} + diff --git a/mozilla/grendel/storage/addressparser/MailboxToken.java b/mozilla/grendel/storage/addressparser/MailboxToken.java new file mode 100644 index 00000000000..a91a4fceb4e --- /dev/null +++ b/mozilla/grendel/storage/addressparser/MailboxToken.java @@ -0,0 +1,249 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class MailboxToken extends AddressToken +{ + private AddressToken addr_spec, phrase, route_addr; + private Vector comments; + private String name, address; + + public MailboxToken(AddressToken addr_spec) + { + this.name = null; + this.address = null; + this.comments = null; + this.addr_spec = addr_spec; + this.phrase = null; + this.route_addr = null; + this.indx1 = addr_spec.getTokenStart(); + this.indx2 = addr_spec.getTokenEnd(); + this.token_type = AddressToken.MAILBOX; + } + + + public MailboxToken(AddressToken phrase, AddressToken route_addr) + { + this.name = null; + this.address = null; + this.comments = null; + this.addr_spec = null; + this.phrase = phrase; + this.route_addr = route_addr; + + // Phrase and route addr must be contiguous. + if (route_addr.getTokenStart() != (phrase.getTokenEnd() + 1)) + { + // ERROR + } + + this.indx1 = phrase.getTokenStart(); + this.indx2 = route_addr.getTokenEnd(); + this.token_type = AddressToken.MAILBOX; + } + + + /* + * Get the address as a string from the list of parsed tokens. + */ + public String getAddress(Vector tokens) + { + String address = null; + + if (this.address != null) + { + return(this.address); + } + + address = new String(""); + /* + * Here we strip any optional route from the route-addr. + */ + if (this.addr_spec == null) + { + int start, end; + + start = this.route_addr.getTokenStart(); + end = this.route_addr.getTokenEnd(); + for (int i=(end - 1); i >= start; i--) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + String str; + + if ((token.isSpecialChar('<') == true)|| + (token.isSpecialChar(':') == true)) + { + break; + } + + str = (String)token.getObject(); + address = str + address; + } + } + /* + * Just compose the addr-spec as a string. + */ + else + { + int start, end; + + start = this.addr_spec.getTokenStart(); + end = this.addr_spec.getTokenEnd(); + for (int i=start; i <= end; i++) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + String str = (String)token.getObject(); + + address = address + str; + } + } + this.address = address; + + return(address); + } + + + /* + * Get the name as a string from the list of parsed tokens. + */ + public String getName(Vector tokens) + { + String name = null; + + if (this.name != null) + { + return(this.name); + } + + name = new String(""); + /* + * Here there was no phrase, but there were some + * comments. Compose a name from the comments, + * replace characters not allowed in the name, + * and enclose the new name as a quoted string. + * when combining multiple comments, use a comma + * separator. + */ + if ((this.phrase == null)&&(this.comments != null)&& + (this.comments.isEmpty() == false)) + { + int num_comments = this.comments.size(); + + for (int c=0; c < num_comments; c++) + { + RFC822Token comment = (RFC822Token)this.comments.elementAt(c); + String str = (String)comment.getObject(); + int len = str.length(); + + if (len < 3) + { + continue; + } + + str = str.substring(1, len - 1); + + /* + * Replace all double quotes with + * single quotes, and replace nested + * comment parens with square brackets. + */ + str = str.replace('\"', '\''); + str = str.replace('(', '['); + str = str.replace(')', ']'); + + name = name + str; + if (c != (num_comments - 1)) + { + name = name + ", "; + } + } + if (name.equalsIgnoreCase("") == false) + { + name = "\"" + name + "\""; + } + } + /* + * Else just compose the phrase as a string + */ + else if (this.phrase != null) + { + int start, end; + + start = this.phrase.getTokenStart(); + end = this.phrase.getTokenEnd(); + for (int i=start; i <= end; i++) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + String str = (String)token.getObject(); + + name = name + str; + if (i != end) + { + name = name + " "; + } + } + } + + /* + * If there was no phrase and no comments, the name + * will become the empty string "" + */ + + this.name = name; + return(name); + } + + + public void addComment(RFC822Token comment) + { + if (this.comments == null) + { + this.comments = new Vector(); + } + this.comments.addElement((Object)comment); + } + + + public Vector GetComments() + { + return(this.comments); + } + + + public boolean noComments() + { + if (this.comments == null) + { + return(true); + } + else if (this.comments.isEmpty() == true) + { + return(true); + } + return(false); + } +} + diff --git a/mozilla/grendel/storage/addressparser/Makefile b/mozilla/grendel/storage/addressparser/Makefile new file mode 100644 index 00000000000..ebbcb77cb69 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/Makefile @@ -0,0 +1,37 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + AddressCorrector.java \ + AddressToken.java \ + GroupToken.java \ + MailAddressToken.java \ + MailboxToken.java \ + P1.java \ + P2.java \ + RFC822AddressList.java \ + RFC822AddressParser.java \ + RFC822Mailbox.java \ + RFC822MailboxList.java \ + RFC822ParserException.java \ + RFC822Token.java \ + RFC822Tokenizer.java \ + RouteAddrNoPhraseException.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/storage/addressparser/P1.java b/mozilla/grendel/storage/addressparser/P1.java new file mode 100644 index 00000000000..bba1bdf6817 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/P1.java @@ -0,0 +1,94 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.util.*; +import java.io.*; + +/** + * A Test program to test the RFC822AddressList and + * RFC822MailboxList classes. + * + * @see RFC822AddressList + * @see RFC822MailboxList + * @author Eric Bina + */ +public class P1 +{ + + public static void main(String args[]) throws IOException + { + RFC822AddressList address_list; + RFC822MailboxList mailbox_list; + + System.out.println("Arg[0] = " + args[0]); + System.out.println("\n\n"); + + System.out.println("Test RFC822AddressList"); + address_list = new RFC822AddressList(args[0]); + if (address_list.isError()) + { + System.out.print("ERROR: "); + System.out.println(address_list.getErrorString()); + } + else + { + System.out.print("Address: "); + for (int i=0; i < address_list.size(); i++) + { + System.out.print(address_list.getAddressString(i)); + if (i != (address_list.size() - 1)) + { + System.out.print(",\n\t"); + } + } + } + System.out.println("\n\n"); + + System.out.println("Test RFC822MailboxList"); + mailbox_list = new RFC822MailboxList(args[0]); + if (mailbox_list.isError()) + { + System.out.print("ERROR: "); + System.out.println(mailbox_list.getErrorString()); + } + else + { + int len; + RFC822Mailbox mailbox_array[]; + + mailbox_array = mailbox_list.getMailboxArray(); + len = mailbox_list.mailboxCount(); + + for (int i=0; i < len; i++) + { + System.out.print(mailbox_array[i].getMailboxString()); + if (i != (len - 1)) + { + System.out.print(", "); + } + } + } + System.out.println("\n\n"); + } +} + diff --git a/mozilla/grendel/storage/addressparser/P2.java b/mozilla/grendel/storage/addressparser/P2.java new file mode 100644 index 00000000000..2b55acb851a --- /dev/null +++ b/mozilla/grendel/storage/addressparser/P2.java @@ -0,0 +1,65 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.util.*; +import java.io.*; + +/** + * A Test program to test the AddressCorrector class. + * + * @see AddressCorrector + * @author Eric Bina + */ +public class P2 +{ + + public static void main(String args[]) throws IOException + { + AddressCorrector address_corr; + + System.out.println("Arg[0] = " + args[0]); + System.out.println("\n\n"); + + System.out.println("Test AddressCorrector"); + address_corr = new AddressCorrector(args[0]); + if (address_corr.isError()) + { + System.out.print("ERROR: "); + System.out.println(address_corr.getErrorString()); + } + else + { + System.out.print("Address: "); + for (int i=0; i < address_corr.size(); i++) + { + System.out.print(address_corr.getAddressString(i)); + if (i != (address_corr.size() - 1)) + { + System.out.print(",\n\t"); + } + } + } + System.out.println("\n\n"); + } +} + diff --git a/mozilla/grendel/storage/addressparser/README b/mozilla/grendel/storage/addressparser/README new file mode 100644 index 00000000000..9718a256ee3 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/README @@ -0,0 +1,6 @@ +This is Grendel -- a Java mail/news client. + +The grendel.storage.addressparser implements a lenient parser for +RFC822 mailboxes. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/storage/addressparser/RFC822AddressList.java b/mozilla/grendel/storage/addressparser/RFC822AddressList.java new file mode 100644 index 00000000000..7a3462c4cbb --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822AddressList.java @@ -0,0 +1,158 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +/** + * This class lets you instantiate an address list object + * from an input string (usually the right hand side to + * a To: field). You can then detect if they were all valid + * addresses, or there was some error. + * You can also access the individual address string to + * send the address list back out again. + * + * @author Eric Bina + */ +public class RFC822AddressList +{ + RFC822AddressParser parser; + Vector all_tokens; + + + /** + * This constructor seems to be needed in order to subclass this + * class. It should never be directly called as it will create a + * null and basically useless address list. + */ + RFC822AddressList() + { + this.all_tokens = null; + this.parser = null; + } + + + /** + * The input string (usually a To: field) is first tokenized, and + * then parsed into a list of addresses. + */ + public RFC822AddressList(String str) + { + RFC822Tokenizer tokenize = new RFC822Tokenizer(str); + + this.all_tokens = tokenize.getTokens(); + try + { + this.parser = new RFC822AddressParser(this.all_tokens); + } + catch (RFC822ParserException ex) + { + this.parser = null; + System.out.println("Exception caught! " + ex.getMessage()); + } + } + + + /** + * @return Returns the number of addresses in the list. + * The string representation for each address + * is indexed from 0 to (size - 1). + * A return of 0 means an empty address list, probably + * a result of an error in parsing. + */ + public int size() + { + if (this.parser == null) + { + return(0); + } + return(this.parser.getAddressList().size()); + } + + + /** + * @param indx an index from 0 to (size - 1). + * @return Either a String representation of the address, + * or null if the indx is outside the list. + * Also returns null is there was an error which + * resulted in no address list. + */ + public String getAddressString(int indx) + { + Vector address_list; + AddressToken address; + + if (this.parser == null) + { + return(null); + } + + address_list = this.parser.getAddressList(); + + if ((indx < 0)||(indx >= address_list.size())) + { + return(null); + } + + address = (AddressToken)address_list.elementAt(indx); + return(address.getTokenString(this.parser.getTokenList())); + } + + + /** + * Were there errors in tokenizing and parsing this string? + */ + /* + * Errors can only happen parsing, so we let the parser + * handle all this. + */ + public boolean isError() + { + if (this.parser == null) + { + return(false); + } + return(this.parser.isError()); + } + + + /** + * @return A string describing the error if there was one. + * Some errors can return an empty string. + * No error in the address list returns "No error." + */ + /* + * Errors can only happen parsing, so we let the parser + * handle all this. + */ + public String getErrorString() + { + if (this.parser == null) + { + return(""); + } + return(this.parser.getErrorString()); + } +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822AddressParser.java b/mozilla/grendel/storage/addressparser/RFC822AddressParser.java new file mode 100644 index 00000000000..3b36ce7bd0c --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822AddressParser.java @@ -0,0 +1,966 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +/* + * Parse a comma separated list of RFC822 addresses. + * The important thing here is all the ERROR cases. + * If you aren't rigidly RFC822 compliant, you will hit + * one of them. + */ +class RFC822AddressParser +{ + static final int NO_ERROR = 0; + static final int LIST_OF_ADDRESSES = -1; + static final int NO_DOMAIN = -2; + static final int NO_VECTOR = -3; + static final int COMMENTS_IN_VECTOR = -4; + static final int EXTRA_TOKENS_AT_START = -5; + static final int NO_ADDR_SPEC = -6; + static final int NO_ROUTE_ADDR = -7; + static final int BAD_ROUTE = -8; + static final int BAD_ROUTE_DOMAIN = -9; + static final int EXTRA_TOKENS_IN_ROUTE_ADDR = -10; + static final int NO_PHRASE = -11; + static final int EXTRA_TOKENS_IN_MAILBOX_LIST = -12; + static final int NO_LOCAL_PART = -13; + static final int BAD_GROUP = -14; + static final int ILLEGAL_SPECIAL = -15; + + private Vector address_list; + private Vector tokens; + private Vector all_tokens; + private int error_code; + + /* + * This is the complete vector of tokens + * straight from the tokenizer. + */ + public RFC822AddressParser(Vector tokens) throws RFC822ParserException + { + AddressToken addr_token = null; + int num; + int start, end; + + this.error_code = NO_ERROR; + this.all_tokens = tokens; + this.address_list = new Vector(); + this.tokens = stripComments(tokens); + + num = this.tokens.size(); + + if (num == 0) + { + // ERROR: zero length input vector. + this.error_code = NO_VECTOR; + return; + } + + start = 0; + end = num - 1; + /* + * Arbitrary commas can start and end this list + */ + start = skipForwardCommas(start, end); + end = skipCommas(end); + if (start > end) + { + // ERROR: zero length input vector. + this.error_code = NO_VECTOR; + return; + } + + addr_token = collectAddress(start, end); + if (addr_token == null) + { + return; + } + addMailAddress(addr_token); + end = addr_token.getTokenStart() - 1; + while (end >= start) + { + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(end); + + /* + * Skip the comma separator if found, but + * don't require it. + */ + if (token.isSpecialChar(',') == true) + { + end = skipCommas(end); + } + + addr_token = collectAddress(start, end); + if (addr_token == null) + { + return; + } + addMailAddress(addr_token); + end = addr_token.getTokenStart() - 1; + } + if (addr_token.getTokenStart() != start) + { + // ERROR: All tokens in input vector not part + // of a legal address list. + this.error_code = EXTRA_TOKENS_AT_START; + return; + } + } + + + public Vector getTokenList() + { + return(this.tokens); + } + + + public boolean isError() + { + if (this.error_code != NO_ERROR) + { + return(true); + } + return(false); + } + + + public String getErrorString() + { + String err; + + switch(this.error_code) + { + case NO_ERROR: + err = new String("No error."); + break; + case LIST_OF_ADDRESSES: + err = new String("Cannot handle non-comma separated list of addresses!"); + break; + case NO_DOMAIN: + err = new String("No valid domain."); + break; + case NO_VECTOR: + err = new String("No input vector."); + break; + case COMMENTS_IN_VECTOR: + err = new String("Comment tokens in input vector."); + break; + case EXTRA_TOKENS_AT_START: + err = new String("Extra tokens at the start of the input vector after parsing address list."); + break; + case NO_ADDR_SPEC: + err = new String("No valid address specification."); + break; + case NO_ROUTE_ADDR: + err = new String("No valid route address."); + break; + case BAD_ROUTE: + err = new String("Bad route format."); + break; + case BAD_ROUTE_DOMAIN: + err = new String("Bad domain in a route list."); + break; + case EXTRA_TOKENS_IN_ROUTE_ADDR: + err = new String("Extra tokens within the bounds of the route address."); + break; + case NO_PHRASE: + err = new String("No valid phrase."); + break; + case EXTRA_TOKENS_IN_MAILBOX_LIST: + err = new String("Extra tokens within the bounds of the mailbox list."); + break; + case NO_LOCAL_PART: + err = new String("No valid local part."); + break; + case BAD_GROUP: + err = new String("Bad group format."); + break; + case ILLEGAL_SPECIAL: + err = new String("Found a special character where another token type was expected."); + break; + default: + err = new String(""); + } + return(err); + } + + + public Vector getAddressList() + { + return(this.address_list); + } + + + /*********************** + *********************** + ** PRIVATE METHODS ** + *********************** + ***********************/ + + + /* + * create a new vector from the passed vector with all + * the comments stripped from it. + */ + private Vector stripComments(Vector tokens) + { + Vector new_vec = new Vector(); + int num = tokens.size(); + + for (int i=0; i < num; i++) + { + RFC822Token token = (RFC822Token)tokens.elementAt(i); + if (token.getType() != RFC822Token.COMMENT) + { + new_vec.addElement(token); + } + } + return(new_vec); + } + + + /* + * Add an address (either group or mailbox) to the + * beginning of an address list vector. + */ + private void addMailAddress(AddressToken addr_token) + { + MailAddressToken address; + + if (addr_token.getType() == AddressToken.GROUP) + { + address = new MailAddressToken((GroupToken)addr_token); + } + else + { + address = new MailAddressToken((MailboxToken)addr_token); + } + this.address_list.insertElementAt((Object)address, 0); + } + + + /* + * Collect an address. Starts from a rightmost + * token and works left. + * In all cases where we return null we set an error code + */ + private AddressToken collectAddress(int start, int end) throws + RFC822ParserException + { + MailboxToken mailbox = null; + AddressToken addr_token = null; + RFC822Token token; + int type; + + token = (RFC822Token)this.tokens.elementAt(end); + type = token.getType(); + if (type == RFC822Token.SPECIAL_CHAR) + { + /* + * A '>' must be the right edge of a + * route-addr which must be a mailbox + * made up of a phrase route-addr pair. + */ + if (token.isSpecialChar('>') == true) + { + mailbox = collectMailbox(end); + if (mailbox == null) + { + return(null); + } + return((AddressToken)mailbox); + } + /* + * A ';' must be the right edge of a + * group which must have a ':' separating + * a phrase from zero or more mailboxes + * in a comma separated list. + */ + else if (token.isSpecialChar(';') == true) + { + GroupToken group; + + group = collectGroup(end); + if (group == null) + { + return(null); + } + return((AddressToken)group); + } + /* + * No other special character can terminate a + * legal address. + */ + else + { + this.error_code = ILLEGAL_SPECIAL; + return(null); + } + } + else if (type == RFC822Token.COMMENT) + { + // ERROR: No comments in address vector. + this.error_code = COMMENTS_IN_VECTOR; + return(null); + } + else + { + addr_token = collectAddrSpec(end); + // if returns null error code already set. + if (addr_token == null) + { + return(null); + } + mailbox = new MailboxToken(addr_token); + return((AddressToken)mailbox); + } + } + + + /* + * Collect a group having been passed the rightmost ';'. + * In all cases where we return null we set an error code. + */ + private GroupToken collectGroup(int indx2) throws RFC822ParserException + { + GroupToken group = null; + Vector mailboxes = new Vector(); + AddressToken phrase; + RFC822Token token; + int colon_at; + int indx; + + /* + * A ';' must be the right edge of a group which + * must have a ':' separating a phrase from zero or + * more mailboxes in a comma separated list. + */ + token = (RFC822Token)this.tokens.elementAt(indx2); + + if (token.isSpecialChar(';') == false) + { + // ERROR: group must be terminated + // by a ';'. + this.error_code = BAD_GROUP; + return(null); + } + + /* + * Stupid RFC allows extra commas here at the end + * of the list. + */ + indx = skipCommas(indx2 - 1); + + token = (RFC822Token)this.tokens.elementAt(indx); + + /* + * We have one or more mailboxes in a comma + * separated list. + */ + if (token.isSpecialChar(':') == false) + { + MailboxToken tmp_mbox; + RFC822Token comma; + + tmp_mbox = collectMailbox(indx); + // if returns null error code already set. + if (tmp_mbox == null) + { + return(null); + } + mailboxes.insertElementAt((Object)tmp_mbox, 0); + + indx = tmp_mbox.getTokenStart(); + comma = (RFC822Token)this.tokens.elementAt(indx - 1); + while (comma.isSpecialChar(',') == true) + { + /* + * Skip the extra commas the RFC allows here + */ + indx = skipCommas(indx - 1); + + /* + * List could end here with commas + */ + comma = (RFC822Token)this.tokens.elementAt(indx); + if (comma.isSpecialChar(':') == true) + { + indx++; + break; + } + + tmp_mbox = collectMailbox(indx); + // if returns null error code already set. + if (tmp_mbox == null) + { + return(null); + } + mailboxes.insertElementAt((Object)tmp_mbox, 0); + + indx = tmp_mbox.getTokenStart(); + comma = (RFC822Token)this.tokens.elementAt(indx - 1); + } + if (comma.isSpecialChar(':') == false) + { + // ERROR: group contained more than + // a comma separated list of + // mailboxes with the + // ':' to ';' pair. + this.error_code = EXTRA_TOKENS_IN_MAILBOX_LIST; + return(null); + } + colon_at = indx - 1; + } + /* + * This is the case of an empty mailbox list + * in the group. + */ + else + { + colon_at = indx; + } + + phrase = collectPhrase(colon_at); + // if returns null error code already set. + if (phrase == null) + { + return(null); + } + group = new GroupToken(phrase, mailboxes, indx2); + return(group); + } + + + /* + * Function to skip arbitrary number of commas. + * Needed because of the RFC's stupid comma separated list + * rules. + * Passed the index of possibly the first comma, + * returns the index of the first non-comma. + */ + private int skipCommas(int indx) + { + RFC822Token comma; + + if (indx >= this.tokens.size()) + { + return(indx); + } + + comma = (RFC822Token)this.tokens.elementAt(indx); + while(comma.isSpecialChar(',') == true) + { + indx--; + if (indx < 0) + { + break; + } + comma = (RFC822Token)this.tokens.elementAt(indx); + } + return(indx); + } + + + /* + * Like skipCommas above but skipping forward + * instead of backwards. + */ + private int skipForwardCommas(int indx, int end) + { + RFC822Token comma; + + comma = (RFC822Token)this.tokens.elementAt(indx); + while(comma.isSpecialChar(',') == true) + { + indx++; + if (indx > end) + { + break; + } + comma = (RFC822Token)this.tokens.elementAt(indx); + } + return(indx); + } + + + /* + * Passing in the final '>' token of a route-addr, work backwards + * through the tokens to the matching opening '<' token. + */ + private int findBeginRouteAddr(int end) + { + int indx = end - 1; + + while (indx >= 0) + { + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(indx); + if (token.isSpecialChar('<') == true) + { + break; + } + indx--; + } + + if (indx >= 0) + { + return(indx); + } + + // ERROR: No matching '<' to '>' pair. + return(NO_ROUTE_ADDR); + } + + + /* + * Starting at the suspected terminal sub-domain token + * parse up to the beginning of the domain. + * All legal domains begin with an '@' symbol. + * To support the illegal but very common case of a local-part + * with no domain, need to return a special error for + * a missing domain. + */ + private int findBeginDomain(int end) + { + int indx = end; + boolean need_dot = false; + + while (indx >= 0) + { + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(indx); + if (token.isSpecialChar('@') == true) + { + break; + } + /* + * Must be a '.' separated list of sub-domains. + */ + else if ((token.isSpecialChar('.') == true)&& + (need_dot == true)) + { + need_dot = false; + } + /* + * Between '.' symbols, only atoms or domain-literals + * make legal sub-domains. + */ + else if (((token.getType() == RFC822Token.ATOM)|| + (token.getType() == RFC822Token.DOMAIN_LITERAL))&& + (need_dot == false)) + { + need_dot = true; + } + else + { + // ERROR: Encountered some token out of + // position to make a legal domain. + return(NO_DOMAIN); + } + indx--; + } + + /* + * Must contain at least one sub-domain after the '@' + * symbol to be a legal domain. + */ + if ((indx >= 0)&&(indx < end)) + { + return(indx); + } + + // ERROR: No legal domain. + return(NO_DOMAIN); + } + + + /* + * A local-part looks a lot like a domain. The difference + * being it can have quoted-strings as well as atoms between the '.' + * symbols, while the domain can have domain-literals as well + * as atoms between the '.' symbols. + * So for a list of just '.' separated atoms, you depend + * on the '@' symbol to know which you are. + * + * It is illegal but common for the domain to be ommitted. + * We will treat this as legal. + */ + private int findBeginLocalPart(int end) + { + int indx = end; + boolean need_dot = false; + + while (indx >= 0) + { + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(indx); + if ((token.isSpecialChar('.') == true)&& + (need_dot == true)) + { + need_dot = false; + } + else if (((token.getType() == RFC822Token.QUOTED_STRING)|| + (token.getType() == RFC822Token.ATOM))&& + (need_dot == false)) + { + need_dot = true; + } + /* + * Break the local part on the first non-matching + * token you find. + */ + else + { + break; + } + indx--; + } + + /* + * Since we broke on the terminating token, increment + * to get to the start of the local-part. + */ + indx++; + + /* + * Must contain at least one word. + * Cannot start with a '.' symbol. + */ + if ((need_dot == true)&&(indx >= 0)&&(indx <= end)) + { + return(indx); + } + + // ERROR: Not a legal local-part. + return(NO_LOCAL_PART); + } + + + /* + * A legal phrase is always followed by a special symbol + * (either ':' or '<'). The passed index is assumed to be that + * symbol, and we search back for the start of the phrase. + */ + private int findBeginPhrase(int end) + { + int indx = end - 1; + + while (indx >= 0) + { + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(indx); + if ((token.getType() == RFC822Token.QUOTED_STRING)|| + (token.getType() == RFC822Token.ATOM)) + { + // OK + } + else + { + break; + } + indx--; + } + + /* + * Since we broke on the terminating token, increment + * to get to the start of the phrase. + */ + indx++; + + /* + * A legal index must have at least one word in + * the phrase. + */ + if ((indx >= 0)&&(indx < end)) + { + return(indx); + } + + // ERROR: Illegal phrase. + return(NO_PHRASE); + } + + + /* + * Having been passed the rightmost token, collect the + * phrase and pass it back as an address token. + * On an error return null and set an error code. + */ + private AddressToken collectPhrase(int indx2) + { + int indx1 = findBeginPhrase(indx2); + AddressToken addr_token; + + if (indx1 == NO_PHRASE) + { + this.error_code = NO_PHRASE; + return(null); + } + + addr_token = new AddressToken(indx1, indx2 - 1, AddressToken.PHRASE); + return(addr_token); + } + + + /* + * Having been passed the rightmost token, collect the + * route-addr and pass it back as an address token. + * On an error return null and set an error code. + */ + private AddressToken collectRouteAddr(int indx2) + { + AddressToken addr_token; + RFC822Token token; + int indx; + int indx1 = findBeginRouteAddr(indx2); + + if ((indx1 == NO_ROUTE_ADDR)||(indx1 >= (indx2 - 1))) + { + // ERROR: Illegal to have a route-addr with + // an empty body. + this.error_code = NO_ROUTE_ADDR; + return(null); + } + addr_token = collectAddrSpec(indx2 - 1); + // if returns null error code already set. + if (addr_token == null) + { + return(null); + } + indx = addr_token.getTokenStart(); + + /* + * The route is optional, look for the closing ':' + * symbol to see if we have one. + */ + token = (RFC822Token)this.tokens.elementAt(indx - 1); + if (token.isSpecialChar(':') == true) + { + AddressToken route = collectRoute(indx - 1); + + // if returns null error code already set. + if (route == null) + { + return(null); + } + indx = route.getTokenStart(); + } + + token = (RFC822Token)this.tokens.elementAt(indx - 1); + if (token.isSpecialChar('<') == false) + { + // ERROR: More in the route-addr than a legal + // addr-spec and an optional route. + this.error_code = EXTRA_TOKENS_IN_ROUTE_ADDR; + return(null); + } + addr_token = new AddressToken(indx - 1, indx2, AddressToken.ROUTE_ADDR); + return(addr_token); + } + + + /* + * Having been passed the rightmost token, collect the + * addr-spec and pass it back as an address token. + * On an error return null and set an error code. + */ + private AddressToken collectAddrSpec(int indx2) + { + int at_sign = findBeginDomain(indx2); + int indx1; + AddressToken addr_token; + + if (at_sign == NO_DOMAIN) + { + indx1 = findBeginLocalPart(indx2); + } + else + { + indx1 = findBeginLocalPart(at_sign - 1); + } + + if (indx1 == NO_LOCAL_PART) + { + this.error_code = NO_ADDR_SPEC; + return(null); + } + + addr_token = new AddressToken(indx1, indx2, AddressToken.ADDR_SPEC); + + return(addr_token); + } + + /* + * Having been passed the rightmost token, collect the + * route and pass it back as an address token. + * On an error return null and set an error code. + */ + private AddressToken collectRoute(int indx2) + { + AddressToken addr_token; + RFC822Token token; + int at_sign; + int indx; + + token = (RFC822Token)this.tokens.elementAt(indx2); + if (token.isSpecialChar(':') == false) + { + // ERROR: Not a legal route, not ':' terminated. + this.error_code = BAD_ROUTE; + return(null); + } + + /* + * Stupid RFC allows many commas at end of list. + */ + indx = skipCommas(indx2 - 1); + + /* + * Must be at least one domain, look for the '@' symbol + * that starts it. + */ + at_sign = findBeginDomain(indx); + if (at_sign < 1) + { + // ERROR: Must always be room to move back from + // the start of a route because it is + // always enclosed in a route-addr. + // So it is always ok to look for the ',' + // In the optional comma separated list. + this.error_code = BAD_ROUTE_DOMAIN; + return(null); + } + + /* + * If there is a comma, there are one or more domains to + * skip over. + */ + token = (RFC822Token)this.tokens.elementAt(at_sign - 1); + while (token.isSpecialChar(',') == true) + { + /* + * Again RFC allows many commas + */ + indx = skipCommas(at_sign - 1); + /* + * These commas may have been the end of the list + */ + token = (RFC822Token)this.tokens.elementAt(indx); + if (token.isSpecialChar('<') == true) + { + at_sign = indx + 1; + break; + } + + at_sign = findBeginDomain(indx); + if (at_sign < 1) + { + // ERROR: Must always be room to move back from + // the start of a route because it is + // always enclosed in a route-addr. + // So it is always ok to look for the',' + // In the optional comma separated list. + this.error_code = BAD_ROUTE_DOMAIN; + return(null); + } + token = (RFC822Token)this.tokens.elementAt(at_sign - 1); + } + + addr_token = new AddressToken(at_sign, indx2, AddressToken.ROUTE); + + return(addr_token); + } + + + /* + * Having been passed the rightmost token, collect the + * mailbox and pass it back as a mailbox token. + * On an error return null and set an error code. + */ + private MailboxToken collectMailbox(int indx2) throws RFC822ParserException + { + MailboxToken mailbox = null; + AddressToken addr_token = null; + AddressToken phrase; + RFC822Token token; + int indx; + + /* + * A legal mailbox either ends in a '>' + * symbol and is a phrase route-addr pair, + * or it is a simple addr-spec. + */ + token = (RFC822Token)this.tokens.elementAt(indx2); + /* + * Get the phrase route-addr pair. + */ + if (token.isSpecialChar('>') == true) + { + addr_token = collectRouteAddr(indx2); + // if returns null error code already set. + if (addr_token == null) + { + return(null); + } + indx = addr_token.getTokenStart(); + phrase = collectPhrase(indx); + if (this.error_code == NO_PHRASE) + { + throw new RouteAddrNoPhraseException("No valid phrase with this route address.", addr_token, this.tokens); + } + // if returns null error code already set. + if (phrase == null) + { + return(null); + } + indx = phrase.getTokenStart(); + mailbox = new MailboxToken(phrase, addr_token); + } + /* + * Get the addr-spec. + */ + else + { + addr_token = collectAddrSpec(indx2); + // if returns null error code already set. + if (addr_token == null) + { + return(null); + } + indx = addr_token.getTokenStart(); + mailbox = new MailboxToken(addr_token); + } + + return(mailbox); + } +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822Mailbox.java b/mozilla/grendel/storage/addressparser/RFC822Mailbox.java new file mode 100644 index 00000000000..5263b6663b9 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822Mailbox.java @@ -0,0 +1,94 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +/** + * Stores a RFC822 mailbox. Defined as being either a + * addr-spec (local-part@domain), or a route-addr + * preceeded by a phrase (phrase <local-part@domain>). + *

+ * Mere humans tends to consider this a name and an + * address. In the first addr-spec form, the name is usually + * within a RFC822 comment preceeding the addr-spec. + * If there is no comment, a mailbox name may be null. + *

+ * Normally used only as a data type returned by the RFC822MailboxList + * class. + * + * @see RFC822MailboxList + * @author Eric Bina + */ +public class RFC822Mailbox +{ + private String name, address; + + /** + * Should be created only by the RFC822MailboxList class. + * + * @see RFC822MailboxList + */ + public RFC822Mailbox(String name, String address) + { + this.name = name; + this.address = address; + } + + + public String getName() + { + return(this.name); + } + + + public String getAddress() + { + return(this.address); + } + + + /** + * Creates a valid RFC822 mailbox. Since we don't like to lose + * the comment information, this will reform addresses like: + *
(Eric Bina) ebina@netscape.com
+ * to addresses like: + *
"Eric Bina" <ebina@netscape.com>
+ */ + public String getMailboxString() + { + String mailbox = null; + + if ((this.name != null)&&(this.name.equalsIgnoreCase("") == false)) + { + mailbox = this.name + " <" + this.address + ">"; + } + else + { + mailbox = this.address; + } + return(mailbox); + } +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822MailboxList.java b/mozilla/grendel/storage/addressparser/RFC822MailboxList.java new file mode 100644 index 00000000000..9769007c3c5 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822MailboxList.java @@ -0,0 +1,317 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +/** + * A subclass of RFC822AddressList. Its purpose is to create + * a more human readable list of all the addresses of this address list. + * To that end it flattens RFC822 groups in place into mailbox lists. + * It also attempts to save those occasions when name information + * is stored in a comment with an addr-spec instead of being put into + * the more complete form of (phrase route-addr). + * + * @see RFC822AddressList + * @author Eric Bina + */ +public class RFC822MailboxList extends RFC822AddressList +{ + private Vector mailbox_list; + private RFC822Mailbox mailboxes[]; + + public RFC822MailboxList(String str) + { + super(str); + if (this.parser != null) + { + createMailboxList(); + createMailboxArray(); + } + } + + + /** + * @return An array of RFC822Mailbox objects. + * Methods on the object are used to get strings + * to display. + * + * @see RFC822Mailbox + */ + public RFC822Mailbox[] getMailboxArray() + { + return(this.mailboxes); + } + + /* + * Size of the mailbox list. + */ + public int mailboxCount() + { + if (this.mailbox_list == null) + { + return(0); + } + else + { + return(this.mailbox_list.size()); + } + } + + + /*********************** + *********************** + ** PRIVATE METHODS ** + *********************** + ***********************/ + + /* + * Make an array of standalone RFC822Mailbox objects + * which contain all the needed string data. + */ + private void createMailboxArray() + { + int cnt = this.mailbox_list.size(); + + this.mailboxes = new RFC822Mailbox[cnt]; + for (int i=0; i= parser_tokens.size()) + { + return(false); + } + + token = (RFC822Token)parser_tokens.elementAt(indx); + + if ((token.isSpecialChar(',') == true)||(token.isSpecialChar(';') == true)) + { + return(true); + } + return(false); + } + + + /* + * Is the index passed into the main token list a comment token? + */ + private boolean isComment(int indx) + { + RFC822Token token; + + if (indx >= this.all_tokens.size()) + { + return(false); + } + token = (RFC822Token)this.all_tokens.elementAt(indx); + + if (token.getType() == RFC822Token.COMMENT) + { + return(true); + } + return(false); + } +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822ParserException.java b/mozilla/grendel/storage/addressparser/RFC822ParserException.java new file mode 100644 index 00000000000..58ce5eb6b71 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822ParserException.java @@ -0,0 +1,48 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class RFC822ParserException extends Exception +{ + public AddressToken token; + public Vector tokens; + + public RFC822ParserException(String str, AddressToken addr_token, + Vector tokens) + { + super(str); + this.token = addr_token; + this.tokens = tokens; + } + + + /*********************** + *********************** + ** PRIVATE METHODS ** + *********************** + ***********************/ +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822Token.java b/mozilla/grendel/storage/addressparser/RFC822Token.java new file mode 100644 index 00000000000..c217b7ec017 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822Token.java @@ -0,0 +1,116 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class RFC822Token +{ + static final int UNKNOWN = 0; + static final int SPECIAL_CHAR = 1; + static final int QUOTED_STRING = 2; + static final int DOMAIN_LITERAL = 3; + static final int COMMENT = 4; + static final int ATOM = 5; + + private Object obj; + private int token_type; + + public RFC822Token(Object obj, int type) + { + this.obj = obj; + if ((type < SPECIAL_CHAR)||(type > ATOM)) + { + this.token_type = UNKNOWN; + } + else + { + this.token_type = type; + } + } + + public Object getObject() + { + return(this.obj); + } + + public int getType() + { + return(this.token_type); + } + + public boolean isSpecialChar(char sp_char) + { + if (this.token_type == SPECIAL_CHAR) + { + String str = (String)this.obj; + char t_char = str.charAt(str.length() - 1); + + if (t_char == sp_char) + { + return(true); + } + } + + return(false); + } + + public void printTokenType() + { + if (this.token_type == SPECIAL_CHAR) + { + System.out.print("Special"); + } + else if (this.token_type == QUOTED_STRING) + { + System.out.print("Quoted String"); + } + else if (this.token_type == DOMAIN_LITERAL) + { + System.out.print("Domain Literal"); + } + else if (this.token_type == COMMENT) + { + System.out.print("Comment"); + } + else if (this.token_type == ATOM) + { + System.out.print("Atom"); + } + else + { + System.out.print("Unknown"); + } + } + + + public void printToken() + { + String str = (String)this.getObject(); + + System.out.print(str); + System.out.flush(); + } +} + diff --git a/mozilla/grendel/storage/addressparser/RFC822Tokenizer.java b/mozilla/grendel/storage/addressparser/RFC822Tokenizer.java new file mode 100644 index 00000000000..c8f4b7d732b --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RFC822Tokenizer.java @@ -0,0 +1,622 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +// Class to tokenize a RFC822 header-body. +// The class is initialized by passing the header-body as +// a string, and it immediatly attempts to tokenize +// the string into the following tokens as defined by rfc822 +// atom +// special character +// quoted string +// domain literal +// comment +class RFC822Tokenizer +{ + // Various constants defining parsing states. + static final int AT_END = 0; + static final int IN_NOTHING = 1; + static final int IN_ATOM = 2; + static final int IN_COMMENT = 3; + static final int IN_DOMAIN_LITERAL = 4; + static final int IN_QUOTED_TEXT = 5; + static final int IN_SPECIAL = 6; + + // Important characters to switch states on. + static final char BEGIN_COMMENT = '('; + static final char END_COMMENT = ')'; + static final char BEGIN_DOMAIN_LITERAL = '['; + static final char END_DOMAIN_LITERAL = ']'; + static final char BEGIN_QUOTE = '\"'; + static final char END_QUOTE = '\"'; + static final char BEGIN_QUOTE_PAIR = '\\'; + static final char CARRIAGE_RETURN = '\r'; + + private StringStream sstr; + private Vector tokens; + + public RFC822Tokenizer(String str) + { + int parse_state = IN_NOTHING; + int len = str.length(); + char t_char; + + // Create to vector to store the tokenized output. + // Wrapped the passed string in a class to feed + // it through the parser. + this.tokens = new Vector(); + this.sstr = new StringStream(str); + t_char = this.sstr.currentChar(); + + // This while loop is the main body of the parser. + while ((parse_state != AT_END)&&(this.sstr.currentIndex() < len)) + { + int indx1, indx2; + + // Big if-else (should be a switch) + // to switch on parser state. + if (parse_state == IN_NOTHING) + { + parse_state = skipNothing(); + } + else if (parse_state == IN_ATOM) + { + indx1 = this.sstr.currentIndex(); + parse_state = skipAtom(); + indx2 = this.sstr.currentIndex(); + addAtom(str, indx1, indx2); + } + else if (parse_state == IN_COMMENT) + { + indx1 = this.sstr.currentIndex(); + t_char = this.sstr.nextChar(); + parse_state = skipComment(); + t_char = this.sstr.nextChar(); + indx2 = this.sstr.currentIndex(); + addComment(str, indx1, indx2); + } + else if (parse_state == IN_DOMAIN_LITERAL) + { + indx1 = this.sstr.currentIndex(); + t_char = this.sstr.nextChar(); + parse_state = skipDomainLiteral(); + t_char = this.sstr.nextChar(); + indx2 = this.sstr.currentIndex(); + addDomainLiteral(str, indx1, indx2); + } + else if (parse_state == IN_QUOTED_TEXT) + { + indx1 = this.sstr.currentIndex(); + t_char = this.sstr.nextChar(); + parse_state = skipQuotedText(); + t_char = this.sstr.nextChar(); + indx2 = this.sstr.currentIndex(); + addQuotedText(str, indx1, indx2); + } + else if (parse_state == IN_SPECIAL) + { + indx1 = this.sstr.currentIndex(); + t_char = this.sstr.nextChar(); + indx2 = this.sstr.currentIndex(); + addSpecial(str, indx1, indx2); + parse_state = IN_NOTHING; + } + } + } + + + public Vector getTokens() + { + return(this.tokens); + } + + + public void showVector() + { + Vector vec = this.tokens; + int num = vec.size(); + + for (int indx=0; indx < num; indx++) + { + RFC822Token token; + String str; + + token = (RFC822Token)vec.elementAt(indx); + str = (String)token.getObject(); + System.out.print("{" + str + "}->"); + token.printTokenType(); + System.out.println(" "); + } + System.out.println("\n"); + } + + + + /********************* + ********************* + ** Private methods ** + ********************* + *********************/ + + /* + * Methods to add tokens to the output vector. + */ + + // Wrap the atom token substring in a RFC822Token object + // and add it to the output vector. + private void addAtom(String str, int indx1, int indx2) + { + RFC822Token token; + String substr = str.substring(indx1, indx2); + + token = new RFC822Token(substr, RFC822Token.ATOM); + this.tokens.addElement(token); + } + + // Wrap the comment token substring in a RFC822Token object + // and add it to the output vector. + private void addComment(String str, int indx1, int indx2) + { + RFC822Token token; + String substr = str.substring(indx1, indx2); + + token = new RFC822Token(substr, RFC822Token.COMMENT); + this.tokens.addElement(token); + } + + // Wrap the domain-literal token substring in a RFC822Token object + // and add it to the output vector. + private void addDomainLiteral(String str, int indx1, int indx2) + { + RFC822Token token; + String substr = str.substring(indx1, indx2); + + token = new RFC822Token(substr, RFC822Token.DOMAIN_LITERAL); + this.tokens.addElement(token); + } + + // Wrap the quoted-string token substring in a RFC822Token object + // and add it to the output vector. + private void addQuotedText(String str, int indx1, int indx2) + { + RFC822Token token; + String substr = str.substring(indx1, indx2); + + token = new RFC822Token(substr, RFC822Token.QUOTED_STRING); + this.tokens.addElement(token); + } + + // Wrap the special character token substring in a RFC822Token object + // and add it to the output vector. + // Should use a Character object here instead. + private void addSpecial(String str, int indx1, int indx2) + { + RFC822Token token; + String substr = str.substring(indx1, indx2); + + token = new RFC822Token(substr, RFC822Token.SPECIAL_CHAR); + this.tokens.addElement(token); + } + + + /* + * Methods to test conditions. + */ + + // Test if the passed character is one of the rfc822 + // special characters. + private boolean isSpecial(char t_char) + { + if ((t_char == '(')|| + (t_char == ')')|| + (t_char == '<')|| + (t_char == '>')|| + (t_char == '@')|| + (t_char == ',')|| + (t_char == ';')|| + (t_char == ':')|| + (t_char == '"')|| + (t_char == '.')|| + (t_char == '[')|| + (t_char == ']')|| + (t_char == '\\')) + return true; + + return false; + } + + + /* + * Methods to test if we remain within character set types. + */ + + // Test if we are still in the outer parse state. + private boolean inNothing(char t_char) + { + if (Character.isSpaceChar(t_char)) + return true; + + if (Character.isISOControl(t_char)) + return true; + + return false; + } + + + // Test if we are still in the atom parse state. + private boolean inAtom(char t_char) + { + // Should test for just ASCII 32 + if (Character.isSpaceChar(t_char)) + return false; + + // Should test for ASCII 0 - 31 inclusive, and + // DEL (ASCII 127). + if (Character.isISOControl(t_char)) + return false; + + if (isSpecial(t_char)) + return false; + + return true; + } + + + // Test if we are still in the comment parse state. + private boolean inComment(char t_char) + { + if (t_char == CARRIAGE_RETURN) + return false; + + if (t_char == '\\') + return false; + + if (t_char == '(') + return false; + + if (t_char == ')') + return false; + + return true; + } + + + // Test if we are still in the domain-literal parse state. + private boolean inDomainLiteral(char t_char) + { + if (t_char == CARRIAGE_RETURN) + return false; + + if (t_char == '\\') + return false; + + if (t_char == '[') + return false; + + if (t_char == ']') + return false; + + return true; + } + + + // Test if we are still in the quoted-string parse state. + private boolean inQuotedText(char t_char) + { + if (t_char == CARRIAGE_RETURN) + { + return false; + } + + if (t_char == '\\') + { + return false; + } + + if (t_char == END_QUOTE) + { + return false; + } + + return true; + } + + + /* + * Methods to skip characters until a state change + * should occur + */ + + // Skip all characters in the outer nothing state. + private int skipNothing() + { + char t_char; + int state = AT_END; + + t_char = this.sstr.currentChar(); + while ((this.sstr.atEnd() == false)&&(inNothing(t_char) != false)) + { + t_char = this.sstr.nextChar(); + } + + if (this.sstr.atEnd()) + { + state = AT_END; + } + else if (t_char == BEGIN_COMMENT) + { + state = IN_COMMENT; + } + else if (t_char == BEGIN_DOMAIN_LITERAL) + { + state = IN_DOMAIN_LITERAL; + } + else if (t_char == BEGIN_QUOTE) + { + state = IN_QUOTED_TEXT; + } + else if (isSpecial(t_char) != false) + { + state = IN_SPECIAL; + } + else + { + state = IN_ATOM; + } + + return state; + } + + + // Skip all characters in the atom state. + private int skipAtom() + { + char t_char; + int state = AT_END; + + t_char = this.sstr.currentChar(); + while ((this.sstr.atEnd() == false)&&(inAtom(t_char) != false)) + { + t_char = this.sstr.nextChar(); + } + + if (this.sstr.atEnd()) + { + state = AT_END; + } + else if (t_char == BEGIN_COMMENT) + { + state = IN_COMMENT; + } + else if (t_char == BEGIN_DOMAIN_LITERAL) + { + state = IN_DOMAIN_LITERAL; + } + else if (t_char == BEGIN_QUOTE) + { + state = IN_QUOTED_TEXT; + } + else if (Character.isSpaceChar(t_char)) + { + state = IN_NOTHING; + } + else if (Character.isISOControl(t_char)) + { + state = IN_NOTHING; + } + else if (isSpecial(t_char) != false) + { + state = IN_SPECIAL; + } + else + { + // ERROR + } + + return state; + } + + + // Skip all characters in the comment state. + private int skipComment() + { + char t_char; + int state = AT_END; + + t_char = this.sstr.currentChar(); + while ((this.sstr.atEnd() == false)&&(inComment(t_char) != false)) + { + t_char = this.sstr.nextChar(); + } + + if (this.sstr.atEnd()) + { + state = AT_END; + } + else if (t_char == BEGIN_COMMENT) + { + t_char = this.sstr.nextChar(); + state = skipComment(); + t_char = this.sstr.nextChar(); + state = skipComment(); + } + else if (t_char == END_COMMENT) + { + state = IN_NOTHING; + } + else if (t_char == BEGIN_QUOTE_PAIR) + { + t_char = this.sstr.nextChar(); + t_char = this.sstr.nextChar(); + state = skipComment(); + } + else if (t_char == CARRIAGE_RETURN) + { + // ERROR + } + else + { + // ERROR + } + + return state; + } + + + // Skip all characters in the domain-literal state. + private int skipDomainLiteral() + { + char t_char; + int state = AT_END; + + t_char = this.sstr.currentChar(); + while ((this.sstr.atEnd() == false)&&(inDomainLiteral(t_char) != false)) + { + t_char = this.sstr.nextChar(); + } + + if (this.sstr.atEnd()) + { + state = AT_END; + } + else if (t_char == BEGIN_DOMAIN_LITERAL) + { + // ERROR + } + else if (t_char == END_DOMAIN_LITERAL) + { + state = IN_NOTHING; + } + else if (t_char == BEGIN_QUOTE_PAIR) + { + t_char = this.sstr.nextChar(); + t_char = this.sstr.nextChar(); + state = skipDomainLiteral(); + } + else if (t_char == CARRIAGE_RETURN) + { + // ERROR + } + else + { + // ERROR + } + + return state; + } + + + // Skip all characters in the quoted-string state. + private int skipQuotedText() + { + char t_char; + int state = AT_END; + + t_char = this.sstr.currentChar(); + while ((this.sstr.atEnd() == false)&&(inQuotedText(t_char) != false)) + { + t_char = this.sstr.nextChar(); + } + + if (this.sstr.atEnd()) + { + state = AT_END; + } + else if (t_char == END_QUOTE) + { + state = IN_NOTHING; + } + else if (t_char == BEGIN_QUOTE_PAIR) + { + t_char = this.sstr.nextChar(); + t_char = this.sstr.nextChar(); + state = skipQuotedText(); + } + else if (t_char == CARRIAGE_RETURN) + { + // ERROR + } + else + { + // ERROR + } + + return state; + } + + + /* + * Member class StringStream + */ + private class StringStream { + private String str; + private int indx; + private int length; + private boolean atEnd; + + public StringStream(String str) + { + this.str = str; + this.indx = 0; + this.length = str.length(); + this.atEnd = false; + } + + public int currentIndex() + { + if (this.atEnd) + { + return(this.length); + } + else + { + return(this.indx); + } + } + + public char currentChar() + { + return(this.str.charAt(this.indx)); + } + + public char nextChar() + { + this.indx++; + if (this.indx >= this.length) + { + this.indx = this.length - 1; + this.atEnd = true; + } + return(this.str.charAt(this.indx)); + } + + public boolean atEnd() + { + return(this.atEnd); + } + } +} + diff --git a/mozilla/grendel/storage/addressparser/RouteAddrNoPhraseException.java b/mozilla/grendel/storage/addressparser/RouteAddrNoPhraseException.java new file mode 100644 index 00000000000..c4cfbc7ad52 --- /dev/null +++ b/mozilla/grendel/storage/addressparser/RouteAddrNoPhraseException.java @@ -0,0 +1,200 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Eric Bina , 30 Oct 1997. + */ + +package grendel.storage.addressparser; + +import java.io.*; +import java.util.*; + + +class RouteAddrNoPhraseException extends RFC822ParserException +{ + public RouteAddrNoPhraseException(String str, AddressToken addr_token, + Vector tokens) + { + super(str, addr_token, tokens); + } + + + public boolean hasRoute() + { + int start = getAddrSpecStart(); + RFC822Token token; + + token = (RFC822Token)this.tokens.elementAt(start - 1); + if (token.isSpecialChar(':') == true) + { + return(true); + } + return(false); + } + + + public boolean hasComments(Vector orig_tokens) + { + int start = this.token.getTokenStart(); + int end = this.token.getTokenEnd(); + int orig_start = matchToken(0, 0, start, orig_tokens, this.tokens); + + /* + * Comments just before the starting '<' count. + */ + if (isComment(orig_tokens, (orig_start - 1)) == true) + { + return(true); + } + + /* + * Look for any comments in the body. + */ + orig_start++; + start++; + while (start < end) + { + if (isComment(orig_tokens, orig_start) == true) + { + return(true); + } + orig_start++; + start++; + } + + /* + * Comments just after the '>' count. + */ + if (isComment(orig_tokens, (orig_start + 1)) == true) + { + return(true); + } + return(false); + } + + + public AddressToken getRouteAddr() + { + return(this.token); + } + + + public int getOrigRouteAddrStart(Vector orig_tokens) + { + int start = this.token.getTokenStart(); + int indx = matchToken(0, 0, start, orig_tokens, this.tokens); + + return(indx); + } + + + public int getOrigRouteAddrEnd(Vector orig_tokens) + { + int start = this.token.getTokenStart(); + int end = this.token.getTokenEnd(); + int indx = matchToken(0, 0, start, orig_tokens, this.tokens); + + indx = matchToken(indx, start, end, orig_tokens, this.tokens); + return(indx); + } + + + public AddressToken getAddrSpec() + { + int start = getAddrSpecStart(); + int end = this.token.getTokenEnd(); + AddressToken addr_spec; + + end--; + addr_spec = new AddressToken(start, end, AddressToken.ADDR_SPEC); + return(addr_spec); + } + + +/* + public AddressToken makePhrase(Vector orig_tokens) + { + } +*/ + + + /*********************** + *********************** + ** PRIVATE METHODS ** + *********************** + ***********************/ + + + private int matchToken(int orig_start, int start, int end, + Vector orig_tokens, Vector tokens) + { + if (start > end) + { + return(orig_start); + } + + while (start <= end) + { + while (isComment(orig_tokens, orig_start) == true) + { + orig_start++; + } + orig_start++; + start++; + } + return(orig_start - 1); + } + + + private boolean isComment(Vector tokens, int indx) + { + RFC822Token token; + + if ((indx < 0)||(indx >= tokens.size())) + { + return(false); + } + + token = (RFC822Token)tokens.elementAt(indx); + if (token.getType() == RFC822Token.COMMENT) + { + return(true); + } + return(false); + } + + + private int getAddrSpecStart() + { + RFC822Token token; + int end = this.token.getTokenEnd(); + + token = (RFC822Token)this.tokens.elementAt(end); + while (token.isSpecialChar('<') == false) + { + if (token.isSpecialChar(':') == true) + { + break; + } + end--; + token = (RFC822Token)this.tokens.elementAt(end); + } + return(end + 1); + } +} + diff --git a/mozilla/grendel/storage/folderstats.el b/mozilla/grendel/storage/folderstats.el new file mode 100644 index 00000000000..07b1b624cc0 --- /dev/null +++ b/mozilla/grendel/storage/folderstats.el @@ -0,0 +1,232 @@ +;;; -*- Mode: Emacs-Lisp -*- +;;; +;;; The contents of this file are subject to the Mozilla Public License +;;; Version 1.0 (the "License"); you may not use this file except in +;;; compliance with the License. You may obtain a copy of the License at +;;; http://www.mozilla.org/MPL/ +;;; +;;; Software distributed under the License is distributed on an "AS IS" +;;; basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +;;; the License for the specific language governing rights and limitations +;;; under the License. +;;; +;;; The Original Code is the Grendel mail/news client. +;;; +;;; The Initial Developer of the Original Code is Netscape Communications +;;; Corporation. Portions created by Netscape are Copyright (C) 1997 +;;; Netscape Communications Corporation. All Rights Reserved. +;;; +;;; Created: 25-Sep-97 by Jamie Zawinski . + + +(defun map-folder (file function) + (cond ((file-directory-p file) + (message "Listing %s..." file) + (let* ((default-directory (expand-file-name file)) + (files (directory-files "." nil))) + (while files + (or (string-match "\\`\\.\\|\\.summary\\'" (car files)) + (map-folder (car files) function)) + (setq files (cdr files))))) + (t + (message "%s (loading)..." file) + (let ((b (find-file-noselect file))) + (save-excursion + (set-buffer b) + (goto-char (point-min)) + (if (looking-at "\n*From ") + (funcall function))) + (kill-buffer b)) + (message "%s ...done." file))) + nil) + +(defun map-messages (function) + (goto-char (point-min)) + (let (start header-end end) + (while (not (eobp)) + (forward-line 1) + (setq start (point)) + (let ((case-fold-search nil)) + (if (search-forward "\nFrom " nil t) + (beginning-of-line) + (goto-char (point-max)))) + (setq end (point)) + (goto-char start) + (if (search-forward "\n\n" nil t) + (forward-char -1) + (goto-char end)) + (setq header-end (point)) + (funcall function start header-end end) + (goto-char end))) + nil) + +(defun get-header (field &optional limit) + (let ((case-fold-search t)) + (save-excursion + (if (re-search-forward (concat "^" field ":[ \t]*") limit t) + (let ((start (point))) + (end-of-line) + (while (looking-at "\n[ \t]") + (forward-line 1) + (end-of-line)) + (buffer-substring start (point))))))) + + +(require 'mail-extr) +(defun strip-address (addr) + (let ((fn (symbol-function 'mail-extr-voodoo))) + (unwind-protect + (progn + (fset 'mail-extr-voodoo #'(lambda (x y z) nil)) + (let ((result (mail-extract-address-components addr))) + (or (car result) (nth 1 result) ""))) + (fset 'mail-extr-voodoo fn)))) + +(defun strip-subject (subj) + (let ((case-fold-search t)) + (while (or (string-match "\\`re:[ \t]*" subj) + (string-match "\\`re\\[[0-9]+\\]:[ \t]*" subj)) + (setq subj (substring subj (match-end 0))))) + subj) + +(defun parse-refs (refs) + (let ((result nil) + (start 0)) + (while (string-match "<[^<>]+>" refs start) + (setq result (cons (match-string 0 refs) result) + start (match-end 0))) + result)) + + +(defun table-count (table) + (let ((i 0.0)) + (mapatoms #'(lambda (x) (setq i (1+ i))) table) + i)) + +(defun table-bytes (table) + (let ((i 0.0)) + (mapatoms #'(lambda (x) (setq i (+ i 1 (length (symbol-name x))))) table) + i)) + +(defvar total-messages) +(defvar total-size) +(defvar author-table) +(defvar recipient-table) +(defvar id-table) +(defvar refs-table) +(defvar all-ids-table) +(defvar subject-table) +(defvar simple-subject-table) +(defvar all-strings-table) +(defvar re-count) +(defvar refs-count) + +(defun reset () + (setq total-messages 0.0 + total-size 0.0 + author-table (make-vector 511 0) + recipient-table (make-vector 511 0) + id-table (make-vector 511 0) + refs-table (make-vector 511 0) + all-ids-table (make-vector 511 0) + all-strings-table (make-vector 511 0) + subject-table (make-vector 511 0) + simple-subject-table (make-vector 511 0) + refs-count 0.0 + re-count 0.0)) + +(defun collect-stats () + (list + 'messages total-messages + 'size total-size + 'authors (table-count author-table) + 'authors-bytes (table-bytes author-table) + 'recipients (table-count recipient-table) + 'recipients-bytes (table-bytes recipient-table) + 'ids (table-count id-table) + 'ids-bytes (table-bytes id-table) + 'refs (table-count refs-table) + 'refs-bytes (table-bytes refs-table) + 'all-strings (table-count all-strings-table) + 'all-strings-bytes (table-bytes all-strings-table) + 'all-ids (table-count all-ids-table) + 'all-ids-bytes (table-bytes all-ids-table) + 'subjects (table-count subject-table) + 'subjects-bytes (table-bytes subject-table) + 'subjects2 (table-count simple-subject-table) + 'subjects2-bytes (table-bytes simple-subject-table) + 're re-count + 'refs refs-count + )) + +(defun message-stats (start header-end end) + (goto-char start) + (setq total-messages (1+ total-messages) + total-size (+ total-size (- end start))) + (if (= 0 (% (floor total-messages) 50)) + (message "%s (%d%%)..." (buffer-name) + (/ (* 100.0 total-size) (buffer-size)))) + (let* ( + (author (strip-address (or (get-header "from" header-end) + (get-header "sender" header-end) + ""))) + (recip (strip-address (or (get-header "to" header-end) + (get-header "cc" header-end) + (get-header "newsgroups" header-end) + ""))) + (id (or (get-header "message-id" header-end) + "")) + (refs (parse-refs (or (get-header "references" header-end) + (get-header "in-reply-to" header-end) + ""))) + (subj (or (get-header "subject" header-end) + "")) + (subj2 (strip-subject subj)) + ) + (intern author author-table) + (intern author all-strings-table) + (intern recip recipient-table) + (intern recip all-strings-table) + (intern id id-table) + (intern id all-ids-table) + (intern id all-strings-table) + (intern subj subject-table) + (intern subj2 simple-subject-table) + (intern subj2 all-strings-table) + (if (not (equal subj subj2)) + (setq re-count (1+ re-count))) + (setq refs-count (+ refs-count (length refs))) + (while refs + (intern (car refs) refs-table) + (intern (car refs) all-ids-table) + (intern (car refs) all-strings-table) + (setq refs (cdr refs))) + )) + +(defun merge-stats (total s) + (let ((r1 (cdr total)) + (r2 (cdr s))) + (while r1 + (setcar r1 (+ (car r1) (car r2))) + (setq r1 (cdr (cdr r1)) + r2 (cdr (cdr r2)))))) + +(defvar all-stats nil) + +(defun folder-stats (directory) + (setq all-stats nil) + (reset) + (map-folder directory + #'(lambda () + (message "%s..." (buffer-name)) + (map-messages 'message-stats) + (let ((s (cons (buffer-name) (collect-stats)))) + (cond (all-stats + (setcdr all-stats (cons s (cdr all-stats))) + (merge-stats (cdr (car all-stats)) (cdr s))) + (t (setq all-stats + (list (cons nil (copy-list (cdr s))) + s))))) + (reset) + )) + all-stats) diff --git a/mozilla/grendel/storage/intertwingle/BGDB.java b/mozilla/grendel/storage/intertwingle/BGDB.java new file mode 100644 index 00000000000..0459b5b6c6e --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/BGDB.java @@ -0,0 +1,211 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 26 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import calypso.util.Assert; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import java.util.Enumeration; +import java.util.Vector; + +/** This implements a RDF-ish database, where changes take very little time. + It works by queueing up those changes, and having a low-priority + background thread process that queue into real changes. The pending changes + also get reflected into a disk file, so that if we crash, we'll recover + unfinished stuff when the app next starts. +

+ Queries on the database will search the queue for pending changes that + could affect that query, and will commit those changes immediately. */ + +public final class BGDB extends BaseDB implements Runnable { + static final boolean DEBUG = false; + static void Spew(String s) { + if (DEBUG) System.err.println("BGDB: " + s); + } + + + class Command { + public String command; + public String name; + public String slot; + public String value; + Command(String c, String n, String s, String v) { + command = c; + name = n; + slot = s; + value = v; + } + public String toString() { + return command + ": '" + name + "','" + slot + "','" + value + "'"; + } + + }; + + DB base; + File log; + RandomAccessFile logfid; + Vector queue = new Vector(); + Thread thread; + BGDB(DB d, File t) throws IOException { + log = t; + base = d; + logfid = new RandomAccessFile(log, "rw"); + long length = logfid.length(); + while (logfid.getFilePointer() < length) { + try { + queue.addElement(new Command(logfid.readUTF(), + logfid.readUTF(), + logfid.readUTF(), + logfid.readUTF())); + } catch (IOException e) { + break; + } + } + thread = new Thread(this); + thread.setDaemon(true); + thread.setPriority(Thread.NORM_PRIORITY - 1); + thread.start(); + } + + public synchronized void assert(String name, String slot, String value) + throws IOException + { + logfid.seek(logfid.length()); + logfid.writeUTF("assert"); + logfid.writeUTF(name); + logfid.writeUTF(slot); + logfid.writeUTF(value); + queue.addElement(new Command("assert", name, slot, value)); + notifyAll(); // Inform bg thread it has work to do. + } + + public synchronized void unassert(String name, String slot, String value) + throws IOException + { + logfid.seek(logfid.length()); + logfid.writeUTF("unassert"); + logfid.writeUTF(name); + logfid.writeUTF(slot); + logfid.writeUTF(value); + queue.addElement(new Command("unassert", name, slot, value)); + notifyAll(); // Inform bg thread it has work to do. + } + + public synchronized String findFirst(String name, String slot, + boolean reverse) throws IOException + { + flushChanges(name, slot, reverse); + return base.findFirst(name, slot, reverse); + } + + public synchronized Enumeration findAll(String name, String slot, + boolean reverse) + throws IOException + { + flushChanges(name, slot, reverse); + return base.findAll(name, slot, reverse); + } + + private void flushChanges(String name, String slot, boolean reverse) + throws IOException + { + for (int i=0 ; i= queue.size()) break; + c = (Command) queue.elementAt(i); + } + } + } + + private void truncateLog() { + try { + logfid.close(); + } catch (IOException e) { + // ### What to do... + System.out.println("logfid.close() failed in BGDB.truncateLog: " + e); + } + logfid = null; + log.delete(); + try { + logfid = new RandomAccessFile(log, "rw"); + } catch (IOException e) { + // ### What to do... + System.out.println("opening logfid failed in BGDB.truncateLog: " + e); + } + } + + public synchronized void flushChanges() throws IOException { + while (queue.size() > 0) { + flushCommand((Command) queue.elementAt(0)); + queue.removeElementAt(0); + } + truncateLog(); + } + + private void flushCommand(Command c) throws IOException { + if (DEBUG) { + String who; + Spew(((Thread.currentThread() == thread) ? "bg" : "fg") + " executing " + + c); + } + if (c.command.equals("assert")) { + base.assert(c.name, c.slot, c.value); + } else if (c.command.equals("unassert")) { + base.unassert(c.name, c.slot, c.value); + } else { + Assert.NotReached("Unknown command type in BGDB.flushCommand"); + } + } + + + public void run() { + Spew("bg Startup."); + for (;;) { + synchronized(this) { + if (queue.size() > 0) { + try { + flushCommand((Command) queue.elementAt(0)); + } catch (IOException e) { + // ### What to do... + System.out.println("flushCommand failed in BGDB.run: " + e); + } + queue.removeElementAt(0); + } else { + truncateLog(); + try { + Spew("bg waiting..."); + wait(); + Spew("bg awake."); + } catch (InterruptedException e) { + return; // ### Is this right??? + } + } + } + } + } +} diff --git a/mozilla/grendel/storage/intertwingle/BaseDB.java b/mozilla/grendel/storage/intertwingle/BaseDB.java new file mode 100644 index 00000000000..b361cda67a2 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/BaseDB.java @@ -0,0 +1,38 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 26 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import java.io.IOException; +import java.util.Enumeration; + +abstract class BaseDB implements DB { + public boolean isAsserted(String name, String slot, String value) + throws IOException + { + for (Enumeration e = findAll(name, slot, false) ; e.hasMoreElements() ; ) { + String t = (String) e.nextElement(); + if (t.equals(value)) return true; + } + return false; + } +} + diff --git a/mozilla/grendel/storage/intertwingle/DB.java b/mozilla/grendel/storage/intertwingle/DB.java new file mode 100644 index 00000000000..1c8d095e4f8 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/DB.java @@ -0,0 +1,44 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 26 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import java.io.IOException; +import java.util.Enumeration; + +/** An interface to a RDF-ish database. */ + +public interface DB { + public void assert(String name, String slot, String value) + throws IOException; + public void unassert(String name, String slot, String value) + throws IOException; + public String findFirst(String name, String slot, boolean reverse) + throws IOException; + public Enumeration findAll(String name, String slot, boolean reverse) + throws IOException; + public boolean isAsserted(String name, String slot, String value) + throws IOException; +}; + + + + diff --git a/mozilla/grendel/storage/intertwingle/Dump.java b/mozilla/grendel/storage/intertwingle/Dump.java new file mode 100644 index 00000000000..e1506ab5dd6 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/Dump.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 7 Oct 1997. + */ + +package grendel.storage.intertwingle; + +import java.io.File; +import java.io.IOException; + +/** Dump a SimpleDB. */ + +public class Dump { + public static void main(String args[]) throws IOException { + SimpleDB db = new SimpleDB(new File(args[0])); + db.dump(System.out); + } +} diff --git a/mozilla/grendel/storage/intertwingle/HackDB.java b/mozilla/grendel/storage/intertwingle/HackDB.java new file mode 100644 index 00000000000..d60c25c5066 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/HackDB.java @@ -0,0 +1,197 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 26 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import calypso.util.Assert; +import calypso.util.NullJavaEnumeration; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.NoSuchElementException; + +/** This is a completely wretched thing that implements a RDF-ish database. + It works by generating zillions of tiny files. We should never ever really + use this; it's just for playing. If we actually ever do ship this, I will + be forced to spend the rest of my life hunting down every copy on every + machine in the world and eradicating it. */ + + +final class HackDB extends BaseDB { + private File top; + private File tmpfile; + private Hashtable slotdirs = new Hashtable(); + private Hashtable reverseslotdirs = new Hashtable(); + + HackDB(File f) throws IOException { + top = f; + ensureDirectory(top); + tmpfile = new File(top, "--tmp--"); + } + + + private void ensureDirectory(File f) throws IOException { + if (!f.exists()) f.mkdirs(); + if (!f.isDirectory()) { + throw new IOException("Must be a directory: " + f); + } + } + + private File findSlotFile(String name, boolean reverse) throws IOException { + Hashtable table = (reverse ? reverseslotdirs : slotdirs); + File result = (File) table.get(name); + if (result == null) { + result = new File(top, reverse ? "reverse-" + name : name); + ensureDirectory(result); + table.put(name, result); + } + return result; + } + + private void putone(File dir, String name, String value) throws IOException { + File n = new File(dir, name); + RandomAccessFile file = new RandomAccessFile(n, "rw"); + String line; + long length = file.length(); + while (file.getFilePointer() < length) { + line = file.readUTF(); + if (line.equals(value)) { + file.close(); + return; + } + } + file.writeUTF(value); + file.close(); + } + + private void nukeone(File dir, String name, String value) + throws IOException + { + File n = new File(dir, name); + if (!n.exists()) return; + RandomAccessFile file = new RandomAccessFile(n, "r"); + tmpfile.delete(); + RandomAccessFile out = new RandomAccessFile(tmpfile, "rw"); + long length = file.length(); + boolean found = false; + int count = 0; + while (file.getFilePointer() < length) { + String line = file.readUTF(); + if (line.equals(value)) { + found = true; + } else { + out.writeUTF(line); + count++; + } + } + file.close(); + out.close(); + if (found) { + n.delete(); + if (count > 0) { + tmpfile.renameTo(n); + } else { + tmpfile.delete(); + } + } else { + tmpfile.delete(); + } + } + + + + public synchronized void assert(String name, String slot, String value) + throws IOException + { + File s = findSlotFile(slot, false); + File r = findSlotFile(slot, true); + putone(s, name, value); + putone(r, value, name); + } + + + + public synchronized void unassert(String name, String slot, String value) + throws IOException + { + File s = findSlotFile(slot, false); + File r = findSlotFile(slot, true); + nukeone(s, name, value); + nukeone(r, value, name); + } + + public synchronized String findFirst(String name, String slot, + boolean reverse) throws IOException { + File f = new File(findSlotFile(slot, reverse), name); + if (!f.exists()) return null; + RandomAccessFile fid = new RandomAccessFile(f, "r"); + String line = null; + if (fid.length() > 0) { + line = fid.readUTF(); + } + fid.close(); + return line; + } + + + + public Enumeration findAll(String name, String slot, boolean reverse) + throws IOException + { + File f = new File(findSlotFile(slot, reverse), name); + if (!f.exists()) return NullJavaEnumeration.kInstance; + final RandomAccessFile thefid = new RandomAccessFile(f, "r"); + return new Enumeration() { + RandomAccessFile fid = thefid; + String next = null; + public boolean hasMoreElements() { + if (next != null) return true; + if (fid == null) return false; + try { + next = fid.readUTF(); + } catch (IOException e) { + next = null; + } + if (next == null) { + try { + fid.close(); + } catch (IOException e) { + } + fid = null; + return false; + } + return true; + } + public Object nextElement() throws NoSuchElementException { + if (!hasMoreElements()) throw new NoSuchElementException(); + String result = next; + next = null; + return result; + } + }; + } + + +}; diff --git a/mozilla/grendel/storage/intertwingle/Makefile b/mozilla/grendel/storage/intertwingle/Makefile new file mode 100644 index 00000000000..c7e3d36f4cf --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/Makefile @@ -0,0 +1,31 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + BGDB.java \ + BaseDB.java \ + DB.java \ + Dump.java \ + HackDB.java \ + PhoneTest.java \ + SelfTest.java \ + SimpleDB.java \ + Twingle.java \ + $(NULL) + +include ../../rules.mk diff --git a/mozilla/grendel/storage/intertwingle/PhoneTest.java b/mozilla/grendel/storage/intertwingle/PhoneTest.java new file mode 100644 index 00000000000..9d3864ce394 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/PhoneTest.java @@ -0,0 +1,156 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 4 Oct 1997. + */ + +package grendel.storage.intertwingle; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.IOException; + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +public class PhoneTest { + + + public static void main(String args[]) + throws IOException + { + String home = System.getProperties().getProperty("user.home"); + String filename = args.length == 0 ? "__phonedb__" : args[0]; + File top = new File(home, filename); + DB db = new SimpleDB(top); + db = new BGDB(db, new File(home, filename + ".queue")); + + + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + + for (;;) { + System.out.print("Phonetest> "); + System.out.flush(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + String line = in.readLine(); + if (line == null) break; + if (line.length() == 0) continue; + if (line.charAt(0) == '<') { + BufferedReader fid = null; + String name = null; + int count = 0; + try { + fid = new BufferedReader(new InputStreamReader(new FileInputStream(line.substring(1)))); + String str; + while ((str = fid.readLine()) != null) { + str = str.trim(); + if (str.length() == 0) { + name = null; + } else if (name == null) { + name = str; + } else { + int i = str.indexOf(':'); + if (i > 0) { + String slot = str.substring(0, i).trim(); + String value = str.substring(i + 1).trim(); +// System.out.println("Adding '" + name + "','" + slot + "','" + +// value + "'"); + System.out.print("."); + count++; + db.assert(name, slot, value); + } else { + name = str; + } + } + } + } catch (IOException e) { + } + System.out.println(""); + System.out.println(count + " assertions made."); + if (fid != null) fid.close(); + } else { + String name = null; + String slot = null; + String value = null; + + + int nameend = line.indexOf(','); + int slotend = line.indexOf(',', nameend + 1); + + if (nameend < 0) { + name = line; + } else { + name = line.substring(0, nameend); + if (slotend < 0) { + slot = line.substring(nameend + 1); + } else { + slot = line.substring(nameend + 1, slotend); + value = line.substring(slotend + 1); + } + } + + if (name != null && name.length() == 0) name = null; + if (slot != null && slot.length() == 0) slot = null; + if (value != null && value.length() == 0) value = null; + + // System.out.println("name: '" + name + "' slot: '" + slot + "' value: '" + value + "'"); + + if (slot == null) { + System.out.println("Gotta have a slot!"); + } else if (name == null && value == null) { + System.out.println("Gotta have a name or value!"); + } else { + Enumeration e = null; + if (name == null) { + e = db.findAll(value, slot, true); + } else if (value == null) { + e = db.findAll(name, slot, false); + } else { + if (name.charAt(0) == '-') { + db.unassert(name.substring(1), slot, value); + System.out.println("Unasserted."); + } else { + db.assert(name, slot, value); + System.out.println("Asserted."); + } + } + if (e != null) { + while (e.hasMoreElements()) { + String result = (String) e.nextElement(); + System.out.println(result); + } + } + } + } + } + + + if (db instanceof BGDB) { + ((BGDB) db).flushChanges(); + } + + } + +} diff --git a/mozilla/grendel/storage/intertwingle/README b/mozilla/grendel/storage/intertwingle/README new file mode 100644 index 00000000000..2febaf82913 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/README @@ -0,0 +1,10 @@ +This is Grendel -- a Java mail/news client. + +The grendel.storage.intertwingle package implements some of the ideas in +. + +There are lots of spiffy ideas in intertwingle but the one focussed on +here is just to keep a global database optimized for interesting +searches across all folders. + +See http://www.mozilla.org/projects/grendel/ for more info. diff --git a/mozilla/grendel/storage/intertwingle/SelfTest.java b/mozilla/grendel/storage/intertwingle/SelfTest.java new file mode 100644 index 00000000000..f10da7c02ed --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/SelfTest.java @@ -0,0 +1,71 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 26 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import java.io.File; +import java.io.IOException; + +import java.util.Enumeration; + +public class SelfTest { + + + public static void main(String args[]) + throws IOException + { + String home = System.getProperties().getProperty("user.home"); + File top = new File(home, "__hackdb__"); +// DB db = new HackDB(top); + DB db = new SimpleDB(top); + db = new BGDB(db, new File(home, "__hackdb__queue")); + db.assert("terry", "fullname", "Terry Weissman"); + db.assert("jwz", "fullname", "Jamie Zawinski"); + db.assert("terry", "phone", "650-937-2756"); + db.assert("terry", "phone", "408-338-8227"); + db.assert("jwz", "phone", "650-937-2620"); + db.assert("jwz", "phone", "415-ACRIDHE"); + + for (int j=0 ; j<2 ; j++) { + + for (int i=0 ; i<2 ; i++) { + String n = (i == 0 ? "terry" : "jwz"); + System.out.println(n + "'s fullname is " + db.findFirst(n, "fullname", + false)); + for (Enumeration e = db.findAll(n, "phone", false) ; + e.hasMoreElements() ; + ) { + String p = (String) e.nextElement(); + System.out.println(n + "'s phone is " + p); + } + } + + db.unassert("jwz", "phone", "415-ACRIDHE"); + System.out.println(""); + } + + if (db instanceof BGDB) { + ((BGDB) db).flushChanges(); + } + + } + +} diff --git a/mozilla/grendel/storage/intertwingle/SimpleDB.java b/mozilla/grendel/storage/intertwingle/SimpleDB.java new file mode 100644 index 00000000000..bb079d65ce0 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/SimpleDB.java @@ -0,0 +1,500 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 30 Sep 1997. + */ + +package grendel.storage.intertwingle; + +import calypso.util.Assert; +import calypso.util.NullEnumeration; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.PrintStream; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +/** Simplistic implementation of a RDF-ish database. This is basically a + massive hashtable hack. Almost certainly we shouldn't ship this, but it + ought to be good enough to play with for a while. + +

+ + Here's how it works: + +

+ + There are five files. One is the "store", and it stores the data. All + data is accessed via a hash table, so each entry in the store contains + one or two pointers to other entries with the same hash. ("two" is in + the case where there are two hash tables into the same data.) +

+ The other four files are head pointers into the hashtable, for various + hashes. +

+ Random strings are stored in the store. They get hashed using + String.hashCode(), the hash value is masked off to HASHMASK, and the + first pointer to the hashtable appear in the "strhead" file. Each + entry consists of a pointer to the next string with the same hashcode, and + then a refcount, and then the UTF version of the string itself. +

+ Assertions are stored in the store. Each assertion is three strings: + name, slot, and value. The strings are first put into the store, and + then the assertion is recorded by writing down the location of the + strings. Each assertion is hashed twice: once by combining the name and + slot, and once by combining the value and slot. So, each assertion records + the next value for both hash tables, followed by the three string + locations. So, each assertion costs 20 bytes on disk, not counting the + string storage. The "head[]" files are the first pointers for each + hashtable. +

+ Finally, a free list of unused chunks in the store are kept. The + "freehead" file points to the first free entry of a given size; each + free entry points to the next free entry of the same size. + +*/ + +public final class SimpleDB extends BaseDB { + private File storename; + private RandomAccessFile store; + private RandomAccessFile head[] = new RandomAccessFile[2]; + private RandomAccessFile strhead; + private RandomAccessFile freehead; + + private static final int MINALLOC = 12; // Try not to leave free chunks + // smaller than this. + private static final int MAXALLOC = 1024;// Don't bother recording free + // chunks bigger than this. + + private static final int HASHMASK = 0xfff; // Only use these bits in our + // hashtables. + + public SimpleDB(File f) throws IOException { + storename = f; + String name = f.getPath(); + store = new RandomAccessFile(f, "rw"); + head[0] = new RandomAccessFile(name + ".key0", "rw"); + head[1] = new RandomAccessFile(name + ".key1", "rw"); + freehead = new RandomAccessFile(name + ".free", "rw"); + strhead = new RandomAccessFile(name + ".strings", "rw"); + if (store.length() == 0) { + store.write(("# DO NOT EDIT THIS FILE; it contains binary data.\n").getBytes()); + } + } + + private int alloc(int length) throws IOException { + int biggest = (int) (freehead.length() / 4); + if (length > biggest) { + return (int) store.length(); + } + freehead.seek(length * 4); + int t = length; + while (t <= biggest) { + int result = freehead.readInt(); + if (result > 0 && (t == length || t > length + MINALLOC)) { + store.seek(result); + freehead.seek(t * 4); + freehead.writeInt(store.readInt()); + free(result + length, t - length); + return result; + } + } + return (int) store.length(); + } + + private void free(int ptr, int length) throws IOException { + if (length == 0) return; + if (length > MAXALLOC) { + while (length > MAXALLOC * 2) { + free(ptr, MAXALLOC); + ptr += MAXALLOC; + length -= MAXALLOC; + } + free(ptr, length / 2); + ptr += length / 2; + length -= (length / 2); + free(ptr, length); + return; + } + int biggest = (int) (freehead.length() / 4); + if (length >= biggest) { + freehead.seek(freehead.length()); + while (biggest <= length) { + freehead.writeInt(0); + biggest++; + } + } + freehead.seek(length * 4); + store.seek(ptr); + store.writeInt(freehead.readInt()); + freehead.seek(length * 4); + freehead.writeInt(ptr); + } + + + private int LookupString(String str, boolean create) throws IOException { + int hash = (str.hashCode() & HASHMASK) * 4; +//System.out.println("LookupString: Looking up '" + str + "' [hashcode is " + hash + "]"); + int result; + int firstval = 0; + if (hash < strhead.length()) { + strhead.seek(hash); + firstval = strhead.readInt(); + result = firstval; + while (result > 0) { +//System.out.println("LookupString: Seeking to " + result); + store.seek(result); + int next = store.readInt(); + int refcount = store.readInt(); + String t = store.readUTF(); + if (str.equals(t)) { + return result; + } +//System.out.println("LookupString: Skipping past " + t); + result = next; + } + } + if (!create) return 0; + if (hash > strhead.length()) { + strhead.seek(strhead.length()); + for (int i=(int) strhead.length() ; i= 0x0001) && (c <= 0x007F)) { + utflen++; + } else if (c > 0x07FF) { + utflen += 3; + } else { + utflen += 2; + } + } + + int totallength = 4 + // Pointer to next entry with same hash + 2 + // Space to write down length of string + utflen; // Length of string itself. + + result = alloc(totallength); + store.seek(result); + store.writeInt(firstval); + store.writeInt(0); // Initial value of refcount. The caller + // better update this soon! + store.writeUTF(str); + strhead.seek(hash); + strhead.writeInt(result); +//System.out.println("LookupString: Added entry at " + result); + return result; + } + + private void addStringRef(int loc) throws IOException { + store.seek(loc + 4); + int refcount = store.readInt(); + store.seek(loc + 4); + store.writeInt(refcount + 1); + } + + + private void removeStringRef(int loc) throws IOException { + store.seek(loc + 4); + int refcount = store.readInt(); + if (refcount <= 1) { + int length = store.readShort() + 8; + free(loc, length); + } else { + store.seek(loc + 4); + store.writeInt(refcount - 1); + } + } + + + + public synchronized void assert(String name, String slot, String value) + throws IOException + { + int n = LookupString(name, true); + int s = LookupString(slot, true); + int v = LookupString(value, true); + int hash0 = ((name.hashCode() + slot.hashCode()) & HASHMASK) * 4; + int firstval0 = 0; + if (head[0].length() > hash0) { + head[0].seek(hash0); + firstval0 = head[0].readInt(); + int ptr = firstval0; + while (ptr > 0) { + store.seek(ptr); + ptr = store.readInt(); + store.readInt(); + if (n == store.readInt()) { + if (s == store.readInt()) { + if (v == store.readInt()) { + // Gee, we already have this one. No-op. + return; + } + } + } + } + } + // OK, we know this isn't already in the database. Add it. + int hash1 = ((value.hashCode() + slot.hashCode()) & HASHMASK) * 4; + int firstval1 = 0; + if (head[1].length() > hash1) { + head[1].seek(hash1); + firstval1 = head[1].readInt(); + } + addStringRef(n); + addStringRef(s); + addStringRef(v); + int result = alloc(5 * 4); + store.seek(result); + store.writeInt(firstval0); + store.writeInt(firstval1); + store.writeInt(n); + store.writeInt(s); + store.writeInt(v); + for (int i=0 ; i<2 ; i++) { + int hash = (i==0 ? hash0 : hash1); + if (hash > head[i].length()) { + head[i].seek(head[i].length()); + for (int j=(int) head[i].length() ; j 0) { + store.seek(ptr); + int next0 = store.readInt(); + int next1 = store.readInt(); + int next = ((w==0) ? next0 : next1); + if (store.readInt() == n) { + if (store.readInt() == s) { + if (store.readInt() == v) { + // Found it! Remove it from the chain. + Assert.Assertion(location == 0 || location == ptr); + location = ptr; + if (last == 0) { + head[w].seek(hash); + head[w].writeInt(next); + } else { + store.seek(last + w*4); + store.writeInt(next); + } + next = 0; // To break out of loop. + } + } + } + last = ptr; + ptr = next; + } + } + if (location > 0) { + free(location, 5 * 4); + removeStringRef(n); + removeStringRef(s); + removeStringRef(v); + } + } + + + + public synchronized String findFirst(String name, String slot, boolean reverse) + throws IOException + { + int w = reverse ? 1 : 0; + int n = LookupString(name, false); + int s = LookupString(slot, false); +//System.out.println("findFirst: Name: '" + name + "' (" + n + ")"); +//System.out.println("findFirst: Slot: '" + slot + "' (" + s + ")"); + if (n == 0 || s == 0) return null; + + int hash = ((name.hashCode() + slot.hashCode()) & HASHMASK) * 4; +//System.out.println("findFirst: hash is " + hash); + if (head[w].length() <= hash) return null; + head[w].seek(hash); + int ptr = head[w].readInt(); + while (ptr > 0) { +//System.out.println("findFirst: checking position " + ptr); + store.seek(ptr); + int next0 = store.readInt(); + int next1 = store.readInt(); + int n1 = store.readInt(); + int s1 = store.readInt(); + if (s1 == s) { + int v1 = store.readInt(); + if ((w == 0 && n == n1) || (w == 1 && n == v1)) { + int result = (w == 0) ? v1 : n1; + store.seek(result + 8); + return store.readUTF(); + } + } + ptr = (w == 0) ? next0 : next1; + } + return null; + } + + public synchronized Enumeration findAll(String name, String slot, boolean reverse) + throws IOException + { + final int w = reverse ? 1 : 0; + final int n = LookupString(name, false); + if (n == 0) return null; + final int s = LookupString(slot, false); + if (s == 0) return null; + int hash = ((name.hashCode() + slot.hashCode()) & HASHMASK) * 4; +//System.out.println("findFirst: hash is " + hash); + if (head[w].length() <= hash) return null; + head[w].seek(hash); + final int ptr = head[w].readInt(); + return new AllEnumerator(this, ptr, n, s, w); + } + + + class AllEnumerator implements Enumeration { + SimpleDB db; + int ptr; + int n; + int s; + int w; + String next; + AllEnumerator(SimpleDB d, int p, int nm, int sl, int ww) { + db = d; + ptr = p; + n = nm; + s = sl; + w = ww; + } + + public boolean hasMoreElements() { + if (next != null) return true; + while (ptr > 0) { + synchronized(db) { + try { + db.store.seek(ptr); + int next0 = db.store.readInt(); + int next1 = db.store.readInt(); + int n1 = db.store.readInt(); + int s1 = db.store.readInt(); + ptr = ((w == 0) ? next0 : next1); + if (s1 == s) { + int v1 = db.store.readInt(); + if ((w == 0 && n == n1) || (w == 1 && n == v1)) { + db.store.seek(((w == 0) ? v1 : n1) + 8); + next = db.store.readUTF(); + return true; + } + } + } catch (IOException e) { + ptr = 0; + return false; + } + } + } + return false; + } + + public Object nextElement() throws NoSuchElementException { + if (!hasMoreElements()) throw new NoSuchElementException(); + String result = next; + next = null; + return result; + } + } + + + private void dumpString(int loc) throws IOException { + store.seek(loc + 4); + int refcount = store.readInt(); + String str = store.readUTF(); + System.out.print(str); + System.out.print("[" + refcount + "]"); + } + + public synchronized void dump(PrintStream out) throws IOException { + int length = (int) head[0].length(); + head[0].seek(0); + int id[] = new int[3]; + for (int hash=0 ; hash < length ; hash += 4) { + int position = head[0].readInt(); + while (position > 0) { + store.seek(position); + position = store.readInt(); + store.readInt(); + for (int i=0 ; i<3 ; i++) { + id[i] = store.readInt(); + } + for (int i=0 ; i<3 ; i++) { + dumpString(id[i]); + if (i < 2) System.out.print(","); + } + System.out.println(""); + } + } + + length = (int) freehead.length(); + for (int hash=0 ; hash < length ; hash += 4) { + int position = freehead.readInt(); + if (position > 0) { + System.out.print(" -- Free chunks of size " + (hash/4) + ":"); + while (position > 0) { + System.out.print(" " + position); + store.seek(position); + position = store.readInt(); + } + System.out.println(""); + } + } + } +} + diff --git a/mozilla/grendel/storage/intertwingle/Twingle.java b/mozilla/grendel/storage/intertwingle/Twingle.java new file mode 100644 index 00000000000..f27772b2ea5 --- /dev/null +++ b/mozilla/grendel/storage/intertwingle/Twingle.java @@ -0,0 +1,195 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 7 Oct 1997. + */ + +package grendel.storage.intertwingle; +import javax.mail.Folder; + +import javax.mail.internet.InternetHeaders; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import java.io.File; +import java.io.IOException; + +import java.util.StringTokenizer; +import java.util.Vector; + +/** This is the glue between the storage of intertwingle data and the rest + of the mail system. It will probably get thrown out or extensively + rewritten; I'm still playing. */ + +public class Twingle implements Runnable { + static final private boolean DEBUG = false; + static void Spew(String s) { + if (DEBUG) System.err.println("Twingle: " + s); + } + + + static private Twingle Singleton = null; + static private boolean Initialized = false; + + static public Twingle GetSingleton() { + if (!Initialized) { + synchronized (Twingle.class) { + if (!Initialized) { + File f = PreferencesFactory.Get().getFile("twingle.database", null); + if (f != null) { + try { + Singleton = new Twingle(f); + } catch (IOException e) { + // ### What to do... + System.out.println("Twingle.GetSingleton() failed: " + e); + } + } + Initialized = true; + } + } + } + return Singleton; + } + + protected DB db; + protected Thread thread; + protected Vector queue = new Vector(); + + Twingle(File f) throws IOException { + db = new BGDB(new SimpleDB(f), new File(f.getPath() + ".queue")); + thread = new Thread(this); + thread.setDaemon(true); + thread.start(); + } + + void assert(String name, String slot, String value) { + if (name != null && value != null) { + try { + db.assert(name, slot, value); + } catch (IOException e) { + // ### What to do... + System.out.println("db.assert() failed in Twingle.assert: " + e); + } + } + } + + class AddCommand { + InternetHeaders headers; + Folder folder; + AddCommand(InternetHeaders h, Folder f) { + headers = h; + folder = f; + } + } + + + + public void add(InternetHeaders headers, Folder folder) { + synchronized (queue) { + queue.addElement(new AddCommand(headers, folder)); + queue.notifyAll(); + } + } + + + /** Find the given header ("to" or "cc"), and assert an entry for each + address in it. This needs the code to parse the zillions of different + kinds of addresses; for now, we just assume everything is of the form + "Full name ". */ + + protected void hackAddressList(String id, InternetHeaders headers, + String slot) { + String hh[] = headers.getHeader(slot); + if (hh == null || hh.length == 0) return; + String list = ""; + for (int i = 0; i < hh.length; i++) { + if (i > 0) list += ",\r\n\t"; + list += hh[i]; + } + StringTokenizer st = new StringTokenizer(list, ","); + while (st.hasMoreTokens()) { + String str = st.nextToken(); + String name; + String addr; + int at = str.indexOf('@'); + if (at < 0) continue; // Some garbage that isn't an email address. + int lt = str.indexOf('<'); + int gt = lt > 0 ? str.indexOf('>', lt) : -1; + if (lt > 0 && lt < at && gt > at) { + name = str.substring(0, lt).trim(); + addr = str.substring(lt + 1, gt).trim(); + } else { + name = null; + addr = str.trim(); + } + if (name != null) { + assert(addr, "fullname", name); + } + assert(id, slot, addr); + } + } + + + public void run() { + for (;;) { + Object cmd; + synchronized(queue) { + while (queue.size() == 0) { + try { + Spew("bg: waiting"); + queue.wait(); + Spew("bg: awake"); + } catch (InterruptedException e) { + return; + } + } + cmd = queue.elementAt(0); + queue.removeElementAt(0); + } + Spew("bg: doing one..."); + if (cmd instanceof AddCommand) { + AddCommand addcmd = (AddCommand) cmd; + InternetHeaders headers = addcmd.headers; + Folder folder = addcmd.folder; + + String ids[] = headers.getHeader("Message-ID"); + String id; + if (ids == null || ids.length == 0) { + // MD5-hash-hack? Well, maybe. Not right now, though. ### + continue; + } + id = ids[0]; + if (id.charAt(0) == '<' && id.endsWith(">")) { + id = id.substring(1, id.length() - 1); + } + assert(id, "parent", folder.getName()); + + String subj[] = headers.getHeader("Subject"); + if (subj != null && subj.length != 0) + assert(id, "subject", subj[0]); + + hackAddressList(id, headers, "from"); + hackAddressList(id, headers, "to"); + hackAddressList(id, headers, "cc"); + } + Spew("bg: ...did one."); + } + } +} + diff --git a/mozilla/grendel/storage/selftestdata/Inbox b/mozilla/grendel/storage/selftestdata/Inbox new file mode 100644 index 00000000000..cbf1df04def --- /dev/null +++ b/mozilla/grendel/storage/selftestdata/Inbox @@ -0,0 +1,449 @@ +From - Mon Jun 10 09:11:27 1996 +Delivery-Date: Fri, 07 Jun 1996 19:41:36 -0700 +Return-Path: list@glacier.mcom.com +Received: from glacier.mcom.com (glacier.mcom.com [205.217.233.39]) by dome.mcom.com (8.6.10/8.6.9) with ESMTP id TAA26191 for ; Fri, 7 Jun 1996 19:41:33 -0700 +Received: (from list@localhost) by glacier.mcom.com (8.7.3/8.7.3) id TAA01443; Fri, 7 Jun 1996 19:38:52 -0700 (PDT) +Resent-Date: Fri, 7 Jun 1996 19:38:52 -0700 (PDT) +To: clienteng@netscape.com +Path: usenet +X-UIDL: 834201705.000 +From: Jamie Zawinski +Newsgroups: mcom.dev.clienteng +Subject: Re: Email bug was [Fwd: Apologies and complaints] +Date: Fri, 07 Jun 1996 18:38:41 -0700 +Organization: Netscape Communications Corporation, Mozilla Division +Lines: 280 +Message-ID: <31B8E7B1.41C6@netscape.com> +References: <31B8AEBF.4A7B@netscape.com> +NNTP-Posting-Host: gruntle.mcom.com +Mime-Version: 1.0 +Content-Type: multipart/digest; boundary="------------167E2781446B" +X-Mailer: Mozilla 3.0b5 (X11; U; IRIX 5.3 IP22) +To: Mike Macgirvin +Content-Disposition: inline +Resent-Message-ID: <"lghyh3.0.VM.wUEkn"@glacier> +Resent-From: clienteng@netscape.com +X-Mailing-List: archive/latest/1926 +X-Loop: clienteng@netscape.com +Precedence: list +Resent-Sender: clienteng-request@netscape.com +Status: U +X-Mozilla-Status: 8010 +Content-Length: 12874 + +This is a multi-part message in MIME format. + +--------------167E2781446B +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Mike Macgirvin wrote: +> +> I guess I need to find out why this is an issue (for us). We are doing +> "proper" delivery using native methods. + +So that means that on Unix, you use sendmail (wearing its MDA hat +instead of its MTA hat) to move messages into /var/spool/mail/$USER ? + +I think that's a totally reasonable thing to do. + +> Seems to me that movemail needs fixing to deal with Solaris' +> Content-Length, or replaced with something that can handle it. Yes, +> we can hack something in on this side too, but +> +> Mlocal, P=/bin/mail, F=EflsSDFMmnP, S=10, R=20, A=mail -d $u +> +> isn't gonna' fly with our customers. A check box that turns on "From +> stuffing" "...IF you use Unix delivery and IF you are using a Solaris +> 2.x OS and IF you have Netscape Navigator users accessing the local +> spool..." isn't very clean either. + +(Well first off, fixing that is really hard, which is why I didn't just +do it.) + +But I think, actually, that telling them that they need to configure +sendmail in that was *does* fly, because *most* people on Solaris do +that anyway -- the simple fact of life is that almost all Unix MUAs +expect /var/spool/mail/$USER to be in BSD Mailbox format, not in +Content-Length format. It's not just Netscape. If any Solaris user +wants to use a mailer other than one that Sun ships, they have to +reconfigure sendmail in this way. + +> Fixing movemail is a much better solution. + +That would be a nice solution, but you can't look at /var/mail/ and know +which of the two formats its in, so you need a new user option in the +client. And its one that no end-user is going to know how to set. + +And it only fixes the "Get Mail" case. The thing that sucks most about +this, actually, is not the "Get Mail" case, but the fact that both Sun's +MailTool and Eudora write folder files using the Content-Length format; +they don't do From mangling. So that means that importing folders is +dicey -- every now and then, a message will get split. + +> 'Nuther Q... (Jamie? and I really don't know the answer to this...) +> Does Nav itself parse From lines? Does it actually rely on +> stuffing in its parsing algorithms? Or is this strictly a movemail +> issue? + +Movemail depends on From-mangling, because it assumes BSD format. +Mozilla has movemail code built-in, and also can use an external +program to do this. + +But, Mozilla *also* uses BSD format for its saved folders. So if it's +pointed at a folder written by another program, and that other program +uses the Content-Length format, messages might get split. + +> Remember, we don't need stuffing on POP/IMAP delivery. Do we +> need to have it to make Nav work right? + +No; this isn't an issue for Get-Mail-using-POP3, only for +Get-Mail-using-movemail (and for folders already on disk.) + +> What are you going to do when you get a message from a Microsoft +> Exchange server which doesn't need From stuffing and doesn't do it? + +Responses from POP3 servers need not be mangled; those are terminated by +`CRLF . CRLF' so there's no ambiguity (and then when we write that +message to disk, we write it in our native folder format, which is to +say, we mangle the internal From lines.) + +> Tell them their server is broken? This isn't strong ground to be +> standing on...because their server may in fact not be. Nothing says +> that messages have to be stuffed for transport (except in the case of +> \r\n.\r\n via SMTP). It's purely a Unix storage issue. + +Yes, I agree. We don't have to tell them their server is broken, +because Get-Mail-via-POP3 isn't a problem for us. + +In fact, in the Get Mail case, it's only an issue on Solaris, and only +when using movemail. All other Unix platforms on which we ship come +with MDAs which use the BSD format, not the Content-Length format. +Solaris can do either, but comes configured for Content-Length. + +Why is supporting Content-Length hard? + + = You can't look at a file and tell whether it's in BSD format or + Content-Length format (the presence of the CL headers doesn't + actually tell you anything.) + + = If you're in a world where you can't really make assumptions about + how many bytes a line terminator is (as all of our code is, since + so far we've managed to support input with CR, LF, and CRLF on all + platforms, regardless of which is native) you will often find + yourself faced with Content-Length lines that no longer match, so + your parser has to do sophisticated error recovery. + + = We are currently writing out-of-spec BSD files anyway -- we are + treating "\nFrom " as a delimiter, when really we should be + treating "\n\nFrom " as a delimiter. This messes with the lengths, + so that would have to be fixed first (there's a bug about it.) + +More background: + +--------------167E2781446B + +Message-ID: <319CEA7A.7A79@netscape.com> +Date: Fri, 17 May 1996 14:07:06 -0700 +From: Jamie Zawinski +Organization: Netscape Communications Corporation, Mozilla Division +X-Mailer: Mozilla 3.0b4 (X11; U; IRIX 5.3 IP22) +MIME-Version: 1.0 +Newsgroups: comp.mail.headers +Subject: Re: "From_" specification +References: <1996May16.171931.26752@jarvis.cs.toronto.edu> <4nhcr1$7sf@lyra.csx.cam.ac.uk> +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Philip Hazel wrote: +> +> In article <1996May16.171931.26752@jarvis.cs.toronto.edu>, edwin@cs.toronto.edu (Edwin Allum) writes: +> |> +> |> Is there a standard specification (rfc) or format for the "From_" line +> |> used to delimit messages in the mail spool file? +> +> There is some discussion of From_ lines in RFC 976 ("UUCP Mail +> Interchange Format Standard"). + +RFCs specify internet protocols, that is, on-the-wire formats. The +thing that the original poster is looking for is a description of the +BSD Mailbox file format (which is not something an RFC would cover.) + +But, here's the good news, there *is* no true specification of this file +format, just a collection of word-of-mouth behaviors of the various +programs over the last few decades which have used that format. + +Essentially the only safe way to parse that file format is to consider +all lines which begin with the characters "From " (From-space), which +are preceeded by a blank line or beginning-of-file, to be the division +between messages. That is, the delimiter is "\n\nFrom .*\n" except for +the very first message in the file, where it is "^From .*\n". + +Some people will tell you that you should do stricter parsing on those +lines: check for user names and dates and so on. They are wrong. The +random crap that has traditionally been dumped into that line is without +bound; comparing the first five characters is the only safe and portable +thing to do. *Usually*, but not always, the next token on the line +after "From " will be a user-id, or email address, or UUCP path, and +*usually* the next thing on the line will be a date specification, in +*some* format, and *usually* there's nothing after that. But you can't +rely on any of this. + +In the BSD format, the only safe way to add a message to a file is to +mangle occurences of the "From " delimiter in the body of messages to +some other string, usually ">From ". This is mangling, not quoting, +because it's not a reversible process (since ">From " is not also +quoted.) + +Now, there are actually two very similar-looking file formats. One is +the BSD format, which I've described. The other, which one might as +well call the "content-length" format, is used by some SYSV-derived +systems, notably Solaris. It's very similar, but subtly incompatible. +This format does not quote "From " lines, but instead relies on a +Content-Length header in the message proper to indicate the exact +byte-position of the end of each message. + +This latter format is non-portable, easily-corruptible, and overall, +brain-damaged (that's a technical term.) But I'll refrain from ranting +about it again right now... + +-- +Jamie Zawinski jwz@netscape.com http://www.netscape.com/people/jwz/ +``A signature isn't a return address, it is the ASCII equivalent of a + black velvet clown painting; it's a rectangle of carets surrounding + a quote from a literary giant of weeniedom like Heinlein or Dr. Who.'' + -- Chris Maeda + +--------------167E2781446B + +Message-ID: <319D3B7A.6201@netscape.com> +Date: Fri, 17 May 1996 19:52:42 -0700 +From: Jamie Zawinski +Organization: Netscape Communications Corporation, Mozilla Division +X-Mailer: Mozilla 3.0b4 (X11; U; IRIX 5.3 IP22) +MIME-Version: 1.0 +Newsgroups: comp.mail.headers +Subject: Re: "From_" specification +References: <1996May16.171931.26752@jarvis.cs.toronto.edu> + <4nhcr1$7sf@lyra.csx.cam.ac.uk> <319CEA7A.7A79@netscape.com> +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + +Soren Dayton wrote: +> +> Our friend, Jamie Zawinski , wrote: +> +> > Now, there are actually two very similar-looking file formats. +> +> +> +> > This latter format is non-portable, easily-corruptible, and overall, +> > brain-damaged (that's a technical term.) But I'll refrain from ranting +> > about it again right now... +> +> So this is to claim that the one style is portable easily-corruptible, +> brain-damage while the other is non-portable? +> +> letter to a file, very hard to forget to quote the '^From_'s + +I'm not sure exactly what you're trying to say, but I'll clarify what I +meant: I'm not saying that the BSD Mailbox format is *good*. Just that +the Content-Length variant of that format is *worse*. + +Ok, so someone took the From_ format, and extended it to not require +mangling by adding a length indicator to the format. At first glance, +this may sound simple and elegant, but it breaks the world, and one +shouldn't encourage its use to spread. + +The thing that breaks is taking an existing, widely-implemented format, +and *adding* a requirement that it have a length indicator. This means +that any existing software that already *thinks* it knows how to +manipulate that format is going to damage the file (any change to the +data will cause the length indicator to be wrong w.r.t. the *new* +specification but not w.r.t. the *old* specification.) + +If the content-length-based format was not otherwise-indistinguishable +from the "From " format, there wouldn't be a problem; the old software +would simply fail to work with this new file format, instead of +"corrupting" the documents (in quotes, because it's really just a +matter of which spec you're following.) + +Also, mailboxes are by their nature a textual format; but, the +content-length header measures in *bytes* rather than lines. This +means that if you move the file to a system which has a different +end-of-line representation (Windows <=> Mac, or Windows <=> Unix) then +the content-lengths will suddenly be wrong, because the linebreaks now +take two bytes instead of one, or vice versa. + +It's impossible for a mail client to look at a file, and tell which of +the two formats (From_ or Content-Length) it is in; they are +programatically indistinguishable. The presence of a Content-Length +header is not enough, because suppose you were on a system which knew +nothing at all about that header, and some *incoming* message just +happened to have that header in it. Then that header would end up in +your mailbox (because nobody would have known to remove or recalculate +it), and it would possibly be incorrect. (Presume further that the +header was not just incorrect, but intentionally malicious...) + +Stricter parsing of the "From " seperator line doesn't help either, +because there are many, many variations on what goes in that line +(since it was never standardized either); and also, some mail readers +include that line verbatim when forwarding messages (Sun's MailTool, +for example) so a stricter parser wouldn't help that case at all, +because message bodies tend to contain "valid" matches. + +Some mail readers attempt to cope with this by recognising the case +where the Content-Length is not obviously spot-on-target, and then +searching forward and backward for the nearest message delimiter; but +this is obviously not foolproof, and makes one's parser much more +inefficient (requiring arbitrary lookahead and backtracking.) + +Conventional wisdom is, ``if you believe the Content-Length header, +I've got a bridge to sell you.'' + +-- +Jamie Zawinski jwz@netscape.com http://www.netscape.com/people/jwz/ +``A signature isn't a return address, it is the ASCII equivalent of a + black velvet clown painting; it's a rectangle of carets surrounding + a quote from a literary giant of weeniedom like Heinlein or Dr. Who.'' + -- Chris Maeda + + +--------------167E2781446B-- + + +From - Wed Dec 03 07:58:41 1997 +Path: pixie.mcom.com!news +From: Eric Mader +Newsgroups: mcom.humour +Subject: [Fwd: Luggage] +Date: Wed, 19 Nov 1997 10:32:22 -0800 +Organization: Netscape Communications Corporation +Lines: 60 +Message-ID: <347330B6.42D3CDD8@netscape.com> +NNTP-Posting-Host: 208.12.37.141 +Mime-Version: 1.0 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg=sha1; boundary="------------ms691B6036709EBB7DE811477F" +X-Mailer: Mozilla 4.01 [en] (WinNT; U) +X-Priority: 3 (Normal) +X-Mozilla-Status: 8009 + +This is a cryptographically signed message in MIME format. + +--------------ms691B6036709EBB7DE811477F +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit + + +----------------------------------------------------------------------- +As migration approached, two elderly vultures doubted they could make +the trip south, so they decided to go by airplane. + +When they checked their baggage, the attendant noticed that they were +carrying two dead raccoons. "Do you wish to check the raccoons +through as luggage?" she asked. + +"No, thanks," replied the vultures. "They're carrion." +--------------ms691B6036709EBB7DE811477F +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" +Content-Description: S/MIME Cryptographic Signature + +MIIHewYJKoZIhvcNAQcCoIIHbDCCB2gCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3DQEHAaCC +BWwwggKrMIICFKADAgECAgIIOjANBgkqhkiG9w0BAQQFADB3MQswCQYDVQQGEwJVUzEsMCoG +A1UEChMjTmV0c2NhcGUgQ29tbXVuaWNhdGlvbnMgQ29ycG9yYXRpb24xHDAaBgNVBAsTE0lu +Zm9ybWF0aW9uIFN5c3RlbXMxHDAaBgNVBAMTE3Jvb3RjYS5uZXRzY2FwZS5jb20wHhcNOTcx +MTE0MDEwMDE0WhcNOTgwNTEzMDEwMDE0WjCBhjELMAkGA1UEBhMCVVMxJjAkBgNVBAoTHU5l +dHNjYXBlIENvbW11bmljYXRpb25zIENvcnAuMRMwEQYDVQQDEwpFcmljIE1hZGVyMSIwIAYJ +KoZIhvcNAQkBFhNlbWFkZXJAbmV0c2NhcGUuY29tMRYwFAYKCZImiZPyLGQBARMGZW1hZGVy +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMFuyovDlaYG44cW2wAEV66eUqhC4RAKOC +76xkYy7c0ZvV2B2RDYeTGqw6ZBJotuY/h6tztgOX61lz2O6ViYl2NRUq+Fl2bbY2rq+QXsLX +/04TveXoxpQMnY0bYe8nOfAUxj7pRNXG2CfWc/QVFti+yg6XwgCmTo1xmQjq5lhf9wIDAQAB +ozYwNDARBglghkgBhvhCAQEEBAMCAKAwHwYDVR0jBBgwFoAU/OBU6Afxld4695nGrvoVDG7E +LpIwDQYJKoZIhvcNAQEEBQADgYEAP/kXpuPBjKW/pypwPfwJYxfmTgaKLcZAjZ8YpbIJEeiU +rY3/uSofuVWhjx6TpMigY8v3o5ufQi1ge/1ERIG3GjbjHhcUUINRU2Z/+a7+80nadciAQV+n +jsJm47w9jDA8vQxazDIz6M720malCHU8xRxkWxjBxX4jxhdkK48iVdMwggK5MIICIqADAgEC +AgEBMA0GCSqGSIb3DQEBBAUAMHcxCzAJBgNVBAYTAlVTMSwwKgYDVQQKEyNOZXRzY2FwZSBD +b21tdW5pY2F0aW9ucyBDb3Jwb3JhdGlvbjEcMBoGA1UECxMTSW5mb3JtYXRpb24gU3lzdGVt +czEcMBoGA1UEAxMTcm9vdGNhLm5ldHNjYXBlLmNvbTAeFw05NzAzMjYwMTQ0MzhaFw05OTAz +MjYwMTQ0MzhaMHcxCzAJBgNVBAYTAlVTMSwwKgYDVQQKEyNOZXRzY2FwZSBDb21tdW5pY2F0 +aW9ucyBDb3Jwb3JhdGlvbjEcMBoGA1UECxMTSW5mb3JtYXRpb24gU3lzdGVtczEcMBoGA1UE +AxMTcm9vdGNhLm5ldHNjYXBlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwao+ +/i0/pYfDR9/72m3YGKBFPfnHm0POJ7RaE5wRfb/S8ohex7+yi3m6p+UoC0CmjpkxVcX4zpYG +XiKEdr8BImLDqZknuwhoERTHCn7csof4x+AkMAG8LZaF5xnDLqGTdyw0GC/736JIs+egr3oD +5IuMdaQtkyCMIDlUp0W6QGUCAwEAAaNVMFMwEQYJYIZIAYb4QgEBBAQDAgAEMB0GA1UdDgQW +BBT84FToB/GV3jr3mcau+hUMbsQukjAfBgNVHSMEGDAWgBT84FToB/GV3jr3mcau+hUMbsQu +kjANBgkqhkiG9w0BAQQFAAOBgQBZ99sbXHoGxObFmGGEGM76BksgsSTK/Fl+Pxjx5L6sENlK +0mmPbvyRyvUEHAquufrKOexNABmmZ5TM5UBbWYQkkvABLBnkCy87HPYPG4VF7MOX8eC6QMvd +V3GJ4ItJcEkf3bbLNG9vzy8h5FPRGWaPZ2Lw3e4dSCrwR3uDdId5yDGCAdcwggHTAgEBMH0w +dzELMAkGA1UEBhMCVVMxLDAqBgNVBAoTI05ldHNjYXBlIENvbW11bmljYXRpb25zIENvcnBv +cmF0aW9uMRwwGgYDVQQLExNJbmZvcm1hdGlvbiBTeXN0ZW1zMRwwGgYDVQQDExNyb290Y2Eu +bmV0c2NhcGUuY29tAgIIOjAJBgUrDgMCGgUAoIGxMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B +BwEwIwYJKoZIhvcNAQkEMRYEFFagObyiR2QAb/2AgG/uvXOz13ByMBwGCSqGSIb3DQEJBTEP +Fw05NzExMTkxODMyMjJaMFIGCSqGSIb3DQEJDzFFMEMwCgYIKoZIhvcNAwcwDgYIKoZIhvcN +AwICAgCAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgFAMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3 +DQEBAQUABIGAD7qsSJAuy1NeHQJOQphJ3Zeo0rvtieSNPDK2mf1MzIDTjD1wk1zyd9wImzFw +Kt99mxZgvmA6Ynqh32zbl2eB0MnZ7CHV2cYAN8Dto+DwKopQvKhTLgpFIWHtqK3jHFiew9JI +EZxq6VIuj+09gq6XaSBOYWLwGcdSr6ZegqXfM2I= +--------------ms691B6036709EBB7DE811477F-- + +From - Wed Dec 03 08:00:37 1997 +Return-Path: +Received: from dome.mcom.com ([208.12.42.33]) by terryw.mcom.com + (Netscape Messaging Server 3.01) with ESMTP id 400 + for ; Tue, 2 Dec 1997 11:29:00 -0800 +Received: from dome.mcom.com ([127.0.0.1]) by dome.mcom.com + (Netscape Mail Server v2.02) with SMTP id AAA18756 + for ; Tue, 2 Dec 1997 11:29:00 -0800 +Return-Path: list@glacier.mcom.com +Return-Path: +Received: from glacier.mcom.com ([205.217.233.39]) by dome.mcom.com + (Netscape Mail Server v2.02) with ESMTP id AAA18742 + for ; Tue, 2 Dec 1997 11:28:58 -0800 +Received: (from list@localhost) by glacier.mcom.com (8.7.3/8.7.3) id LAA09662; Tue, 2 Dec 1997 11:28:46 -0800 (PST) +Resent-Date: Tue, 2 Dec 1997 11:28:46 -0800 (PST) +Message-ID: <3484604F.AED7FEED@netscape.com> +Date: Tue, 02 Dec 1997 11:23:59 -0800 +From: dcasados@netscape.com (Debbie Casados) +Reply-To: dcasados@netscape.com +Organization: Netscape +X-Mailer: Mozilla 4.02 [en]C-NSCP (Win95; U) +MIME-Version: 1.0 +To: stp-all@netscape.com, cpd-all@netscape.com, spd-all@netscape.com +CC: pdhr@netscape.com, beer@netscape.com, campsey@netscape.com +Subject: SITN REGISTRATION INFO +Content-Type: text/html; charset=us-ascii +Content-Transfer-Encoding: 7bit +Resent-Message-ID: <"H4WE_2.0.CM2.W56Xq"@glacier> +Resent-From: cpd-all@netscape.com +X-Mailing-List: archive/latest/63 +X-Loop: cpd-all@netscape.com +Precedence: list +Resent-Sender: cpd-all-request@netscape.com +X-Mozilla-Status: 8001 + + +I wanted to update you on another developmental opportunity that Netscape +is sponsoring for extended education.  SITN, Stanford Instructional +Television Network, will be broadcast to the Netscape campus beginning +in the winter quarter.  This is an opportunity for individuals to +enroll in Stanford's School of Engineering to study for a technical, graduate +degree. + +

Registration for non-degree classes and credit ends on December +5 while registration for auditing courses ends on Dec. 19th. + +

We will be having an information session tomorrow, December +3, from 11-12pm in building 14 Grab-n-Go area.  If you +are interested in learning more about SITN please drop by the session.  +If you would like to register for SITN please check out Stanford's SITN +website.  SCPD.stanford.edu + +

-- +
Debbie Casados +
HR Director, Product Development +
415-937-3883 +
Fax 415-938-9039 +
  + diff --git a/mozilla/grendel/ui/ActionFactory.java b/mozilla/grendel/ui/ActionFactory.java new file mode 100644 index 00000000000..e6981f57fd5 --- /dev/null +++ b/mozilla/grendel/ui/ActionFactory.java @@ -0,0 +1,214 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 8 Sep 1997. + */ + +package grendel.ui; + +import java.awt.Component; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.ResourceBundle; + +import com.sun.java.swing.JFrame; +import com.sun.java.swing.ToolTipManager; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import grendel.storage.MailDrop; +import grendel.search.SearchFrame; +import grendel.filters.FilterMaster; + +import grendel.composition.Composition; + +public class ActionFactory { + static IUICmd fExitAction = new ExitAction(); + static IUICmd fNewMailAction = new NewMailAction(); + static IUICmd fComposeMessageAction = new ComposeMessageAction(); + static IUICmd fPrefsAction = new PreferencesAction(); + static IUICmd fSearchAction = new SearchAction(); + static IUICmd fRunFiltersAction = new RunFiltersAction(); + static IUICmd fShowTooltipsAction = new ShowTooltipsAction(); + + static public IUICmd GetExitAction() { + return fExitAction; + } + + static public IUICmd GetNewMailAction() { + return fNewMailAction; + } + + static public IUICmd GetComposeMessageAction() { + return fComposeMessageAction; + } + + static public void SetComposeMessageAction(IUICmd aAction) { + fComposeMessageAction = aAction; + } + + static public IUICmd GetPreferencesAction() { + return fPrefsAction; + } + + static public IUICmd GetSearchAction() { + return fSearchAction; + } + + static public IUICmd GetRunFiltersAction() { + return fRunFiltersAction; + } + + static public IUICmd GetShowTooltipsAction() { + return fShowTooltipsAction; + } +} + +class ExitAction extends AbstractUICmd { + public ExitAction() { + super("appExit"); + + setEnabled(true); + } + + public void actionPerformed(ActionEvent aEvent) { + GeneralFrame.CloseAllFrames(); + + if (GeneralFrame.GetFrameList().length == 0) { + System.exit(0); + } + } +} + +class NewMailAction extends AbstractUICmd { + public NewMailAction() { + super("msgGetNew"); + + setEnabled(true); + } + + public void actionPerformed(ActionEvent aEvent) { + ProgressFactory.NewMailProgress(); + } +} + +class ComposeMessageAction extends AbstractUICmd { + public ComposeMessageAction() { + super("msgNew"); + + setEnabled(true); + } + + public void actionPerformed(ActionEvent aEvent) { + new Thread(new ComposeThread(), "Composition Starter").start(); + } + + class ComposeThread implements Runnable { + public void run() { + Composition frame = new Composition(); + + frame.show(); + frame.requestFocus(); + } + } +} + +class PreferencesAction extends AbstractUICmd { + PreferencesAction fThis; + + public PreferencesAction() { + super("appPrefs"); + fThis = this; + + setEnabled(true); + } + + public void actionPerformed(ActionEvent aEvent) { + Object source = aEvent.getSource(); + if (source instanceof Component) { + Frame frame = Util.GetParentFrame((Component) source); + if (frame instanceof JFrame) { + new Thread(new PrefThread((JFrame) frame), "Prefs").start(); + } + } + } + + class PrefThread implements Runnable { + JFrame fFrame; + PrefThread(JFrame aFrame) { + fFrame = aFrame; + } + public void run() { + synchronized(fThis) { + setEnabled(false); + PrefsDialog.EditPrefs(fFrame); + setEnabled(true); + } + } + } +} + +class SearchAction extends AbstractUICmd { + SearchAction() { + super("appSearch"); + } + + public void actionPerformed(ActionEvent aEvent) { + Frame frame = new SearchFrame(); + frame.show(); + frame.toFront(); + frame.requestFocus(); + } +} + +class RunFiltersAction extends AbstractUICmd { + RunFiltersAction() { + super("appRunFilters"); + } + + public void actionPerformed(ActionEvent aEvent) { + FilterMaster fm = FilterMaster.Get(); + fm.applyFiltersToTestInbox(); + } +} + +class ShowTooltipsAction extends AbstractUICmd { + ShowTooltipsAction() { + super("appShowTooltips"); + + boolean enabled = + PreferencesFactory.Get().getBoolean("app.tooltips", true); + ToolTipManager.sharedInstance().setEnabled(enabled); + + setSelected(enabled ? AbstractUICmd.kSelected : AbstractUICmd.kUnselected); + } + + public void actionPerformed(ActionEvent aEvent) { + boolean enabled = !ToolTipManager.sharedInstance().isEnabled(); + ToolTipManager.sharedInstance().setEnabled(enabled); + setSelected(enabled ? AbstractUICmd.kSelected : AbstractUICmd.kUnselected); + PreferencesFactory.Get().putBoolean("app.tooltips", enabled); + } +} diff --git a/mozilla/grendel/ui/BiffIcon.java b/mozilla/grendel/ui/BiffIcon.java new file mode 100644 index 00000000000..8a6e445a20a --- /dev/null +++ b/mozilla/grendel/ui/BiffIcon.java @@ -0,0 +1,84 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 19 Sep 1997. + */ + +package grendel.ui; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.event.ChangeListener; +import com.sun.java.swing.event.ChangeEvent; + +import grendel.storage.MailDrop; + +/** + * This is a simple biff label. It watches the BiffThread + * to update its state. + */ + +class BiffIcon extends JLabel implements ChangeListener { + BiffThread fThread; + + static Icon fIconUnknown; + static Icon fIconNew; + static Icon fIconNone; + + public BiffIcon() { + if (fIconUnknown == null) { + fIconUnknown = + new ImageIcon(getClass().getResource("images/biffUnknown.gif")); + fIconNew = + new ImageIcon(getClass().getResource("images/biffNew.gif")); + fIconNone = + new ImageIcon(getClass().getResource("images/biffNone.gif")); + } + setText(null); // Necessary for icon size to be used for preferred size + setBiffState(MailDrop.UNKNOWN); + + fThread = BiffThread.Get(); + fThread.addChangeListener(this); + + setBiffState(fThread.getBiffState()); + } + + public void dispose() { + fThread.removeChangeListener(this); + } + + public void stateChanged(ChangeEvent aEvent) { + setBiffState(fThread.getBiffState()); + repaint(); + } + + void setBiffState(int aState) { + switch (aState) { + case MailDrop.NEW: + setIcon(fIconNew); + break; + case MailDrop.NONE: + setIcon(fIconNone); + break; + case MailDrop.UNKNOWN: + setIcon(fIconUnknown); + break; + } + } +} diff --git a/mozilla/grendel/ui/BiffThread.java b/mozilla/grendel/ui/BiffThread.java new file mode 100644 index 00000000000..ec1da8f99b6 --- /dev/null +++ b/mozilla/grendel/ui/BiffThread.java @@ -0,0 +1,133 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.ui; + +import java.io.IOException; +import java.util.Enumeration; + +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.ChangeListener; +import com.sun.java.swing.event.EventListenerList; + +import calypso.util.PreferencesFactory; +import calypso.util.Preferences; + +import grendel.storage.MailDrop; + +class BiffThread extends Thread { + private static BiffThread fThread = null; + + EventListenerList fListeners = new EventListenerList(); + int fBiffState; + + static BiffThread Get() { + if (fThread == null) { + fThread = new BiffThread(); + fThread.start(); + } + return fThread; + } + + private BiffThread() { + super("biff"); + setDaemon(true); + + } + + public void addChangeListener(ChangeListener aListener) { + fListeners.add(ChangeListener.class, aListener); + } + + public void removeChangeListener(ChangeListener aListener) { + fListeners.remove(ChangeListener.class, aListener); + } + + public int getBiffState() { + return fBiffState; + } + + public void setBiffState(int aBiffState) { + if (aBiffState != fBiffState) { + fBiffState = aBiffState; + + Object[] listeners = fListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == ChangeListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((ChangeListener)listeners[i+1]).stateChanged(changeEvent); + } + } + } + } + + public void run() { + // ### Stubbing out maildrop stuff for now. + +// Preferences prefs = PreferencesFactory.Get(); +// try { +// while (true) { +// boolean unknown = false; +// boolean newMail = false; +// boolean none = false; +// +// Enumeration maildrops = fMaster.getMailDrops(); +// while (maildrops.hasMoreElements()) { +// MailDrop maildrop = (MailDrop) maildrops.nextElement(); +// +// try { +// maildrop.doBiff(); +// switch (maildrop.getBiffState()) { +// case MailDrop.NEW: +// newMail = true; +// break; +// +// case MailDrop.NONE: +// none = true; +// break; +// +// case MailDrop.UNKNOWN: +// unknown = true; +// break; +// } +// } catch (IOException e) { +// System.err.println("BiffIcon: " + e); +// } +// } +// +// if (newMail) { +// setBiffState(MailDrop.NEW); +// } else { +// if (none && !unknown) { +// setBiffState(MailDrop.NONE); +// } else { +// setBiffState(MailDrop.UNKNOWN); +// } +// } +// +// int interval = prefs.getInt("mail.biff_interval", 300); +// Thread.sleep(interval * 1000); +// } +// } catch (InterruptedException e) { +// } + } +} diff --git a/mozilla/grendel/ui/DialogAuthenticator.java b/mozilla/grendel/ui/DialogAuthenticator.java new file mode 100644 index 00000000000..4411988ac97 --- /dev/null +++ b/mozilla/grendel/ui/DialogAuthenticator.java @@ -0,0 +1,267 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 24 Nov 1997. + */ + +package grendel.ui; + +import java.awt.Canvas; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Frame; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.InetAddress; +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javax.mail.Authenticator; +import javax.mail.PasswordAuthentication; + +import com.sun.java.swing.JButton; +import com.sun.java.swing.JCheckBox; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JOptionPane; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JTextField; +import com.sun.java.swing.JPasswordField; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + + +public class DialogAuthenticator extends Authenticator { + + boolean fCancelled = false; + boolean fRemember = false; + + String fPrefBase; + + protected PasswordAuthentication getPasswordAuthentication() { + fCancelled = false; + Preferences prefs = PreferencesFactory.Get(); + + InetAddress site = getRequestingSite(); + String protocol = getRequestingProtocol(); + + fPrefBase = "mail." + protocol + "-" + site.getHostName(); + + String user = null; + user = prefs.getString(fPrefBase + ".user", null); + + String password = null; + password = prefs.getString(fPrefBase + ".password", null); + + if (user == null) { + user = getDefaultUserName(); + } + + if (password == null) { + password = prefs.getString("mail.password", null); + } + + if (user == null || password == null || password.equals("")) { + String prompt = getRequestingPrompt(); + Frame parent = GeneralFrame.GetDefaultFrame(); + if (parent == null) { + parent = JOptionPane.getRootFrame(); + } + + AuthenticatorDialog d = + new AuthenticatorDialog(parent, + site.getHostName(), prompt, user); + + fCancelled = d.isCancelled(); + + user = d.getUser(); + password = d.getPassword(); + if (d.rememberPassword()) { + prefs.putString(fPrefBase + ".user", user); + prefs.putString(fPrefBase + ".password", password); + } + } + + return new PasswordAuthentication(user, password); + } + + public boolean isCancelled() { + return fCancelled; + } + + public void resetPassword() { + Preferences prefs = PreferencesFactory.Get(); + prefs.putString(fPrefBase + ".password", ""); + } + + class AuthenticatorDialog extends GeneralDialog { + JTextField fUserField; + JPasswordField fPasswordField; + JCheckBox fRememberCheckBox; + + Container fPanel; + GridBagLayout fLayout; + + boolean fOK; + boolean fRemember; + + public AuthenticatorDialog(Frame aParent, String aSite, String aPrompt, + String aUser) { + super(aParent, "..."); + setFont(Font.decode("Dialog-12")); + + setTitle(MessageFormat.format(fLabels.getString("passwordDialogLabel"), + new Object[] {aSite})); + // fPanel = getContentPane(); + fPanel = new JPanel(); + fLayout = new GridBagLayout(); + fPanel.setLayout(fLayout); + setModal(true); + + fOK = false; + fRemember = false; + + GridBagConstraints c = new GridBagConstraints(); + + c.anchor = GridBagConstraints.NORTHWEST; + c.weightx = 1; + c.weighty = 0; + c.gridwidth = GridBagConstraints.REMAINDER; + c.fill = GridBagConstraints.HORIZONTAL; + + // Prompt label + + JLabel label; + + label = new JLabel(aPrompt); + c.gridwidth = GridBagConstraints.REMAINDER; + c.insets = new Insets(5,5,0,5); + fPanel.add(label, c); + + // User field + + label = new JLabel(fLabels.getString("userPrompt"), JLabel.RIGHT); + c.gridwidth = 1; + c.insets = new Insets(5,5,0,5); + fPanel.add(label, c); + + fUserField = new JTextField(aUser != null ? aUser : "", 20); + c.gridwidth = GridBagConstraints.REMAINDER; + c.insets = new Insets(5,0,0,5); + fPanel.add(fUserField, c); + + // Password field + + label = new JLabel(fLabels.getString("passwordPrompt"), JLabel.RIGHT); + c.gridwidth = 1; + c.insets = new Insets(5,5,0,5); + fPanel.add(label, c); + + fPasswordField = new JPasswordField(20); + c.gridwidth = GridBagConstraints.REMAINDER; + c.insets = new Insets(5,0,0,5); + fPanel.add(fPasswordField, c); + + // Remember password checkbox + + fRememberCheckBox = + new JCheckBox(fLabels.getString("rememberPasswordLabel")); + fRememberCheckBox.setHorizontalAlignment(JCheckBox.LEFT); + c.gridwidth = GridBagConstraints.REMAINDER; + c.insets = new Insets(5,5,0,5); + fPanel.add(fRememberCheckBox, c); + + // Action buttons + JOptionPane actionPanel = new JOptionPane(fPanel, + JOptionPane.PLAIN_MESSAGE, + JOptionPane.OK_CANCEL_OPTION); + actionPanel.addPropertyChangeListener(new OptionListener()); + add(actionPanel); + + Dimension size = getPreferredSize(); + Dimension screen = getToolkit().getScreenSize(); + // ### WHS tempory JDK 1.1.x hack + setBounds((screen.width - size.width - 10) / 2, + (screen.height - size.height - 32) / 2, + size.width + 10, + size.height + 32); + + setVisible(true); + requestFocus(); + } + + public String getUser() { + if (fOK) { + return fUserField.getText(); + } else { + return null; + } + } + + public String getPassword() { + if (fOK) { + return fPasswordField.getText(); + } else { + return null; + } + } + + public boolean isCancelled() { + return !fOK; + } + + public boolean rememberPassword() { + return fRemember; + } + + class CancelAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + setVisible(false); + } + } + + class OKAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + fOK = true; + fRemember = fRememberCheckBox.isSelected(); + setVisible(false); + } + } + + class OptionListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent aEvent) { + if (aEvent.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)){ + int value = ((Integer) aEvent.getNewValue()).intValue(); + + if (value == JOptionPane.OK_OPTION) { + fOK = true; + fRemember = fRememberCheckBox.isSelected(); + } + setVisible(false); + } + } + } + } +} diff --git a/mozilla/grendel/ui/EditHostDialog.java b/mozilla/grendel/ui/EditHostDialog.java new file mode 100644 index 00000000000..f00cb4e3141 --- /dev/null +++ b/mozilla/grendel/ui/EditHostDialog.java @@ -0,0 +1,194 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 26 Nov 1997. + */ + +package grendel.ui; + +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.text.MessageFormat; +import java.util.Hashtable; +import java.util.ResourceBundle; + +import javax.mail.URLName; + +import com.sun.java.swing.JOptionPane; + +import netscape.orion.dialogs.AttrNotFoundException; +import netscape.orion.dialogs.PageModel; +import netscape.orion.dialogs.PageUI; + +import xml.tree.XMLNode; +import xml.tree.TreeBuilder; + +public class EditHostDialog extends GeneralDialog { + static final String kIMAPRadioKey="imapRadio"; + static final String kPOPRadioKey="popRadio"; + static final String kOtherRadioKey="otherRadio"; + static final String kOtherFieldKey="otherField"; + static final String kHostFieldKey="hostField"; + static final String kUserFieldKey="userField"; + + URLName fResult = null; + + Hashtable fValues = null; + + PageUI fPanel; + + class EditHostModel extends PageModel { + public EditHostModel(URLName aURL) { + String proto = "imap"; + String host = "mail"; + String user = ""; + + if (aURL != null) { + proto = aURL.getProtocol(); + host = aURL.getHost(); + user = aURL.getUsername(); + } + + boolean other = (!proto.equalsIgnoreCase("imap") && + !proto.equalsIgnoreCase("pop3")); + + fValues = new Hashtable(); + fValues.put(kIMAPRadioKey, proto.equalsIgnoreCase("imap") ? + Boolean.TRUE : Boolean.FALSE); + fValues.put(kPOPRadioKey, proto.equalsIgnoreCase("pop3") ? + Boolean.TRUE : Boolean.FALSE); + fValues.put(kOtherRadioKey, other ? Boolean.TRUE : Boolean.FALSE); + fValues.put(kOtherFieldKey, other ? proto : ""); + fValues.put(kHostFieldKey, host == null ? "" : host); + fValues.put(kUserFieldKey, user == null ? "" : user); + } + + public Object getAttribute(String aAttrib) throws AttrNotFoundException { + Object res = fValues.get(aAttrib); + if (res == null) { + res = fLabels.getString(aAttrib); + } + if (res == null) { + throw new AttrNotFoundException(aAttrib); + } + return res; + } + + public void setAttribute(String aAttrib, Object aValue) { + if (fValues.containsKey(aAttrib)) { + fValues.put(aAttrib, aValue); + } + } + + public void actionPerformed(ActionEvent aEvent) { + } + } + + public EditHostDialog(Frame aParent, URLName aURL) { + super(aParent); + + setModal(true); + EditHostModel model = new EditHostModel(aURL); + + // use the XML parser to get the root XML node of the resource tree + XMLNode root = null; + URL url = getClass().getResource("dialogs.xml"); + try { + root = xml.tree.TreeBuilder.build(url, getClass()); + } catch (Exception e) { + e.printStackTrace(); + } + + XMLNode editHost = root.getChild("dialog", "id", "editHost"); + + fPanel = new PageUI(url, editHost, model); + + JOptionPane actionPanel = new JOptionPane(fPanel, + JOptionPane.PLAIN_MESSAGE, + JOptionPane.OK_CANCEL_OPTION); + actionPanel.addPropertyChangeListener(new OptionListener()); + add(actionPanel); + + // XXX This is a stupid hack because PageUI doesn't to a resource lookup + // on it's title. Bleh. + String title = fPanel.getTitle(); + if (title.charAt(0) == '$') { + try { + title = (String) model.getAttribute(title.substring(1)); + } catch (AttrNotFoundException e) {} + } + setTitle(title); + + Dimension size = getPreferredSize(); + Dimension screenSize = getToolkit().getScreenSize(); + setBounds((screenSize.width - size.width - 10) / 2, + (screenSize.height - size.height - 32) / 2, + size.width + 10, size.height + 32); + pack(); + setVisible(true); + requestFocus(); + } + + public URLName getURLName() { + return fResult; + } + + class OptionListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent aEvent) { + if (aEvent.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)){ + int value = ((Integer) aEvent.getNewValue()).intValue(); + + if (value == JOptionPane.OK_OPTION) { + // Grab all the values + fPanel.saveAll(); + + String proto; + Boolean imap = (Boolean) fValues.get(kIMAPRadioKey); + Boolean pop3 = (Boolean) fValues.get(kPOPRadioKey); + if (imap.booleanValue()) { + proto = "imap"; + } else if (pop3.booleanValue()) { + proto = "pop3"; + } else { + proto = (String) fValues.get(kOtherFieldKey); + } + String host = (String) fValues.get(kHostFieldKey); + String user = (String) fValues.get(kUserFieldKey); + + if (user.equals("")) { + user = null; + } + + if (proto.length() > 0 && + host.length() > 0) { + fResult = new URLName(proto, host, -1, null, user, null); + setVisible(false); + } else { + getToolkit().beep(); + } + } else { + setVisible(false); + } + } + } + } +} diff --git a/mozilla/grendel/ui/FolderCombo.java b/mozilla/grendel/ui/FolderCombo.java new file mode 100644 index 00000000000..356d3410525 --- /dev/null +++ b/mozilla/grendel/ui/FolderCombo.java @@ -0,0 +1,174 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Nov 1997. + */ + +package grendel.ui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Insets; +import java.util.StringTokenizer; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.Folder; + +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.ListCellRenderer; +import com.sun.java.swing.JComboBox; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JList; +import com.sun.java.swing.border.EmptyBorder; +import com.sun.java.swing.UIManager; + +import grendel.view.ViewedStore; +import grendel.view.ViewedFolder; + +public class FolderCombo extends JComboBox { + int fExclude; + int fInclude; + + Color fTextColor; + Color fHighlightTextColor; + Color fWindowColor; + Color fHighlightColor; + + public FolderCombo() { + updateUI(); + } + + public void populate() { + populate(-1, 0); + } + + public void populate(int aInclude, int aExclude) { + ViewedStore stores[] = StoreFactory.Instance().getStores(); + fInclude = aInclude; + fExclude = aExclude; + + removeAllItems(); + + try { + for (int i = 0; i < stores.length; i++) { + populateRecurse(stores[i], fInclude, fExclude); + } + } catch (MessagingException e) { + e.printStackTrace(); + } + } + + int populateRecurse(ViewedFolder aFolder, int fInclude, int fExclude) + throws MessagingException { + + int total = 0; + + while (aFolder != null) { + int idx = getItemCount(); + int count = populateRecurse(aFolder.getFirstSubFolder(), + fInclude, fExclude); + total += count; + + if ((aFolder.getFolder().getType() & fInclude) != 0 && + (aFolder.getFolder().getType() & fExclude) == 0) { + insertItemAt(aFolder, idx); + total++; + } else if (count > 0) { + insertItemAt(aFolder, idx); + total++; + } + aFolder = aFolder.getNextFolder(); + } + + return total; + } + + public ViewedFolder getSelectedFolder() { + return (ViewedFolder) getSelectedItem(); + } + + class FolderRenderer extends JLabel implements ListCellRenderer { + public FolderRenderer() { + setIcon(new ImageIcon(getClass().getResource("/grendel/ui/images/folder-small.gif"))); + setOpaque(true); + } + + public Component getListCellRendererComponent(JList aList, + Object aValue, + int aIndex, + boolean aIsSelected, + boolean aHasFocus) { + boolean isEnabled = true; + + Folder folder = null; + + if (aValue != null) { + folder = ((ViewedFolder) aValue).getFolder(); + } + + if (folder != null) { + try { + StringTokenizer counter = + new StringTokenizer(folder.getFullName(), + "" + + folder.getSeparator()); + + int depth = counter.countTokens(); + setBorder(new EmptyBorder(0, depth * 16, 0, 0)); + setText(folder.getName()); + + if ((folder.getType() & fInclude) == 0 || + (folder.getType() & fExclude) != 0) { + isEnabled = false; + } + } catch (MessagingException e) { + e.printStackTrace(); + } + } else { + setText(""); + } + + setEnabled(isEnabled); + + if (aIsSelected) { + setBackground(fHighlightColor); + setForeground(fHighlightTextColor); + } else { + setBackground(fWindowColor); + setForeground(fTextColor); + } + + return this; + } + } + + public void updateUI() { + super.updateUI(); + + fTextColor = UIManager.getColor("textText"); + fHighlightTextColor = UIManager.getColor("textHighlightText"); + fWindowColor = UIManager.getColor("window"); + fHighlightColor = UIManager.getColor("textHighlight"); + + setRenderer(new FolderRenderer()); + } +} + diff --git a/mozilla/grendel/ui/FolderFrame.java b/mozilla/grendel/ui/FolderFrame.java new file mode 100644 index 00000000000..028d77b1847 --- /dev/null +++ b/mozilla/grendel/ui/FolderFrame.java @@ -0,0 +1,149 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 18 Nov 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.util.Enumeration; +import java.util.Vector; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; + +import grendel.view.ViewedMessage; + +import com.sun.java.swing.event.ChangeEvent; + +import netscape.orion.toolbars.ToolbarFactory; +import netscape.orion.toolbars.ToolBarLayout; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import grendel.widgets.StatusEvent; +import grendel.widgets.TreePath; + +public class FolderFrame extends GeneralFrame { + static Vector fFolderFrames = new Vector(); + FolderPanel fFolderPanel; + + /** + * Identifying String + */ + + public static final String kID = "mail.folder"; + + public FolderFrame(Folder aFolder) { + super("folderFrameLabel", kID); + + fFolderPanel = new FolderPanel(); + fFolderPanel.addFolderPanelListener(new MessageSelectionListener()); + fPanel.add(fFolderPanel); + fMenu = buildMenu("folderMain", Util.MergeActions(actions, + fFolderPanel.getActions())); + getRootPane().setMenuBar(fMenu); + + fToolBar = fFolderPanel.getToolBar(); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(ToolBarLayout.CreateSpring(), + null)); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(fAnimation, null)); + fToolBarPanel.add(fToolBar); + + fStatusBar = buildStatusBar(); + fPanel.add(BorderLayout.SOUTH, fStatusBar); + + setFolder(aFolder); + + fFolderFrames.addElement(this); + + restoreBounds(); + } + + public void dispose() { + saveBounds(); + fFolderFrames.removeElement(this); + + fFolderPanel.dispose(); + + super.dispose(); + } + + public Folder getFolder() { + return fFolderPanel.getFolder(); + } + + public void setFolder(Folder aFolder) { + setTitle(fLabels.getString("folderFrameLabel")); + fFolderPanel.setFolder(aFolder); + if (aFolder != null) { + setTitle(aFolder.getName() + fLabels.getString("folderSuffixFrameLabel")); + } + } + + public static FolderFrame FindFolderFrame(Folder aFolder) { + Enumeration frames = fFolderFrames.elements(); + while (frames.hasMoreElements()) { + FolderFrame frame = (FolderFrame) frames.nextElement(); + if (frame.getFolder() == aFolder) { + return frame; + } + } + return null; + } + + // + // MessageSelectionListener class + // + + class MessageSelectionListener implements FolderPanelListener { + public void loadingFolder(ChangeEvent aEvent) { + startAnimation(); + } + + public void folderLoaded(ChangeEvent aEvent) { + stopAnimation(); + } + + public void folderStatus(StatusEvent aEvent) { + setStatusText(aEvent.getStatus()); + } + + public void folderSelectionChanged(ChangeEvent aEvent) { + } + public void folderSelectionDoubleClicked(ChangeEvent aEvent) { + TreePath path = null; + Enumeration selection = ((FolderPanel)aEvent.getSource()).getSelection(); + + MessageDisplayManager master = MultiMessageDisplayManager.Get(); + + while (selection.hasMoreElements()) { + path = (TreePath) selection.nextElement(); + if (path != null) { + ViewedMessage node = (ViewedMessage) path.getTip(); + master.displayMessage(node.getMessage()); + } + } + } + } + IUICmd actions[] = { ActionFactory.GetExitAction(), + ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction()}; +} diff --git a/mozilla/grendel/ui/FolderListTransferable.java b/mozilla/grendel/ui/FolderListTransferable.java new file mode 100644 index 00000000000..33fa5bc6e53 --- /dev/null +++ b/mozilla/grendel/ui/FolderListTransferable.java @@ -0,0 +1,67 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 17 Nov 1997. + */ + +package grendel.ui; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.Vector; + +import javax.mail.Message; + +public class FolderListTransferable implements Transferable { + Vector fList; + + static DataFlavor fFlavors[] = { + new DataFlavor(FolderListTransferable.class, "Netscape Folder List") + }; + + static DataFlavor GetDataFlavor() { + return fFlavors[0]; + } + + public FolderListTransferable(Vector aList) { + fList = aList; + } + + public Vector getFolderList() { + return fList; + } + + public Object getTransferData(DataFlavor aFlavor) + throws UnsupportedFlavorException, IOException { + if (aFlavor.equals(fFlavors[0])) { + return this; + } else { + throw new UnsupportedFlavorException(aFlavor); + } + } + + public DataFlavor[] getTransferDataFlavors() { + return fFlavors; + } + + public boolean isDataFlavorSupported(DataFlavor aFlavor) { + return aFlavor.equals(fFlavors[0]); + } +} diff --git a/mozilla/grendel/ui/FolderPanel.java b/mozilla/grendel/ui/FolderPanel.java new file mode 100644 index 00000000000..d8b59f69a0d --- /dev/null +++ b/mozilla/grendel/ui/FolderPanel.java @@ -0,0 +1,1072 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Vector; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Flags; + +import com.sun.java.swing.BorderFactory; +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JMenu; +import com.sun.java.swing.JMenuItem; +import com.sun.java.swing.JPopupMenu; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.KeyStroke; +import com.sun.java.swing.ToolTipManager; +import com.sun.java.swing.event.ChangeEvent; +//import com.sun.java.swing.plaf.BorderUIResource; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import grendel.composition.Composition; +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; +import grendel.view.FolderView; +import grendel.view.FolderViewFactory; +import grendel.view.MessageSetView; +import grendel.view.MessageSetViewObserver; +import grendel.view.ViewedFolder; +import grendel.view.ViewedMessage; +import grendel.view.ViewedStore; +import grendel.widgets.CellRenderer; +import grendel.widgets.Column; +import grendel.widgets.ColumnHeader; +import grendel.widgets.ColumnModel; +import grendel.widgets.ColumnModelEvent; +import grendel.widgets.ColumnModelListener; +import grendel.widgets.DefaultCellRenderer; +import grendel.widgets.SelectionEvent; +import grendel.widgets.SelectionListener; +import grendel.widgets.SelectionManager; +import grendel.widgets.StatusEvent; +import grendel.widgets.ToggleCellEditor; +import grendel.widgets.ToggleCellRenderer; +import grendel.widgets.TreePath; +import grendel.widgets.TreeTable; +import grendel.widgets.TreeTableDataModel; + +import calypso.util.Assert; + +/** + * Panel to display the contents of a folder. + */ + + +public class FolderPanel extends GeneralPanel { + + // + // Private members + // + + TreeTable fMessageTree; + Folder fFolder = null; + Thread fFolderLoadThread = null; + FolderView fView = null; + MessageModel fModel = null; + FolderPanel fPanel; + FolderPanelListener fListeners = null; + SelectionListener fSelectionListener = null; + ColumnModel fColumnModel = null; + + /** + * The Sender column ID + */ + + public static final String kSenderID = new String("sender"); + + /** + * The Subject column ID + */ + + public static final String kSubjectID = new String("subject"); + + /** + * The Date column ID + */ + + public static final String kDateID = new String("date"); + + /** + * The read column ID + */ + + public static final String kReadID = new String("read"); + + /** + * The flag column ID + */ + + public static final String kFlagID = new String("flag"); + + /** + * The Deleted (X) column ID + */ + + public static final String kDeletedID = new String("deleted"); + + /** + * The individual message scope + */ + + public static final int kMessage = 0; + + /** + * The thread scope + */ + + public static final int kThread = 1; + + /** + * The all messages scope + */ + + public static final int kAll = 2; + + // + // Actions that can be enabled/disabled + // + + IUICmd fDeleteMessageAction = new DeleteMessageAction(); + IUICmd fOpenMessageAction = new OpenMessageAction(); + + IUICmd fReplyAction = new ReplyAction("msgReply", false); + IUICmd fReplyAllAction = new ReplyAction("msgReplyAll", true); + + IUICmd fMarkMsgReadAction = new MarkAction("markMsgRead", kMessage); + IUICmd fMarkThreadReadAction = new MarkAction("markThreadRead", kThread); + IUICmd fMarkAllReadAction = new MarkAction("markAllRead", kAll); + + IUICmd fThreadAction = new ThreadAction(); + // The big action list + + IUICmd fActions[] = {ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction(), + fDeleteMessageAction, + fOpenMessageAction, + new CopyToClipboardAction(), + new PasteFromClipboardAction(), + fThreadAction, + fReplyAction, + fReplyAllAction, + fMarkMsgReadAction, + fMarkThreadReadAction, + fMarkAllReadAction + }; + + SortAction fSortActions[] = {new SortAction("sortAuthor", + MessageSetView.AUTHOR), + new SortAction("sortSubject", + MessageSetView.SUBJECT), + new SortAction("sortDate", + MessageSetView.DATE), + new SortAction("sortNumber", + MessageSetView.NUMBER) + }; + + /** + * Constructs a folder panel + */ + + public FolderPanel() { + JScrollPane scrollPane = new JScrollPane(); + //scrollPane.setBorder(BorderUIResource.getLoweredBevelBorderUIResource()); + scrollPane.setBorder(BorderFactory.createLoweredBevelBorder()); + Util.RegisterScrollingKeys(scrollPane); + + fPanel = this; + + fModel = new MessageModel(); + + fMessageTree = new TreeTable(null); + ToolTipManager.sharedInstance().registerComponent(fMessageTree); + fMessageTree.setTreeColumn(kSubjectID); + + fColumnModel = fMessageTree.getColumnModel(); + fColumnModel.addColumnModelListener(new ColumnListener()); + + Column column; + CellRenderer cellrenderer = new MessageCellRenderer(); + + column = new Column(kSubjectID, fLabels.getString("subjectLabel")); + column.setWidth(350); + column.setSelectable(true); + column.setCellRenderer(cellrenderer); + fMessageTree.addColumn(column); + + ToggleCellRenderer renderer; + ToggleCellEditor editor; + + Icon unreadIcon = new ImageIcon(getClass().getResource("images/unread.gif")); + Icon readIcon = new ImageIcon(getClass().getResource("images/read.gif")); + + renderer = new ToggleCellRenderer(); + renderer.getCheckBox().setIcon(unreadIcon); + renderer.getCheckBox().setSelectedIcon(readIcon); + editor = new ToggleCellEditor(); + editor.getCheckBox().setIcon(unreadIcon); + editor.getCheckBox().setSelectedIcon(readIcon); + + column = new Column(kReadID, unreadIcon); + column.setWidth(20); + column.setMinWidth(20); + column.setMaxWidth(20); + column.setSelectable(true); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + fMessageTree.addColumn(column); + + Icon unflaggedIcon = new ImageIcon(getClass().getResource("images/unflagged.gif")); + Icon flaggedIcon = new ImageIcon(getClass().getResource("images/flagged.gif")); + + renderer = new ToggleCellRenderer(); + renderer.getCheckBox().setIcon(unflaggedIcon); + renderer.getCheckBox().setSelectedIcon(flaggedIcon); + editor = new ToggleCellEditor(); + editor.getCheckBox().setIcon(unflaggedIcon); + editor.getCheckBox().setSelectedIcon(flaggedIcon); + + column = new Column(kFlagID, flaggedIcon); + column.setWidth(20); + column.setMinWidth(20); + column.setMaxWidth(20); + column.setSelectable(true); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + fMessageTree.addColumn(column); + + Icon deletedIcon = new ImageIcon(getClass().getResource("images/deleted.gif")); + Icon undeletedIcon = new ImageIcon(getClass().getResource("images/unflagged.gif")); + + renderer = new ToggleCellRenderer(); + renderer.getCheckBox().setIcon(undeletedIcon); + renderer.getCheckBox().setSelectedIcon(deletedIcon); + editor = new ToggleCellEditor(); + editor.getCheckBox().setIcon(undeletedIcon); + editor.getCheckBox().setSelectedIcon(deletedIcon); + + column = new Column(kDeletedID, deletedIcon); + column.setWidth(20); + column.setMinWidth(20); + column.setMaxWidth(20); + column.setSelectable(true); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + fMessageTree.addColumn(column); + + column = new Column(kSenderID, fLabels.getString("senderLabel")); + column.setWidth(250); + column.setSelectable(true); + column.setCellRenderer(cellrenderer); + fMessageTree.addColumn(column); + + column = new Column(kDateID, fLabels.getString("dateLabel")); + column.setWidth(100); + column.setSelectable(true); + column.setCellRenderer(cellrenderer); + fMessageTree.addColumn(column); + + Preferences prefs = PreferencesFactory.Get(); + fColumnModel.setPrefString(prefs.getString("mail.folder_panel.column_layout", "")); + + // Setup keys + registerKeyboardAction(new DeleteMessageAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + registerKeyboardAction(new CopyToClipboardAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_C, + KeyEvent.CTRL_MASK), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + registerKeyboardAction(new PasteFromClipboardAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_V, + KeyEvent.CTRL_MASK), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + registerKeyboardAction(new OpenMessageAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_O, + KeyEvent.CTRL_MASK), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + ColumnHeader columnHeader = fMessageTree.getColumnHeader(); + scrollPane.setColumnHeaderView(columnHeader); + scrollPane.setViewportView(fMessageTree); + + add(scrollPane); + + fSelectionListener = new MessageSelectionListener(); + fMessageTree.getSelectionManager().addSelectionListener(fSelectionListener); + } + + public void dispose() { + setFolder(null); + + Preferences prefs = PreferencesFactory.Get(); + prefs.putString("mail.folder_panel.column_layout", + fColumnModel.getPrefString()); + + fMessageTree.getSelectionManager().removeSelectionListener(fSelectionListener); + } + + /** + * Sets the folder displayed to the given folder. If the folder is + * null, the folder display is cleared. + */ + + public synchronized void setFolder(Folder aFolder) { + if (fFolderLoadThread != null) { + fFolderLoadThread.stop(); + if (fListeners != null) { + fListeners.folderLoaded(new ChangeEvent(fPanel)); + fListeners.folderStatus(new StatusEvent(fPanel, + fLabels.getString("folderLoadedStatus"))); + } + } + fFolderLoadThread = + new Thread(new LoadFolderThread(aFolder), "folderLoad"); + int maxPriority = fFolderLoadThread.getThreadGroup().getMaxPriority(); + fFolderLoadThread.setPriority(maxPriority); + fFolderLoadThread.start(); + } + + /** + * Returns the current folder + */ + + public Folder getFolder() { + return fFolder; + } + + /** + * Returns an enumeration of TreePathss representing + * the current selection. The tips of the paths are ViewedMessages. + */ + + public Enumeration getSelection() { + return fMessageTree.getSelectionManager().getSelection(); + } + + /** + * Returns the actions available for this panel + */ + + public IUICmd[] getActions() { + return Util.MergeActions(fActions, fSortActions); + } + + /** + * Returns the toolbar associated with this panel. + */ + + public NSToolbar getToolBar() { + return buildToolBar("folderToolBar", getActions()); + } + + /** + * Adds a FolderPanelListener + */ + + public void addFolderPanelListener(FolderPanelListener aListener) { + fListeners = FolderMulticaster.add(fListeners, aListener); + } + + /** + * Removes a FolderPanelListener + */ + + public void removeFolderPanelListener(FolderPanelListener aListener) { + fListeners = FolderMulticaster.remove(fListeners, aListener); + } + + + // + // Component overloads + // + + public boolean isOpaque() { + return true; + } + + /** + * Syncs the sort menu items when the view or sort changes + */ + + void updateSortMenu() { + if (fView != null) { + int order[] = fView.getSortOrder(); + if (order != null && order.length > 0) { + for (int i = 0; i < fSortActions.length; i++) { + if (fSortActions[i].getType() == order[0]) { + fSortActions[i].setSelected(IUICmd.kSelected); + } + } + } + fThreadAction.setSelected(fView.isThreaded() ? + IUICmd.kSelected : IUICmd.kUnselected); + } + } + + // + // MessageSelectionListener class + // + + class MessageSelectionListener implements SelectionListener { + public void selectionChanged(SelectionEvent aEvent) { + if (fListeners != null) { + fListeners.folderSelectionChanged(new ChangeEvent(fPanel)); + } + + // Interpret selection + SelectionManager sm = (SelectionManager) aEvent.getSource(); + boolean one = sm.getSelectionCount() == 1; + boolean many = sm.getSelectionCount() > 0; + + fDeleteMessageAction.setEnabled(many); + fOpenMessageAction.setEnabled(many); + + fReplyAction.setEnabled(one); + fReplyAllAction.setEnabled(one); + + fMarkMsgReadAction.setEnabled(many); + fMarkThreadReadAction.setEnabled(many); + } + + public void selectionDoubleClicked(MouseEvent aEvent) { + if (fListeners != null) { + fListeners.folderSelectionDoubleClicked(new ChangeEvent(fPanel)); + } + } + + protected void buildMenus(ViewedFolder aFolder, JMenu aCopy, JMenu aMove) { + int goodMask = Folder.HOLDS_MESSAGES|Folder.READ_WRITE; + if (aFolder != null) { + Folder folder = aFolder.getFolder(); + ViewedFolder temp = aFolder.getFirstSubFolder(); + + if (temp == null) { + try { + if ((aFolder.getFolder().getType() & goodMask) == goodMask) { + JMenuItem copyItem = new JMenuItem(folder.getName()); + JMenuItem moveItem = new JMenuItem(folder.getName()); + + copyItem.addActionListener(new CopyMessageAction(folder)); + moveItem.addActionListener(new MoveMessageAction(folder)); + + aCopy.add(copyItem); + aMove.add(moveItem); + } + } catch (MessagingException e) {} + } else { + JMenu copyMenu = new JMenu(folder.getName()); + JMenu moveMenu = new JMenu(folder.getName()); + + while (temp != null) { + buildMenus(temp, copyMenu, moveMenu); + temp = temp.getNextFolder(); + } + + if (copyMenu.getMenuComponentCount() > 0) { + aCopy.add(copyMenu); + aMove.add(moveMenu); + } + } + } + } + + /** + * Attempt to bring up a context menu + */ + + public void selectionContextClicked(MouseEvent aEvent) { + JPopupMenu popup = new JPopupMenu(); + JMenuItem item; + + ResourceBundle strings = ResourceBundle.getBundle("grendel.ui.MenuLabels", + getLocale()); + + item = new JMenuItem(strings.getString("msgDeletePopupLabel")); + item.addActionListener(new DeleteMessageAction()); + popup.add(item); + + JMenu copyMenu = new JMenu(strings.getString("msgCopyPopupLabel")); + JMenu moveMenu = new JMenu(strings.getString("msgMovePopupLabel")); + + ViewedStore stores[] = StoreFactory.Instance().getStores(); + + for (int i = 0; i < stores.length; i++) { + buildMenus(stores[i], copyMenu, moveMenu); + } + + popup.add(copyMenu); + popup.add(moveMenu); + + popup.show(fPanel, aEvent.getX(), aEvent.getY()); + } + + public void selectionDragged(MouseEvent aEvent) { + System.out.println("Dragging started"); + } + } + + // + // ColumnModelListener + // + + class ColumnListener implements ColumnModelListener { + public void columnAdded(ColumnModelEvent e) {} + public void columnRemoved(ColumnModelEvent e) {} + public void columnMoved(ColumnModelEvent e) {} + public void columnMarginChanged(ChangeEvent e) {} + public void columnWidthChanged(ColumnModelEvent e) {} + + public void columnSelectionChanged(ChangeEvent e) { + Column column = fColumnModel.getSelectedColumn(); + if (fView != null && column != null) { + Object id = column.getID(); + if (id == kSenderID) { + fView.prependSortOrder(MessageSetView.AUTHOR); + } else if (id == kSubjectID) { + fView.prependSortOrder(MessageSetView.SUBJECT); + } else if (id == kDateID) { + fView.prependSortOrder(MessageSetView.DATE); + } else if (id == kReadID) { + fView.prependSortOrder(MessageSetView.READ); + } else if (id == kFlagID) { + fView.prependSortOrder(MessageSetView.FLAGGED); + } else if (id == kDeletedID) { + fView.prependSortOrder(MessageSetView.DELETED); + } + fView.reThread(); + updateSortMenu(); + } + } + } + + /** + * Returns the current selection as a vector + */ + + synchronized Vector getSelectionVector() { + Vector msgVector = new Vector(); + SelectionManager selection = fMessageTree.getSelectionManager(); + Enumeration messages = selection.getSelection(); + while (messages.hasMoreElements()) { + msgVector.insertElementAt(messages.nextElement(), msgVector.size()); + } + return msgVector; + } + + synchronized Vector getSelectedMessageVector() { + Vector msgVector = new Vector(); + SelectionManager selection = fMessageTree.getSelectionManager(); + Enumeration messages = selection.getSelection(); + while (messages.hasMoreElements()) { + TreePath path = (TreePath) messages.nextElement(); + Message msg = ((ViewedMessage) path.getTip()).getMessage(); + if (msg != null) { + msgVector.insertElementAt(msg, msgVector.size()); + } + } + return msgVector; + } + + + /** Like getSelectedMessageVector() but returns a Vector of ViewedMessages + instead of a Vector of Messages. */ + synchronized Vector getSelectedViewedMessageVector() { + Vector msgVector = new Vector(); + SelectionManager selection = fMessageTree.getSelectionManager(); + Enumeration messages = selection.getSelection(); + while (messages.hasMoreElements()) { + TreePath path = (TreePath) messages.nextElement(); + ViewedMessage msg = (ViewedMessage) path.getTip(); + if (msg != null) { + msgVector.insertElementAt(msg, msgVector.size()); + } + } + return msgVector; + } + + + /** Returns a Vector of all Messages (not including dummies.) */ + synchronized Vector getAllMessagesVector() { + Vector v = new Vector(); + ViewedMessage m = fView.getMessageRoot(); + while (m != null) { + getAllMessagesVector(m, v); + m = m.getNext(); + } + return v; + } + + /** Writes the message and all of its children into the vector. + */ + private synchronized void getAllMessagesVector(ViewedMessage m, Vector v) { + if (!m.isDummy()) + v.insertElementAt(m.getMessage(), v.size()); + ViewedMessage kid = m.getChild(); + while (kid != null) { + getAllMessagesVector(kid, v); + kid = kid.getNext(); + } + } + + + // + // OpenFolderAction class + // + + class LoadFolderThread implements Runnable { + Folder fFolder; + + public LoadFolderThread(Folder aFolder) { + fFolder = aFolder; + } + + public void run() { + synchronized (fPanel) { + if (fPanel.fFolder == fFolder) { + fFolderLoadThread = null; + return; // save ourselves some + } + + fPanel.fFolder = fFolder; + } + if (fListeners != null) { + fListeners.folderStatus(new StatusEvent(fPanel, + fLabels.getString("folderLoadingStatus"))); + fListeners.loadingFolder(new ChangeEvent(fPanel)); + } + + fMessageTree.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + fMessageTree.getSelectionManager().clearSelection(); + if (fFolder != null) { + // Clear out the old folder manually + synchronized (fPanel) { + fMessageTree.setDataModel(null); + } + + fView = FolderViewFactory.Make(fFolder); + synchronized (fPanel) { + fModel.setFolderView(fView); + fMessageTree.setDataModel(fModel); + } + updateSortMenu(); + } else { + synchronized (fPanel) { + fMessageTree.setDataModel(null); + fModel.setFolderView(null); + } + } + fMessageTree.setCursor(Cursor.getDefaultCursor()); + synchronized (fPanel) { + fFolderLoadThread = null; + } + + if (fListeners != null) { + fListeners.folderLoaded(new ChangeEvent(fPanel)); + fListeners.folderStatus(new StatusEvent(fPanel, + fLabels.getString("folderLoadedStatus"))); + } + } + } + + class OpenMessageAction extends AbstractUICmd implements Runnable { + OpenMessageAction() { + super("msgOpen"); + } + + public void actionPerformed(ActionEvent aEvent) { + new Thread(this, "msgOpen").start(); + } + + public void run() { + Vector selection = getSelectedMessageVector(); + MessageFrame frame = null; + for (int i = 0; i < selection.size(); i++) { + Message msg = (Message) selection.elementAt(i); + frame = MessageFrame.FindMessageFrame(msg); + if (frame != null) { + frame.toFront(); + } else { + frame = new MessageFrame(msg); + frame.setVisible(true); + frame.toFront(); + } + } + if (frame != null) { + frame.requestFocus(); + } + } + } + + // + // DeleteMessageAction class + // + + class DeleteMessageAction extends AbstractUICmd { + DeleteMessageAction() { + super("msgDelete"); + setEnabled(false); + } + + public void actionPerformed(ActionEvent aEvent) { + ProgressFactory.DeleteMessagesProgress(getSelectedMessageVector()); + } + } + + // + // CopyMessageAction class + // + + class CopyMessageAction extends AbstractUICmd { + Folder fDest; + + CopyMessageAction(Folder aFolder) { + super("msgCopy"); + + fDest = aFolder; + } + + public void actionPerformed(ActionEvent aEvent) { + ProgressFactory.CopyMessagesProgress(getSelectedMessageVector(), fDest); + } + } + + // + // MoveMessageAction class + // + + class MoveMessageAction extends AbstractUICmd { + Folder fDest; + + MoveMessageAction(Folder aFolder) { + super("msgMove"); + + fDest = aFolder; + } + + public void actionPerformed(ActionEvent aEvent) { + ProgressFactory.MoveMessagesProgress(getSelectedMessageVector(), fDest); + } + } + + // + // ThreadAction class + // + + class ThreadAction extends AbstractUICmd { + public ThreadAction() { + super("toggleThreading"); + } + + public void actionPerformed(ActionEvent aEvent) { + if (fView != null) { + boolean selected = !fView.isThreaded(); + fView.setIsThreaded(selected); + setSelected(selected ? IUICmd.kSelected : IUICmd.kUnselected); + fView.reThread(); + } + } + } + + // + // SortAction class + // + + class SortAction extends AbstractUICmd { + int fType; + + public SortAction(String aAction, int aType) { + super(aAction); + fType = aType; + } + + public int getType() { + return fType; + } + + public void actionPerformed(ActionEvent aEvent) { + String action = aEvent.getActionCommand(); + if (fView != null) { + fView.prependSortOrder(fType); + setSelected(kSelected); + fView.reThread(); + } + } + } + + // + // ReplyAction class + // + + class ReplyAction extends AbstractUICmd { + boolean replyall; + public ReplyAction(String aAction, boolean r) { + super(aAction); + setEnabled(false); + + replyall = r; + } + public void actionPerformed(ActionEvent aEvent) { + Vector selection = getSelectedMessageVector(); + if (selection.size() != 1) { + Assert.Assertion(false, + "Need to have exactly one message selected to reply"); + } + Composition frame = new Composition(); + frame.initializeAsReply((Message) (selection.elementAt(0)), replyall); + frame.show(); + frame.requestFocus(); + } + } + + // + // MarkAction class + // + + class MarkAction extends AbstractUICmd { + int fScope; + + MarkAction(String aName, int aScope) { + super(aName); + fScope = aScope; + + setEnabled(aScope == kAll); + } + + public void actionPerformed(ActionEvent aEvent) { + + Message msgs[] = null; + + // gather the messages... + // + switch (fScope) { + case kMessage: + { + Vector selection = getSelectedMessageVector(); + if (selection.size() == 0) break; + msgs = new Message[selection.size()]; + Enumeration e = selection.elements(); + for (int i = 0; e.hasMoreElements(); i++) + msgs[i] = (Message) e.nextElement(); + } + break; + + case kThread: + { + Vector selection = getSelectedViewedMessageVector(); + if (selection.size() == 0) break; + Vector thread_msgs = new Vector(); + for (Enumeration e = selection.elements(); e.hasMoreElements(); ) { + ViewedMessage m = (ViewedMessage) e.nextElement(); + ViewedMessage p; + // Move up to the root of the thread... + while ((p = m.getParent()) != null) + m = p; + // Add the thread subtree to the vector. + getAllMessagesVector(m, thread_msgs); + } + msgs = new Message[thread_msgs.size()]; + Enumeration e = thread_msgs.elements(); + for (int i = 0; e.hasMoreElements(); i++) + msgs[i] = (Message) e.nextElement(); + } + break; + + case kAll: + { + Vector all = getAllMessagesVector(); + if (all.size() == 0) break; + msgs = new Message[all.size()]; + Enumeration e = all.elements(); + for (int i = 0; e.hasMoreElements(); i++) + msgs[i] = (Message) e.nextElement(); + } + break; + } + + // ...then mark them read. + // + if (msgs != null) { + try { + fFolder.setFlags(msgs, new Flags(Flags.SEEN), true); + } catch (MessagingException e) { + // #### pop up a dialog or something + synchronized(System.err) { + System.err.print("Error: "); + e.printStackTrace(System.err); + } + } + } + } + } + + // + // Cut-n-paste stuff + // + + class FPClipboardOwner implements ClipboardOwner { + public void lostOwnership(Clipboard aClipboard, + Transferable aTransferable) { + } + } + + class CopyToClipboardAction extends AbstractUICmd { + CopyToClipboardAction() { + super("copy-to-clipboard"); + } + + public void actionPerformed(ActionEvent aEvent) { + System.out.println("FolderPanel: copy-to-clipboard"); + // Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard(); + Clipboard cb = fPrivateClipboard; + + Transferable transferable = + new MessageListTransferable(getSelectedMessageVector()); + + cb.setContents(transferable, new FPClipboardOwner()); + } + } + + class PasteFromClipboardAction extends AbstractUICmd { + PasteFromClipboardAction() { + super("paste-from-clipboard"); + } + + public void actionPerformed(ActionEvent aEvent) { + System.out.println("FolderPanel: paste-from-clipboard"); + // Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard(); + Clipboard cb = fPrivateClipboard; + Transferable transferable = cb.getContents(fPanel); + DataFlavor flavor = MessageListTransferable.GetDataFlavor(); + + try { + MessageListTransferable messageList = + (MessageListTransferable) transferable.getTransferData(flavor); + + ProgressFactory.CopyMessagesProgress(messageList.getMessageList(), + fFolder); + } catch (UnsupportedFlavorException e) { + System.err.println("FolderPanel: " + e); + } catch (IOException e) { + System.err.println("FolderPanel: " + e); + } + } + } +} + +// +// Multicaster class for FolderPanelListeners +// + +class FolderMulticaster implements FolderPanelListener { + FolderPanelListener a, b; + + public FolderMulticaster(FolderPanelListener a, FolderPanelListener b) { + this.a = a; + this.b = b; + } + + public static FolderPanelListener add(FolderPanelListener a, FolderPanelListener b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return new FolderMulticaster(a, b); + } + + public static FolderPanelListener remove(FolderPanelListener a, FolderPanelListener b) { + if (a == b || a == null) { + return null; + } else if (a instanceof FolderMulticaster) { + return ((FolderMulticaster)a).remove(b); + } else { + return a; // it's not here + } + } + + public FolderPanelListener remove(FolderPanelListener c) { + if (c == a) { + return b; + } + if (c == b) { + return a; + } + FolderPanelListener a2 = remove(a, c); + FolderPanelListener b2 = remove(b, c); + if (a2 == a && b2 == b) { + return this; // it's not here + } + return add(a2, b2); + } + + public void loadingFolder(ChangeEvent aEvent) { + a.loadingFolder(aEvent); + b.loadingFolder(aEvent); + } + + public void folderLoaded(ChangeEvent aEvent) { + a.folderLoaded(aEvent); + b.folderLoaded(aEvent); + } + + public void folderStatus(StatusEvent aEvent) { + a.folderStatus(aEvent); + b.folderStatus(aEvent); + } + + public void folderSelectionChanged(ChangeEvent aEvent) { + a.folderSelectionChanged(aEvent); + b.folderSelectionChanged(aEvent); + } + + public void folderSelectionDoubleClicked(ChangeEvent aEvent) { + a.folderSelectionDoubleClicked(aEvent); + b.folderSelectionDoubleClicked(aEvent); + } +} diff --git a/mozilla/grendel/ui/FolderPanelListener.java b/mozilla/grendel/ui/FolderPanelListener.java new file mode 100644 index 00000000000..a798b414e31 --- /dev/null +++ b/mozilla/grendel/ui/FolderPanelListener.java @@ -0,0 +1,63 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +import grendel.widgets.StatusEvent; + +/** + * Interface for information about changes in the FolderPanel + */ + +public interface FolderPanelListener { + /** + * Called when a folder starts to load + */ + public void loadingFolder(ChangeEvent aEvent); + + /** + * Called when a folder is done loading + */ + + public void folderLoaded(ChangeEvent aEvent); + + /** + * Called when the selection changes. + */ + + public void folderSelectionChanged(ChangeEvent aEvent); + + /** + * Called for status text updates + */ + + public void folderStatus(StatusEvent aEvent); + + /** + * Called when the user double clicks. + */ + + public void folderSelectionDoubleClicked(ChangeEvent aEvent); +} diff --git a/mozilla/grendel/ui/GeneralDialog.java b/mozilla/grendel/ui/GeneralDialog.java new file mode 100644 index 00000000000..9c25a0520c2 --- /dev/null +++ b/mozilla/grendel/ui/GeneralDialog.java @@ -0,0 +1,75 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 6 Jan 1998. + */ + +package grendel.ui; + +import java.awt.Frame; +import java.util.Locale; +import java.util.ResourceBundle; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import com.sun.java.swing.JDialog; +import com.sun.java.swing.SwingUtilities; +import com.sun.java.swing.UIManager; + +public class GeneralDialog extends JDialog { + static ResourceBundle fLabels = ResourceBundle.getBundle("grendel.ui.Labels", + Locale.getDefault()); + protected GeneralDialog fThis; + + private LAFListener fLAFListener; + + public GeneralDialog(Frame f) { + super(f); + init(); + } + public GeneralDialog(Frame f, boolean m) { + super(f, m); + init(); + } + public GeneralDialog(Frame f, String t) { + super(f, t); + init(); + } + + void init() { + fThis = this; + + fLAFListener = new LAFListener(); + UIManager.addPropertyChangeListener(fLAFListener); + } + + public void dispose() { + super.dispose(); + + UIManager.removePropertyChangeListener(fLAFListener); + } + + class LAFListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent aEvent) { + SwingUtilities.updateComponentTreeUI(fThis); + invalidate(); + validate(); + repaint(); + } + } +} diff --git a/mozilla/grendel/ui/GeneralFrame.java b/mozilla/grendel/ui/GeneralFrame.java new file mode 100644 index 00000000000..8d267d966bf --- /dev/null +++ b/mozilla/grendel/ui/GeneralFrame.java @@ -0,0 +1,344 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.Vector; + +import com.sun.java.swing.Action; +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JButton; +import com.sun.java.swing.JFrame; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JMenuBar; +import com.sun.java.swing.JOptionPane; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JToolBar; +import com.sun.java.swing.SwingUtilities; +import com.sun.java.swing.UIManager; + +import netscape.orion.toolbars.BarLayout; +import netscape.orion.toolbars.CollapsibleToolbarPanel; +import netscape.orion.toolbars.NSButton; +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.toolbars.ToolBarLayout; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; +import netscape.orion.uimanager.IUIMMenuBar; +import netscape.orion.uimanager.UIMConstants; + +import xml.tree.TreeBuilder; +import xml.tree.XMLNode; + +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import grendel.widgets.Animation; + +public class GeneralFrame extends JFrame +{ + GeneralFrame fThis; + + BiffIcon fBiffIcon; + + protected Container fPanel; + protected Animation fAnimation; + protected JMenuBar fMenu; + protected CollapsibleToolbarPanel fToolBarPanel; + protected NSToolbar fToolBar; + protected Component fStatusBar; + protected String fResourceBase = "grendel.ui"; + protected String fID; + protected JLabel fStatusLabel; + + protected netscape.orion.uimanager.UIManager fUIManager; + + private LAFListener fLAFListener; + + static Vector fFrameList = new Vector(); + static boolean sExternalShell = false; + + protected ResourceBundle fLabels = + ResourceBundle.getBundle("grendel.ui.Labels", getLocale()); + + public GeneralFrame(String aTitle, String aID) { + fThis = this; + fID = aID; + + String title = aTitle; + try { + title = fLabels.getString(title); + } catch (MissingResourceException e) {} + + setTitle(title); + + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + dispose(); + } + }); + + updateUI(); + fLAFListener = new LAFListener(); + UIManager.addPropertyChangeListener(fLAFListener); + + fPanel = getContentPane(); + + fAnimation = new Animation(); + fAnimation.setImageTemplate("/grendel/ui/images/animation/AnimHuge{0,number,00}.gif", + 40); + + fToolBarPanel = new CollapsibleToolbarPanel(this); + fPanel.add(BorderLayout.NORTH, fToolBarPanel); + fUIManager = new netscape.orion.uimanager.UIManager(fToolBarPanel); + + // We need to use Class.forName because getClass() might return a child + // class in another package. + + try { + URL url = Class.forName("grendel.ui.GeneralFrame").getResource("images/GrendelIcon32.gif"); + setIconImage(getToolkit().getImage(url)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + fFrameList.addElement(this); + } + + public void dispose() { + if (fBiffIcon != null) { + fBiffIcon.dispose(); + } + + fFrameList.removeElement(this); + + super.dispose(); + + if (!sExternalShell && fFrameList.size() == 0) { + ActionFactory.GetExitAction().actionPerformed(null); + } + + UIManager.removePropertyChangeListener(fLAFListener); + } + + public void updateUI() { + setBackground(UIManager.getColor("control")); + getContentPane().setBackground(UIManager.getColor("control")); + } + /** + * Sets the frame's id. + */ + + public void setID(String aID) { + fID = aID; + } + + /** + * Returns the frame's id. + */ + + public String getID() { + return fID; + } + + /** + * Finds the last created frame of a given id. + */ + + public static GeneralFrame FindFrameByID(String aID) { + return FindFrameByID(aID, null); + } + + /** + * Finds the last created frame of a given id. + */ + + public static GeneralFrame FindFrameByID(String aID, GeneralFrame aExclude) { + for (int i = fFrameList.size() - 1; i >= 0; i--) { + GeneralFrame frame = (GeneralFrame) fFrameList.elementAt(i); + if (frame.getID().equals(aID) && frame != aExclude) { + return frame; + } + } + return null; + } + + /** + * Returns a frame for use by dialogs + */ + + static public GeneralFrame GetDefaultFrame() { + if (fFrameList.size() > 0) { + return (GeneralFrame) fFrameList.elementAt(0); + } + return null; + } + + /** + * Sets whether or not this frame is working with an external application. + * Important to prevent exiting the system when all our frames close. + */ + + static public void SetExternalShell(boolean aShell) { + sExternalShell = aShell; + } + + static public boolean IsExternalShell() { + return sExternalShell; + } + + static public GeneralFrame[] GetFrameList() { + GeneralFrame res[] = new GeneralFrame[fFrameList.size()]; + fFrameList.copyInto(res); + + return res; + } + + static public synchronized void CloseAllFrames() { + while (fFrameList.size() > 0) { + GeneralFrame frame = (GeneralFrame) fFrameList.elementAt(0); + frame.dispose(); + } + } + + protected JMenuBar buildMenu(String aMenu, IUICmd aActions[]) { + JMenuBar res = null; + try { + IUIMMenuBar bar = fUIManager.getMenuBar(getID()); + + URL url = getClass().getResource("menus.xml"); + XMLNode root = TreeBuilder.build(url, getClass()); + XMLNode node = root.getChild(UIMConstants.kMenubarType, + UIMConstants.kIDAttribute, + getID()); + + bar.addMenus(node, aActions, this); + bar.configureForOwner(this); + + res = bar.getComponent(); + } catch (Exception e) { + e.printStackTrace(); + } + + return res; + } + + protected Component buildStatusBar() { + JPanel res = new JPanel(new BarLayout()); + + fBiffIcon = new BiffIcon(); + fBiffIcon.setSize(fBiffIcon.getPreferredSize()); + + fStatusLabel = new JLabel("Grendel"); + fStatusLabel.setFont(Font.decode("Dialog-12")); + + res.add(fStatusLabel, BarLayout.kSpring); + res.add(fBiffIcon, BarLayout.kStrut); + + return res; + } + + protected void startAnimation() { + fAnimation.start(); + } + + protected void stopAnimation() { + fAnimation.stop(); + } + + protected void setStatusText(String aString) { + if (fStatusLabel != null) { + fStatusLabel.setText(aString); + } + } + + private void saveBounds(String aName) { + Preferences prefs = PreferencesFactory.Get(); + Rectangle bounds = getBounds(); + + prefs.putInt(aName + ".x", bounds.x); + prefs.putInt(aName + ".y", bounds.y); + prefs.putInt(aName + ".width", bounds.width); + prefs.putInt(aName + ".height", bounds.height); + } + + protected void saveBounds() { + saveBounds(fID); + } + + private void restoreBounds(String aName, int aWidth, int aHeight) { + Preferences prefs = PreferencesFactory.Get(); + int x, y, w, h; + + x = prefs.getInt(aName + ".x", 100); + y = prefs.getInt(aName + ".y", 100); + w = prefs.getInt(aName + ".width", aWidth); + h = prefs.getInt(aName + ".height", aHeight); + + setBounds(x, y, w, h); + } + + protected void restoreBounds(int aWidth, int aHeight) { + GeneralFrame frame = FindFrameByID(fID, this); + if (frame == null) { + restoreBounds(fID, aWidth, aHeight); + } else { + Rectangle bounds = frame.getBounds(); + Insets insets = frame.getInsets(); + setBounds(bounds.x + insets.top, bounds.y + insets.top, + bounds.width, bounds.height); + } + } + + protected void restoreBounds() { + Dimension screenSize = getToolkit().getScreenSize(); + + restoreBounds(screenSize.width * 2 / 3, + screenSize.height * 2 / 3); + } + + class LAFListener implements PropertyChangeListener { + public void propertyChange(PropertyChangeEvent aEvent) { + SwingUtilities.updateComponentTreeUI(fThis); + invalidate(); + validate(); + repaint(); + } + } +} diff --git a/mozilla/grendel/ui/GeneralPanel.java b/mozilla/grendel/ui/GeneralPanel.java new file mode 100644 index 00000000000..18e613cf820 --- /dev/null +++ b/mozilla/grendel/ui/GeneralPanel.java @@ -0,0 +1,113 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 13 Oct 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.awt.Image; +import java.awt.datatransfer.Clipboard; +import java.util.Hashtable; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.StringTokenizer; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JPanel; + +import netscape.orion.toolbars.NSButton; +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.IUICmd; + +public class GeneralPanel extends JPanel { + static ResourceBundle fLabels = ResourceBundle.getBundle("grendel.ui.Labels", + Locale.getDefault()); + + static Clipboard fPrivateClipboard = new Clipboard("Grendel"); + + protected String fResourceBase = "grendel.ui"; + protected NSToolbar fToolBar; + + public GeneralPanel() { + setLayout(new BorderLayout()); + } + + public IUICmd[] getActions() { + return null; + } + + protected NSButton makeToolbarButton(String aAction) { + Icon icon = new ImageIcon(getClass().getResource("images/toolbar/" + aAction + ".gif")); + Icon iconDisabled = new ImageIcon(getClass().getResource("images/toolbar/" + aAction + "-disabled.gif")); + Icon iconPressed = new ImageIcon(getClass().getResource("images/toolbar/" + aAction + "-pressed.gif")); + Icon iconRollover = new ImageIcon(getClass().getResource("images/toolbar/" + aAction + "-rollover.gif")); + + NSButton res = new NSButton(); + + res.setIcon(icon); + res.setDisabledIcon(iconDisabled); + res.setPressedIcon(iconPressed); + res.setRolloverIcon(iconRollover); + res.setActionCommand(aAction); + + return res; + } + + protected NSToolbar buildToolBar(String aToolbar, IUICmd aActions[]) { + NSToolbar res = null; + + Hashtable commands = new Hashtable(); + for (int i = 0; i < aActions.length; i++) + { + IUICmd a = aActions[i]; + commands.put(a.getText(IUICmd.NAME), a); + } + + try { + res = new NSToolbar(); + + ResourceBundle resources = ResourceBundle.getBundle(fResourceBase + ".Menus"); + String toolbar = resources.getString(aToolbar); + StringTokenizer tokens = new StringTokenizer(toolbar, " ", false); + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken(); + NSButton button = makeToolbarButton(token); + IUICmd action = (IUICmd) commands.get(token); + + if (action != null) { + button.addActionListener(action); + } else { + button.setEnabled(false); + } + res.addItem(button); + } + } catch (MissingResourceException e) { + System.err.println(e); + } + + return res; + } + + public NSToolbar getToolBar() { + return fToolBar; + } +} diff --git a/mozilla/grendel/ui/Labels.properties b/mozilla/grendel/ui/Labels.properties new file mode 100644 index 00000000000..ea86536f46e --- /dev/null +++ b/mozilla/grendel/ui/Labels.properties @@ -0,0 +1,110 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +# +# General string resources +# + +appNameLabel=Grendel +subjectLabel=Subject +senderLabel=Sender +dateLabel=Date +nameLabel=Name +unreadLabel=Unread +totalLabel=Total +folderLabel=Folder +messageLabel=Message +okLabel=OK +cancelLabel=Cancel +imapLabel=IMAP +popLabel=POP3 + +rememberPasswordLabel=Remember Password +loginFailedLabel=Login Failed! + +localStoreLabel=Local Messages +remoteStoreLabel=Messages on {0} + +# +# Window Labels +# + +passwordDialogLabel=Password for {0} +newFolderDialogLabel=Create New Folder +errorDialogLabel=Grendel Error +prefDialogLabel=Grendel Preferences +masterFrameLabel=Grendel +folderFrameLabel=Grendel Folder +messageFrameLabel=Grendel Message +folderSuffixFrameLabel= - Grendel Folder +messageSuffixFrameLabel= - Grendel Message +editHostLabel=Edit Host +exceptionDialogLabel=Error + +# +# Status Labels +# + +movingMessageStatus=Moving "{0}" to "{1}" +copyingMessageStatus=Copying "{0}" to "{1}" +deletingMessageStatus=Deleting "{0}" +newMailStatus=Getting new messages... + +folderLoadingStatus=Folder Loading... +folderLoadedStatus=Folder Loaded +messageLoadingStatus=Message Loading... +messageLoadedStatus=Message Loaded + +# +# Progress Labels +# + +movingMessageLabel=Moving Messages +copyingMessageLabel=Copying Messages +deletingMessageLabel=Deleting Messages +newMailProgressLabel=Getting New Messages + +# +# Error messages +# + +folderCreateError=Error Creating Folder +folderExistsError=Folder "{0}" already exists +folderCantCreateError=Could't create folder "{0}" +folderInvalidCharacter=Invalid character "{0}" + +errorOccurredLabel=An error has occurred: + +# +# Header Lables +# + +toHeader=To: +fromHeader=From: +subjectHeader=Subject: + +# +# Prompts +# + +passwordPrompt=Password: +userPrompt=User: +parentPrompt=Parent: +namePrompt=Name: +otherPrompt=Other: +hostPrompt=Host: + diff --git a/mozilla/grendel/ui/Makefile b/mozilla/grendel/ui/Makefile new file mode 100644 index 00000000000..08ada557839 --- /dev/null +++ b/mozilla/grendel/ui/Makefile @@ -0,0 +1,56 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + ActionFactory.java \ + BiffIcon.java \ + BiffThread.java \ + DialogAuthenticator.java \ + EditHostDialog.java \ + FolderCombo.java \ + FolderFrame.java \ + FolderListTransferable.java \ + FolderPanel.java \ + FolderPanelListener.java \ + GeneralDialog.java \ + GeneralFrame.java \ + GeneralPanel.java \ + MasterPanel.java \ + MasterPanelListener.java \ + MessageCellRenderer.java \ + MessageDisplayManager.java \ + MessageFrame.java \ + MessageHeader.java \ + MessageListTransferable.java \ + MessageModel.java \ + MessagePanel.java \ + MessagePanelListener.java \ + MultiMessageDisplayManager.java \ + NewFolderDialog.java \ + PrefsDialog.java \ + ProgressFactory.java \ + ProgressFrame.java \ + SimpleAuthenticator.java \ + SimpleMessageHeader.java \ + StoreFactory.java \ + UIFactory.java \ + UnifiedMessageDisplayManager.java \ + Util.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/ui/MasterPanel.java b/mozilla/grendel/ui/MasterPanel.java new file mode 100644 index 00000000000..95710ce9b78 --- /dev/null +++ b/mozilla/grendel/ui/MasterPanel.java @@ -0,0 +1,851 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.ClipboardOwner; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.StringTokenizer; +import java.util.Vector; + +import com.sun.java.swing.BorderFactory; +import com.sun.java.swing.BoxLayout; +import com.sun.java.swing.Icon; +import com.sun.java.swing.JOptionPane; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.JViewport; +import com.sun.java.swing.KeyStroke; +import com.sun.java.swing.ToolTipManager; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.ChangeListener; +//import com.sun.java.swing.plaf.BorderUIResource; + +import calypso.util.ArrayEnumeration; +import calypso.util.Assert; +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; + +import grendel.storage.FolderExtraFactory; +import grendel.storage.SearchResultsFolderFactory; +import grendel.view.ViewedFolder; +import grendel.view.ViewedStore; +import grendel.view.ViewedStoreEvent; +import grendel.view.ViewedStoreListener; +import grendel.widgets.Column; +import grendel.widgets.ColumnHeader; +import grendel.widgets.ColumnModel; +import grendel.widgets.ColumnChangeListener; +import grendel.widgets.DefaultCellRenderer; +import grendel.widgets.SelectionEvent; +import grendel.widgets.SelectionListener; +import grendel.widgets.SelectionManager; +import grendel.widgets.StatusEvent; +import grendel.widgets.TextCellEditor; +import grendel.widgets.TreePath; +import grendel.widgets.TreeTable; +import grendel.widgets.TreeTableDataModel; +import grendel.widgets.TreeTableModelBroadcaster; +import grendel.widgets.TreeTableModelEvent; +import grendel.widgets.TreeTableModelListener; + +/** + * Panel to display the contents of a folder. + */ + +public class MasterPanel extends GeneralPanel { + TreeTable fFolderTree; + FolderModel fModel = null; + MasterPanelListener fListeners = null; + MasterPanel fPanel; + SelectionListener fSelectionListener = null; + StoreChangeListener fStoreChangeListener = null; + ViewedStore fStores[]; + + IUICmd fActions[] = {ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction(), + new CopyToClipboardAction(), + new PasteFromClipboardAction(), + new NewFolderAction(), + new DeleteFolderAction()}; + + public static final String kNameID = new String("Name"); + public static final String kUnreadID = new String("Unread"); + public static final String kTotalID = new String("Total"); + + /** + * Constructs a new master panel. + */ + + public MasterPanel() { + fPanel = this; + + JScrollPane scrollPane = new JScrollPane(); + //scrollPane.setBorder(BorderUIResource.getLoweredBevelBorderUIResource()); + scrollPane.setBorder(BorderFactory.createLoweredBevelBorder()); + Util.RegisterScrollingKeys(scrollPane); + + fFolderTree = new TreeTable(null); + ToolTipManager.sharedInstance().registerComponent(fFolderTree); + fFolderTree.setTreeColumn(kNameID); + + Column column; + FolderCellRenderer renderer = new FolderCellRenderer(); + TextCellEditor editor = new TextCellEditor(); + + column = new Column(kNameID, fLabels.getString("nameLabel")); + column.setWidth(250); + column.setCellRenderer(renderer); + column.setCellEditor(editor); + fFolderTree.addColumn(column); + + column = new Column(kUnreadID, fLabels.getString("unreadLabel")); + column.setWidth(50); + column.setCellRenderer(renderer); + fFolderTree.addColumn(column); + + column = new Column(kTotalID, fLabels.getString("totalLabel")); + column.setWidth(50); + column.setCellRenderer(renderer); + fFolderTree.addColumn(column); + + Preferences prefs = PreferencesFactory.Get(); + fFolderTree.getColumnModel().setPrefString(prefs.getString("mail.master_panel.column_layout", "")); + + registerKeyboardAction(new CopyToClipboardAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_C, + KeyEvent.CTRL_MASK), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + registerKeyboardAction(new PasteFromClipboardAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_V, + KeyEvent.CTRL_MASK), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + ColumnHeader columnHeader = fFolderTree.getColumnHeader(); + + scrollPane.setColumnHeaderView(columnHeader); + scrollPane.setViewportView(fFolderTree); + + add(scrollPane); + + // ###HACKHACKHACK Remove me when javamail fixes their stuff. + java.io.File mailcapfile = new java.io.File("mailcap"); + if (!mailcapfile.exists()) { + try { + (new java.io.RandomAccessFile(mailcapfile, "rw")).close(); + System.out.println("*** Created empty mailcap file in current"); + System.out.println("*** directory (to work around buggy javamail"); + System.out.println("*** software from JavaSoft)."); + } catch (java.io.IOException e) { + System.out.println("*** Couldn't create empty mailcap file: " + e); + System.out.println("*** Immanent crash is likely due to buggy"); + System.out.println("*** javamail software from JavaSoft."); + } + } + + fModel = new FolderModel(); + ViewedStore stores[] = StoreFactory.Instance().getStores(); + fStores = new ViewedStore[stores.length]; + for (int i = 0; i < stores.length; i++) { + fModel.addStore(stores[i]); + fStores[i] = stores[i]; + } + + fFolderTree.setDataModel(fModel); + + fSelectionListener = new MasterSelectionListener(); + fFolderTree.getSelectionManager().addSelectionListener(fSelectionListener); + + fStoreChangeListener = new StoreChangeListener(); + StoreFactory.Instance().addChangeListener(fStoreChangeListener); + } + + public void dispose() { + Preferences prefs = PreferencesFactory.Get(); + prefs.putString("mail.master_panel.column_layout", + fFolderTree.getColumnModel().getPrefString()); + + fFolderTree.getSelectionManager().removeSelectionListener(fSelectionListener); + StoreFactory.Instance().removeChangeListener(fStoreChangeListener); + } + + /** + * Returns the actions associated with this panel. + */ + + public IUICmd[] getActions() { + return fActions; + } + + /** + * Returns the toolbar associated with this panel. + */ + + public NSToolbar getToolBar() { + return buildToolBar("masterToolBar", fActions); + } + + /** + * Returns an enumeration of TreePathss representing the + * current selection. The tips of the paths are Folders. + */ + + public Enumeration getSelection() { + return fFolderTree.getSelectionManager().getSelection(); + } + + /** + * Adds a MasterPanelListener + */ + + public void addMasterPanelListener(MasterPanelListener aListener) { + fListeners = MasterMulticaster.add(fListeners, aListener); + } + + /** + * Removes a MasterPanelListener + */ + + public void removeMasterPanelListener(MasterPanelListener aListener) { + fListeners = MasterMulticaster.remove(fListeners, aListener); + } + + // + // Component overloads + // + + public boolean isOpaque() { + return true; + } + + synchronized ViewedFolder getSelectedViewedFolder() { + ViewedFolder res = null; + SelectionManager selection = fFolderTree.getSelectionManager(); + if (selection.getSelectionCount() == 1) { + TreePath path = (TreePath) selection.getSelection().nextElement(); + res = GetViewedFolder(path.getTip()); + } + return res; + } + + synchronized Vector getSelectedFolderVector() { + Vector folderVector = new Vector(); + SelectionManager selection = fFolderTree.getSelectionManager(); + Enumeration folders = selection.getSelection(); + while (folders.hasMoreElements()) { + TreePath path = (TreePath) folders.nextElement(); + Object folder = path.getTip(); + if (folder != null) { + folderVector.insertElementAt(folder, folderVector.size()); + } + } + return folderVector; + } + + public static ViewedFolder GetViewedFolder(Object aObject) { + ViewedFolder res = null; + + if (aObject instanceof ViewedFolder) { + res = (ViewedFolder) aObject; + } + return res; + } + public static Folder getFolder(Object aObject) { + Folder res = null; + if (aObject instanceof Store) { + try { + res = ((Store) aObject).getDefaultFolder(); + } catch (MessagingException e) {} + } else if (aObject instanceof Folder) { + res = (Folder) aObject; + } if (aObject instanceof ViewedFolder) { + res = ((ViewedFolder) aObject).getFolder(); + } + return res; + } + + // + // MasterSelectionListener class + // + + class MasterSelectionListener implements SelectionListener { + public void selectionChanged(SelectionEvent aEvent) { + if (fListeners != null) { + fListeners.masterSelectionChanged(new ChangeEvent(fPanel)); + } + } + + public void selectionDoubleClicked(MouseEvent aEvent) { + if (fListeners != null) { + fListeners.masterSelectionDoubleClicked(new ChangeEvent(fPanel)); + } + } + public void selectionContextClicked(MouseEvent aEvent) { + } + public void selectionDragged(MouseEvent aEvent) { + } + } + + class NewFolderAction extends AbstractUICmd { + NewFolderAction() { + super("folderNew"); + } + + public void actionPerformed(ActionEvent aEvent) { + new Thread(new NewFolderThread(), "NewFolder").start(); + } + + class NewFolderThread implements Runnable { + public void run() { + new NewFolderDialog(Util.GetParentFrame(fPanel), + getSelectedViewedFolder()); + } + } + } + + class DeleteFolderAction extends AbstractUICmd { + DeleteFolderAction() { + super("folderDelete"); + } + + public void actionPerformed(ActionEvent aEvent) { + System.out.println("Delete Folder"); + ViewedFolder viewedFolder = getSelectedViewedFolder(); + if (viewedFolder != null) { + Folder folder = viewedFolder.getFolder(); + System.out.println("Deleting Folder: " + folder.getName()); + try { + folder.delete(true); + } catch (MessagingException e) { + e.printStackTrace(); + } + } + } + } + + class MPClipboardOwner implements ClipboardOwner { + public void lostOwnership(Clipboard aClipboard, + Transferable aTransferable) { + } + } + + class CopyToClipboardAction extends AbstractUICmd { + CopyToClipboardAction() { + super("copy-to-clipboard"); + } + + public void actionPerformed(ActionEvent aEvent) { + System.out.println("MasterPanel: copy-to-clipboard"); + // Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard(); + Clipboard cb = fPrivateClipboard; + + Transferable transferable = + new FolderListTransferable(getSelectedFolderVector()); + + cb.setContents(transferable, new MPClipboardOwner()); + } + } + + class PasteFromClipboardAction extends AbstractUICmd { + PasteFromClipboardAction() { + super("paste-from-clipboard"); + } + + public void actionPerformed(ActionEvent aEvent) { + System.out.println("MasterPanel: paste-from-clipboard"); + // Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard(); + Clipboard cb = fPrivateClipboard; + Transferable transferable = cb.getContents(fPanel); + + DataFlavor msgFlavor = MessageListTransferable.GetDataFlavor(); + DataFlavor folderFlavor = FolderListTransferable.GetDataFlavor(); + ViewedFolder viewedFolder = getSelectedViewedFolder(); + Folder folder = null; + if (viewedFolder == null) { + return; + } else { + folder = viewedFolder.getFolder(); + } + + if (transferable.isDataFlavorSupported(msgFlavor)) { + try { + MessageListTransferable messageList = + (MessageListTransferable) transferable.getTransferData(msgFlavor); + + ProgressFactory.CopyMessagesProgress(messageList.getMessageList(), + folder); + } catch (UnsupportedFlavorException e) { + System.err.println("MasterPanel: " + e); + } catch (IOException e) { + System.err.println("MasterPanel: " + e); + } + } else if (transferable.isDataFlavorSupported(folderFlavor)) { + try { + FolderListTransferable messageList = + (FolderListTransferable) transferable.getTransferData(folderFlavor); + } catch (UnsupportedFlavorException e) { + System.err.println("MasterPanel: " + e); + } catch (IOException e) { + System.err.println("MasterPanel: " + e); + } + } + } + } + + class StoreChangeListener implements ChangeListener { + public void stateChanged(ChangeEvent aEvent) { + int i; + for (i = 0; i < fStores.length; i++) { + fModel.removeStore(fStores[i]); + } + + ViewedStore stores[] = StoreFactory.Instance().getStores(); + fStores = new ViewedStore[stores.length]; + + for (i = 0; i < stores.length; i++) { + fModel.addStore(stores[i]); + fStores[i] = stores[i]; + } + } + } +} + +// +// Multicaster for MasterPanelListeners +// + +class MasterMulticaster implements MasterPanelListener { + MasterPanelListener a, b; + + public MasterMulticaster(MasterPanelListener a, MasterPanelListener b) { + this.a = a; + this.b = b; + } + + public static MasterPanelListener add(MasterPanelListener a, MasterPanelListener b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return new MasterMulticaster(a, b); + } + + public static MasterPanelListener remove(MasterPanelListener a, MasterPanelListener b) { + if (a == b || a == null) { + return null; + } else if (a instanceof MasterMulticaster) { + return ((MasterMulticaster)a).remove(b); + } else { + return a; // it's not here + } + } + + public MasterPanelListener remove(MasterPanelListener c) { + if (c == a) { + return b; + } + if (c == b) { + return a; + } + MasterPanelListener a2 = remove(a, c); + MasterPanelListener b2 = remove(b, c); + if (a2 == a && b2 == b) { + return this; // it's not here + } + return add(a2, b2); + } + + public void masterSelectionChanged(ChangeEvent aEvent) { + a.masterSelectionChanged(aEvent); + b.masterSelectionChanged(aEvent); + } + public void masterSelectionDoubleClicked(ChangeEvent aEvent) { + a.masterSelectionDoubleClicked(aEvent); + b.masterSelectionDoubleClicked(aEvent); + } +} + +// +// FolderDataModel. Maps a Store into a TreeTableDataModel +// + +class FolderModel implements TreeTableDataModel { + Vector fStores = new Vector(); + TreeTableModelListener fListeners = null; + Hashtable fCollapsed = new Hashtable(); + + StoreObserver fStoreObserver; + + ResourceBundle fLabels = ResourceBundle.getBundle("grendel.ui.Labels"); + + public FolderModel() { + fStoreObserver = new StoreObserver(); + } + + public void addStore(ViewedStore aStore) { + aStore.addViewedStoreListener(fStoreObserver); + fStores.addElement(aStore); + updateFolderCreated(aStore); + } + + public void removeStore(ViewedStore aStore) { + aStore.removeViewedStoreListener(fStoreObserver); + + fStores.removeElement(aStore); + updateFolderDeleted(aStore); + } + + public void addFolder(ViewedFolder aFolder) { + fStores.addElement(aFolder); + updateFolderCreated(aFolder); + } + + public void removeFolder(ViewedFolder aFolder) { + fStores.removeElement(aFolder); + updateFolderDeleted(aFolder); + } + + public boolean showRoot() { + return true; + } + + // Navigation stuff + public Object getRoot() { + if (fStores.size() > 0) { + return fStores.elementAt(0); + } else { + return null; + } + } + + public boolean isLeaf(Object aNode) { + ViewedFolder folder = (ViewedFolder) aNode; + if (folder != null) { + ViewedStore store = folder.getViewedStore(); + if (folder == store && !store.isConnected()) { + return false; + } + } + + if (aNode == null) { + return (getChildren(aNode) == null); + } else { + return (getChild(aNode) == null); + } + } + + public Enumeration getChildren(Object aNode) { + if (aNode == null) { + return fStores.elements(); + } + return null; + } + + public Object getChild(Object aNode) { + if (aNode instanceof ViewedFolder) { + return ((ViewedFolder) aNode).getFirstSubFolder(); + } + return null; + } + + public Object getNextSibling(Object aNode) { + if (aNode instanceof ViewedFolder) { + return ((ViewedFolder) aNode).getNextFolder(); + } + return null; + } + + // Attributes + public void setCollapsed(TreePath aPath, boolean aCollapsed) { + TreeTableModelEvent event = + new TreeTableModelEvent(this, aPath); + + if (aCollapsed) { + if (fCollapsed.remove(aPath) != null) { + if (fListeners != null) { + fListeners.nodeCollapsed(event); + } + } + } else { + if (fCollapsed.put(aPath, "x") == null) { + if (fListeners != null) { + fListeners.nodeExpanded(event); + } + } + } + } + + public boolean isCollapsed(TreePath aPath) { + return !fCollapsed.containsKey(aPath); + } + + public Object getData(Object aNode, Object aID) { + ViewedFolder node = null; + + if (aNode instanceof ViewedStore) { + if (aID == MasterPanel.kNameID) { + String host = ((ViewedStore) aNode).getHost(); + if (host != null) { + return MessageFormat.format(fLabels.getString("remoteStoreLabel"), + new Object[] {host}); + } else { + return fLabels.getString("localStoreLabel"); + } + } + return ""; + } + + node = getViewedFolder(aNode); + + if (node == null) { + return ""; + } + + if (aID == MasterPanel.kNameID) { + return node.getFolder().getName(); + } else if (aID == MasterPanel.kUnreadID) { + int n = node.getUnreadMessageCount(); + if (n < 0) + return "???"; + else + return Integer.toString(n); + } else if (aID == MasterPanel.kTotalID) { + int n = node.getUndeletedMessageCount(); + if (n < 0) + return "???"; + else + return Integer.toString(n); + } else { + throw new Error("unknown column"); + } + } + + public Icon getIcon(Object aNode) { + return UIFactory.Instance().getFolderIcon((ViewedFolder) aNode, + false, false); + } + + public Icon getOverlayIcon(Object aNode) { + return UIFactory.Instance().getFolderOverlayIcon((ViewedFolder) aNode, + false, false); + } + + public void setData(Object aNode, Object aID, Object aValue) { + if (aValue.equals(getData(aNode, aID))) { + return; + } + Folder node = getFolder(aNode); + + if (aID == MasterPanel.kNameID) { + Folder parent = node.getParent(); + String newName = (String) aValue; + + Folder newFolder = null; + + // Check name validity + try { + if (newName.indexOf(node.getSeparator()) >= 0) { + Object args[] = {new String("" + node.getSeparator())}; + String err = + MessageFormat.format(fLabels.getString("folderInvalidCharacter"), + args); + JOptionPane.showMessageDialog(null, err, + fLabels.getString("folderCreateError"), + JOptionPane.ERROR_MESSAGE); + return; + } + + if (parent != null) { + newFolder = parent.getFolder(newName); + } else { + newFolder = node.getStore().getFolder(newName); + } + } catch (MessagingException e) { + System.err.println("setData: " + e); + } + if (newFolder != null) { + if (newFolder.exists()) { + Object args[] = {newName}; + String err = + MessageFormat.format(fLabels.getString("folderExistsError"), + args); + JOptionPane.showMessageDialog(null, err, + fLabels.getString("folderCreateError"), + JOptionPane.ERROR_MESSAGE); + } else { + try { + node.renameTo(newFolder); + } catch (MessagingException e) { + System.err.println("renameTo: " + e); + } + } + } + } + } + + Folder getFolder(Object aObject) { + Folder res = null; + if (aObject instanceof ViewedFolder) { + res = ((ViewedFolder) aObject).getFolder(); + } + return res; + } + + ViewedFolder getViewedFolder(Object aObject) { + ViewedFolder res = null; + if (aObject instanceof ViewedFolder) { + res = ((ViewedFolder) aObject); + } + return res; + } + + public void addTreeTableModelListener(TreeTableModelListener aListener) { + fListeners = TreeTableModelBroadcaster.add(fListeners, aListener); + } + + public void removeTreeTableModelListener(TreeTableModelListener aListener) { + fListeners = TreeTableModelBroadcaster.remove(fListeners, aListener); + } + + TreePath createTreePath(ViewedFolder aNode) { + Vector pathVector = new Vector(); + if (aNode != null) { + while (aNode != null) { + pathVector.insertElementAt(aNode, 0); + aNode = aNode.getParentFolder(); + } + } + return new TreePath(pathVector); + } + + TreePath createTreePath(Folder aFolder) { + ViewedStore store = + StoreFactory.Instance().getViewedStore(aFolder.getStore()); + if (store != null) { + try { + ViewedFolder folder = store.getViewedFolder(aFolder); + if (folder != null) { + return createTreePath(folder); + } + } catch (MessagingException e) { + e.printStackTrace(); + } + } + return null; + } + + void updateFolder(ViewedFolder aFolder) { + TreePath path = createTreePath(aFolder); + if (fListeners != null && path != null) { + fListeners.nodeChanged(new TreeTableModelEvent(this, path, null)); + } + } + + void updateFolderCreated(ViewedFolder aFolder) { + TreePath path = createTreePath(aFolder.getParentFolder()); + if (fListeners != null && path != null) { + fListeners.nodeInserted(new TreeTableModelEvent(this, path, + new Object[] {aFolder})); + } + } + + void updateFolderDeleted(ViewedFolder aFolder) { + TreePath path = createTreePath(aFolder.getParentFolder()); + if (fListeners != null && path != null) { + fListeners.nodeDeleted(new TreeTableModelEvent(this, path, + new Object[] {aFolder})); + } + } + + class StoreObserver implements ViewedStoreListener { + public void folderCreated(ViewedStoreEvent aEvent) { + updateFolderCreated(aEvent.getFolder()); + } + + public void folderDeleted(ViewedStoreEvent aEvent) { + updateFolderDeleted(aEvent.getFolder()); + } + + public void folderChanged(ViewedStoreEvent aEvent) { + updateFolder(aEvent.getFolder()); + } + + public void storeNotification(ViewedStoreEvent aEvent) { + } + } +} + +// +// FolderCellRenderer Class +// + +class FolderCellRenderer extends DefaultCellRenderer { + Font fPlain; + Font fBold; + + public FolderCellRenderer() { + fPlain = Font.decode("SansSerif-12"); + fBold = Font.decode("SansSerif-bold-12"); + } + + public void paint(Graphics g) { + Font font = fPlain; + ViewedFolder f = null; + if (fObject instanceof ViewedStore) { + font = fBold; + } else if (fObject instanceof ViewedFolder) { + f = (ViewedFolder) fObject; + } + if (f != null) { + font = f.getUnreadMessageCount() == 0 ? fPlain : fBold; + } + setFont(font); + + super.paint(g); + } +} diff --git a/mozilla/grendel/ui/MasterPanelListener.java b/mozilla/grendel/ui/MasterPanelListener.java new file mode 100644 index 00000000000..c9b7b33e506 --- /dev/null +++ b/mozilla/grendel/ui/MasterPanelListener.java @@ -0,0 +1,44 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +/** + * Interface for information about changes in the MasterPanel + */ + +public interface MasterPanelListener extends EventListener { + /** + * Called when the selection changes. + */ + + public void masterSelectionChanged(ChangeEvent aEvent); + + /** + * Called when the user double clicks. + */ + + public void masterSelectionDoubleClicked(ChangeEvent aEvent); +} diff --git a/mozilla/grendel/ui/MenuLabels.properties b/mozilla/grendel/ui/MenuLabels.properties new file mode 100644 index 00000000000..9d166050695 --- /dev/null +++ b/mozilla/grendel/ui/MenuLabels.properties @@ -0,0 +1,192 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +# +# multipane menu labels +# + +multiFileLabel=File +multiFileAccel=F +multiEditLabel=Edit +multiEditAccel=E +multiViewLabel=View +multiViewAccel=V +multiMessageLabel=Message +multiMessageAccel=M + +# +# Master menu labels +# + +masterFileLabel=File +masterFileAccel=F +masterEditLabel=Edit +masterEditAccel=E + +# +# Folder menu labels +# + +folderFileLabel=File +folderFileAccel=F +folderEditLabel=Edit +folderEditAccel=E +folderMessageLabel=Message +folderMessageAccel=M + +# +# Message menu labels +# + +messageFileLabel=File +messageFileAccel=F +messageEditLabel=Edit +messageEditAccel=E +messageMessageLabel=Message +messageMessageAccel=M + +# +# View menu labels +# + +viewLayoutLabel=Layout +viewLayoutAccel=L +splitLeftLabel=Split Left +splitLeftAccel=L +splitRightLabel=Split Right +splitRightAccel=R +splitTopLabel=Split Top +splitTopAccel=T +stackedLabel=Stacked +stackedAccel=S +appShowTooltipsLabel=Show Tooltips +appShowTooltipsAccel=T + +# +# Common menu labels +# + +windowLabel=Window +windowAccel=W + +# +# Application menu item labels +# + +appExitLabel=Exit +appExitAccel=x +appPrefsLabel=Preferences... +appPrefsAccel=r +appSearchLabel=Search +appSearchAccel=S +appRunFiltersLabel=Run Filters on TestInbox +appRunFiltersAccel=F + +# +# Edit menu item labels +# + +editUndoLabel=Undo +editUndoAccel=U +editCutLabel=Cut +editCutAccel=t +editCopyLabel=Copy +editCopyAccel=C +editPasteLabel=Paste +editPasteAccel=P +editClearLabel=Delete +editClearAccel=D + +# +# Folder menu item labels +# + +folderOpenLabel=Open Folder +folderOpenAccel=O + +# +# Message menu item labels +# + +folderNewLabel=New Folder... +folderNewAccel=F +folderDeleteLabel=Delete Folder +folderDeleteAccel=D + +msgGetNewLabel=Get New Messages +msgGetNewAccel=G +msgOpenLabel=Open Message +msgOpenAccel=M +msgSaveAsLabel=Save As... +msgSaveAsAccel=A +msgNewLabel=New Message +msgNewAccel=N +msgReplyLabel=Reply +msgReplyAccel=R +msgReplyAllLabel=Reply All +msgReplyAllAccel=A +msgForwardLabel=Forward +msgForwardAccel=F +msgForwardQuotedLabel=Forward Quoted +msgForwardQuotedAccel=Q + +msgMoveLabel=File to +msgMoveAccel=F +msgCopyLabel=Copy to +msgCopyAccel=C +msgDeleteLabel=Delete Message +msgDeleteAccel=D + +msgMarkLabel=Mark +msgMarkAccel=M + +# +# Mark menu labels +# + +markMsgReadLabel=As Read +markMsgReadAccel=R +markThreadReadLabel=Thread Read +markThreadReadAccel=T +markAllReadLabel=All Read +markAllReadAccel=A + +# +# Sort menu labels +# + +viewSortLabel=Sort +viewSortAccel=S + +toggleThreadingLabel=Toggle Threading +toggleThreadingAccel=T +sortDateLabel=by Date +sortDateAccel=D +sortSubjectLabel=by Subject +sortSubjectAccel=S +sortAuthorLabel=by Author +sortAuthorAccel=A +sortNumberLabel=by Number +sortNumberAccel=N + +# +# Window menu item labels +# + +windowListLabel=Window List... +windowListAccel=W + diff --git a/mozilla/grendel/ui/Menus.properties b/mozilla/grendel/ui/Menus.properties new file mode 100644 index 00000000000..3d15049022c --- /dev/null +++ b/mozilla/grendel/ui/Menus.properties @@ -0,0 +1,106 @@ +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +# +# multipane menus +# +multiMain=multiFile multiEdit multiView multiMessage +multiFile=msgNew folderNew msgOpen msgSaveAs - msgGetNew - appExit +multiEdit=editUndo - editCut editCopy editPaste - folderDelete - appSearch - appPrefs - appRunFilters +multiView=viewLayout viewSort +multiMessage=msgNew - msgReply msgReplyAll msgForward msgForwardQuoted - msgMark + +multiToolBar=msgGetNew msgNew msgReply msgForward print msgDelete stop + +viewLayout=splitTop splitLeft splitRight stacked + +# +# Message Frame menus +# + +messageMain=messageFile messageEdit messageMessage window +messageFile=msgSaveAs - appExit +messageEdit=editUndo - editCut editCopy editPaste editClear - appPrefs +messageMessage=msgNew - msgReply msgReplyAll msgForward msgForwardQuoted + +messageToolBar=msgGetNew msgNew msgReply msgForward print msgDelete stop + +# +# Folder Frame menus +# + +folderMain=folderFile folderEdit folderMessage window +folderFile=msgNew msgOpen msgSaveAs - msgGetNew - appExit +folderEdit=editUndo - editCut editCopy editPaste editClear - appPrefs +folderMessage=msgNew - msgReply msgReplyAll msgForward msgForwardQuoted + +folderToolBar=msgGetNew msgNew msgReply msgForward markAllRead print msgDelete stop + +# +# Master Frame menus +# + +masterMain=masterFile masterEdit window +masterFile=msgNew folderOpen - msgGetNew - appExit +masterEdit=editUndo - editCut editCopy editPaste editClear - appPrefs + +masterToolBar=msgGetNew msgNew stop + +# +# Integrator Frame menus +# + +integratorMessage=msgGetNew - msgNew msgReply msgForward - msgDelete + +# +# Common menus +# + +viewSort=toggleThreading sortAuthor sortDate sortSubject sortNumber +window=windowList +msgMark=markMsgRead markThreadRead markAllRead + +# +# Application Shortcuts +# +appExitKeyboard=CTRL+F4 + +# +# Edit shortcuts +# +editUndoKeyboard=CTRL+z +editCutKeyboard=CTRL+x +editCopyKeyboard=CTRL+c +editPasteKeyboard=CTRL+v + +# +# Message Shortcuts +# +msgNewKeyboard=CTRL+m +msgGetNewKeyboard=CTRL+t +msgSaveAsKeyboard=CTRL+s +msgReplyKeyboard=CTRL+r +msgForwardKeyboard=CTRL+l +msgOpenKeyboard=CTRL+o + +# +# Icons +# +splitLeftImage=grendel/ui/images/splitLeft.gif +splitRightImage=grendel/ui/images/splitRight.gif +splitTopImage=grendel/ui/images/splitTop.gif +stackedImage=grendel/ui/images/stacked.gif diff --git a/mozilla/grendel/ui/MessageCellRenderer.java b/mozilla/grendel/ui/MessageCellRenderer.java new file mode 100644 index 00000000000..38942c0cead --- /dev/null +++ b/mozilla/grendel/ui/MessageCellRenderer.java @@ -0,0 +1,66 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 17 Dec 1997. + */ + +package grendel.ui; + +import java.awt.Font; +import java.awt.Graphics; + +import javax.mail.Message; +import javax.mail.MessagingException; + +import grendel.storage.MessageExtraFactory; +import grendel.view.ViewedMessage; +import grendel.widgets.DefaultCellRenderer; + +// +// MessageCellRenderer Class +// + +class MessageCellRenderer extends DefaultCellRenderer { + Font fPlain; + Font fBold; + Font fItalic; + + public MessageCellRenderer() { + fPlain = Font.decode("SansSerif-12"); + fBold = Font.decode("SansSerif-bold-12"); + fItalic = Font.decode("SansSerif-italic-12"); + } + + public void paint(Graphics g) { + Font font = fPlain; + if (fObject != null) { + Message m = ((ViewedMessage) fObject).getMessage(); + if (m == null) { + font = fItalic; + } else { + try { + font = MessageExtraFactory.Get(m).isRead() ? fPlain : fBold; + } catch (MessagingException e) { + } + } + } + setFont(font); + + super.paint(g); + } +} diff --git a/mozilla/grendel/ui/MessageDisplayManager.java b/mozilla/grendel/ui/MessageDisplayManager.java new file mode 100644 index 00000000000..b2e204c1ccf --- /dev/null +++ b/mozilla/grendel/ui/MessageDisplayManager.java @@ -0,0 +1,113 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import javax.mail.Folder; +import javax.mail.Message; + +/** + * Handles attempts to view messages, folders and their contents, + * without assuptions about the UI layout. + * Potential side affects for given actions are detailed. + * The resulting window state should be order independent if + * overlapping arguments are the same with the exception + * of window focus. That is,

+ * displayMaster(folderA);
+ * displayFolder(folderA, messageA);
+ * 
+ * should result in the same windows and selections as
+ * displayFolder(foldarA, messageA);
+ * displayMaster(folderA);
+ * 
+ * but the result of
+ * displayFolder(folderA, messageA);
+ * displayMaster(folderB):
+ * 
+ * is undefined. If multiple windows are opened as the result of + * multiple calls, focus will be shifted to the last opened + * window. Focus will always be shifted to the last acted upon + * view. + */ + +public abstract class MessageDisplayManager { + static volatile MessageDisplayManager fManager; + + static { + System.out.println("MessageDisplayManager."); + } + + /** + * Sets the default message display manager + */ + + public static synchronized void SetDefaultManager(MessageDisplayManager + aManager) { + fManager = aManager; + System.out.println("Setting manager: " + aManager); + } + + /** + * Gets the default message display manager + */ + + public static synchronized MessageDisplayManager GetDefaultManager() { + System.out.println("Getting manager: " + fManager); + return fManager; + } + + /** + * Displays a message given a Message object. This + * may affect displayed folders. + */ + + public abstract void displayMessage(Message aMessage); + + /** + * Displays a folder given a folder object. This may + * affect displayed messages. + */ + + public abstract void displayFolder(Folder aFolder); + + /** + * Displays folder given a Folder object and + * selects a message in that folder given a Message + * object. This may or may not also display the message, but + * it will not open multiple windows. + */ + + public abstract void displayFolder(Folder aFolder, Message aMessage); + + /** + * Displays the master (A folder tree, for now). This should not + * affect displayed folders or messages. + */ + + public abstract void displayMaster(); + + /** + * Displays the master with the given folder selected. This + * may affect displayed folders and messages. + */ + + public abstract void displayMaster(Folder aFolder); +} diff --git a/mozilla/grendel/ui/MessageFrame.java b/mozilla/grendel/ui/MessageFrame.java new file mode 100644 index 00000000000..6537a71180f --- /dev/null +++ b/mozilla/grendel/ui/MessageFrame.java @@ -0,0 +1,129 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 18 Nov 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.util.Enumeration; +import java.util.Vector; + +import javax.mail.Message; +import javax.mail.MessagingException; + +import netscape.orion.toolbars.ToolbarFactory; +import netscape.orion.toolbars.ToolBarLayout; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +public class MessageFrame extends GeneralFrame { + static Vector fMessageFrames = new Vector(); + MessagePanel fMessagePanel; + + /** + * Identifying String + */ + + public static final String kID = "mail.message"; + + /** + * Creates a MessageFrame displaying the given message. + */ + + public MessageFrame(Message aMessage) { + super("messageFrameLabel", "mail.message"); + + fMessagePanel = new MessagePanel(); + + fPanel.add(fMessagePanel); + fMenu = buildMenu("messageMain", Util.MergeActions(actions, + fMessagePanel.getActions())); + getRootPane().setMenuBar(fMenu); + + fToolBar = fMessagePanel.getToolBar(); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(ToolBarLayout.CreateSpring(), + null)); + fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(fAnimation, null)); + fToolBarPanel.add(fToolBar); + + fStatusBar = buildStatusBar(); + fPanel.add(BorderLayout.SOUTH, fStatusBar); + + setMessage(aMessage); + + restoreBounds(); + + fMessageFrames.addElement(this); + } + + public void dispose() { + saveBounds(); + fMessageFrames.removeElement(this); + + fMessagePanel.dispose(); + + super.dispose(); + } + + /** + * Returns the current message, which may not be fully loaded or + * displayed. + */ + + public Message getMessage() { + return fMessagePanel.getMessage(); + } + + /** + * Sets the current message, which will result in the message being + * loaded and displayed. + */ + + public void setMessage(Message aMessage) { + setTitle(fLabels.getString("messageFrameLabel")); + fMessagePanel.setMessage(aMessage); + if (aMessage != null) { + try { + setTitle(aMessage.getSubject() + + fLabels.getString("messageSuffixFrameLabel")); + } catch (MessagingException e) { + } + } + } + + /** + * Returns the frame displaying, or in the process of displaying, + * the given message. + */ + + public static MessageFrame FindMessageFrame(Message aMessage) { + Enumeration frames = fMessageFrames.elements(); + while (frames.hasMoreElements()) { + MessageFrame frame = (MessageFrame) frames.nextElement(); + if (frame.getMessage() == aMessage) { + return frame; + } + } + return null; + } + + IUICmd actions[] = { ActionFactory.GetExitAction(), + ActionFactory.GetComposeMessageAction() }; +} diff --git a/mozilla/grendel/ui/MessageHeader.java b/mozilla/grendel/ui/MessageHeader.java new file mode 100644 index 00000000000..a54f385f207 --- /dev/null +++ b/mozilla/grendel/ui/MessageHeader.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 20 Nov 1997. + */ + +package grendel.ui; + +import com.sun.java.swing.JComponent; + +import javax.mail.Message; + +public abstract class MessageHeader extends JComponent { + protected Message fMessage; + + public abstract void setMessage(Message aMessage); +} diff --git a/mozilla/grendel/ui/MessageListTransferable.java b/mozilla/grendel/ui/MessageListTransferable.java new file mode 100644 index 00000000000..53a12b8a392 --- /dev/null +++ b/mozilla/grendel/ui/MessageListTransferable.java @@ -0,0 +1,67 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 11 Nov 1997. + */ + +package grendel.ui; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.Vector; + +import javax.mail.Message; + +public class MessageListTransferable implements Transferable { + Vector fList; + + static DataFlavor fFlavors[] = { + new DataFlavor(MessageListTransferable.class, "Netscape Message List") + }; + + static DataFlavor GetDataFlavor() { + return fFlavors[0]; + } + + public MessageListTransferable(Vector aList) { + fList = aList; + } + + public Vector getMessageList() { + return fList; + } + + public Object getTransferData(DataFlavor aFlavor) + throws UnsupportedFlavorException, IOException { + if (aFlavor.equals(fFlavors[0])) { + return this; + } else { + throw new UnsupportedFlavorException(aFlavor); + } + } + + public DataFlavor[] getTransferDataFlavors() { + return fFlavors; + } + + public boolean isDataFlavorSupported(DataFlavor aFlavor) { + return aFlavor.equals(fFlavors[0]); + } +} diff --git a/mozilla/grendel/ui/MessageModel.java b/mozilla/grendel/ui/MessageModel.java new file mode 100644 index 00000000000..7aa01db4164 --- /dev/null +++ b/mozilla/grendel/ui/MessageModel.java @@ -0,0 +1,282 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 17 Dec 1997. + */ + +package grendel.ui; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; + +import javax.mail.Message; +import javax.mail.MessagingException; + +import calypso.util.Assert; + +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; +import grendel.view.FolderView; +import grendel.view.MessageSetViewObserver; +import grendel.view.ViewedMessage; +import grendel.widgets.TreePath; +import grendel.widgets.TreeTableDataModel; +import grendel.widgets.TreeTableModelBroadcaster; +import grendel.widgets.TreeTableModelEvent; +import grendel.widgets.TreeTableModelListener; + + +// +// MessageModel. Maps a FolderView into a TreeTableDataModel. +// + +class MessageModel implements TreeTableDataModel { + Hashtable fCollapsed = new Hashtable(); + FolderView fView = null; + TreeTableModelListener fListeners; + ViewObserver fViewObserver = new ViewObserver(); + + Icon fMessageIcon; + Icon fMessageReadIcon; + + public MessageModel() { + fMessageIcon = new ImageIcon(getClass().getResource("images/msg-small.gif")); + fMessageReadIcon = new ImageIcon(getClass().getResource("images/msgRead-small.gif")); + } + + public void setFolderView(FolderView aView) { + if (fView != null) { + fView.removeObserver(fViewObserver); + } + fView = aView; + if (fView != null) { + fView.addObserver(fViewObserver); + } + } + + public boolean showRoot() { + return true; + } + + // Navigation stuff + public Object getRoot() { + return fView.getMessageRoot(); + } + + public boolean isLeaf(Object aNode) { + ViewedMessage node = (ViewedMessage) aNode; + + return node.getChild() == null; + } + + public Enumeration getChildren(Object aNode) { + return null; + } + + public Object getChild(Object aNode) { + ViewedMessage node = (ViewedMessage) aNode; + + return node.getChild(); + } + + public Object getNextSibling(Object aNode) { + ViewedMessage node = (ViewedMessage) aNode; + + return node.getNext(); + } + + // Attributes + public void setCollapsed(TreePath aPath, boolean aCollapsed) { + TreeTableModelEvent event = + new TreeTableModelEvent(this, aPath); + + if (aCollapsed) { + if (fCollapsed.put(aPath, "x") == null) { + if (fListeners != null) { + fListeners.nodeCollapsed(event); + } + } + } else { + if (fCollapsed.remove(aPath) != null) { + if (fListeners != null) { + fListeners.nodeExpanded(event); + } + } + } + } + + public boolean isCollapsed(TreePath aPath) { + return fCollapsed.containsKey(aPath); + } + + public Object getData(Object aNode, Object aID) { + ViewedMessage node = (ViewedMessage) aNode; + + if (node.isDummy()) { + Message m = node.getChild().getMessage(); + if (aID == FolderPanel.kReadID) + return Boolean.TRUE; + else if (aID == FolderPanel.kSubjectID) { + return Util.GetSubject(m); + } + return ""; + } + + Message m = node.getMessage(); + Assert.Assertion(m != null); + + if (m == null) { + return ""; + } else if (aID == FolderPanel.kSubjectID) { + return Util.GetSubject(m); + } else { + MessageExtra mextra = MessageExtraFactory.Get(m); + Object result = null; + try { + if (aID == FolderPanel.kSenderID) { + result = mextra.getAuthor(); + } else if (aID == FolderPanel.kDateID) { + result = mextra.simplifiedDate(); + } else if (aID == FolderPanel.kReadID) { + result = mextra.isRead() ? Boolean.TRUE : Boolean.FALSE; + } else if (aID == FolderPanel.kFlagID) { + result = mextra.isFlagged() ? Boolean.TRUE : Boolean.FALSE; + } else if (aID == FolderPanel.kDeletedID) { + result = mextra.isDeleted() ? Boolean.TRUE : Boolean.FALSE; + } else { + throw new Error("unknown column"); + } + } catch (MessagingException e) { + result = "???"; // ### Is this reasonable? + } + if (result == null) result = ""; + return result; + } + } + + public void setData(Object aNode, Object aID, Object aValue) { + ViewedMessage node = (ViewedMessage) aNode; + Message m = node.getMessage(); + if (m != null) { + MessageExtra mextra = MessageExtraFactory.Get(m); + boolean b = ((Boolean) aValue).booleanValue(); + + try { + if (aID == FolderPanel.kReadID) { + System.out.println("marking " + Util.GetSubject(m) + " read " + b); + mextra.setIsRead(b); + } else if (aID == FolderPanel.kFlagID) { + mextra.setFlagged(b); + } else if (aID == FolderPanel.kDeletedID) { + mextra.setDeleted(b); + } + } catch (MessagingException e) { + // ### Shouldn't we report the error to the user or something? + } + } + } + + public Icon getIcon(Object aNode) { + ViewedMessage node = (ViewedMessage) aNode; + Message m = node.getMessage(); + if (m == null) { + return fMessageIcon; + } else { + try { + if (MessageExtraFactory.Get(m).isRead()) { + return fMessageReadIcon; + } + } catch (MessagingException e) { + } + return fMessageIcon; + } + } + + public Icon getOverlayIcon(Object aNode) { + return null; + } + + public void addTreeTableModelListener(TreeTableModelListener aListener) { + fListeners = TreeTableModelBroadcaster.add(fListeners, aListener); + } + + public void removeTreeTableModelListener(TreeTableModelListener aListener) { + fListeners = TreeTableModelBroadcaster.remove(fListeners, aListener); + } + + TreePath createTreePath(ViewedMessage aNode) { + Vector pathVector = new Vector(); + + while (aNode != null) { + pathVector.insertElementAt(aNode, 0); + aNode = aNode.getParent(); + } + + return new TreePath(pathVector); + } + + class ViewObserver implements MessageSetViewObserver { + public void messagesChanged(Enumeration inserted, + Enumeration deleted, + Enumeration changed) { + if (fListeners != null) { + Object children[] = new Object[1]; + ViewedMessage node; + TreePath path; + + if (inserted != null) { + System.out.println("Inserted messages"); + while (inserted.hasMoreElements()) { + node = (ViewedMessage) inserted.nextElement(); + + path = createTreePath(node.getParent()); + children[0] = node; + + fListeners.nodeInserted(new TreeTableModelEvent(this, path, children)); + } + } + if (deleted != null) { + System.out.println("Deleted messages"); + while (deleted.hasMoreElements()) { + node = (ViewedMessage) deleted.nextElement(); + + path = createTreePath(node.getParent()); + children[0] = node; + + fListeners.nodeDeleted(new TreeTableModelEvent(this, path, children)); + } + } + if (changed != null) { + while (changed.hasMoreElements()) { + node = (ViewedMessage) changed.nextElement(); + + Message m = node.getMessage(); + System.out.println("'" + Util.GetSubject(m) + "' changed"); + + path = createTreePath(node); + fListeners.nodeChanged(new TreeTableModelEvent(this, path, null)); + } + } + } + } + } +} diff --git a/mozilla/grendel/ui/MessagePanel.java b/mozilla/grendel/ui/MessagePanel.java new file mode 100644 index 00000000000..c3154b7bc3e --- /dev/null +++ b/mozilla/grendel/ui/MessagePanel.java @@ -0,0 +1,440 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 3 Sep 1997. + */ + +package grendel.ui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.PrintStream; +import java.io.StringBufferInputStream; +import java.net.URL; +import java.net.MalformedURLException; +import java.util.Locale; +import java.util.ResourceBundle; + +import com.sun.java.swing.Action; +import com.sun.java.swing.AbstractAction; +import com.sun.java.swing.BorderFactory; +import com.sun.java.swing.JPanel; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.JTextArea; +import com.sun.java.swing.UIManager; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.EventListenerList; +import com.sun.java.swing.text.Document; +import com.sun.java.swing.text.BadLocationException; + +import netscape.orion.toolbars.NSToolbar; + +import javax.mail.Message; +import javax.mail.MessagingException; + +import grendel.mime.IMimeParser; +import grendel.mime.IMimeOperator; +import grendel.mime.parser.MimeParserFactory; +import grendel.mime.html.MimeHTMLOperatorFactory; +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; +import grendel.widgets.StatusEvent; + +/* #### +import calypso.net.URLSource; +*/ + +import calypso.util.ByteBuf; +import calypso.util.Preferences; +import calypso.util.PreferencesFactory; + +import netscape.orion.uimanager.IUICmd; + +import lego.document.HTMLDocument; + +import mg.edge.embed.jfc.URLComponent; +import mg.edge.embed.jfc.URLComponentFactory; +import mg.magellan.document.IDocument; + +public class MessagePanel extends GeneralPanel { + JTextArea fTextArea; + URLComponent fViewer; + Thread fMessageLoadThread; + Message fMessage; + MessageHeader fHeader; + MessagePanel fPanel; + boolean useMagellan; + boolean makeRealHTML; + + EventListenerList fListeners = new EventListenerList(); + + IUICmd fActions[] = {ActionFactory.GetNewMailAction(), + ActionFactory.GetComposeMessageAction()}; + /** + * Constructs a new message panel. + */ + + public MessagePanel() { + fPanel = this; + + Preferences prefs = PreferencesFactory.Get(); + useMagellan = prefs.getBoolean("usemagellan", true); + makeRealHTML = prefs.getBoolean("makerealhtml", true); + + if (useMagellan) { + fViewer = (URLComponent) URLComponentFactory.NewURLComponent(null); + Component viewComponent = fViewer.getComponent(); + add(viewComponent); + } else { + fTextArea = new JTextArea(); + fTextArea.setEditable(false); + fTextArea.setBorder(BorderFactory.createLoweredBevelBorder()); + add(new JScrollPane(fTextArea)); + } + + fHeader = new SimpleMessageHeader(); + add(BorderLayout.NORTH, fHeader); + } + + /** + * Disposes of message panel resources. Currently kills off + * message loading thread to avoid having magellan try and + * access an invalid peer. + */ + + public void dispose() { + synchronized (fPanel) { + if (fMessageLoadThread != null) { + System.out.println("Killing msg thread"); + fMessageLoadThread.stop(); + fMessageLoadThread = null; + } + } + } + + /** + * Returns the toolbar associated with this panel. + */ + + public NSToolbar getToolBar() { + return buildToolBar("messageToolBar", fActions); + } + + /** + * Sets message displayed to the given message. If the argument is + * null, clears message area + */ + + public synchronized void setMessage(Message aMessage) { + if (fMessageLoadThread != null) { + System.out.println("Killing msg thread"); + fMessageLoadThread.stop(); + notifyStatus(fLabels.getString("messageLoadedStatus")); + } + fMessageLoadThread = + new Thread(new LoadMessageThread(aMessage), "msgLoad"); + System.out.println("Starting msg thread"); + fMessageLoadThread.start(); + } + + /** + * Returns the current message + */ + + public Message getMessage() { + return fMessage; + } + + public void addMessagePanelListener(MessagePanelListener l) { + fListeners.add(MessagePanelListener.class, l); + } + + public void removeMessagePanelListener(MessagePanelListener l) { + fListeners.remove(MessagePanelListener.class, l); + } + + protected void notifyLoading() { + Object[] listeners = fListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == MessagePanelListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((MessagePanelListener)listeners[i+1]).loadingMessage(changeEvent); + } + } + } + + protected void notifyLoaded() { + Object[] listeners = fListeners.getListenerList(); + ChangeEvent changeEvent = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == MessagePanelListener.class) { + // Lazily create the event: + if (changeEvent == null) + changeEvent = new ChangeEvent(this); + ((MessagePanelListener)listeners[i+1]).messageLoaded(changeEvent); + } + } + } + + protected void notifyStatus(String aStatus) { + Object[] listeners = fListeners.getListenerList(); + StatusEvent event = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + if (listeners[i] == MessagePanelListener.class) { + // Lazily create the event: + if (event == null) + event = new StatusEvent(this, aStatus); + ((MessagePanelListener)listeners[i+1]).messageStatus(event); + } + } + } + + public boolean isOpaque() { + return true; + } + + class LoadMessageThread implements Runnable { + Message fMessage; + + LoadMessageThread(Message aMessage) { + fMessage = aMessage; + } + + public void run() { + synchronized (fPanel) { + if (fPanel.fMessage == fMessage) { + fMessageLoadThread = null; + return; // save ourselves some work + } + fPanel.fMessage = fMessage; + } + MessageExtra mextra = + fMessage != null ? MessageExtraFactory.Get(fMessage) : null; + notifyLoading(); + notifyStatus(fLabels.getString("messageLoadingStatus")); + + fHeader.setMessage(fMessage); + + try { + if (!useMagellan) { + synchronized (fTextArea) { + if (fMessage != null) { + fTextArea.setText(""); + try { + InputStream stream = mextra.getInputStreamWithHeaders(); + if (makeRealHTML) { + stream = new MakeItHTML(stream).getHTMLInputStream(); + } + InputStreamReader reader = new InputStreamReader(stream); + char buff[] = new char[4096]; + int count; + while ((count = reader.read(buff, 0, 4096)) != -1) { + fTextArea.append(new String(buff, 0, count)); + } + } catch (MessagingException me) { + fTextArea.setText(me.toString()); + me.printStackTrace(); + } catch (IOException e) { + fTextArea.setText(e.toString()); + e.printStackTrace(); + } + } else { + fTextArea.setText("This space intentionally left blank"); + } + } + } else { + InputStream in = null; + if (fMessage != null) { + InputStream rawin; + try { + rawin = mextra.getInputStreamWithHeaders(); + if (makeRealHTML) { + in = (new MakeItHTML(rawin)).getHTMLInputStream(); + } else { + in = new StupidHackToMakeHTML(rawin); + } + } catch (MessagingException me) { + throw new Error("Can't get the input stream??? " + me); + } catch (IOException e) { + throw new Error("Can't get the input stream??? " + e); + } + } else { + // Love them deprecated classes. Oh, well, this is just a temp hack. + in = new StringBufferInputStream("This space intentionally left blank"); + } + + synchronized (fViewer) { +/* #### + URL url = new URL("http://home.netscape.com/"); + URLSource source = new URLSource(url, null, in); + IDocument doc = new HTMLDocument(source); + fViewer.goTo(doc); +*/ + } + } + } catch (Exception e) { + notifyStatus(e.toString()); + e.printStackTrace(); + fMessage = null; + } catch (Error e) { + notifyStatus(e.toString()); + e.printStackTrace(); + fMessage = null; + } + + if (fMessage != null) { + try { + mextra.setIsRead(true); // ### Probably shouldn't do it + // until we know we got some data... + } catch (MessagingException e) { + // ### Do anything here? For now, we drop it... + } + notifyStatus(fLabels.getString("messageLoadedStatus")); + } + + synchronized (fPanel) { + fMessageLoadThread = null; + } + notifyLoaded(); + } + } +} + +/** Debugging hack. */ +class TeeInputStream extends InputStream { + InputStream real; + OutputStream out; + TeeInputStream(InputStream r, OutputStream o) { + real = r; + out = o; + } + public int read() throws IOException { + int c = real.read(); + out.write((byte) c); + return c; + } +} + + +class MakeItHTML implements Runnable { + protected PipedInputStream inpipe; + protected PipedOutputStream outpipe; + protected InputStream rawin; + + MakeItHTML(InputStream in) throws IOException { + rawin = in; + inpipe = new PipedInputStream(); + outpipe = new PipedOutputStream(inpipe); + Thread t = new Thread(this); + t.start(); + } + + InputStream getHTMLInputStream() { + if (false) { + return new TeeInputStream(inpipe, System.out); + } else if (false) { + try { + FileOutputStream out = new FileOutputStream("dump.html"); + return new TeeInputStream(inpipe, out); + } catch (IOException e) { + e.printStackTrace(); + } + } + return inpipe; + } + + public void run() { + IMimeParser parser = MimeParserFactory.Make("message/rfc822"); + IMimeOperator op = MimeHTMLOperatorFactory.Make(parser.getObject(), + new PrintStream(outpipe) + /*System.out*/); + + parser.setOperator(op); + + byte[] bytes = new byte[1024]; + ByteBuf buf = new ByteBuf(bytes.length); + int bread; + do { + try { + bread = rawin.read(bytes, 0, bytes.length); + } catch (IOException e) { + break; + } + if (bread > 0) { + buf.setLength(0); + buf.append(bytes, 0, bread); // this sucks! + parser.pushBytes(buf); + } + } while (bread >= 0); + bytes = null; + buf = null; + parser.pushEOF(); + try { + outpipe.close(); + } catch (IOException e) { + } + } +} + + + + +class StupidHackToMakeHTML extends InputStream { + InputStream fRaw; + ByteBuf buffer = new ByteBuf(); + + StupidHackToMakeHTML(InputStream raw) { + fRaw = raw; + buffer.append("
");
+  }
+
+
+  public int read() throws IOException {
+    int result;
+    if (buffer.length() > 0) {
+      result = buffer.byteAt(0);
+      buffer.remove(0, 1);
+    } else {
+      result = fRaw.read();
+      if (result == '<') {
+        result = '&';
+        buffer.append("lt;");
+      } else if (result == '>') {
+        result = '&';
+        buffer.append("gt;");
+      } else if (result == '&') {
+        buffer.append("amp;");
+      } else if (result == '\n') {
+        result = '<';
+        buffer.append("BR>");
+      }
+    }
+    return result;
+  }
+}
diff --git a/mozilla/grendel/ui/MessagePanelListener.java b/mozilla/grendel/ui/MessagePanelListener.java
new file mode 100644
index 00000000000..bc01be33dfd
--- /dev/null
+++ b/mozilla/grendel/ui/MessagePanelListener.java
@@ -0,0 +1,34 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 23 Oct 1997.
+ */
+
+package grendel.ui;
+
+import java.util.EventListener;
+
+import com.sun.java.swing.event.ChangeEvent;
+
+import grendel.widgets.StatusEvent;
+
+public interface MessagePanelListener extends EventListener {
+  public void loadingMessage(ChangeEvent aEvent);
+  public void messageLoaded(ChangeEvent aEvent);
+  public void messageStatus(StatusEvent aEvent);
+}
diff --git a/mozilla/grendel/ui/MultiMessageDisplayManager.java b/mozilla/grendel/ui/MultiMessageDisplayManager.java
new file mode 100644
index 00000000000..61e6f85fd08
--- /dev/null
+++ b/mozilla/grendel/ui/MultiMessageDisplayManager.java
@@ -0,0 +1,199 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin ,  3 Sep 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.BorderLayout;
+import java.util.Enumeration;
+import java.util.Vector;
+
+import com.sun.java.swing.event.ChangeEvent;
+
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Store;
+
+import netscape.orion.uimanager.IUICmd;
+
+import grendel.view.ViewedFolder;
+import grendel.view.ViewedMessage;
+import grendel.widgets.TreePath;
+
+public class MultiMessageDisplayManager extends MessageDisplayManager {
+  MasterFrame     fMasterFrame;
+
+  static MultiMessageDisplayManager fDisplayMaster = null;
+
+  public static MultiMessageDisplayManager Get() {
+    if (fDisplayMaster == null) {
+      fDisplayMaster = new MultiMessageDisplayManager();
+    }
+    return fDisplayMaster;
+  }
+
+  /**
+   * Displays a message given a Message object.
+   */
+
+  public void displayMessage(Message aMessage) {
+    MessageFrame frame = MessageFrame.FindMessageFrame(aMessage);
+    if (frame == null) {
+      frame = new MessageFrame(null);
+    }
+    frame.setVisible(true);
+    frame.toFront();
+    frame.requestFocus();
+    frame.setMessage(aMessage);
+  }
+
+  /**
+   * Displays a folder given a folder object.
+   */
+
+  public void displayFolder(Folder aFolder) {
+    displayFolder(aFolder, null);
+  }
+
+  /**
+   * Displays folder given a Folder object and
+   * selects a message in that folder given a Message
+   * object.
+   */
+
+  public void displayFolder(Folder aFolder, Message aMessage) {
+    FolderFrame frame = FolderFrame.FindFolderFrame(aFolder);
+    if (frame == null) {
+      frame = new FolderFrame(aFolder);
+      frame.setVisible(true);
+    }
+    frame.toFront();
+    frame.requestFocus();
+
+    if (aMessage != null) {
+    }
+  }
+
+  /**
+   * Displays the master.
+   */
+
+  public void displayMaster() {
+    displayMaster(null);
+  }
+
+  /**
+   * Displays the master with the given folder selected.
+   */
+
+  public void displayMaster(Folder aFolder) {
+    if (fMasterFrame == null) {
+      fMasterFrame = new MasterFrame();
+    }
+    fMasterFrame.setVisible(true);
+    fMasterFrame.toFront();
+    fMasterFrame.requestFocus();
+
+    if (aFolder != null) {
+
+    }
+  }
+}
+
+class MasterFrame extends GeneralFrame {
+  MasterPanel fMasterPanel;
+
+  public MasterFrame() {
+    super("masterFrameLabel", "mail.session");
+
+    PrefsDialog.CheckPrefs(this);
+
+    fMasterPanel = new MasterPanel();
+    fMasterPanel.addMasterPanelListener(new FolderSelectionListener());
+    fPanel.add(fMasterPanel);
+    fMenu = buildMenu("masterMain", actions);
+    getRootPane().setMenuBar(fMenu);
+
+    fToolBar = fMasterPanel.getToolBar();
+    fToolBarPanel.add(fToolBar);
+
+    fStatusBar = buildStatusBar();
+    fPanel.add(BorderLayout.SOUTH, fStatusBar);
+
+    restoreBounds();
+  }
+
+  public void dispose() {
+    saveBounds();
+
+    fMasterPanel.dispose();
+
+    super.dispose();
+  }
+
+  //
+  // FolderSelectionListener class
+  //
+
+  class FolderSelectionListener implements MasterPanelListener {
+    public void masterSelectionChanged(ChangeEvent aEvent) {
+    }
+
+    public void masterSelectionDoubleClicked(ChangeEvent aEvent) {
+      TreePath path = null;
+      Enumeration selection = ((MasterPanel) aEvent.getSource()).getSelection();
+
+      MessageDisplayManager master = MultiMessageDisplayManager.Get();
+
+      while (selection.hasMoreElements()) {
+        path = (TreePath) selection.nextElement();
+
+        if (path != null) {
+          Object node = path.getTip();
+          Folder folder = null;
+
+          if (node instanceof ViewedFolder) {
+            folder = ((ViewedFolder) node).getFolder();
+          }
+          if (folder != null) {
+            try {
+              if ((folder.getType() & Folder.HOLDS_MESSAGES) == 0) {
+                folder = null;
+              }
+            } catch (MessagingException e) {
+              folder = null;
+            }
+          }
+
+          if (folder != null) {
+            master.displayFolder(folder);
+          }
+        }
+      }
+    }
+  }
+
+  // Action array
+
+  IUICmd actions[] = { ActionFactory.GetExitAction(),
+                       ActionFactory.GetNewMailAction(),
+                       ActionFactory.GetComposeMessageAction()};
+}
diff --git a/mozilla/grendel/ui/NewFolderDialog.java b/mozilla/grendel/ui/NewFolderDialog.java
new file mode 100644
index 00000000000..40303eef719
--- /dev/null
+++ b/mozilla/grendel/ui/NewFolderDialog.java
@@ -0,0 +1,179 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 20 Nov 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.Hashtable;
+
+import javax.mail.Folder;
+import javax.mail.MessagingException;
+
+import com.sun.java.swing.JOptionPane;
+
+import netscape.orion.dialogs.AttrNotFoundException;
+import netscape.orion.dialogs.PageModel;
+import netscape.orion.dialogs.PageUI;
+
+import xml.tree.XMLNode;
+import xml.tree.TreeBuilder;
+
+import grendel.view.ViewedFolder;
+
+public class NewFolderDialog extends GeneralDialog {
+  PageUI     fPanel;
+
+  ViewedFolder fFolder;
+  FolderCombo fParentCombo;
+
+  Hashtable fValues;
+
+  private static final String kFolderComboKey = "folderCombo";
+  private static final String kNameKey = "nameField";
+
+  class NewFolderModel extends PageModel {
+    public NewFolderModel(FolderCombo aCombo) {
+      fValues = new Hashtable();
+      fValues.put(kNameKey, "");
+      fValues.put(kFolderComboKey, aCombo);
+    }
+
+    public Object getAttribute(String aAttrib) throws AttrNotFoundException {
+      Object res = fValues.get(aAttrib);
+      if (res == null) {
+        res = fLabels.getString(aAttrib);
+      }
+      if (res == null) {
+        throw new AttrNotFoundException(aAttrib);
+      }
+      return res;
+    }
+
+    public void setAttribute(String aAttrib, Object aValue) {
+      if (fValues.containsKey(aAttrib)) {
+        fValues.put(aAttrib, aValue);
+      }
+    }
+
+    public void actionPerformed(ActionEvent aEvent) {
+    }
+  }
+
+
+  public NewFolderDialog(Frame aParent, ViewedFolder aFolder) {
+    super(aParent);
+
+    fParentCombo = new FolderCombo();
+    fParentCombo.populate(Folder.HOLDS_FOLDERS, 0);
+    fParentCombo.setSelectedItem(fFolder);
+
+    setModal(true);
+    NewFolderModel model = new NewFolderModel(fParentCombo);
+
+    // use the XML parser to get the root XML node of the resource tree
+    XMLNode root = null;
+    URL url = getClass().getResource("dialogs.xml");
+    try {
+      root = xml.tree.TreeBuilder.build(url, getClass());
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+    XMLNode editHost = root.getChild("dialog", "id", "newFolderDialog");
+
+    fPanel = new PageUI(url, editHost, model);
+
+    JOptionPane actionPanel = new JOptionPane(fPanel,
+                                              JOptionPane.PLAIN_MESSAGE,
+                                              JOptionPane.OK_CANCEL_OPTION);
+    actionPanel.addPropertyChangeListener(new OptionListener());
+    add(actionPanel);
+
+    // XXX This is a stupid hack because PageUI doesn't to a resource lookup
+    // on it's title. Bleh.
+    String title = fPanel.getTitle();
+    if (title.charAt(0) == '$') {
+      try {
+        title = (String) model.getAttribute(title.substring(1));
+      } catch (AttrNotFoundException e) {}
+    }
+    setTitle(title);
+
+    Dimension size = getPreferredSize();
+    Dimension screenSize = getToolkit().getScreenSize();
+    setBounds((screenSize.width - size.width - 10) / 2,
+              (screenSize.height - size.height - 32) / 2,
+              size.width + 10, size.height + 32);
+    pack();
+    setVisible(true);
+    requestFocus();
+  }
+
+  public NewFolderDialog(Frame aParent) {
+    this(aParent, null);
+  }
+
+  boolean createFolder() {
+    try {
+      String newName = (String) fValues.get(kNameKey);
+      ViewedFolder parent = fParentCombo.getSelectedFolder();
+      Folder newFolder = parent.getFolder().getFolder(newName);
+
+      if (newFolder.exists()) {
+        Object args[] = {newName};
+        String err =
+          MessageFormat.format(fLabels.getString("folderExistsError"),
+                               args);
+        JOptionPane.showMessageDialog(null, err,
+                                      fLabels.getString("folderCreateError"),
+                                      JOptionPane.ERROR_MESSAGE);
+      } else {
+        if (newFolder.create(Folder.HOLDS_FOLDERS|Folder.HOLDS_MESSAGES)) {
+          return true;
+        }
+      }
+    } catch (MessagingException e) {
+      System.out.println("Create Folder: " + e);
+    }
+    return false;
+  }
+
+  class OptionListener implements PropertyChangeListener {
+    public void propertyChange(PropertyChangeEvent aEvent) {
+      if (aEvent.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)){
+        int value = ((Integer) aEvent.getNewValue()).intValue();
+
+        if (value == JOptionPane.OK_OPTION) {
+          fPanel.saveAll();
+          setVisible(!createFolder());
+        } else {
+          setVisible(false);
+        }
+      }
+    }
+  }
+}
diff --git a/mozilla/grendel/ui/PrefsDialog.java b/mozilla/grendel/ui/PrefsDialog.java
new file mode 100644
index 00000000000..1733bf94142
--- /dev/null
+++ b/mozilla/grendel/ui/PrefsDialog.java
@@ -0,0 +1,63 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ */
+
+package grendel.ui;
+
+import com.sun.java.swing.JFrame;
+
+import calypso.util.Preferences;
+import calypso.util.PreferencesFactory;
+
+import netscape.orion.propeditor.PropertyEditorDlg;
+
+import grendel.prefs.Prefs;
+
+public class PrefsDialog {
+  static String sPrefNames[] = {"mail.email_address",
+                                "mail.host",
+                                "mail.user",
+                                "mail.directory",
+                                "smtp.host"};
+
+  public static void CheckPrefs(JFrame aParent) {
+    if (!ValidPrefs()) {
+      EditPrefs(aParent);
+    }
+  }
+
+  public static void EditPrefs(JFrame aParent) {
+    Object objs[] = {new Prefs()};
+    PropertyEditorDlg.Edit(aParent, objs, false, true, "",
+                           PropertyEditorDlg.UI_TREE);
+
+  }
+
+  public static boolean ValidPrefs() {
+    Preferences prefs = PreferencesFactory.Get();
+
+    boolean res = true;
+    /*
+    for (int i = 0; i < sPrefNames.length; i++) {
+      res &= !prefs.getString(sPrefNames[i], "").equals("");
+    }
+    */
+    return res;
+  }
+}
+
diff --git a/mozilla/grendel/ui/ProgressFactory.java b/mozilla/grendel/ui/ProgressFactory.java
new file mode 100644
index 00000000000..e2bb007cd5f
--- /dev/null
+++ b/mozilla/grendel/ui/ProgressFactory.java
@@ -0,0 +1,201 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 17 Nov 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Frame;
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+import java.util.Vector;
+
+import javax.mail.Flags;
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+
+public class ProgressFactory {
+  static Frame fNewMailFrame = null;
+
+  public static synchronized Frame NewMailProgress() {
+    if (fNewMailFrame == null) {
+      fNewMailFrame = new NewMailProgress();
+    } else {
+      fNewMailFrame.toFront();
+    }
+    return fNewMailFrame;
+  }
+
+  public static Frame CopyMessagesProgress(Vector aMessages, Folder aDest) {
+    return new CopyMessageProgress(aMessages, aDest);
+  }
+
+  public static Frame MoveMessagesProgress(Vector aMessages, Folder aDest) {
+    return new MoveMessageProgress(aMessages, aDest);
+  }
+
+  public static Frame DeleteMessagesProgress(Vector aMessages) {
+    return new DeleteMessageProgress(aMessages);
+  }
+}
+
+class NewMailProgress extends ProgressFrame {
+  public NewMailProgress() {
+    super("newMailProgressLabel");
+
+    start();
+  }
+
+  public void progressLoop() {
+    // ### Stubbing out all maildrop stuff for now...
+    //      Enumeration maildrops = MasterFactory.Get().getMailDrops();
+    //      while (maildrops.hasMoreElements() && !isCanceled()) {
+    //        MailDrop maildrop = (MailDrop) maildrops.nextElement();
+    //        Object args[] = {};
+    //        setStatus(MessageFormat.format(fLabels.getString("newMailStatus"),
+    //                                       args));
+    //        try {
+    //          maildrop.fetchNewMail();
+    //        } catch (IOException e) {
+    //          System.err.println("NewMailAction: " + e);
+    //        }
+    //      }
+  }
+}
+
+//
+// CopyMessageProgress class
+//
+
+class CopyMessageProgress extends ProgressFrame {
+  Vector fMessages;
+  Folder fDest;
+
+  CopyMessageProgress(Vector aMessages, Folder aDest) {
+    super("copyingMessageLabel");
+
+    fMessages = aMessages;
+    fDest = aDest;
+
+    setMax(aMessages.size());
+
+    start();
+  }
+
+  public void progressLoop() {
+    int idx = 0;
+
+    while (idx < fMessages.size()  && !isCanceled()) {
+      Message message = (Message) fMessages.elementAt(idx);
+      Object args[] = {Util.GetSubject(message),
+                       fDest.getName()};
+      setStatus(MessageFormat.format(fLabels.getString("copyingMessageStatus"),
+                                     args));
+      try {
+        Message mlist[] = { message };
+        fDest.appendMessages(mlist);
+      } catch (MessagingException e) {
+        System.err.println("CopyMessageProgress: " + e);
+      }
+      idx++;
+      setProgress(idx);
+    }
+  }
+}
+
+  //
+  // MoveMessageProgress class
+  //
+
+class MoveMessageProgress extends ProgressFrame {
+  Vector fMessages;
+  Folder fDest;
+
+  MoveMessageProgress(Vector aMessages, Folder aDest) {
+    super("movingMessageLabel");
+
+    fMessages = aMessages;
+    fDest = aDest;
+
+    setMax(aMessages.size());
+
+    start();
+    }
+
+  public void progressLoop() {
+    int idx = 0;
+
+    while (idx < fMessages.size() && !isCanceled()) {
+      Message message = (Message) fMessages.elementAt(idx);
+      Object args[] = {Util.GetSubject(message),
+                       fDest.getName()};
+      setStatus(MessageFormat.format(fLabels.getString("movingMessageStatus"),
+                                     args));
+      try {
+        Message mlist[] = { message };
+        fDest.appendMessages(mlist);
+        message.setFlags(Flags.DeletedFlag, true);
+      } catch (MessagingException e) {
+        System.err.println("DeleteMessageProgress: " + e);
+      }
+      idx++;
+      setProgress(idx);
+    }
+  }
+}
+
+  //
+  // DeleteMessageProgress class
+  //
+
+class DeleteMessageProgress extends ProgressFrame {
+  Vector fMessages;
+
+  DeleteMessageProgress(Vector aMessages) {
+    super("deletingMessageLabel");
+
+    fMessages = aMessages;
+    setMax(aMessages.size());
+
+    start();
+  }
+
+  public void progressLoop() {
+    int idx = 0;
+
+    while (idx < fMessages.size() && !isCanceled()) {
+      Message message = (Message) fMessages.elementAt(idx);
+      Folder source = message.getFolder();
+
+      Object args[] = {Util.GetSubject(message)};
+
+     setStatus(MessageFormat.format(fLabels.getString("deletingMessageStatus"),
+                                     args));
+      // source.deleteMessage(message);
+      try {
+        Thread.sleep(500);
+      } catch (InterruptedException e) {
+      }
+      idx++;
+      setProgress(idx);
+    }
+  }
+}
+
diff --git a/mozilla/grendel/ui/ProgressFrame.java b/mozilla/grendel/ui/ProgressFrame.java
new file mode 100644
index 00000000000..c592fb2f6c6
--- /dev/null
+++ b/mozilla/grendel/ui/ProgressFrame.java
@@ -0,0 +1,171 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 26 Sep 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Color;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Panel;
+import java.awt.event.ActionEvent;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import com.sun.java.swing.AbstractAction;
+import com.sun.java.swing.Box;
+import com.sun.java.swing.JButton;
+import com.sun.java.swing.JFrame;
+import com.sun.java.swing.JLabel;
+import com.sun.java.swing.JProgressBar;
+import com.sun.java.swing.UIManager;
+
+public abstract class ProgressFrame extends GeneralFrame implements Runnable {
+  int             fMax;
+  JProgressBar    fMeter;
+  JButton         fCancel;
+  JLabel          fStatus;
+  Thread          fThread;
+  boolean         fDone;
+  boolean         fCanceled;
+  boolean         fDisposed;
+  Container       fContentPanel;
+
+  public ProgressFrame(String aCaption) {
+    super("", "mail.progress");
+
+    setTitle(fLabels.getString(aCaption));
+    setBackground(UIManager.getColor("control"));
+
+    fContentPanel = new Panel();
+    getContentPane().add(fContentPanel);
+
+    GridBagLayout gridbag = new GridBagLayout();
+    GridBagConstraints c = new GridBagConstraints();
+
+    fContentPanel.setLayout(gridbag);
+
+    fStatus = new JLabel("Status");
+    fMeter = new JProgressBar();
+    fMeter.setMinimum(0);
+    fMeter.setMaximum(1);
+    fMeter.setValue(0);
+    fCancel = new JButton("Cancel");
+    fCancel.addActionListener(new AbstractAction() {
+      public void actionPerformed(ActionEvent aEvent) {
+        setCanceled(true);
+      }
+    });
+
+    c.weightx = 1;
+    c.weighty = 1;
+    c.gridheight = 1;
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    c.insets = new Insets(5,5,5,5);
+    gridbag.setConstraints(fStatus, c);
+    fContentPanel.add(fStatus);
+
+    c.fill = GridBagConstraints.HORIZONTAL;
+    c.gridwidth = GridBagConstraints.REMAINDER;
+    c.insets = new Insets(0,5,0,5);
+    gridbag.setConstraints(fMeter, c);
+    fContentPanel.add(fMeter);
+
+    c.fill = GridBagConstraints.NONE;
+    c.gridwidth = 1;
+    c.insets = new Insets(5,5,5,5);
+    gridbag.setConstraints(fCancel, c);
+    fContentPanel.add(fCancel);
+
+    // Dimension size = getPreferredSize();
+    restoreBounds(320, 200);
+    setVisible(true);
+  }
+
+  public synchronized void dispose() {
+    if (!fDisposed) {
+      super.dispose();
+
+      fDisposed = true;
+
+      setCanceled(true);
+    }
+  }
+
+  public synchronized void setMax(int aMax) {
+    fMax = aMax;
+    fMeter.setMaximum(aMax);
+  }
+
+  public synchronized int getMax() {
+    return fMax;
+  }
+
+  public synchronized void setProgress(int aProgress) {
+    fMeter.setValue(aProgress);
+  }
+
+  public synchronized int getProgress() {
+    return fMeter.getValue();
+  }
+
+  public synchronized void setStatus(String aStatus) {
+    fStatus.setText(aStatus);
+  }
+
+  public synchronized String getStatus() {
+    return fStatus.getText();
+  }
+
+  public synchronized void setDone(boolean aDone) {
+    fDone = aDone;
+  }
+
+  public synchronized boolean getDone() {
+    return fDone;
+  }
+
+  public synchronized void setCanceled(boolean aCanceled) {
+    fCanceled = aCanceled;
+  }
+
+  public synchronized boolean isCanceled() {
+    return fCanceled;
+  }
+
+  public void start() {
+    fThread = new Thread(this);
+    fThread.start();
+  }
+
+  public void run() {
+    progressLoop();
+
+    fThread = null;
+    this.dispose();
+  }
+
+  public abstract void progressLoop();
+}
diff --git a/mozilla/grendel/ui/README b/mozilla/grendel/ui/README
new file mode 100644
index 00000000000..fa6d25f9f42
--- /dev/null
+++ b/mozilla/grendel/ui/README
@@ -0,0 +1,7 @@
+This is Grendel -- a Java mail/news client.
+
+The grendel.ui package implements the grendel GUI.  Much of the work is
+to create the outline view of a set of messages, although much of that
+work is split off into the grendel.view package.
+
+See http://www.mozilla.org/projects/grendel/ for more info.
diff --git a/mozilla/grendel/ui/SimpleAuthenticator.java b/mozilla/grendel/ui/SimpleAuthenticator.java
new file mode 100644
index 00000000000..ac9bb8e2a33
--- /dev/null
+++ b/mozilla/grendel/ui/SimpleAuthenticator.java
@@ -0,0 +1,37 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Terry Weissman , 18 Nov 1997.
+ */
+
+package grendel.ui;
+
+import calypso.util.Preferences;
+import calypso.util.PreferencesFactory;
+import javax.mail.Authenticator;
+import javax.mail.PasswordAuthentication;
+
+class SimpleAuthenticator extends Authenticator {
+  protected PasswordAuthentication getPasswordAuthentication() {
+    Preferences prefs = PreferencesFactory.Get();
+
+    return new PasswordAuthentication(prefs.getString("mail.user"),
+                                      prefs.getString("mail.password"));
+  }
+}
+
diff --git a/mozilla/grendel/ui/SimpleMessageHeader.java b/mozilla/grendel/ui/SimpleMessageHeader.java
new file mode 100644
index 00000000000..ca2ca72006e
--- /dev/null
+++ b/mozilla/grendel/ui/SimpleMessageHeader.java
@@ -0,0 +1,216 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 20 Nov 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Insets;
+
+import javax.mail.Address;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetAddress;
+
+import com.sun.java.swing.JLabel;
+import com.sun.java.swing.border.BevelBorder;
+
+public class SimpleMessageHeader extends MessageHeader {
+  static final int kMargin = 3;
+
+  JLabel fToLabel;
+  JLabel fTo;
+  JLabel fFromLabel;
+  JLabel fFrom;
+  JLabel fSubjectLabel;
+  JLabel fSubject;
+
+  public SimpleMessageHeader() {
+    setLayout(null);
+    setBorder(new BevelBorder(BevelBorder.RAISED));
+
+    Font bold = Font.decode("Dialog-bold-12");
+    Font plain = Font.decode("Dialog-12");
+
+    fToLabel = new JLabel("To: ");
+    fToLabel.setFont(bold);
+    add(fToLabel);
+    fTo = new JLabel();
+    fTo.setFont(plain);
+    add(fTo);
+
+    fFromLabel = new JLabel("From: ");
+    fFromLabel.setFont(bold);
+    add(fFromLabel);
+    fFrom = new JLabel();
+    fFrom.setFont(plain);
+    add(fFrom);
+
+    fSubjectLabel = new JLabel("Subject: ");
+    fSubjectLabel.setFont(bold);
+    add(fSubjectLabel);
+    fSubject = new JLabel();
+    fSubject.setFont(plain);
+    add(fSubject);
+  }
+
+  public void setMessage(Message aMessage) {
+    fMessage = aMessage;
+
+    if (fMessage != null) {
+      try {
+        StringBuffer toString = new StringBuffer();
+        StringBuffer fromString = new StringBuffer();
+        String personal = null;
+
+        Address recipients[] = fMessage.getRecipients(Message.TO);
+        Address sender[] = fMessage.getFrom();
+
+        int i;
+        if (recipients != null) {
+          for (i = 0; i < recipients.length; i++) {
+            personal = null;
+            if (recipients[i] instanceof InternetAddress) {
+              personal = ((InternetAddress) recipients[i]).getPersonal();
+            }
+            if (personal != null) {
+              toString.append(personal);
+            } else {
+              toString.append(recipients[i].toString());
+            }
+            if (i < (recipients.length - 1)) {
+              toString.append(", ");
+            }
+          }
+        }
+
+        personal = null;
+        if (sender != null && sender[0] instanceof InternetAddress) {
+          personal = ((InternetAddress) sender[0]).getPersonal();
+        }
+
+        if (personal != null) {
+          fromString.append(personal);
+        } else if (sender != null && sender.length > 0) {
+          fromString.append(sender[0].toString());
+        }
+
+        fTo.setText(toString.toString());
+        fFrom.setText(fromString.toString());
+        fSubject.setText(fMessage.getSubject());
+      } catch (MessagingException e) {
+        System.err.println("SimpleMessageHeader.setMessage(): " + e);
+      }
+    } else {
+      fTo.setText("");
+      fFrom.setText("");
+      fSubject.setText("");
+    }
+
+    invalidate();
+    validate();
+  }
+
+  public Dimension getPreferredSize() {
+    Insets insets = getInsets();
+    insets.top += kMargin;
+    insets.left += kMargin;
+    insets.bottom += kMargin;
+    insets.right += kMargin;
+
+    int width1 = 0;
+    int height1 = 0;
+    int width2 = 0;
+    int height2 = 0;
+    Dimension dim;
+
+    dim = fToLabel.getPreferredSize();
+    width1 = dim.width;
+    height1 = dim.height;
+
+    dim = fTo.getPreferredSize();
+    width1 += dim.width;
+    height1 = Math.max(height1, dim.height);
+
+    width1 += kMargin * 2;
+
+    dim = fFromLabel.getPreferredSize();
+    width1 += dim.width;
+    height1 = Math.max(height1, dim.height);
+
+    dim = fFrom.getPreferredSize();
+    width1 += dim.width;
+    height1 = Math.max(height1, dim.height);
+
+    dim = fSubjectLabel.getPreferredSize();
+    width2 = dim.width;
+    height2 = dim.height;
+
+    dim = fSubject.getPreferredSize();
+    width2 += dim.width;
+    height2 = Math.max(height2, dim.height);
+
+    return new Dimension(insets.left + insets.right + Math.max(width1, width2),
+                         insets.top + insets.bottom + height1 + height2);
+  }
+
+  public void doLayout() {
+    Insets insets = getInsets();
+    insets.top += kMargin;
+    insets.left += kMargin;
+    insets.bottom += kMargin;
+    insets.right += kMargin;
+
+    int width = insets.left;
+    int height = insets.top;
+    Dimension dim;
+
+    dim = fToLabel.getPreferredSize();
+    fToLabel.setBounds(width, insets.top, dim.width, dim.height);
+    width += dim.width;
+    height += dim.height;
+
+    dim = fTo.getPreferredSize();
+    fTo.setBounds(width, insets.top, dim.width, dim.height);
+    width += dim.width;
+    height = Math.max(height, insets.top + dim.height);
+
+    width += kMargin * 2;
+
+    dim = fFromLabel.getPreferredSize();
+    fFromLabel.setBounds(width, insets.top, dim.width, dim.height);
+    width += dim.width;
+    height = Math.max(height, insets.top + dim.height);
+
+    dim = fFrom.getPreferredSize();
+    fFrom.setBounds(width, insets.top, dim.width, dim.height);
+    height = Math.max(height, insets.top + dim.height);
+
+    width = insets.left;
+
+    dim = fSubjectLabel.getPreferredSize();
+    fSubjectLabel.setBounds(width, height, dim.width, dim.height);
+    width += dim.width;
+
+    dim = fSubject.getPreferredSize();
+    fSubject.setBounds(width, height, dim.width, dim.height);
+  }
+}
diff --git a/mozilla/grendel/ui/StoreFactory.java b/mozilla/grendel/ui/StoreFactory.java
new file mode 100644
index 00000000000..3c13f79ab11
--- /dev/null
+++ b/mozilla/grendel/ui/StoreFactory.java
@@ -0,0 +1,253 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 19 Nov 1997.
+ */
+
+package grendel.ui;
+
+import java.util.ResourceBundle;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+import javax.mail.AuthenticationFailedException;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Store;
+import javax.mail.URLName;
+
+import com.sun.java.swing.JOptionPane;
+import com.sun.java.swing.event.ChangeEvent;
+import com.sun.java.swing.event.ChangeListener;
+import com.sun.java.swing.event.EventListenerList;
+
+import calypso.util.Preferences;
+import calypso.util.PreferencesFactory;
+
+import grendel.view.ViewedStore;
+import grendel.view.ViewedStoreBase;
+
+import java.lang.ClassNotFoundException;
+import java.lang.NoSuchMethodException;
+import java.lang.IllegalAccessException;
+import java.lang.IllegalArgumentException;
+import java.lang.InstantiationException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+
+public synchronized class StoreFactory {
+  static StoreFactory fInstance = null;
+
+  Session fSession = null;
+  ViewedStore[] fStores = null;
+  DialogAuthenticator fAuthenticator = null;
+  EventListenerList fListeners = new EventListenerList();
+
+  private StoreFactory() {
+    Preferences prefs = PreferencesFactory.Get();
+    fAuthenticator = new DialogAuthenticator();
+    fSession =
+      Session.getDefaultInstance(prefs.getAsProperties(),
+                                 fAuthenticator);
+    fSession.setDebug(true);
+  }
+
+  public static StoreFactory Instance() {
+    if (fInstance == null) {
+      fInstance = new StoreFactory();
+    }
+    return fInstance;
+  }
+
+  public Session getSession() {
+    return fSession;
+  }
+
+  public ViewedStore[] getStores() {
+    if (fStores == null) {
+      updateStores();
+    }
+
+    return fStores;
+  }
+
+  ViewedStore createStore(String storename) {
+    ResourceBundle labels = ResourceBundle.getBundle("grendel.ui.Labels");
+    URLName urlName = null;
+
+    String proto;
+
+    if (storename.indexOf(":") != -1) {
+      urlName = new URLName(storename);
+    } else {
+      urlName = new URLName(storename, null, -1, null, null, null);
+    }
+
+    proto = urlName.getProtocol();
+
+    // ### Very wrong temporary hack -- special case "berkeley" and "pop3",
+    // since they doesn't play with the proper registration mechanism right
+    // now.
+    Store store = null;
+    ViewedStore viewedStore = null;
+
+    try {
+      Class c = null;
+      if (proto.equalsIgnoreCase("berkeley")) {
+        c = Class.forName("grendel.storage.BerkeleyStore");
+      } else if (proto.equalsIgnoreCase("pop3")) {
+        c = Class.forName("grendel.storage.PopStore");
+      } else if (proto.equalsIgnoreCase("news")) {
+        c = Class.forName("grendel.storage.NewsStore");
+      }
+
+      if (c != null) {
+        Object args[] = { fSession, urlName };
+        Class types[] = { args[0].getClass(), args[1].getClass() };
+        Constructor cc = c.getConstructor(types);
+        store = (Store) cc.newInstance(args);
+      }
+    } catch (ClassNotFoundException c) {        // Class.forName
+    } catch (NoSuchMethodException c) {         // Constructor.getConstructor
+    } catch (IllegalAccessException c) {        // Constructor.newInstance
+    } catch (IllegalArgumentException c) {      // Constructor.newInstance
+    } catch (InstantiationException c) {        // Constructor.newInstance
+    } catch (InvocationTargetException c) {     // Constructor.newInstance
+    }
+
+    if (store == null) {
+      try {
+        store = fSession.getStore(proto);
+      } catch (MessagingException e) {
+        System.out.println("Got exception " + e +
+                           " while creating store of type" + proto);
+        e.printStackTrace();
+      }
+    }
+
+    viewedStore = new ViewedStoreBase(store,
+                                      urlName.getProtocol(),
+                                      urlName.getHost(),
+                                      urlName.getPort(),
+                                      urlName.getUsername());
+    return viewedStore;
+  }
+
+  public ViewedStore getViewedStore(Store aStore) {
+    if (fStores == null) {
+      return null;
+    }
+
+    for (int i = 0; i < fStores.length; i++) {
+      if (fStores[i].getStore().equals(aStore)) {
+        return fStores[i];
+      }
+    }
+    return null;
+  }
+
+  boolean SafeEquals(Object a, Object b) {
+    if (a == b) {
+      return true;
+    } else if (a != null && b != null) {
+      return a.equals(b);
+    } else {
+      return false;
+    }
+  }
+
+  public ViewedStore findStore(String storename) {
+    URLName urlName = null;
+
+    if (storename.indexOf(":") != -1) {
+      urlName = new URLName(storename);
+    } else {
+      urlName = new URLName(storename, null, -1, null, null, null);
+    }
+    for (int i = 0; fStores != null && i < fStores.length; i++) {
+      if (urlName.getProtocol().equals(fStores[i].getProtocol())) {
+        if (SafeEquals(urlName.getHost(), fStores[i].getHost()) &&
+            SafeEquals(urlName.getUsername(), fStores[i].getUsername()) &&
+            //SafeEquals(urlName.getFile(), fStores[i].getFile()) &&
+            urlName.getPort() == fStores[i].getPort()) {
+          return fStores[i];
+        }
+      }
+    }
+    return null;
+  }
+
+  void updateStores() {
+    if (fSession == null) {
+      getSession();
+    }
+
+    Preferences prefs = PreferencesFactory.Get();
+    Vector resVector = new Vector();
+
+    String defStore = "";
+    if (!prefs.getString("mail.directory","").equals("")) {
+      defStore = "berkeley";
+    }
+
+    String storelist = prefs.getString("mail.storelist", defStore);
+    StringTokenizer st = new StringTokenizer(storelist, " ,;");
+
+    while (st.hasMoreTokens()) {
+      String storename = st.nextToken().trim();
+
+      ViewedStore viewedStore = null;
+
+      viewedStore = findStore(storename);
+      if (viewedStore == null) {
+        viewedStore = createStore(storename);
+        System.out.println("created " + viewedStore);
+      } else {
+        System.out.println("recycled " + viewedStore);
+      }
+
+      if (viewedStore != null) {
+        resVector.addElement(viewedStore);
+      }
+    }
+    fStores = new ViewedStore[resVector.size()];
+    resVector.copyInto(fStores);
+  }
+
+  public void refreshStores() {
+    updateStores();
+
+    Object[] listeners = fListeners.getListenerList();
+    ChangeEvent event = null;
+    for (int i = 0; i < listeners.length - 1; i += 2) {
+      // Lazily create the event:
+      if (event == null)
+        event = new ChangeEvent(this);
+      ((ChangeListener)listeners[i+1]).stateChanged(event);
+    }
+  }
+
+  public void addChangeListener(ChangeListener l) {
+    fListeners.add(ChangeListener.class, l);
+  }
+
+  public void removeChangeListener(ChangeListener l) {
+    fListeners.remove(ChangeListener.class, l);
+  }
+}
diff --git a/mozilla/grendel/ui/UIFactory.java b/mozilla/grendel/ui/UIFactory.java
new file mode 100644
index 00000000000..b6b106a3e2c
--- /dev/null
+++ b/mozilla/grendel/ui/UIFactory.java
@@ -0,0 +1,118 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin , 10 Dec 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Image;
+
+import com.sun.java.swing.Icon;
+import com.sun.java.swing.ImageIcon;
+
+import grendel.view.ViewedFolder;
+import grendel.view.ViewedStore;
+
+public class UIFactory {
+  static UIFactory fInstance;
+
+  ImageIcon   fInboxIcon;
+  ImageIcon   fFolderIcon;
+  ImageIcon   fNewsgroupIcon;
+  ImageIcon   fLocalStoreIcon;
+  ImageIcon   fRemoteStoreIcon;
+  ImageIcon   fConnectedIcon;
+
+  private UIFactory() {
+    fFolderIcon =
+      new ImageIcon(getClass().getResource("images/folder-small.gif"));
+    fNewsgroupIcon =
+      new ImageIcon(getClass().getResource("images/newsgroup-small.gif"));
+    fLocalStoreIcon =
+      new ImageIcon(getClass().getResource("images/storeLocal-small.gif"));
+    fRemoteStoreIcon =
+      new ImageIcon(getClass().getResource("images/storeRemote-small.gif"));
+    fInboxIcon =
+      new ImageIcon(getClass().getResource("images/inbox-small.gif"));
+    fConnectedIcon =
+      new ImageIcon(getClass().getResource("images/connected-small.gif"));
+  }
+
+  public static UIFactory Instance() {
+    if (fInstance == null) {
+      fInstance = new UIFactory();
+    }
+    return fInstance;
+  }
+
+  ImageIcon getFolderImageIcon(ViewedFolder aFolder,
+                               boolean open,
+                               boolean large) {
+    ViewedStore store = aFolder.getViewedStore();
+    if (aFolder == store) {
+      if (store.getHost() == null) {
+        return fLocalStoreIcon;
+      } else {
+        return fRemoteStoreIcon;
+      }
+    } else {
+      if (store.getProtocol().equals("news")) {
+        return fNewsgroupIcon;
+      } else {
+        if (aFolder.isInbox()) {
+          return fInboxIcon;
+        } else {
+          return fFolderIcon;
+        }
+      }
+    }
+  }
+
+  ImageIcon getFolderOverlayImageIcon(ViewedFolder aFolder,
+                                      boolean open,
+                                      boolean large) {
+
+    ViewedStore store = aFolder.getViewedStore();
+    if (aFolder == store) {
+      if (store.isConnected()) {
+        return fConnectedIcon;
+      }
+    }
+    return null;
+  }
+
+  public Icon getFolderIcon(ViewedFolder aFolder,
+                            boolean open,
+                            boolean large) {
+    return getFolderImageIcon(aFolder, open, large);
+  }
+
+  public Icon getFolderOverlayIcon(ViewedFolder aFolder,
+                                   boolean open,
+                                   boolean large) {
+    return getFolderOverlayImageIcon(aFolder, open, large);
+  }
+
+  public Image getFolderImage(ViewedFolder aFolder,
+                             boolean open,
+                             boolean large) {
+    return getFolderImageIcon(aFolder, open, large).getImage();
+  }
+}
+
diff --git a/mozilla/grendel/ui/UnifiedMessageDisplayManager.java b/mozilla/grendel/ui/UnifiedMessageDisplayManager.java
new file mode 100644
index 00000000000..ee9a35eb04e
--- /dev/null
+++ b/mozilla/grendel/ui/UnifiedMessageDisplayManager.java
@@ -0,0 +1,465 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin ,  3 Sep 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.BorderLayout;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.util.Enumeration;
+
+import com.sun.java.swing.BoxLayout;
+import com.sun.java.swing.Icon;
+import com.sun.java.swing.ImageIcon;
+import com.sun.java.swing.JComponent;
+import com.sun.java.swing.JFrame;
+import com.sun.java.swing.JMenuBar;
+import com.sun.java.swing.event.ChangeEvent;
+
+import calypso.util.Preferences;
+import calypso.util.PreferencesFactory;
+
+import netscape.orion.toolbars.NSToolbar;
+import netscape.orion.toolbars.ToolbarFactory;
+import netscape.orion.toolbars.ToolBarLayout;
+import netscape.orion.uimanager.AbstractUICmd;
+import netscape.orion.uimanager.IUICmd;
+
+import javax.mail.Store;
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+
+import grendel.view.ViewedMessage;
+import grendel.widgets.Splitter;
+import grendel.widgets.StatusEvent;
+import grendel.widgets.TreePath;
+
+/**
+ * The legendary three pane UI.
+ */
+
+public class UnifiedMessageDisplayManager extends MessageDisplayManager {
+  UnifiedMessageFrame fMainFrame;
+
+  public final static String SPLIT_TOP = "splitTop";
+  public final static String SPLIT_LEFT = "splitLeft";
+  public final static String SPLIT_RIGHT = "splitRight";
+  public final static String STACKED = "stacked";
+
+  /**
+   * Displays a message given a Message object. If the message
+   * is not in the currently selected folder, that folder will
+   * be selected, loaded and displayed.
+   */
+
+  public void displayMessage(Message aMessage) {
+    checkFrame();
+    fMainFrame.display(aMessage.getFolder(), aMessage);
+  }
+
+  /**
+   * Displays a folder given a folder object. If the message
+   * being displayed is not in that folder, the message
+   * display pane will be cleared.
+   */
+
+  public void displayFolder(Folder aFolder) {
+    checkFrame();
+    fMainFrame.display(aFolder, null);
+  }
+
+  /**
+   * Displays folder given a Folder object and
+   * selects and displays a message in that folder given a Message
+   * object.
+   */
+
+  public void displayFolder(Folder aFolder, Message aMessage) {
+    checkFrame();
+    fMainFrame.display(aFolder, aMessage);
+  }
+
+  /**
+   * Displays the master (A folder tree, for now). This should not
+   * affect displayed folders or messages.
+   */
+
+  public void displayMaster() {
+    checkFrame();
+    fMainFrame.display(null, null);
+  }
+
+  /**
+   * Displays the master with the given folder selected. If the
+   * folder is not currently displayed, the folder will be loaded
+   * in the folder message list pane, and the message pane will be
+   * cleared.
+   */
+
+  public void displayMaster(Folder aFolder) {
+    checkFrame();
+    fMainFrame.display(aFolder, null);
+  }
+
+  void checkFrame() {
+    if (fMainFrame == null) {
+      fMainFrame = new UnifiedMessageFrame();
+      fMainFrame.setVisible(true);
+    }
+    fMainFrame.toFront();
+    fMainFrame.requestFocus();
+  }
+}
+
+class UnifiedMessageFrame extends GeneralFrame {
+  MasterPanel   fFolders = null;
+  FolderPanel   fThreads = null;
+  MessagePanel  fMessage = null;
+  Splitter      splitter1 = null, splitter2 = null;
+  String        fLayout = null;
+
+  public UnifiedMessageFrame() {
+    super("appNameLabel", "mail.multi_pane");
+
+    PrefsDialog.CheckPrefs(this);
+
+    fFolders = new MasterPanel();
+    fThreads = new FolderPanel();
+    fMessage = new MessagePanel();
+
+    PanelListener listener = new PanelListener();
+
+    fFolders.addMasterPanelListener(listener);
+    fThreads.addFolderPanelListener(listener);
+    fMessage.addMessagePanelListener(listener);
+
+    Preferences prefs = PreferencesFactory.Get();
+    String layout = prefs.getString("mail.multi_pane.layout",
+                                    UnifiedMessageDisplayManager.SPLIT_TOP);
+
+
+    layoutPanels(layout);
+
+    fMenu = buildMenu("multiMain",
+                      Util.MergeActions(actions,
+                        Util.MergeActions(fFolders.getActions(),
+                          Util.MergeActions(fThreads.getActions(),
+                                            fMessage.getActions()))));
+
+    getRootPane().setMenuBar(fMenu);
+
+    NSToolbar masterToolBar = fFolders.getToolBar();
+    NSToolbar folderToolBar = fThreads.getToolBar();
+    NSToolbar messageToolBar = fMessage.getToolBar();
+
+    fToolBar = Util.MergeToolBars(masterToolBar,
+                                  Util.MergeToolBars(folderToolBar,
+                                                     messageToolBar));
+
+    fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(ToolBarLayout.CreateSpring(),
+                                                       null));
+    fToolBar.addItem(ToolbarFactory.MakeINSToolbarItem(fAnimation, null));
+    fToolBarPanel.add(fToolBar);
+
+    fStatusBar = buildStatusBar();
+    fPanel.add(BorderLayout.SOUTH, fStatusBar);
+
+    restoreBounds();
+  }
+
+  public void dispose() {
+    saveBounds();
+
+    Preferences prefs = PreferencesFactory.Get();
+
+    String masterWeight = "1.0";
+    String folderWeight = "2.0";
+    String messageWeight = "2.0";
+    String splitWeight = "1.0";
+
+    if (fLayout.equals(UnifiedMessageDisplayManager.STACKED)) {
+      masterWeight = splitter1.getWeight(fFolders).toString();
+      folderWeight = splitter1.getWeight(fThreads).toString();
+      messageWeight = splitter1.getWeight(fMessage).toString();
+    } else if (fLayout.equals(UnifiedMessageDisplayManager.SPLIT_RIGHT)) {
+      masterWeight = splitter1.getWeight(fFolders).toString();
+      folderWeight = splitter2.getWeight(fThreads).toString();
+      messageWeight = splitter2.getWeight(fMessage).toString();
+      splitWeight = splitter1.getWeight(splitter2).toString();
+    } else {
+      masterWeight = splitter2.getWeight(fFolders).toString();
+      folderWeight = splitter2.getWeight(fThreads).toString();
+      messageWeight = splitter1.getWeight(fMessage).toString();
+      splitWeight = splitter1.getWeight(splitter2).toString();
+    }
+
+    prefs.putString("mail.multi_pane.master_weight", masterWeight);
+    prefs.putString("mail.multi_pane.folder_weight", folderWeight);
+    prefs.putString("mail.multi_pane.message_weight", messageWeight);
+    prefs.putString("mail.multi_pane.split_weight", splitWeight);
+
+    prefs.putString("mail.multi_pane.layout", fLayout);
+
+    fFolders.dispose();
+    fThreads.dispose();
+    fMessage.dispose();
+
+    super.dispose();
+  }
+
+  public void display(Folder aFolder, Message aMessage) {
+    if (aFolder != null) {
+      fThreads.setFolder(aFolder);
+      fMessage.setMessage(aMessage);
+    }
+  }
+
+  void layoutPanels(String layout) {
+    if (fLayout != null) {
+      if (fLayout.equals(layout)) {
+        return; // nothing to do
+      }
+      if (fLayout.equals(UnifiedMessageDisplayManager.STACKED)) {
+        splitter1.removeAll();
+      } else {
+        splitter1.removeAll();
+        splitter2.removeAll();
+      }
+      fPanel.remove(splitter1);
+    }
+
+    Preferences prefs = PreferencesFactory.Get();
+
+    String masterWeight = prefs.getString("mail.multi_pane.master_weight", "1.0");
+    String folderWeight = prefs.getString("mail.multi_pane.folder_weight", "2.0");
+    String messageWeight = prefs.getString("mail.multi_pane.message_weight", "2.0");
+    String splitWeight = prefs.getString("mail.multi_pane.split_weight", "1.0");
+
+    if (layout.equals(UnifiedMessageDisplayManager.STACKED)) {
+      splitter1 = new Splitter(Splitter.VERTICAL);
+      splitter1.add(fFolders, new Float(masterWeight));
+      splitter1.add(splitter1.createSeparator(4));
+      splitter1.add(fThreads, new Float(folderWeight));
+      splitter1.add(splitter1.createSeparator(4));
+      splitter1.add(fMessage, new Float(messageWeight));
+
+      fStackedLayoutAction.setSelected(IUICmd.kSelected);
+    } else if (layout.equals(UnifiedMessageDisplayManager.SPLIT_LEFT)) {
+      splitter1 = new Splitter(Splitter.HORIZONTAL);
+
+      splitter2 = new Splitter(Splitter.VERTICAL);
+      splitter2.add(fFolders, new Float(masterWeight));
+      splitter2.add(splitter2.createSeparator(4));
+      splitter2.add(fThreads, new Float(folderWeight));
+
+      splitter1.add(splitter2, new Float(splitWeight));
+      splitter1.add(splitter1.createSeparator(4));
+      splitter1.add(fMessage, new Float(messageWeight));
+
+      fSplitLeftLayoutAction.setSelected(IUICmd.kSelected);
+    } else if (layout.equals(UnifiedMessageDisplayManager.SPLIT_RIGHT)) {
+
+      splitter2 = new Splitter(Splitter.VERTICAL);
+      splitter2.add(fThreads, new Float(folderWeight));
+      splitter2.add(splitter2.createSeparator(4));
+      splitter2.add(fMessage, new Float(messageWeight));
+
+      splitter1 = new Splitter(Splitter.HORIZONTAL);
+      splitter1.add(fFolders, new Float(masterWeight));
+      splitter1.add(splitter1.createSeparator(4));
+      splitter1.add(splitter2, new Float(splitWeight));
+
+      fSplitRightLayoutAction.setSelected(IUICmd.kSelected);
+    } else { // Default: SPLIT_TOP
+      splitter1 = new Splitter(Splitter.VERTICAL);
+
+      splitter2 = new Splitter(Splitter.HORIZONTAL);
+      splitter2.add(fFolders, new Float(masterWeight));
+      splitter2.add(splitter2.createSeparator(4));
+      splitter2.add(fThreads, new Float(folderWeight));
+
+      splitter1.add(splitter2, new Float(splitWeight));
+      splitter1.add(splitter1.createSeparator(4));
+      splitter1.add(fMessage, new Float(messageWeight));
+
+      fSplitTopLayoutAction.setSelected(IUICmd.kSelected);
+    }
+    fPanel.add("Center", splitter1);
+
+    invalidate();
+    validate();
+
+    fLayout = layout;
+  }
+
+  IUICmd fSplitLeftLayoutAction =
+    new LayoutAction(UnifiedMessageDisplayManager.SPLIT_LEFT);
+  IUICmd fSplitRightLayoutAction =
+    new LayoutAction(UnifiedMessageDisplayManager.SPLIT_RIGHT);
+  IUICmd fSplitTopLayoutAction =
+    new LayoutAction(UnifiedMessageDisplayManager.SPLIT_TOP);
+  IUICmd fStackedLayoutAction =
+    new LayoutAction(UnifiedMessageDisplayManager.STACKED);
+
+  IUICmd actions[] = { ActionFactory.GetExitAction(),
+                       ActionFactory.GetNewMailAction(),
+                       ActionFactory.GetComposeMessageAction(),
+                       ActionFactory.GetPreferencesAction(),
+                       ActionFactory.GetSearchAction(),
+                       ActionFactory.GetRunFiltersAction(),
+                       ActionFactory.GetShowTooltipsAction(),
+                       fSplitLeftLayoutAction,
+                       fSplitRightLayoutAction,
+                       fSplitTopLayoutAction,
+                       fStackedLayoutAction
+  };
+
+
+  class PanelListener implements MasterPanelListener, FolderPanelListener,
+                                 MessagePanelListener
+  {
+    // FolderPanelListener
+
+    public void loadingFolder(ChangeEvent aEvent) {
+      // start animation
+      fAnimation.start();
+    }
+
+    public void folderLoaded(ChangeEvent aEvent) {
+      // stop animation
+      fAnimation.stop();
+    }
+
+    public void folderStatus(StatusEvent aEvent) {
+      setStatusText(aEvent.getStatus());
+    }
+
+    public void folderSelectionChanged(ChangeEvent aEvent) {
+      TreePath path = null;
+      Enumeration selection =
+        ((FolderPanel) aEvent.getSource()).getSelection();
+
+      if (selection.hasMoreElements()) {
+        path = (TreePath) selection.nextElement();
+      }
+      if (path != null && !selection.hasMoreElements()) {
+        // not multiple selection
+        ViewedMessage node = (ViewedMessage) path.getTip();
+        fMessage.setMessage(node.getMessage());
+      } else {
+        fMessage.setMessage(null);
+      }
+    }
+    public void folderSelectionDoubleClicked(ChangeEvent aEvent) {
+      TreePath path = null;
+      Enumeration selection = ((FolderPanel)aEvent.getSource()).getSelection();
+
+      MessageDisplayManager master = MultiMessageDisplayManager.Get();
+
+      while (selection.hasMoreElements()) {
+        path = (TreePath) selection.nextElement();
+        if (path != null) {
+          ViewedMessage node = (ViewedMessage) path.getTip();
+          master.displayMessage(node.getMessage());
+        }
+      }
+    }
+
+    // MasterPanelListener
+
+    public void masterSelectionChanged(ChangeEvent aEvent) {
+      TreePath path = null;
+      Enumeration selection =
+        ((MasterPanel) aEvent.getSource()).getSelection();
+      if (selection.hasMoreElements()) {
+        path = (TreePath) selection.nextElement();
+      }
+      Object node = null;
+      Folder folder = null;
+      if (path != null && !selection.hasMoreElements()) {
+        // not multiple selection
+        node = path.getTip();
+      }
+
+      folder = MasterPanel.getFolder(node);
+
+      if (folder != null) {
+        try {
+          if ((folder.getType() & Folder.HOLDS_MESSAGES) == 0) {
+            folder = null;
+          }
+        } catch (MessagingException e) {
+          folder = null;
+        }
+      }
+
+      if (folder != null) {
+        setTitle(folder.getName() +
+                 fLabels.getString("folderSuffixFrameLabel"));
+        fThreads.setFolder(folder);
+      } else {
+        setTitle(fLabels.getString("appNameLabel"));
+        fThreads.setFolder(null);
+      }
+    }
+
+    public void masterSelectionDoubleClicked(ChangeEvent aEvent) {
+    }
+
+    // MessagePanelListener
+
+    public void loadingMessage(ChangeEvent aEvent) {
+      startAnimation();
+    }
+
+    public void messageLoaded(ChangeEvent aEvent) {
+      stopAnimation();
+    }
+
+    public void messageStatus(StatusEvent aEvent) {
+      setStatusText(aEvent.getStatus());
+    }
+  }
+
+  //
+  // LayoutAction class
+  //
+
+  class LayoutAction extends AbstractUICmd {
+    ImageIcon fIcon;
+    public LayoutAction(String aAction) {
+      super(aAction);
+      setEnabled(true);
+
+      fIcon = new ImageIcon(getClass().getResource("images/" +
+                                                   aAction + ".gif"));
+    }
+
+    public void actionPerformed(ActionEvent aEvent) {
+      layoutPanels(aEvent.getActionCommand());
+    }
+
+    public ImageIcon getEnabledIcon() {
+      return fIcon;
+    }
+  }
+}
diff --git a/mozilla/grendel/ui/Util.java b/mozilla/grendel/ui/Util.java
new file mode 100644
index 00000000000..a43dc15aed7
--- /dev/null
+++ b/mozilla/grendel/ui/Util.java
@@ -0,0 +1,298 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Will Scullin ,  9 Sep 1997.
+ */
+
+package grendel.ui;
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Frame;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.UnsupportedEncodingException;
+import java.util.Vector;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeUtility;
+
+import netscape.orion.toolbars.NSButton;
+import netscape.orion.toolbars.NSToolbar;
+import netscape.orion.uimanager.AbstractUICmd;
+import netscape.orion.uimanager.IUICmd;
+
+import com.sun.java.swing.JComponent;
+import com.sun.java.swing.JPopupMenu;
+import com.sun.java.swing.JScrollBar;
+import com.sun.java.swing.JScrollPane;
+import com.sun.java.swing.KeyStroke;
+
+public class Util {
+  public static final int LEFT = 0;
+  public static final int CENTER = 0;
+  public static final int RIGHT = 0;
+
+  // Recycled character array
+  private static char fChars[] = new char[128];
+
+  public static void DrawTrimmedString(Graphics g, String aString,
+                                       FontMetrics aFM, int aWhere,
+                                       int x, int y, int aWidth) {
+    int w = aFM.stringWidth(aString);
+    if (w <= aWidth) {
+      g.drawString(aString, x, y);
+      return;
+    }
+
+    int i;
+    int first = 0;
+    int length = aString.length();
+    if (length > fChars.length) {
+      fChars = new char[length];
+    }
+    aString.getChars(0, length, fChars, 0);
+
+    if (aWhere == RIGHT) {
+      fChars[length - 3] = '.';
+      fChars[length - 2] = '.';
+      fChars[length - 1] = '.';
+    } else { // Only left cut for now
+      fChars[0] = '.';
+      fChars[1] = '.';
+      fChars[2] = '.';
+    }
+
+    w = aFM.charsWidth(fChars, first, length);
+
+    if (aWhere == RIGHT) {
+      while (w > aWidth) {
+        length--;
+        w -= aFM.charWidth(fChars[length - 3]);
+        fChars[length - 3] = '.';
+      }
+    } else { // Only left cut for now
+      while (w > aWidth) {
+        length--;
+        first++;
+        w -= aFM.charWidth(fChars[first + 2]);
+        fChars[first + 2] = '.';
+      }
+    }
+    g.drawChars(fChars, first, length, x, y);
+  }
+
+  static IUICmd FindAction(Vector aVector, String aAction) {
+    for (int i = 0; i < aVector.size(); i++) {
+      IUICmd action = (IUICmd) aVector.elementAt(i);
+      if (action.getText(IUICmd.NAME).equals(aAction)) {
+        return action;
+      }
+    }
+    return null;
+  }
+
+  static public IUICmd[] MergeActions(IUICmd aActions1[], IUICmd aActions2[]) {
+    Vector resVector = new Vector();
+    int i;
+    if (aActions1 != null) {
+      for (i = 0; i < aActions1.length; i++) {
+        resVector.addElement(aActions1[i]);
+      }
+    }
+    if (aActions2 != null) {
+      for (i = 0; i < aActions2.length; i++) {
+        if (FindAction(resVector, aActions2[i].getText(IUICmd.NAME)) == null) {
+          resVector.addElement(aActions2[i]);
+        }
+      }
+    }
+    IUICmd res[] = new IUICmd[resVector.size()];
+    resVector.copyInto(res);
+    return res;
+  }
+
+  static public NSToolbar MergeToolBars(NSToolbar aBar1, NSToolbar aBar2) {
+    NSToolbar res = new NSToolbar();
+    int count1 = aBar1.getItemCount();
+    int count2 = aBar2.getItemCount();
+    int i = 0, j = 0, k, l;
+
+    while (i < count1) {
+      NSButton button1 = (NSButton) aBar1.getItemAt(i);
+      if (j < count2) {
+        NSButton button2 = (NSButton) aBar2.getItemAt(j);
+        if (button1.getActionCommand().equals(button2.getActionCommand())) {
+          res.addItem(button1);
+          i++;
+          j++;
+        } else {
+          boolean merge = false;
+          for (k = j; k < count2; k++) {
+            button2 = (NSButton) aBar2.getItemAt(k);
+            if (button1.getActionCommand().equals(button2.getActionCommand())) {
+              merge = true;
+              while (j < k) {
+                NSButton button3 = (NSButton) aBar2.getItemAt(j);
+                res.addItem(button3);
+                j++;
+              }
+              break;
+            }
+          }
+          if (merge) {
+            res.addItem(button1);
+            j++;
+          } else {
+            res.addItem(button1);
+          }
+          i++;
+        }
+      } else {
+        res.addItem(button1);
+        i++;
+      }
+    }
+
+    while (j < count2) {
+      NSButton button2 = (NSButton) aBar2.getItemAt(j);
+      res.addItem(button2);
+      j++;
+    }
+
+    return res;
+  }
+
+  public static void RegisterScrollingKeys(JScrollPane aScrollPane) {
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_UP),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_UP,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_DOWN),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_DOWN,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_LEFT),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_LEFT,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_RIGHT),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_PAGE_UP),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_PAGE_DOWN),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_HOME),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_HOME,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+    aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_END),
+                                       KeyStroke.getKeyStroke(KeyEvent.VK_END,
+                                                              KeyEvent.CTRL_MASK),
+                                       JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
+
+  }
+
+  static public Frame GetParentFrame(Component aComponent) {
+    Component parent = aComponent.getParent();
+    while (parent != null && !(parent instanceof Frame)) {
+      if (parent instanceof JPopupMenu) {
+        parent = ((JPopupMenu) parent).getInvoker();
+      } else {
+        parent = parent.getParent();
+      }
+    }
+    return (Frame) parent;
+  }
+
+  static String GetSubject(Message aMessage) {
+    String result = "";
+    try {
+      String rawvalue = null;
+      try {
+        if ((rawvalue = aMessage.getSubject()) != null)
+          result = MimeUtility.decodeText(rawvalue);
+      } catch (UnsupportedEncodingException e) {
+        System.err.println(e + ":" + rawvalue);
+        // Don't care
+        result = rawvalue;
+      }
+    } catch (MessagingException e) {
+    }
+    return result;
+  }
+}
+
+class ScrollAction extends AbstractUICmd {
+  JScrollPane fScrollPane;
+  int         fAction;
+
+  ScrollAction(JScrollPane aScrollPane, int aAction) {
+    super("");
+    fScrollPane = aScrollPane;
+    fAction = aAction;
+  }
+
+  public void actionPerformed(ActionEvent aEvent) {
+    JScrollBar vScroll = fScrollPane.getVerticalScrollBar();
+    JScrollBar hScroll = fScrollPane.getHorizontalScrollBar();
+
+    switch (fAction) {
+    case KeyEvent.VK_UP:
+      vScroll.setValue(vScroll.getValue() - vScroll.getUnitIncrement());
+      break;
+    case KeyEvent.VK_DOWN:
+      vScroll.setValue(vScroll.getValue() + vScroll.getUnitIncrement());
+      break;
+    case KeyEvent.VK_LEFT:
+      hScroll.setValue(hScroll.getValue() - hScroll.getUnitIncrement());
+      break;
+    case KeyEvent.VK_RIGHT:
+      hScroll.setValue(hScroll.getValue() + hScroll.getUnitIncrement());
+      break;
+    case KeyEvent.VK_PAGE_UP:
+      vScroll.setValue(vScroll.getValue() - vScroll.getVisibleAmount());
+      break;
+    case KeyEvent.VK_PAGE_DOWN:
+      vScroll.setValue(vScroll.getValue() + vScroll.getVisibleAmount());
+      break;
+    case KeyEvent.VK_HOME:
+      vScroll.setValue(vScroll.getMinimum());
+      break;
+    case KeyEvent.VK_END:
+      vScroll.setValue(vScroll.getMaximum() - vScroll.getVisibleAmount());
+      break;
+    }
+  }
+}
diff --git a/mozilla/grendel/ui/dialogs.xml b/mozilla/grendel/ui/dialogs.xml
new file mode 100644
index 00000000000..aa44cc48261
--- /dev/null
+++ b/mozilla/grendel/ui/dialogs.xml
@@ -0,0 +1,70 @@
+
+
+  
+
+
+
+
+  
+    
+
+    
+
+    
+
+    
+
+    
+
+
+
+  
+    
+
+
+
+  
+  
+
+
+
+
+
+
+
diff --git a/mozilla/grendel/ui/images/GrendelIcon16.gif b/mozilla/grendel/ui/images/GrendelIcon16.gif
new file mode 100644
index 0000000000000000000000000000000000000000..7641cd0465f8b0c8c9a4549affb6fa82cfbcd585
GIT binary patch
literal 134
zcmZ?wbhEHb6krfw*vtR||Ns97)A)%03{W+SKUo+V72FNDN4Nj@zciB$tGs2-)&8NQnq8yxnq?j8Ns_(-`o|KQkBqDX=flD9Nf{Vlh&iC
b<1%5Q#uV*o(`R_h5^bKlV4;{G1A{dHr$R*_

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/README b/mozilla/grendel/ui/images/README
new file mode 100644
index 00000000000..fd9524084fe
--- /dev/null
+++ b/mozilla/grendel/ui/images/README
@@ -0,0 +1,16 @@
+#
+# The contents of this directory are subject to the Mozilla Public
+# License Version 1.0 (the "License"); you may not use this file except
+# in compliance with the License.  You may obtain a copy of the License
+# at http://www.mozilla.org/MPL/
+# 
+# Software distributed under the License is distributed on an "AS IS"
+# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+# the License for the specific language governing rights and limitations
+# under the License.
+# 
+# The Original Code is the Grendel mail/news client.
+# 
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation.  Portions created by Netscape are Copyright (C) 1997
+# Netscape Communications Corporation.  All Rights Reserved.
diff --git a/mozilla/grendel/ui/images/animation/AnimHuge00.gif b/mozilla/grendel/ui/images/animation/AnimHuge00.gif
new file mode 100644
index 0000000000000000000000000000000000000000..b4dffa9fbf4e7ea6cb457698553b4d8ab2a94c93
GIT binary patch
literal 1350
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~b29fJccY;k5&%M`6N9@k1QvRKq!
z+1jzFkNs1|;z}#M4Ud~EpUz0?mvP(S5bb@)_3C7f1Fbiv6v$>ip6YG(^FVHV>XQp8
zHv_k(bXjL*Udj^BVYw0^!TaF}v$xaMNwZmlLIXXkSE@{z?d^K?>iiwNrGegDOG5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~b29fJccY;k5&%M`6N9@k1QvRKq!
z+1jzFkNs1|;z}#M4Ud~EpUz0?mvP(S5bb@)_3C7f1Fbiv6v$>ip6YG(^FVHV>XQp8
zHv_k(bXjL*Udj^BVYw0^!TaF}v$xaMNwZmlLIXXkSE@{z?d^K?>iiwNrGegDOG5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~b29fJccY;k5&%M`6N9@k1QvRKq!
z+1jzFkNs1|;z}#M4Ud~EpUz0?mvP(S5bb@)_3C7f1Fbiv6v$>ip6YG(^FVHV>XQp8
zHv_k(bXjL*Udj^BVYw0^!TaF}v$xaMNwZmlLIXXkSE@{z?d^K?>iiwNrGegDOG5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~cjmJa#MTdgl1bbrt7P+}^u-QZAF
z+1jzFkNuN{a-Nl4LIWcokEcUpmI$xT&K4rG`z*mY?@Pk
zR!Oi^uH3CF9zwA$7Z(TIr-&sNa>>pJwx#so#(hi=Z)+Ly+?v%>KmDN(J+J48fT(lS%85yhr
DGzS&x

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge04.gif b/mozilla/grendel/ui/images/animation/AnimHuge04.gif
new file mode 100644
index 0000000000000000000000000000000000000000..f8473ac6cbfa931dc949283ff774f291f9af42fe
GIT binary patch
literal 1352
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~cjmJa#MTdgl1bbrt7P+}^u-QZAF
z+1jzFkNuN{a-Nl4LIWcokEcUpmI$xT&K4rG`z*mY?@Pk
zR!Oi^uH3CF9zwA$7Z(TIr-&sNa>>pJwx#so#(hi=Z)+Ly+?v%>KmDN(J+J48fT(lS%85yhr
DGzS&x

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge05.gif b/mozilla/grendel/ui/images/animation/AnimHuge05.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4276f10b0cedcb353c24e24f5a3d612169ffd77e
GIT binary patch
literal 1364
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+5N&28u+y&9=1MptWb#gp=cr6Ce5_tKq{Nu_5;jp0tpSwtXx~Vu
zXOY0jD7NNCQi4Fwi+df0hcZ$o#_CRZ)WxRrvVoD6&Fp3`zer|Ei@MS?hEVk&*Qe7u
zS|z7D7zk-T>WsB3nBC|kn{u-#o^R$|rp#GeZiRDoE)mF$o5yk0i+Rgb@tE9YB@>k15<
zCS_?g&2i?fB_b=<%~A<*U7S{OVTt|DNP+cho2=d}p6Iq^`l4k_UawszZ`&Cdp?vp=
R=ISYOvjr1d!=wZltO2b*7#;us

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge06.gif b/mozilla/grendel/ui/images/animation/AnimHuge06.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4276f10b0cedcb353c24e24f5a3d612169ffd77e
GIT binary patch
literal 1364
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+5N&28u+y&9=1MptWb#gp=cr6Ce5_tKq{Nu_5;jp0tpSwtXx~Vu
zXOY0jD7NNCQi4Fwi+df0hcZ$o#_CRZ)WxRrvVoD6&Fp3`zer|Ei@MS?hEVk&*Qe7u
zS|z7D7zk-T>WsB3nBC|kn{u-#o^R$|rp#GeZiRDoE)mF$o5yk0i+Rgb@tE9YB@>k15<
zCS_?g&2i?fB_b=<%~A<*U7S{OVTt|DNP+cho2=d}p6Iq^`l4k_UawszZ`&Cdp?vp=
R=ISYOvjr1d!=wZltO2b*7#;us

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge07.gif b/mozilla/grendel/ui/images/animation/AnimHuge07.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2989cb11ab65b03e3228b603730dbc174c415341
GIT binary patch
literal 1368
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T}bX>w9(c{#U*I8LQS<_U>W$Jyw<|wfohKavTaw
z+%aZ%0#DgyJZu$G`q044CvjmR^Nd>6fC7P}3i$@XNp1~{a#05su__8VIF#-WG>vbz
z*p}MB$``R=5i^U0fkP(C4AH7
z{k1(3=(9SlYx3H8w@zuz-=6ntm0ObUt(7ax14DFHvwocQW?k}9myFGuwv|lW#O`@I
WbM;j|*ERm9K50!^b(D>T!5RRWJ|N@(

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge08.gif b/mozilla/grendel/ui/images/animation/AnimHuge08.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2989cb11ab65b03e3228b603730dbc174c415341
GIT binary patch
literal 1368
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T}bX>w9(c{#U*I8LQS<_U>W$Jyw<|wfohKavTaw
z+%aZ%0#DgyJZu$G`q044CvjmR^Nd>6fC7P}3i$@XNp1~{a#05su__8VIF#-WG>vbz
z*p}MB$``R=5i^U0fkP(C4AH7
z{k1(3=(9SlYx3H8w@zuz-=6ntm0ObUt(7ax14DFHvwocQW?k}9myFGuwv|lW#O`@I
WbM;j|*ERm9K50!^b(D>T!5RRWJ|N@(

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge09.gif b/mozilla/grendel/ui/images/animation/AnimHuge09.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fa4075a545fa36a24d57d08a1c873d03f69206cf
GIT binary patch
literal 1384
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s2HiV9Ef=~wzcf1Pak
zH-@F#v-u}X|MBhea^2nPcK?4Bzg@c0Z@+Em@1Kt!{9G&&sJ2QcoXz`aJBGZozYAwY6eO
zZcb~LT%MKYv@|2S^5E0U)1@tsUQCX6TM`-`E4%7~uX&`_MOXHdR|Hqi@AI0tYH3tT
zmZMFtZy62stvZXA%qp1};KkS~yn2Ep
o%ayqKPSznSF1&LM*}P&|mgf3ZLd$a2?(TY;v*sL=fB=Iv08;KP5&!@I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge10.gif b/mozilla/grendel/ui/images/animation/AnimHuge10.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fa4075a545fa36a24d57d08a1c873d03f69206cf
GIT binary patch
literal 1384
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s2HiV9Ef=~wzcf1Pak
zH-@F#v-u}X|MBhea^2nPcK?4Bzg@c0Z@+Em@1Kt!{9G&&sJ2QcoXz`aJBGZozYAwY6eO
zZcb~LT%MKYv@|2S^5E0U)1@tsUQCX6TM`-`E4%7~uX&`_MOXHdR|Hqi@AI0tYH3tT
zmZMFtZy62stvZXA%qp1};KkS~yn2Ep
o%ayqKPSznSF1&LM*}P&|mgf3ZLd$a2?(TY;v*sL=fB=Iv08;KP5&!@I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge11.gif b/mozilla/grendel/ui/images/animation/AnimHuge11.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2921cfff2a72b87f9dda5f7599a94ba95c056149
GIT binary patch
literal 1385
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRS%-q*D
zXN~f5k8C`DXernBgDIQ&8CWJne}Boq!63jggNM<)<#al)g~5gcMQ>J4TlxCGU2j8G
z<(sFn5eKRp8(5icvCd*;_`WOk6UVye_xH?v^xPj;9YQKO7PrSt>WGXx
zF*S4nBg?GRtMPLCRy>^+yVX;Mi7()(a^r)gA@^o+>wIXK7jn0(F+j@qSwp}6zM1NU
zYP(pC+DNgM`2L@{Zp%*0)

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge12.gif b/mozilla/grendel/ui/images/animation/AnimHuge12.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2921cfff2a72b87f9dda5f7599a94ba95c056149
GIT binary patch
literal 1385
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRS%-q*D
zXN~f5k8C`DXernBgDIQ&8CWJne}Boq!63jggNM<)<#al)g~5gcMQ>J4TlxCGU2j8G
z<(sFn5eKRp8(5icvCd*;_`WOk6UVye_xH?v^xPj;9YQKO7PrSt>WGXx
zF*S4nBg?GRtMPLCRy>^+yVX;Mi7()(a^r)gA@^o+>wIXK7jn0(F+j@qSwp}6zM1NU
zYP(pC+DNgM`2L@{Zp%*0)

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge13.gif b/mozilla/grendel/ui/images/animation/AnimHuge13.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ea715984a59e38e17d9f4c620e9c96f534c171d1
GIT binary patch
literal 1404
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUlt0zCCYea
zj=rFSt_=g@qa)mc0axc-Ui0v{LabKbZPE83EG#@05f2s^u`nz>Xtl3v>o&h58_ru9
zBq*9Pva_(vT5BV{(vQRXu~%B>4@SlVm9lTPB{VdAW{jO!WcRD&KsF1biu3;$r*0oe
zD)|I*K4Os$@
z%ml+Z#XRoRvX!aIwlK_KODt{VT{^RYS*$~$>8s*0xx$dS*XLD=8*E`<;g;xl*c+pG
zEi^p+X=zxCkj1eJ%%zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUlt0zCCYea
zj=rFSt_=g@qa)mc0axc-Ui0v{LabKbZPE83EG#@05f2s^u`nz>Xtl3v>o&h58_ru9
zBq*9Pva_(vT5BV{(vQRXu~%B>4@SlVm9lTPB{VdAW{jO!WcRD&KsF1biu3;$r*0oe
zD)|I*K4Os$@
z%ml+Z#XRoRvX!aIwlK_KODt{VT{^RYS*$~$>8s*0xx$dS*XLD=8*E`<;g;xl*c+pG
zEi^p+X=zxCkj1eJ%%zf^p>l9H0KahlVt0Hob;|Qtw2GEF&lQ&Us8HCdab=FPxW|MArK?!x>bFLv
z{(R7MbA5QMlEj9vRX4f$1yoqMi{1q{rs{iSY!G8&WZ_<6m(k(y-igKFwV98V$f}mj
z{4x?73}!a4GO@{UTq;WO|yGkDO*$WSF2oZ2%*?><|!_?&Zd
zBvZ1Oq6N3JtogCx+Xq$8|8Y13++sa8b6I%t6hVfVO1`zC4+g7`g&v`R5QaXQ3J)xXttHkDzw`L*BjY{_*35f!sKT~IAOX;=Gh~!#l
z8jv7#Of;^T>)P?u;+&rqy0uB=e=U4*r_W(q*|bRhoD)&WYkgNftXNg3
zeq)j4x{QZR8C_RyE^G{voD;zQ@=7E#x5b77<<4_8ltT|SK6&VMPS+f
zxZQ6yU*&2M$}kXH8nEG^*);8KkN%y?*_4zyJJ0>{vX-^mPOW3n-Sx`px3J$YE$w%E
YW~QZtx;@)=T6l-e*Al^PjEoG{00%}ZX8-^I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge16.gif b/mozilla/grendel/ui/images/animation/AnimHuge16.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fbe6755127565aed15ceeeebaad49cd14d3ccd5d
GIT binary patch
literal 1422
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Hob;|Qtw2GEF&lQ&Us8HCdab=FPxW|MArK?!x>bFLv
z{(R7MbA5QMlEj9vRX4f$1yoqMi{1q{rs{iSY!G8&WZ_<6m(k(y-igKFwV98V$f}mj
z{4x?73}!a4GO@{UTq;WO|yGkDO*$WSF2oZ2%*?><|!_?&Zd
zBvZ1Oq6N3JtogCx+Xq$8|8Y13++sa8b6I%t6hVfVO1`zC4+g7`g&v`R5QaXQ3J)xXttHkDzw`L*BjY{_*35f!sKT~IAOX;=Gh~!#l
z8jv7#Of;^T>)P?u;+&rqy0uB=e=U4*r_W(q*|bRhoD)&WYkgNftXNg3
zeq)j4x{QZR8C_RyE^G{voD;zQ@=7E#x5b77<<4_8ltT|SK6&VMPS+f
zxZQ6yU*&2M$}kXH8nEG^*);8KkN%y?*_4zyJJ0>{vX-^mPOW3n-Sx`px3J$YE$w%E
YW~QZtx;@)=T6l-e*Al^PjEoG{00%}ZX8-^I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge17.gif b/mozilla/grendel/ui/images/animation/AnimHuge17.gif
new file mode 100644
index 0000000000000000000000000000000000000000..593d9b323aeeeb0bd3d572275b9dca8ae41135d9
GIT binary patch
literal 1428
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVtvqneUYsTtwofHNRF>8svmFgZWdg6ThYO0LWms{`dGD}FDvcx0Z&l5SX^2v1Ip|Zq?!T|iyqZV5
zQctd{z1?8V#1X-Mz+IrBqVn?-(fOtqLh|1KaG3e+uK%aMTc$PPwO^`c&;WhhonWmM}oTS&$``?(7kmvI)ht-BQmqHb&CoUw}F~e(ILyP
z4Z$f7pD9jyVOemSL9?&ydd_Ug@@o;xcB;j6Pnh^sKES@z@=2XxmD#ldt)m%o@$r*R
zr1lqCT}=viD4nXDka$z_)@=T-C!aFu7imexN-K&;WI2>t-ASF}b!(1uXRFpl*Sw>Z
z*RC>HHf0C~moZ(t=4K-LG%R$(uW9!i4@7+na_8Jy8ssMU@yJETh2NGubw6`0<;vQ1
zcCW96-aIvBjnjr(w{l%A?@kGEe5CaCRp^Vl+qyd*M7`GCBodqbdi6)Kk~Ql!x2}F0
ge&N`Xb(36vzMi)6tkLeEZReb3>u>$R#=>9?0E|F4(EtDd

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge18.gif b/mozilla/grendel/ui/images/animation/AnimHuge18.gif
new file mode 100644
index 0000000000000000000000000000000000000000..593d9b323aeeeb0bd3d572275b9dca8ae41135d9
GIT binary patch
literal 1428
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVtvqneUYsTtwofHNRF>8svmFgZWdg6ThYO0LWms{`dGD}FDvcx0Z&l5SX^2v1Ip|Zq?!T|iyqZV5
zQctd{z1?8V#1X-Mz+IrBqVn?-(fOtqLh|1KaG3e+uK%aMTc$PPwO^`c&;WhhonWmM}oTS&$``?(7kmvI)ht-BQmqHb&CoUw}F~e(ILyP
z4Z$f7pD9jyVOemSL9?&ydd_Ug@@o;xcB;j6Pnh^sKES@z@=2XxmD#ldt)m%o@$r*R
zr1lqCT}=viD4nXDka$z_)@=T-C!aFu7imexN-K&;WI2>t-ASF}b!(1uXRFpl*Sw>Z
z*RC>HHf0C~moZ(t=4K-LG%R$(uW9!i4@7+na_8Jy8ssMU@yJETh2NGubw6`0<;vQ1
zcCW96-aIvBjnjr(w{l%A?@kGEe5CaCRp^Vl+qyd*M7`GCBodqbdi6)Kk~Ql!x2}F0
ge&N`Xb(36vzMi)6tkLeEZReb3>u>$R#=>9?0E|F4(EtDd

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge19.gif b/mozilla/grendel/ui/images/animation/AnimHuge19.gif
new file mode 100644
index 0000000000000000000000000000000000000000..78ac51461c498030df3fc3fb1b3a81e28c746a5f
GIT binary patch
literal 1475
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt-Q-5b!3I_){rIXa8XKOt
z^oaQf)ofr`a=f3-rm4o^z`+x2rny2cRufhp=ogb=n40xP(Va2z}YMCdLQzl;q2;`X1BXUS6Kl#NoAU
z+l^^TmoCjww>OEo#&Gc%zn;-q-@qmL?HpM
zC=L^iYm@%vnm%!}>%0@y+SaKs}(%PlD^I0uLXZUG2N?&&@w7osm#pGj|o*Rp;g#O<6qpae)e{Wj-
z&iAF+)cmj#!I<~k-mBhOxBu6r(*_4#KjO|mvhSz$`@=uJ-M(`8ncSau2iR6l%XN)Y
fyt6L+cazQaBRuy{7o6m8ezWmNajZ>%1A{dHq6btn

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge20.gif b/mozilla/grendel/ui/images/animation/AnimHuge20.gif
new file mode 100644
index 0000000000000000000000000000000000000000..78ac51461c498030df3fc3fb1b3a81e28c746a5f
GIT binary patch
literal 1475
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt-Q-5b!3I_){rIXa8XKOt
z^oaQf)ofr`a=f3-rm4o^z`+x2rny2cRufhp=ogb=n40xP(Va2z}YMCdLQzl;q2;`X1BXUS6Kl#NoAU
z+l^^TmoCjww>OEo#&Gc%zn;-q-@qmL?HpM
zC=L^iYm@%vnm%!}>%0@y+SaKs}(%PlD^I0uLXZUG2N?&&@w7osm#pGj|o*Rp;g#O<6qpae)e{Wj-
z&iAF+)cmj#!I<~k-mBhOxBu6r(*_4#KjO|mvhSz$`@=uJ-M(`8ncSau2iR6l%XN)Y
fyt6L+cazQaBRuy{7o6m8ezWmNajZ>%1A{dHq6btn

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge21.gif b/mozilla/grendel/ui/images/animation/AnimHuge21.gif
new file mode 100644
index 0000000000000000000000000000000000000000..43ddf321c0e106af767a9c279a5a27ccfbd81fdf
GIT binary patch
literal 1530
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVtuG8)@6CdQ5D_`Qt!@_nD}a{=9g!F3>us|tOnPa7+CDA*Bz-4a`K3J@DgT1Unm(^_5uB#>UgO;!*$26B-!4
zzIh^_(tcNTH|snfZ)Vx(x-BKl#T&WR6As$Eef`ApgIrSk%=W;_4OgemWtDj8KCkk<
z?g!aY-G}aRWiqB$O<6y$
zB`SQhT$JMA*uAXbl3(@9`Qkhomwazd3rpT{L99DvZLZL+YdiC{&VIe?p3^V6;F?aC
zDGsalonE)a^HZfzi1FT48ywDed9RJiy1HbICqs*O&W;_4TQ_)~61!Tkt|sryTZdC#
zTQ=-IYGn|Z!seT^=_Gq>Pkx~3-_?dkjVf*4#!NlvbJzf^p>l9H0KahlVtuG8)@6CdQ5D_`Qt!@_nD}a{=9g!F3>us|tOnPa7+CDA*Bz-4a`K3J@DgT1Unm(^_5uB#>UgO;!*$26B-!4
zzIh^_(tcNTH|snfZ)Vx(x-BKl#T&WR6As$Eef`ApgIrSk%=W;_4OgemWtDj8KCkk<
z?g!aY-G}aRWiqB$O<6y$
zB`SQhT$JMA*uAXbl3(@9`Qkhomwazd3rpT{L99DvZLZL+YdiC{&VIe?p3^V6;F?aC
zDGsalonE)a^HZfzi1FT48ywDed9RJiy1HbICqs*O&W;_4TQ_)~61!Tkt|sryTZdC#
zTQ=-IYGn|Z!seT^=_Gq>Pkx~3-_?dkjVf*4#!NlvbJzf^p>l9H0KahlVtuG8)@6CdQ5D_`Qt!@_nD}a{=9g!F3>us|tOnPa7+CDA*Bz-4a`K3J@DgT1Unm(^_5uB#>UgO;!*$26B-!4
zzIh^_(tcNTH|snfZ)Vx(x-BKl#T&WR6As$Eef`ApgIrSk%=W;_4OgemWtDj8KCkk<
z?g!aY-G}aRWiqB$O<6y$
zB`SQhT$JMA*uAXbl3(@9`Qkhomwazd3rpT{L99DvZLZL+YdiC{&VIe?p3^V6;F?aC
zDGsalonE)a^HZfzi1FT48ywDed9RJiy1HbICqs*O&W;_4TQ_)~61!Tkt|sryTZdC#
zTQ=-IYGn|Z!seT^=_Gq>Pkx~3-_?dkjVf*4#!NlvbJzf^p>l9H0KahlVtuG8)@6CdQ5D_`Qt!@_nD}a{=9g!F3>us|tOnPa7+CDA*Bz-4a`K3J@DgT1Unm(^_5uB#>UgO;!*$26B-!4
zzIh^_(tcNTH|snfZ)Vx(x-BKl#T&WR6As$Eef`ApgIrSk%=W;_4OgemWtDj8KCkk<
z?g!aY-G}aRWiqB$O<6y$
zB`SQhT$JMA*uAXbl3(@9`Qkhomwazd3rpT{L99DvZLZL+YdiC{&VIe?p3^V6;F?aC
zDGsalonE)a^HZfzi1FT48ywDed9RJiy1HbICqs*O&W;_4TQ_)~61!Tkt|sryTZdC#
zTQ=-IYGn|Z!seT^=_Gq>Pkx~3-_?dkjVf*4#!NlvbJzf^p>l9H0KahlVt-Q-5b!3I_){rIXa8XKOt
z^oaQf)ofr`a=f3-rm4o^z`+x2rny2cRufhp=ogb=n40xP(Va2z}YMCdLQzl;q2;`X1BXUS6Kl#NoAU
z+l^^TmoCjww>OEo#&Gc%zn;-q-@qmL?HpM
zC=L^iYm@%vnm%!}>%0@y+SaKs}(%PlD^I0uLXZUG2N?&&@w7osm#pGj|o*Rp;g#O<6qpae)e{Wj-
z&iAF+)cmj#!I<~k-mBhOxBu6r(*_4#KjO|mvhSz$`@=uJ-M(`8ncSau2iR6l%XN)Y
fyt6L+cazQaBRuy{7o6m8ezWmNajZ>%1A{dHq6btn

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge26.gif b/mozilla/grendel/ui/images/animation/AnimHuge26.gif
new file mode 100644
index 0000000000000000000000000000000000000000..78ac51461c498030df3fc3fb1b3a81e28c746a5f
GIT binary patch
literal 1475
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt-Q-5b!3I_){rIXa8XKOt
z^oaQf)ofr`a=f3-rm4o^z`+x2rny2cRufhp=ogb=n40xP(Va2z}YMCdLQzl;q2;`X1BXUS6Kl#NoAU
z+l^^TmoCjww>OEo#&Gc%zn;-q-@qmL?HpM
zC=L^iYm@%vnm%!}>%0@y+SaKs}(%PlD^I0uLXZUG2N?&&@w7osm#pGj|o*Rp;g#O<6qpae)e{Wj-
z&iAF+)cmj#!I<~k-mBhOxBu6r(*_4#KjO|mvhSz$`@=uJ-M(`8ncSau2iR6l%XN)Y
fyt6L+cazQaBRuy{7o6m8ezWmNajZ>%1A{dHq6btn

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge27.gif b/mozilla/grendel/ui/images/animation/AnimHuge27.gif
new file mode 100644
index 0000000000000000000000000000000000000000..593d9b323aeeeb0bd3d572275b9dca8ae41135d9
GIT binary patch
literal 1428
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVtvqneUYsTtwofHNRF>8svmFgZWdg6ThYO0LWms{`dGD}FDvcx0Z&l5SX^2v1Ip|Zq?!T|iyqZV5
zQctd{z1?8V#1X-Mz+IrBqVn?-(fOtqLh|1KaG3e+uK%aMTc$PPwO^`c&;WhhonWmM}oTS&$``?(7kmvI)ht-BQmqHb&CoUw}F~e(ILyP
z4Z$f7pD9jyVOemSL9?&ydd_Ug@@o;xcB;j6Pnh^sKES@z@=2XxmD#ldt)m%o@$r*R
zr1lqCT}=viD4nXDka$z_)@=T-C!aFu7imexN-K&;WI2>t-ASF}b!(1uXRFpl*Sw>Z
z*RC>HHf0C~moZ(t=4K-LG%R$(uW9!i4@7+na_8Jy8ssMU@yJETh2NGubw6`0<;vQ1
zcCW96-aIvBjnjr(w{l%A?@kGEe5CaCRp^Vl+qyd*M7`GCBodqbdi6)Kk~Ql!x2}F0
ge&N`Xb(36vzMi)6tkLeEZReb3>u>$R#=>9?0E|F4(EtDd

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge28.gif b/mozilla/grendel/ui/images/animation/AnimHuge28.gif
new file mode 100644
index 0000000000000000000000000000000000000000..593d9b323aeeeb0bd3d572275b9dca8ae41135d9
GIT binary patch
literal 1428
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVtvqneUYsTtwofHNRF>8svmFgZWdg6ThYO0LWms{`dGD}FDvcx0Z&l5SX^2v1Ip|Zq?!T|iyqZV5
zQctd{z1?8V#1X-Mz+IrBqVn?-(fOtqLh|1KaG3e+uK%aMTc$PPwO^`c&;WhhonWmM}oTS&$``?(7kmvI)ht-BQmqHb&CoUw}F~e(ILyP
z4Z$f7pD9jyVOemSL9?&ydd_Ug@@o;xcB;j6Pnh^sKES@z@=2XxmD#ldt)m%o@$r*R
zr1lqCT}=viD4nXDka$z_)@=T-C!aFu7imexN-K&;WI2>t-ASF}b!(1uXRFpl*Sw>Z
z*RC>HHf0C~moZ(t=4K-LG%R$(uW9!i4@7+na_8Jy8ssMU@yJETh2NGubw6`0<;vQ1
zcCW96-aIvBjnjr(w{l%A?@kGEe5CaCRp^Vl+qyd*M7`GCBodqbdi6)Kk~Ql!x2}F0
ge&N`Xb(36vzMi)6tkLeEZReb3>u>$R#=>9?0E|F4(EtDd

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge29.gif b/mozilla/grendel/ui/images/animation/AnimHuge29.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fbe6755127565aed15ceeeebaad49cd14d3ccd5d
GIT binary patch
literal 1422
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Hob;|Qtw2GEF&lQ&Us8HCdab=FPxW|MArK?!x>bFLv
z{(R7MbA5QMlEj9vRX4f$1yoqMi{1q{rs{iSY!G8&WZ_<6m(k(y-igKFwV98V$f}mj
z{4x?73}!a4GO@{UTq;WO|yGkDO*$WSF2oZ2%*?><|!_?&Zd
zBvZ1Oq6N3JtogCx+Xq$8|8Y13++sa8b6I%t6hVfVO1`zC4+g7`g&v`R5QaXQ3J)xXttHkDzw`L*BjY{_*35f!sKT~IAOX;=Gh~!#l
z8jv7#Of;^T>)P?u;+&rqy0uB=e=U4*r_W(q*|bRhoD)&WYkgNftXNg3
zeq)j4x{QZR8C_RyE^G{voD;zQ@=7E#x5b77<<4_8ltT|SK6&VMPS+f
zxZQ6yU*&2M$}kXH8nEG^*);8KkN%y?*_4zyJJ0>{vX-^mPOW3n-Sx`px3J$YE$w%E
YW~QZtx;@)=T6l-e*Al^PjEoG{00%}ZX8-^I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge30.gif b/mozilla/grendel/ui/images/animation/AnimHuge30.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fbe6755127565aed15ceeeebaad49cd14d3ccd5d
GIT binary patch
literal 1422
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Hob;|Qtw2GEF&lQ&Us8HCdab=FPxW|MArK?!x>bFLv
z{(R7MbA5QMlEj9vRX4f$1yoqMi{1q{rs{iSY!G8&WZ_<6m(k(y-igKFwV98V$f}mj
z{4x?73}!a4GO@{UTq;WO|yGkDO*$WSF2oZ2%*?><|!_?&Zd
zBvZ1Oq6N3JtogCx+Xq$8|8Y13++sa8b6I%t6hVfVO1`zC4+g7`g&v`R5QaXQ3J)xXttHkDzw`L*BjY{_*35f!sKT~IAOX;=Gh~!#l
z8jv7#Of;^T>)P?u;+&rqy0uB=e=U4*r_W(q*|bRhoD)&WYkgNftXNg3
zeq)j4x{QZR8C_RyE^G{voD;zQ@=7E#x5b77<<4_8ltT|SK6&VMPS+f
zxZQ6yU*&2M$}kXH8nEG^*);8KkN%y?*_4zyJJ0>{vX-^mPOW3n-Sx`px3J$YE$w%E
YW~QZtx;@)=T6l-e*Al^PjEoG{00%}ZX8-^I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge31.gif b/mozilla/grendel/ui/images/animation/AnimHuge31.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ea715984a59e38e17d9f4c620e9c96f534c171d1
GIT binary patch
literal 1404
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUlt0zCCYea
zj=rFSt_=g@qa)mc0axc-Ui0v{LabKbZPE83EG#@05f2s^u`nz>Xtl3v>o&h58_ru9
zBq*9Pva_(vT5BV{(vQRXu~%B>4@SlVm9lTPB{VdAW{jO!WcRD&KsF1biu3;$r*0oe
zD)|I*K4Os$@
z%ml+Z#XRoRvX!aIwlK_KODt{VT{^RYS*$~$>8s*0xx$dS*XLD=8*E`<;g;xl*c+pG
zEi^p+X=zxCkj1eJ%%zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUlt0zCCYea
zj=rFSt_=g@qa)mc0axc-Ui0v{LabKbZPE83EG#@05f2s^u`nz>Xtl3v>o&h58_ru9
zBq*9Pva_(vT5BV{(vQRXu~%B>4@SlVm9lTPB{VdAW{jO!WcRD&KsF1biu3;$r*0oe
zD)|I*K4Os$@
z%ml+Z#XRoRvX!aIwlK_KODt{VT{^RYS*$~$>8s*0xx$dS*XLD=8*E`<;g;xl*c+pG
zEi^p+X=zxCkj1eJ%%zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRS%-q*D
zXN~f5k8C`DXernBgDIQ&8CWJne}Boq!63jggNM<)<#al)g~5gcMQ>J4TlxCGU2j8G
z<(sFn5eKRp8(5icvCd*;_`WOk6UVye_xH?v^xPj;9YQKO7PrSt>WGXx
zF*S4nBg?GRtMPLCRy>^+yVX;Mi7()(a^r)gA@^o+>wIXK7jn0(F+j@qSwp}6zM1NU
zYP(pC+DNgM`2L@{Zp%*0)

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge34.gif b/mozilla/grendel/ui/images/animation/AnimHuge34.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2921cfff2a72b87f9dda5f7599a94ba95c056149
GIT binary patch
literal 1385
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRS%-q*D
zXN~f5k8C`DXernBgDIQ&8CWJne}Boq!63jggNM<)<#al)g~5gcMQ>J4TlxCGU2j8G
z<(sFn5eKRp8(5icvCd*;_`WOk6UVye_xH?v^xPj;9YQKO7PrSt>WGXx
zF*S4nBg?GRtMPLCRy>^+yVX;Mi7()(a^r)gA@^o+>wIXK7jn0(F+j@qSwp}6zM1NU
zYP(pC+DNgM`2L@{Zp%*0)

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge35.gif b/mozilla/grendel/ui/images/animation/AnimHuge35.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fa4075a545fa36a24d57d08a1c873d03f69206cf
GIT binary patch
literal 1384
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s2HiV9Ef=~wzcf1Pak
zH-@F#v-u}X|MBhea^2nPcK?4Bzg@c0Z@+Em@1Kt!{9G&&sJ2QcoXz`aJBGZozYAwY6eO
zZcb~LT%MKYv@|2S^5E0U)1@tsUQCX6TM`-`E4%7~uX&`_MOXHdR|Hqi@AI0tYH3tT
zmZMFtZy62stvZXA%qp1};KkS~yn2Ep
o%ayqKPSznSF1&LM*}P&|mgf3ZLd$a2?(TY;v*sL=fB=Iv08;KP5&!@I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge36.gif b/mozilla/grendel/ui/images/animation/AnimHuge36.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fa4075a545fa36a24d57d08a1c873d03f69206cf
GIT binary patch
literal 1384
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s2HiV9Ef=~wzcf1Pak
zH-@F#v-u}X|MBhea^2nPcK?4Bzg@c0Z@+Em@1Kt!{9G&&sJ2QcoXz`aJBGZozYAwY6eO
zZcb~LT%MKYv@|2S^5E0U)1@tsUQCX6TM`-`E4%7~uX&`_MOXHdR|Hqi@AI0tYH3tT
zmZMFtZy62stvZXA%qp1};KkS~yn2Ep
o%ayqKPSznSF1&LM*}P&|mgf3ZLd$a2?(TY;v*sL=fB=Iv08;KP5&!@I

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge37.gif b/mozilla/grendel/ui/images/animation/AnimHuge37.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2989cb11ab65b03e3228b603730dbc174c415341
GIT binary patch
literal 1368
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T}bX>w9(c{#U*I8LQS<_U>W$Jyw<|wfohKavTaw
z+%aZ%0#DgyJZu$G`q044CvjmR^Nd>6fC7P}3i$@XNp1~{a#05su__8VIF#-WG>vbz
z*p}MB$``R=5i^U0fkP(C4AH7
z{k1(3=(9SlYx3H8w@zuz-=6ntm0ObUt(7ax14DFHvwocQW?k}9myFGuwv|lW#O`@I
WbM;j|*ERm9K50!^b(D>T!5RRWJ|N@(

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge38.gif b/mozilla/grendel/ui/images/animation/AnimHuge38.gif
new file mode 100644
index 0000000000000000000000000000000000000000..2989cb11ab65b03e3228b603730dbc174c415341
GIT binary patch
literal 1368
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T}bX>w9(c{#U*I8LQS<_U>W$Jyw<|wfohKavTaw
z+%aZ%0#DgyJZu$G`q044CvjmR^Nd>6fC7P}3i$@XNp1~{a#05su__8VIF#-WG>vbz
z*p}MB$``R=5i^U0fkP(C4AH7
z{k1(3=(9SlYx3H8w@zuz-=6ntm0ObUt(7ax14DFHvwocQW?k}9myFGuwv|lW#O`@I
WbM;j|*ERm9K50!^b(D>T!5RRWJ|N@(

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge39.gif b/mozilla/grendel/ui/images/animation/AnimHuge39.gif
new file mode 100644
index 0000000000000000000000000000000000000000..4276f10b0cedcb353c24e24f5a3d612169ffd77e
GIT binary patch
literal 1364
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+5N&28u+y&9=1MptWb#gp=cr6Ce5_tKq{Nu_5;jp0tpSwtXx~Vu
zXOY0jD7NNCQi4Fwi+df0hcZ$o#_CRZ)WxRrvVoD6&Fp3`zer|Ei@MS?hEVk&*Qe7u
zS|z7D7zk-T>WsB3nBC|kn{u-#o^R$|rp#GeZiRDoE)mF$o5yk0i+Rgb@tE9YB@>k15<
zCS_?g&2i?fB_b=<%~A<*U7S{OVTt|DNP+cho2=d}p6Iq^`l4k_UawszZ`&Cdp?vp=
R=ISYOvjr1d!=wZltO2b*7#;us

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/AnimHuge40.gif b/mozilla/grendel/ui/images/animation/AnimHuge40.gif
new file mode 100644
index 0000000000000000000000000000000000000000..f8473ac6cbfa931dc949283ff774f291f9af42fe
GIT binary patch
literal 1352
zcmZ?wbh9u|)L_tH_|5zf^p>l9H0KahlVt0Ho^~=|(NE9t|o+~WxQK7I^-KuGGBgRleo$P9?9=o4Xl-(mvS}A-#BrmUsk)j(?FkT
zhGo1`LsSI=(~-_%qj?=S3{ocOi5`F3^RrRSe)Zee
zDrcqMxz&+8zqLVuq3QB2S@Rd&zg{wMFm(C1UHJO*?PiWm^7s29${9}Y=~wzcf1Pak
zH-@F#qxIg+`0?%Xmb=2!>;LZj_T~2G$=mAf{#U)dDVQEtH^Y-bfvJ!2#l7mEx)nEB
z+0)n~nwXqc%xxBQyD_&#Mr?&_D|bqTe1qaF_m~cjmJa#MTdgl1bbrt7P+}^u-QZAF
z+1jzFkNuN{a-Nl4LIWcokEcUpmI$xT&K4rG`z*mY?@Pk
zR!Oi^uH3CF9zwA$7Z(TIr-&sNa>>pJwx#so#(hi=Z)+Ly+?v%>KmDN(J+J48fT(lS%85yhr
DGzS&x

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/animation/README b/mozilla/grendel/ui/images/animation/README
new file mode 100644
index 00000000000..fd9524084fe
--- /dev/null
+++ b/mozilla/grendel/ui/images/animation/README
@@ -0,0 +1,16 @@
+#
+# The contents of this directory are subject to the Mozilla Public
+# License Version 1.0 (the "License"); you may not use this file except
+# in compliance with the License.  You may obtain a copy of the License
+# at http://www.mozilla.org/MPL/
+# 
+# Software distributed under the License is distributed on an "AS IS"
+# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+# the License for the specific language governing rights and limitations
+# under the License.
+# 
+# The Original Code is the Grendel mail/news client.
+# 
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation.  Portions created by Netscape are Copyright (C) 1997
+# Netscape Communications Corporation.  All Rights Reserved.
diff --git a/mozilla/grendel/ui/images/biffNew.gif b/mozilla/grendel/ui/images/biffNew.gif
new file mode 100644
index 0000000000000000000000000000000000000000..0a8f200066f2e25cdac17f8bf9c2c9b944b5d8de
GIT binary patch
literal 160
zcmZ?wbhEHblwjat*v!E2|Nnmmh6W&g;J^V8-O#`Q6!`yNDn4DmuES~8a*!BM6pVK*
zSg!b!g^>XWbU*~i3XWbU*~i3!j5`a~X_u!ZAK3P^yZ5QibqxcH
e-LpXWbU*~i3JtxIRIwJkB5xul%6Z_3<<3g?Tj1x0Y=Y}vDM
zhX6;z%a08PxBEC&S=MY9J-+pGYR!y{=eJfXcFxMol=1bHd9lUH_GGS#AcHjk_2WHK

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/connected-small.gif b/mozilla/grendel/ui/images/connected-small.gif
new file mode 100644
index 0000000000000000000000000000000000000000..9071fceda3366548cc79b797199cce2af2fdc0a0
GIT binary patch
literal 112
zcmZ?wbhEHb6krfw*vtR|4Pe{=rW$~}hK2(N4m310{0EBvXZR1MfHaT=1d2ad7#SFN
x8FYY>KobNQ7+7RCob+71*W&fr-Rm3Jbsm~cyLGac`{v4J(u*bcgb6ZO0|446C&d5&

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/deleted.gif b/mozilla/grendel/ui/images/deleted.gif
new file mode 100644
index 0000000000000000000000000000000000000000..652d2a368449642d15f410d26324b54b6a234a57
GIT binary patch
literal 118
zcmZ?wbhEHb6krfw*vtR|4Pe{=rW$~}hK2(N4m310{0EBvXZR1MfHaT=1d2ad7#SFN
z8FYY>KobNQ7+91yob+71*Ft@F3>1H|FfuSOGw6VLAhit4
zo))|A2pf2bckLCs;KZR96I4$kWfddB^8XEpH{QnOWZ)lh~^FJ6cAOppp
zEQ|~cYz#UeL68{?ETI=pc&e_LF>m&3L56gm8y`C_IfN;isjN<2pvkz}VbYE+r#}ty
ziVX{Rs!p^fZIloT-rE$Sm@B02zU1038P}z)a^kCF^^OND{8fIGEuJ~RLT

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/read.gif b/mozilla/grendel/ui/images/read.gif
new file mode 100644
index 0000000000000000000000000000000000000000..1431b5da2be9cd9b22b8501e2ad7d22e3d9f20a0
GIT binary patch
literal 50
zcmZ?wbhEHb0r5dH3`}w@`HiPf@yo;%&C5!5vuNe7
UyKSnrG^}hg*T(+aHmnTR0A=D6ssI20

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/splitRight.gif b/mozilla/grendel/ui/images/splitRight.gif
new file mode 100644
index 0000000000000000000000000000000000000000..aeace9aab57b78c45b9c81d7f81da87baa2f7a6d
GIT binary patch
literal 70
zcmZ?wbhEHb6krfwXkY+=|Ns9h{$ycfU|?j>0r5dH3{0{u{Eeqi&3{lVbTLcsn(2@K
UzB5ea=9D0r5dH3`|lj{Eeqi&3{lVbTLcsn)(m^
RHW{~R&pMZB1v4;M0{}3t6Egq+

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/stacked.gif b/mozilla/grendel/ui/images/stacked.gif
new file mode 100644
index 0000000000000000000000000000000000000000..173cb1d8f21f2a03d16b823b81e6c8372dc4b525
GIT binary patch
literal 61
zcmZ?wbhEHb6krfwXkY+=|Ns9h{$ycfU|?j>0r5dH3``;|{VPwu<@cSj<(9zadp39d
LOCRkJVXy`O%)k;Q

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/storeLocal-small.gif b/mozilla/grendel/ui/images/storeLocal-small.gif
new file mode 100644
index 0000000000000000000000000000000000000000..7b51d9558094902fb155887172885e1a6af36916
GIT binary patch
literal 130
zcmZ?wbhEHb6krfwIK;qUY@C*scILo=1OFNRH#9WNocSLJ7#J87f3h$#FfcRd0NFs5
z0zkIA#l|}}1~WWYGX_NN((9a@TykJ}ZYXD;Z)%87tO3f9CM#fI`4S4K@ak
gh_fGv~hQf98gWISdTe0NPqGkN^Mx

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/storeRemote-small.gif b/mozilla/grendel/ui/images/storeRemote-small.gif
new file mode 100644
index 0000000000000000000000000000000000000000..93721e0c14060acc5cea844dd45f0a2b09b6aca7
GIT binary patch
literal 128
zcmZ?wbhEHb6krfwIK;qUY@C*scILo=1OFNRH#9WNocSLJ7#J87f3h$#FfcRd0NFs5
z0zkH_#l|}}1~WWYGX_NN((9a@TykJ}ZYXD;Z)%87tO3f9CM#fP#UujuANfddB^8XEpH{BLMzm^t%57%(t2FgVRx
z&Hw|7KUo+V7}yweKms5$7+6X!obX(|cLvku@^g39s}?29$T$|#GI57nc2vIM%x!;_
zVz#oi9bNKOYo4C=LKgPTA3a2#3VfT?+0uT)&~!Hkd)u0mJ6^l;Y8Pyq+FYu7XbEFZ
rPyp}wt`F8LB}?4JYLd$J)dGaG%qw~-m4qu5`-M6tPn#|&$Y2cs8tF`c

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/markAllRead-pressed.gif b/mozilla/grendel/ui/images/toolbar/markAllRead-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ce824fe86bcf15a07fe026c7bbcee0351329018e
GIT binary patch
literal 216
zcmZ?wbhEHb6lV}+c+9|HY@C*sc80;+=D>ji3=Iwc8U8mkG|Zg&9}E~68W@z6l$>TQ
zXMh34pDc_F46F<~AOVmW3@oi1PI#`~Yw?X&9ANfddB^8XEpH{BLMzm^t%57%(t2FeoW0
zF~ET0PZmZ71~vvAkO0UG2A0+fCp=g0ov}`V<@AGZjSp5x9>20ydmHZxMFG!P;SPon
zZy}uxhu1jhTq)XpQtbotB9VX>fycAiSUHofb!^d@qv;q@_*zB#AZymNzpPW+P(B#Aw71h>|U{sRKk|yq`Wx+ACSCWB)Vg3RY5eBA3bC#_T
I6J)Rk0H_X44*&oF

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/markAllRead.gif b/mozilla/grendel/ui/images/toolbar/markAllRead.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dbb7b414f2cb815c456f014e138a539d7eb66177
GIT binary patch
literal 216
zcmZ?wbhEHb6lV}+c+9|HY@EjMpCK*n41>ANfddB^8XEpH{BLMzm^t%57%(t2FgVRx
z&Hw|7KUo+V7}yweKms5$7+6{_obX(|cSh6R&ClaFBDxEGoK`KnafoM`qJU?taEC#V
z_aTuDhu1jhTq&~d>=umBNic4GyJGc@IgXA^J4}^4&)>+IrLbhulmyR?=(SP$!F^`A
zi+T6PPYWQ!BX=fPBZ4Mkb@Sov-Lqo&Nng4+REQwAi{$ycfU|?m?
z0ZD+&U|=cQa3WH`&?Gxc_g;c)N%)G1$2>TmwXB-9VOEw`)Ipw8-E$y0&ihw2jfNQ)PO-E_Jxs99;G#_NQM;
ck-W55Rc)jZi*7?_X^UE9lhK5NX@U&a0LLjr3IG5A

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgDelete-pressed.gif b/mozilla/grendel/ui/images/toolbar/msgDelete-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d0bef06586afa641435ca912ea101faa621f29bf
GIT binary patch
literal 223
zcmZ?wbhEHb6lV}+c+3C-#>Q!BX=fPBZ4Mkb@Sov-Lqo&Nng4+REQwAi{$ycfU|?m?
z0ZD+&U|{LlaKdx-USY-N-B+ZZM@lu$c=ThgwDywNo3oY~aL6^zW9wyMka_0%`fObT
zW17H)ja61nj2degm;zSjSFy%4F*fpxvXwFUG(Xg2h&$h&eSmkxY}-a=Gkbk@H#TOL
z8g?end}T)t{n|DIiDLFCM
zz{a2h5(AmRz%u#5iOe7_lk67kOaTOjfyBYA6Bb|8ZSLgfh*QAZ0WKb
zoK+mmB}_VO)&`9MEE#s}3I!aE2KoM4sai~&EDYAc{fcfJZu0}$RAZ~=8%@w*m^*Kt
M*1Gm>LV^s|0P#{rHvj+t

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgDelete.gif b/mozilla/grendel/ui/images/toolbar/msgDelete.gif
new file mode 100644
index 0000000000000000000000000000000000000000..81cf0b29506d09196a063bf321e673369d6b2857
GIT binary patch
literal 224
zcmZ?wbhEHb6lV}+c+3C-#>Q!BX=fPBZ4Mkb@Sov-Lqo&Nng4+REQwAi{$ycfU|?m?
z0ZD+&U|{Lpa3WH`$Rs;U_g=!)%7tc9b3aUd-oeov(DlNUt$}A%Y^?GQ#tZ?4IX!O7
ze$yFQcsw+eIuGX-F)|iaos4p1T*Nq4AV_YG&iZ3>NkW9vHJ)
z>a&y?BzGiawX!f3FllK^MW(efM@&dobYXXi%(t8ro|cv0$2`lJVfq3~t;P)^f(+IG
DzOhFW

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgForward-disabled.gif b/mozilla/grendel/ui/images/toolbar/msgForward-disabled.gif
new file mode 100644
index 0000000000000000000000000000000000000000..9b771484175adae567b2af4b7b4a3438eff9125c
GIT binary patch
literal 212
zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1WHOu3=BX46rlK%
zg^_`Qg+T`-3NnL%rE!9cCzIxtBa4|Eics?5vl_N`}$pAy(J$yX-Nc82`)x$`D2aGxV9$Y2csOp#Bn

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgForward-pressed.gif b/mozilla/grendel/ui/images/toolbar/msgForward-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..04a5873bd3050fed39863d1b17264b3512e5c6fd
GIT binary patch
literal 220
zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNR|7T#BIrG1gl9JP`k=KA_M^X7}qDOoCBx^h*VAcHjk
D_{~YL

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgForward-rollover.gif b/mozilla/grendel/ui/images/toolbar/msgForward-rollover.gif
new file mode 100644
index 0000000000000000000000000000000000000000..7a87cb0a7c2abe3aef4943f45f282ed955867145
GIT binary patch
literal 247
zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1`J9{N(>A@F$7Tj
z$->CMz`~#d5(1gQz_Mh6Oyr?0rFzc0)Q<~Ixg3w0iTiCv-Q-#wR2uGfbY6%szMC+!?bX7S9dlGS_Kc#lV!p
kuzZD1$L12(^|7;hwbpFkI7_Z&!~V&84^QrO5@fIj04LI2vH$=8

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgForward.gif b/mozilla/grendel/ui/images/toolbar/msgForward.gif
new file mode 100644
index 0000000000000000000000000000000000000000..935e92d1d96a806d1620d5d302402e71ba3e2ac8
GIT binary patch
literal 244
zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1WHOu3=BX46rlK%
zg^_`Qg+T`-3NnL%W#I;y$U|F7wVZdU9~TJmFr6TpP$0ihp+M(w`>8rsqZfbHX{bkY
zvq*FF9Y4q)7|k2MJHuaHU|Uhh5{IY_bu5<~e<*lJ?47qZBISzgnbwx92my^(NE=7W8`7yd{17
zDVlM4>Rl~Gw?g|Y!c#kyE={bDgM{Ws6IVcJo4+rOQ_>7i6#o
E013cQCjbBd

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgGetNew-pressed.gif b/mozilla/grendel/ui/images/toolbar/msgGetNew-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3094330a5b3aa121b18186848cc37b13b6d25dbf
GIT binary patch
literal 242
zcmZ?wbhEHb6lV}+c+9}iz`$T^oR*e$hQZwCz<~q*8U8mkG|Zg&9|)9`lo%NPGoS*+
zpDc_F46F<~AYqUh3@r0EobX(|RPnjUnZJh=<(SkpPnmPNc+c|SIdk7*&q0~@S%r60
zeHXMkaB4|~YaaCZ!(n|WP^f~5m2pj@!33qWhRlV(lvAy@o0zBxOy+-{D=87&VyLt>
z(Vky{uUw>{s@R^XojY(jsl;$+dxB9@v=%PtYN;(5hPGsW0+Qm6GS
e6lY7B;J|1rys?#K>$XLkcJC40xL;n7!5RQ?(@&8A

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgGetNew-rollover.gif b/mozilla/grendel/ui/images/toolbar/msgGetNew-rollover.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3c86b34dd5ba6788aeef703fc9018096d6612ceb
GIT binary patch
literal 269
zcmZ?wbhEHb6lV}+c+9|HY@C*sc80;+=D>ji{~7-OXJBY(XqY+kKNv75DJd~90L2hM
z@h1x-0|N_#4oC=O1_R5k2`!eQ3Kp+*`|TSi>2hyp2~5%rmyQKlh;C03>Esk?o-%<^(P2Rn>#7uiuNl>@E5ma_H*VX0MWGji{~7-OXJBY(XqY+kKNv75DJd~90L2hM
z@h1x-0|N_#4oC=O1_R5s2`!eQ2F)+dR$4p|jJUO_B%?v0*<+oD;^JF|JRRI8K6vGu
z%O+Gf&tRIXs9+IfA*vlCvXb+`43SnwMTZqBtgD_}W9r=&D3-HnZ|RIEY~w!kd#$Vm#(Z-3$<4l3go!6Nm3*$@WHkf`}tovA6sB*x@pnst7o21;M1&

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgNew-pressed.gif b/mozilla/grendel/ui/images/toolbar/msgNew-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..f83f0c4712923fb0e0ee5a2fcaf3761090e6f649
GIT binary patch
literal 247
zcmZ?wbhEHb6lV}+c+3C-4Gav%#%cf4jMLK2FqqpMIB?)U!~cechM6<}110~10E3c}
zQUg#O1B2pE7DfgJb_N{~2V@2V%aRW#JXg=bP$m2tAo<=FZGyn4q)Vx0$bTie91-*T$!>C6ilDmqJO(EZ7BL7nwKH#&dSQoSjxuJBx}#c$HP-vUMbqmV-Os}QO3sNB*MYjZ98*L
r*Hlpsmc?DR)91?xt>#=eecq;pYqoZ8+_{T$@q+ylEPOt~f(+IGepFdt

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgNew-rollover.gif b/mozilla/grendel/ui/images/toolbar/msgNew-rollover.gif
new file mode 100644
index 0000000000000000000000000000000000000000..741ac77761b769c81e8c2979ccc38431f722bdce
GIT binary patch
literal 322
zcmZ?wbhEHb6lV}+Xkh?>1_lOW<1~i<4FA)N)6&iyIB||iwpTNYxnKV!1(voL}lQflQ^E~2l{ix7pJx$%s
zS;ORR8HdAr)+v_Ky)K6vOf+kkEI06)Ak}qko`!a}8b@#5p~@hw87>batzs6g^Eo^{
zG2%3r4hN^VeNC_nTSH^B9=8A^4;PnFXQK(btpKk8cU^p!PBc3^4#QQQU(Ev
zKUo+V7}yweKwOX+3@ocIFckFcy|FIm)RLKtViqUNI`+_4bY;%^(x@w3Qj2zUxOZNi
zKd)&*v`pV@AKrsnLKEE2@*8ppl^#FbryLl${@B8#1GcxSSPnVas!ZR@>E_TVvF>h+
ztK-oZ>Mbr6iq5R8T#Q9ojWJ?LEIi!V#V#z&x{}$}{_g&hnVF_d*W}2a;_S|tHGi56
rJM)sRvQ=tLlIu9SW=>tuBe8u)`JTPo)-O49cr7!}q7!Gu1sSXX8XI0c

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgReply-disabled.gif b/mozilla/grendel/ui/images/toolbar/msgReply-disabled.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ff285b6cdfcaa271311deb4f4a112deb340f39c0
GIT binary patch
literal 210
zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1WHOu3=BX46rlK%
zg^_`Qg+T`-3NnL%rGCN*&((WpFm2jy!=fRO99b&rwBk)wrSOey$!06X^4j0|yc3dj
z^^hpgQl6hRMdMR~LgcBPjJZ7Kai>>gNv)mSt+0*j8
wNz=?4HH~tiLN~@VduiY9M3%y0mi8ncmWXMh)0GR8njNOvPnow!NRYuA0N$%h8UO$Q

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgReply-pressed.gif b/mozilla/grendel/ui/images/toolbar/msgReply-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..90830252759f38481a8c8d013f62d022013a3ce6
GIT binary patch
literal 224
zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNR|7T!mXlR%@^S_dk5(5K}hXpA9WMO1r
zU}n$($$`vZVCgM5;kkOR#cQ5E_Q{HDDV|}n5?ndA*{1F4GU3f{3%I_0nsn=e3l~oR
zDO)_j!%TsN%OP=sgauC>L!@4Vj3gh!8lH{4e#}<=6;2m8cbJ9wP7mGnFeChQz;uOo
zDhvg&wZ74LH5F=%tr;2iB{4mbQ;ZnvCqy*L%?YdX<6!h#)U%Y<0jc{cH|Z(?ShUdFa}ugE?&Wgezovpt0NDjz>_mPL@k8UX9&UC#gj

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/msgReply.gif b/mozilla/grendel/ui/images/toolbar/msgReply.gif
new file mode 100644
index 0000000000000000000000000000000000000000..0b92c8f73d1a74dfba1e80402b8a416260745117
GIT binary patch
literal 246
zcmZ?wbhEHb6lV}+c+9|HY@EjM|9@KAnF9w7{0Fia7#bQHX3qQ%1WHOu3=BX46rlK%
zg^_`Qg+T`-3NnL%W$}a)o~!rHXxjVyr;|pTsJpDks#Q&_AsR*}zFbHXvU>BU=JCdv
z0pCu)sIorN|9zGS%Py(F2?{)Rb7dYWJXy-p%<`b>hJ;F&$o90!szNE7mn?{S&$3BQ
ztU%3WyIr(=b9HT|OLIU=OA$*+t_eezvSM#SMbIR6CYialOk6gMwv*=MFXycF=UUCq
ix|nqm!%DWTTSd09DRD7v++1rRyj5xc!DC{A4AuZKYFYOH

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/print-disabled.gif b/mozilla/grendel/ui/images/toolbar/print-disabled.gif
new file mode 100644
index 0000000000000000000000000000000000000000..829a220f8eb1926723967e625b3307b3e87e1d9b
GIT binary patch
literal 202
zcmZ?wbhEHb6lV}+c+9|HY@C*scILo=1OFNRH#9WNocSLJ7#bLql$02-0mYvzj0_CS
z3_2i5kQodt$QwS>4yv%_x+xAA8WSsCG2|I
jV8w3UQfnq;7SdJC)aH@V*;3=vuRdw2(ac$5f(+IG_47wh

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/print-pressed.gif b/mozilla/grendel/ui/images/toolbar/print-pressed.gif
new file mode 100644
index 0000000000000000000000000000000000000000..185bb17a40fb029e582bb85d57b6a866c1bfa4ae
GIT binary patch
literal 205
zcmZ?wbhEHb6lV}+c+9|HY@EjMpCK*n%z*<3{xkeZW
z#@ZIEp2nj(9C_l7=MpAFt@oF4y|B1ybaauSqVSopK~)#6{@|iVrAf9YmjxemvC_8UA{Xb
zZcB7tdxq6q*2cF@%u*~j)?8Ctz!9`Z!R*9aKOUR-SM5*lX=JRvbA9vOCChfy@Ne-J
z4ra4&YO!y&G&gHdNN#IoV`S{;VlZS4jEbtAVkkB}w~T?aTSwHiVd=_MD+L*>0X54^
A1poj5

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/print.gif b/mozilla/grendel/ui/images/toolbar/print.gif
new file mode 100644
index 0000000000000000000000000000000000000000..54b84d18c1d668607a4b58e0edf5797a1355094a
GIT binary patch
literal 213
zcmZ?wbhEHb6lV}+c+9|HY@EjMpCK*n%z*<3{xkeWGyPcMo1cdm6eJkT_-gD$%FS%{fUsF-O>bXTkqCJIqx#Rx
yhHER!8ojE_Ol#$Wnw!cPIV=j>g<1U~68jk@N=yzaY@4Y&X@o;;1X$KgTF}&}vUZwj@WDc@*;?Pz+m>Fx{O#uVdmg)%u1~AIwI|*$
x!%nxXx|3
zvaIF4zktj!riRA~k_I}A`e*r#*H+DDk8kjHs1R;&DKlSpX4(3d7~iB@cWsWm6+5@6
zr^41J!&u4HM?9E8xj8~t(^9Ofhl#VbY@)DEL1LMlpHHBb4SRK_Q+|VRVpnmCij>hz
lA&*T{4eL5OSy-BT?vgYH%vd@{zS(BrM2a4FKCXPz?Y8

literal 0
HcmV?d00001

diff --git a/mozilla/grendel/ui/images/toolbar/stop-rollover.gif b/mozilla/grendel/ui/images/toolbar/stop-rollover.gif
new file mode 100644
index 0000000000000000000000000000000000000000..fe9821bd91003b08f384bf2743fc6483f72adc16
GIT binary patch
literal 269
zcmZ?wbhEHb6lV}+c+3C-4Gav%#%cf4jMLK295`^`Kg0k3K*5HF|7mAt&ioGs3`$B$
zV6_a2KUo+V7}yweKwOX+3@p1YczCYb+O*)LdSI(k-zg=IyDdUTgE$%*TDS|3%?;Rn
zP>_Ye#Z%$XQx0zC`O6g@r&@(#IqOok^MZz!ri
tTc~w26K9y!yl7VI@|~F*E9Ir6wTp@k
+
+  
+
+
+  
+  
+    
+    
+    
+    
+    
+    
+    
+    
+  
+  
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+  
+  
+    
+      
+      
+      
+      
+      
+      
+    
+    
+      
+      
+      
+      
+    
+    
+    
+  
+  
+   
+    
+   
+   
+   
+   
+    
+    
+     
+     
+     
+    
+  
+  
+
+
diff --git a/mozilla/grendel/util/Makefile b/mozilla/grendel/util/Makefile
new file mode 100644
index 00000000000..f3213970977
--- /dev/null
+++ b/mozilla/grendel/util/Makefile
@@ -0,0 +1,23 @@
+#!gmake
+#
+# The contents of this file are subject to the Mozilla Public License
+# Version 1.0 (the "License"); you may not use this file except in
+# compliance with the License.  You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+# 
+# Software distributed under the License is distributed on an "AS IS"
+# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+# the License for the specific language governing rights and limitations
+# under the License.
+# 
+# The Original Code is the Grendel mail/news client.
+# 
+# The Initial Developer of the Original Code is Netscape Communications
+# Corporation.  Portions created by Netscape are Copyright (C) 1997
+# Netscape Communications Corporation.  All Rights Reserved.
+
+SRCS=					\
+	MessageIDGenerator.java		\
+	$(NULL)
+
+include ../rules.mk
diff --git a/mozilla/grendel/util/MessageIDGenerator.java b/mozilla/grendel/util/MessageIDGenerator.java
new file mode 100644
index 00000000000..35023b8a16a
--- /dev/null
+++ b/mozilla/grendel/util/MessageIDGenerator.java
@@ -0,0 +1,99 @@
+/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is the Grendel mail/news client.
+ *
+ * The Initial Developer of the Original Code is Netscape Communications
+ * Corporation.  Portions created by Netscape are Copyright (C) 1997
+ * Netscape Communications Corporation.  All Rights Reserved.
+ *
+ * Created: Jamie Zawinski , 26 Sep 1997.
+ */
+
+package grendel.util;
+
+import calypso.util.ByteBuf;
+
+/** This class generates globally-unique message IDs.
+    These IDs conform to RFC 822, RFC 1036, and the
+    so-called son-of-1036 draft.
+  */
+public class MessageIDGenerator {
+
+  private MessageIDGenerator() { }  // only static methods
+
+
+  /** Generates a new Message-ID string.
+      @param hostname   The `domain' part of the ID to generate.
+                        

+ Generally, this should be the fully-qualified name of + the local host. In situations where that is impossible + to determine, or where the local host name might be + considered confidential information, the host part of + the return address of the authoring user will do. +

+ If the provided host name is not syntactically valid, + an error is signalled. +

+ The host name may be null; in that case, a random + sequence of characters will be used for the host name, + resulting in a syntactically valid Message-ID. Note, + however, that this is strongly discouraged by the + relevant RFCs: Message-IDs should have valid host + names in them if at all possible. + + @return The new ID, a String. + */ + public static String generate(String host_name) { + if (host_name != null) { + checkHostName(host_name); + if (host_name.length() == 0) + host_name = null; + } + + int radix = 36; // keep the strings short + long time = System.currentTimeMillis(); // millisec resolution + long salt = Double.doubleToLongBits(Math.random()); // 64 random bits + + ByteBuf out = ByteBuf.Alloc(); + out.append((byte) '<'); + out.append(Long.toString(Math.abs(time), radix)); + out.append((byte) '.'); + out.append(Long.toString(Math.abs(salt), radix)); + out.append((byte) '@'); + + if (host_name != null) + out.append(host_name); + else { + out.append((byte) 'h'); // domain part must begin with a letter + salt = (Double.doubleToLongBits(Math.random()) & 0xFFFFFFFFL); // 32 bits + out.append(Long.toString(Math.abs(salt), radix)); + } + out.append((byte) '>'); + + String result = out.toString(); + ByteBuf.Recycle(out); + return result; + } + + private static void checkHostName(String string) { + int L = string.length(); + for (int i = 0; i < L; i++) { + char c = string.charAt(i); + if (!Character.isLetterOrDigit(c) && c != '-' && c != '_' && c != '.') + throw new Error("illegal character in host name for Message-ID: " + + "`" + + new String(new char[] { c }) + + "' (" + string + ")"); + } + } +} diff --git a/mozilla/grendel/view/DummyThreadable.java b/mozilla/grendel/view/DummyThreadable.java new file mode 100644 index 00000000000..225b3a9c661 --- /dev/null +++ b/mozilla/grendel/view/DummyThreadable.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 21 Aug 1997. + */ + +package grendel.view; + +import javax.mail.Message; + +class DummyThreadable extends ViewedMessageBase { + DummyThreadable(MessageSetView v, Message m) { + super(v, m); + } + + public boolean isDummy() { + return true; + } +} diff --git a/mozilla/grendel/view/FolderView.java b/mozilla/grendel/view/FolderView.java new file mode 100644 index 00000000000..2b1504a1cd4 --- /dev/null +++ b/mozilla/grendel/view/FolderView.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 25 Aug 1997. + */ + +package grendel.view; + +import javax.mail.Folder; + +/** Implements a view on a storage folder; this shows all the messages + currently stored in a folder. */ + +public interface FolderView extends MessageSetView { + /** Get the storage Folder that we're viewing messages for. */ + public Folder getFolder(); +} diff --git a/mozilla/grendel/view/FolderViewBase.java b/mozilla/grendel/view/FolderViewBase.java new file mode 100644 index 00000000000..0b1de269f28 --- /dev/null +++ b/mozilla/grendel/view/FolderViewBase.java @@ -0,0 +1,582 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 25 Aug 1997. + */ + +package grendel.view; + +import calypso.util.ArrayEnumeration; +import calypso.util.Assert; +import calypso.util.Comparer; +import calypso.util.PrefetchEnumeration; +import calypso.util.QSort; + +import java.util.Enumeration; +import java.util.Date; +import java.util.Vector; + +import javax.mail.FetchProfile; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.event.MessageChangedEvent; +import javax.mail.event.MessageChangedListener; +import javax.mail.event.MessageCountEvent; +import javax.mail.event.MessageCountListener; + +import grendel.storage.MessageExtraFactory; + +class FolderViewBase implements FolderView, MessageChangedListener, + MessageCountListener +{ + ViewedMessageBase fRoot; + Folder fFolder; + Vector observers = new Vector(); + + boolean threaded; + int sortorder[] = {NUMBER}; + + public void loadFrom(Folder f) { + if (fFolder != null) { + fFolder.removeMessageChangedListener(this); + fFolder.removeMessageCountListener(this); + } + fFolder = f; + fFolder.addMessageChangedListener(this); + fFolder.addMessageCountListener(this); + + Message messages[] = null; + try { + if (!fFolder.isOpen()) { + fFolder.open(Folder.READ_WRITE); + } + messages = fFolder.getMessages(); + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + f.fetch(messages, fp); + } catch (MessagingException e) { + } + + if (messages != null) { + for (int i=0 ; i=0 ; i--) { + ((MessageSetViewObserver) observers.elementAt(i)).messagesChanged + (null, new ViewedMessageEnumeration(fRoot), null); + } + + + + if (threaded) { + Threader t = new Threader(); + fRoot = (ViewedMessageBase) t.thread(fRoot); + } else { + // Flatten out the list, and remove any dummies. + ViewedMessageBase base = fRoot; + fRoot = null; + flattenList(base); + } + + if (fRoot == null) return; // empty folder + + int nummsgs = fRoot.getMessageCount(); + + int numkeys = sortorder.length; + int n = 0; + int count = generateSortKeys(fRoot, 0); + Assert.Assertion(count == nummsgs); + + Comparer comp = new NullComparer(); + for (int i=sortorder.length - 1 ; i>=0 ; i--) { + switch (sortorder[i]) { + case NUMBER: + comp = new NumberComparer(nummsgs, comp); + break; + case DATE: + comp = new DateComparer(nummsgs, comp); + break; + case SUBJECT: + comp = new SubjectComparer(nummsgs, comp); + break; + case AUTHOR: + comp = new AuthorComparer(nummsgs, comp); + break; + case READ: + comp = new ReadComparer(nummsgs, comp); + break; + case FLAGGED: + comp = new FlaggedComparer(nummsgs, comp); + break; + case SIZE: + comp = new SizeComparer(nummsgs, comp); + break; + case DELETED: + comp = new DeletedComparer(nummsgs, comp); + break; + default: + throw new IllegalArgumentException("Bad sort key " + sortorder[i]); + } + } + + Sorter s = new Sorter(comp); + DummyThreadable dummy = new DummyThreadable(null, null); + dummy.fChildren = fRoot; + s.sortMessageChildren(dummy); + fRoot = (ViewedMessageBase) dummy.fChildren; + dummy = null; + + fRoot.setParentPointers(null); + + + // ### What a hack, part two. Tell our observers that everything + // reappeared. + for (int i=observers.size() - 1 ; i>=0 ; i--) { + ((MessageSetViewObserver) observers.elementAt(i)).messagesChanged + (new ViewedMessageEnumeration(fRoot), null, null); + } + } + + + public void setSortOrder(int value[]) { + sortorder = new int[value.length]; + int j=0; + + // Strip out duplicates in the sortorder array. + NEXTI: + for (int i=0 ; i=0 ; i--) { + ((MessageSetViewObserver) observers.elementAt(i)).messagesChanged + (null, null, new ArrayEnumeration(viewed)); + } + } + + public void messagesAdded(MessageCountEvent event) { + Message[] messages = event.getMessages(); + ViewedMessage viewed[] = new ViewedMessage[messages.length]; + for (int i=0 ; i=0 ; i--) { + ((MessageSetViewObserver) observers.elementAt(i)).messagesChanged + (new ArrayEnumeration(viewed), null, null); + } + } + + public void messagesRemoved(MessageCountEvent event) { + Message[] messages = event.getMessages(); + ViewedMessage viewed[] = new ViewedMessage[messages.length]; + for (int i=0 ; i=0 ; i--) { + ((MessageSetViewObserver) observers.elementAt(i)).messagesChanged + (null, new ArrayEnumeration(viewed), null); + } + } + + protected ViewedMessage findViewedMessage(Message m, ViewedMessage v) { + for ( ; v != null ; v = v.getNext()) { + if (v.getMessage() == m) return v; + ViewedMessage result = findViewedMessage(m, v.getChild()); + if (result != null) return result; + } + return null; + } + + protected ViewedMessage findViewedMessage(Message m) { + ViewedMessage result = findViewedMessage(m, fRoot); + if (result == null) { + result = new ViewedMessageBase(this, m); + } + return result; + } + + public void addObserver(MessageSetViewObserver obs) { + observers.addElement(obs); + } + + public void removeObserver(MessageSetViewObserver obs) { + observers.removeElement(obs); + } + + + final class NullComparer implements Comparer { + public int compare(Object a, Object b) { + return 0; + } + } + + final class NumberComparer implements Comparer { + Comparer next; + int cache[]; + NumberComparer(int nummsgs, Comparer next) { + cache = new int[nummsgs]; + this.next = next; + } + int getMessageNumber(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + cache[key] = v.getMessage().getMessageNumber(); + return cache[key]; + } + public int compare(Object a, Object b) { + int k1 = cache[((ViewedMessageBase)a).sortkey]; + int k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getMessageNumber(a); + if (k2 == 0) k2 = getMessageNumber(b); + if (k1 != k2) return k2 - k1; + return next.compare(a, b); + } + } + + final class DateComparer implements Comparer { + Comparer next; + long cache[]; + DateComparer(int nummsgs, Comparer next) { + cache = new long[nummsgs]; + this.next = next; + } + long getTime(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = v.getMessage().getSentDate().getTime(); + } catch (MessagingException e) { + } catch (NullPointerException e) { + } + if (cache[key] == 0) { + cache[key] = 1; + } + return cache[key]; + } + public int compare(Object a, Object b) { + long k1 = cache[((ViewedMessageBase)a).sortkey]; + long k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getTime(a); + if (k2 == 0) k2 = getTime(b); + if (k1 != k2) { + // I'd like to just cast the difference to an int and return that, but + // I'm afraid that sign conversion will screw up. So we do it by hand. + if (k1 < k2) return 1; + else return -1; + } + return next.compare(a, b); + } + } + + final class SubjectComparer implements Comparer { + Comparer next; + String cache[]; + SubjectComparer(int nummsgs, Comparer next) { + cache = new String[nummsgs]; + this.next = next; + } + String getSubject(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = + MessageExtraFactory.Get(v.getMessage()).simplifiedSubject(); + } catch (MessagingException e) { + } + if (cache[key] == null) { + cache[key] = ""; + } + return cache[key]; + } + public int compare(Object a, Object b) { + String k1 = cache[((ViewedMessageBase)a).sortkey]; + String k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == null) k1 = getSubject(a); + if (k2 == null) k2 = getSubject(b); + int result = k1.compareTo(k2); + if (result != 0) return result; + return next.compare(a, b); + } + } + + final class AuthorComparer implements Comparer { + Comparer next; + String cache[]; + AuthorComparer(int nummsgs, Comparer next) { + cache = new String[nummsgs]; + this.next = next; + } + String getAuthor(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = + MessageExtraFactory.Get(v.getMessage()).getAuthor(); + } catch (MessagingException e) { + } + if (cache[key] == null) { + cache[key] = ""; + } + return cache[key]; + } + public int compare(Object a, Object b) { + String k1 = cache[((ViewedMessageBase)a).sortkey]; + String k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == null) k1 = getAuthor(a); + if (k2 == null) k2 = getAuthor(b); + int result = k1.compareTo(k2); + if (result != 0) return result; + return next.compare(a, b); + } + } + + final class ReadComparer implements Comparer { + Comparer next; + int cache[]; + ReadComparer(int nummsgs, Comparer next) { + cache = new int[nummsgs]; + this.next = next; + } + int getRead(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = + MessageExtraFactory.Get(v.getMessage()).isRead() ? 1 : 2; + } catch (MessagingException e) { + cache[key] = 2; // False by default. + } + return cache[key]; + } + public int compare(Object a, Object b) { + int k1 = cache[((ViewedMessageBase)a).sortkey]; + int k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getRead(a); + if (k2 == 0) k2 = getRead(b); + if (k1 != k2) return k2 - k1; + return next.compare(a, b); + } + } + + final class FlaggedComparer implements Comparer { + Comparer next; + int cache[]; + FlaggedComparer(int nummsgs, Comparer next) { + cache = new int[nummsgs]; + this.next = next; + } + int getFlagged(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = + MessageExtraFactory.Get(v.getMessage()).isFlagged() ? 1 : 2; + } catch (MessagingException e) { + cache[key] = 2; // False by default. + } + return cache[key]; + } + public int compare(Object a, Object b) { + int k1 = cache[((ViewedMessageBase)a).sortkey]; + int k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getFlagged(a); + if (k2 == 0) k2 = getFlagged(b); + if (k1 != k2) return k2 - k1; + return next.compare(a, b); + } + } + + + final class SizeComparer implements Comparer { + Comparer next; + int cache[]; + SizeComparer(int nummsgs, Comparer next) { + cache = new int[nummsgs]; + this.next = next; + } + int getSize(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = v.getMessage().getSize(); + } catch (MessagingException e) { + } + if (cache[key] == 0) cache[key] = -1; + return cache[key]; + } + public int compare(Object a, Object b) { + int k1 = cache[((ViewedMessageBase)a).sortkey]; + int k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getSize(a); + if (k2 == 0) k2 = getSize(b); + if (k1 != k2) return k2 - k1; + return next.compare(a, b); + } + } + + + final class DeletedComparer implements Comparer { + Comparer next; + int cache[]; + DeletedComparer(int nummsgs, Comparer next) { + cache = new int[nummsgs]; + this.next = next; + } + int getDeleted(Object a) { + ViewedMessageBase v = (ViewedMessageBase) a; + int key = v.sortkey; + try { + cache[key] = + MessageExtraFactory.Get(v.getMessage()).isDeleted() ? 1 : 2; + } catch (MessagingException e) { + cache[key] = 2; // False by default. + } + return cache[key]; + } + public int compare(Object a, Object b) { + int k1 = cache[((ViewedMessageBase)a).sortkey]; + int k2 = cache[((ViewedMessageBase)b).sortkey]; + if (k1 == 0) k1 = getDeleted(a); + if (k2 == 0) k2 = getDeleted(b); + if (k1 != k2) return k2 - k1; + return next.compare(a, b); + } + } + + + final class ViewedMessageEnumeration extends PrefetchEnumeration { + ViewedMessage cur; + ViewedMessageEnumeration(ViewedMessage obj) { + cur = obj; + } + protected Object fetch() { + Object result = cur; + if (cur != null) { + ViewedMessage tmp = cur.getChild(); + if (tmp != null) cur = tmp; + else { + tmp = cur.getNext(); + if (tmp != null) cur = tmp; + else { + tmp = cur.getParent(); + while (tmp != null && tmp.getNext() == null) { + tmp = tmp.getParent(); + } + if (tmp != null) cur = tmp.getNext(); + else cur = null; + } + } + } + return result; + } + } + +} diff --git a/mozilla/grendel/view/FolderViewFactory.java b/mozilla/grendel/view/FolderViewFactory.java new file mode 100644 index 00000000000..eeb56cab6ee --- /dev/null +++ b/mozilla/grendel/view/FolderViewFactory.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 27 Aug 1997. + */ + +package grendel.view; + +import javax.mail.Folder; + +/** Factory to create a new FolderView object, viewing the given Folder. */ + +public class FolderViewFactory { + static public FolderView Make(Folder f) { + FolderViewBase view = new FolderViewBase(); + view.loadFrom(f); + return view; + } +} diff --git a/mozilla/grendel/view/ISortable.java b/mozilla/grendel/view/ISortable.java new file mode 100644 index 00000000000..5eb3fbd6a4a --- /dev/null +++ b/mozilla/grendel/view/ISortable.java @@ -0,0 +1,40 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 13 Jun 1995. + * Ported from C on 14 Aug 1997. + */ + +/** + This is an interface representing messages in a thread. + It is the interface to the message Sorter. + + @see IThreadable + @see Threader + @see Sorter + */ + +package grendel.view; + +import java.util.Enumeration; + +interface ISortable { + Enumeration children(); + void setNext(Object next); + void setChild(Object kid); +} diff --git a/mozilla/grendel/view/IThreadable.java b/mozilla/grendel/view/IThreadable.java new file mode 100644 index 00000000000..31ac9767cfa --- /dev/null +++ b/mozilla/grendel/view/IThreadable.java @@ -0,0 +1,107 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 13 Jun 1995. + * Ported from C on 14 Aug 1997. + */ + +/** + This is an interface representing messages to be threaded by references. + It is the interface to the Threader. + + @see Threader + @see ISortable + @see Sorter + */ + +package grendel.view; + +import java.util.Enumeration; + + +interface IThreadable { + + /** Returns each subsequent element in the set of messages of + which this IThreadable is the root. Order is unimportant. + */ + Enumeration allElements(); + + /** Returns an object identifying this message. + Generally this will be a representation of the contents of the + Message-ID header. It may be a String, or it may be something else; + the only constraint is that equals and hashCode + work properly on it. (That is, two ID objects are equal and hash the + same if they represent the same underlying ID-string.) + */ + Object messageThreadID(); + + /** Returns the IDs of the set of messages referenced by this one. + This list should be ordered from oldest-ancestor to youngest-ancestor. + As for the messageThreadID method, the contents of this array + may be any type of object, so long as equals and + hashCode work properly on them. + */ + Object[] messageThreadReferences(); + + /** When no references are present, subjects will be used to thread together + messages. This method should return a threadable subject: two messages + with the same simplifiedSubject will be considered to belong to the same + thread. This string should not have `Re:' on the front, and may have + been simplified in whatever other ways seem appropriate. +

+ This is a String of Unicode characters, and should have had any encodings + (such as RFC 2047 charset encodings) removed first. +

+ If you aren't interested in threading by subject at all, return null. + */ + String simplifiedSubject(); + + /** Whether the original subject was one that appeared to be a reply (that + is, had a `Re:' or some other indicator.) When threading by subject, + this property is used to tell whether two messages appear to be siblings, + or in a parent/child relationship. + */ + boolean subjectIsReply(); + + /** When the proper thread order has been computed, these two methods will + be called on each IThreadable in the chain, to set up the proper tree + structure. + */ + void setNext(Object next); + void setChild(Object kid); + + /** Creates a dummy parent object. +

+ With some set of messages, the only way to achieve proper threading is + to introduce an element into the tree which represents messages which are + not present in the set: for example, when two messages share a common + ancestor, but that ancestor is not in the set. This method is used to + make a placeholder for those sorts of ancestors. It should return an + object which is also a IThreadable. The setNext() and setChild() methods + will be used on this placeholder, as either the object or the argument, + just as for other elements of the tree. + */ + IThreadable makeDummy(); + + /** This should return true of dummy messages, false otherwise. + It is legal to pass dummy messages in with the list returned by + elements(); the isDummy() method is the mechanism by which they are + noted and ignored. + */ + boolean isDummy(); +} diff --git a/mozilla/grendel/view/Makefile b/mozilla/grendel/view/Makefile new file mode 100644 index 00000000000..491caf57088 --- /dev/null +++ b/mozilla/grendel/view/Makefile @@ -0,0 +1,41 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + DummyThreadable.java \ + FolderView.java \ + FolderViewBase.java \ + FolderViewFactory.java \ + ISortable.java \ + IThreadable.java \ + MessageSetView.java \ + MessageSetViewObserver.java \ + Sorter.java \ + TestThreader.java \ + Threader.java \ + ViewedFolder.java \ + ViewedFolderBase.java \ + ViewedMessage.java \ + ViewedMessageBase.java \ + ViewedStore.java \ + ViewedStoreBase.java \ + ViewedStoreEvent.java \ + ViewedStoreListener.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/view/MessageSetView.java b/mozilla/grendel/view/MessageSetView.java new file mode 100644 index 00000000000..cb0fcd524ed --- /dev/null +++ b/mozilla/grendel/view/MessageSetView.java @@ -0,0 +1,79 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 25 Aug 1997. + */ + + +package grendel.view; + +import javax.mail.Message; + +/** Represents a view on messages. There is no implication that these + messages all come from the same Folder, or even from the same kind of + Folder.

+ + The messages are presented in a particular sorted order, and have + also possibly been threaded. + */ + +public interface MessageSetView { + // The various ways we know to sort messages. + final static int NUMBER = 1; + final static int DATE = 2; + final static int SUBJECT = 3; + final static int AUTHOR = 4; + final static int READ = 5; + final static int FLAGGED = 6; + final static int SIZE = 7; + final static int DELETED = 8; + +// public SearchTerm getSearchTerm(); +// public void setSearchTerm(SearchTerm); + /** Gets the root of the tree of messages that are being viewed. */ + public ViewedMessage getMessageRoot(); + + /** Set the sort order for display. This is an array of the sort keys listed + above. It is an array so that the caller can define secondary sorts. + Duplicates will be removed and other optimizations possibly made, so + getSortOrder() may not necessarily return the same thing. Note that + the messages won't actually get resorted until reThread() is called. */ + public void setSortOrder(int order[]); + + /** Prepend a new sort order. This will become the new primary sort; + whatever sort was being done becomes secondary sorts. Note that + the messages won't actually get resorted until reThread() is called. */ + public void prependSortOrder(int order); + + public int[] getSortOrder(); + + /** Set whether to do threading on these messages. If false, then things + just get sorted into one long list. Note that the messages won't actually + get resorted or threaded until reThread() is called. */ + public void setIsThreaded(boolean b); + + public boolean isThreaded(); + + /** Cause the messages to get resorted and possibly threaded, according to + previous calls to setSortOrder(), prependSortOrder(), and setIsThreaded(). + */ + public void reThread(); + + public void addObserver(MessageSetViewObserver obs); + public void removeObserver(MessageSetViewObserver obs); +} diff --git a/mozilla/grendel/view/MessageSetViewObserver.java b/mozilla/grendel/view/MessageSetViewObserver.java new file mode 100644 index 00000000000..9dcff8c4039 --- /dev/null +++ b/mozilla/grendel/view/MessageSetViewObserver.java @@ -0,0 +1,44 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 17 Sep 1997. + */ + +package grendel.view; + +import java.util.Enumeration; + +/** Anyone who wants to know when messages are added, removed, or tweaked in + * a MessageSetView can implement this and add themselves to that + * MessageSetView's list of observers. + */ + +public interface MessageSetViewObserver { + /** Some messages changed. Each enumeration parameter might be null, or + * it might be an enumeration of ViewedMessage objects. + * @param inserted new messages that have appeared + * @param deleted old messages that are no longer considered + * part of the MessageSetView + * @param changed Messages that have had their internals tweaked + * in some way + */ + + public void messagesChanged(Enumeration inserted, + Enumeration deleted, + Enumeration changed); +}; diff --git a/mozilla/grendel/view/Sorter.java b/mozilla/grendel/view/Sorter.java new file mode 100644 index 00000000000..4228b798e59 --- /dev/null +++ b/mozilla/grendel/view/Sorter.java @@ -0,0 +1,101 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 13 Jun 1995. + * Ported from C on 14 Aug 1997. + */ + +package grendel.view; + +import java.util.Vector; +import java.util.Enumeration; +import calypso.util.QSort; +import calypso.util.Comparer; + +/** + This sorts a set of messages by some metric. + The set of messages may already be arranged into a thread hierarchy; in + that case, siblings are sorted while leaving parent/child relationships + intact. The sets of messages are accessed via the ISortable interface. + + @see ISortable + @see Threader + @see Comparer + @see QSort + */ + +class Sorter { + + private Vector v = null; + private QSort sorter = null; + + /** Creates an object for sorting messages. + Use the sortMessageChildren() method to sort them. + @param comparer The object which compares two ISortable objects + for ordering. + */ + public Sorter(Comparer comparer) { + this.sorter = new QSort(comparer); + v = new Vector(100); + } + + /** Sorts the set of messages indicated by sortable_root. + The child-list of sortable_root will be modified (reordered) + upon completion (as will all grandchildren.) + @param sortable_root The root object; it should have + children, but no siblings. + */ + public void sortMessageChildren(ISortable sortable_root) { + sortMessageChildren_1(sortable_root); + sorter = null; + v.setSize(0); + v.trimToSize(); + v = null; + } + + private void sortMessageChildren_1(ISortable parent) { + + int count = 0; + + // Populate v with the kids. + for (Enumeration e = parent.children(); + e != null && e.hasMoreElements();) { + v.addElement((ISortable) e.nextElement()); + count++; + } + + // Copy v to a, growing a if necessary. + ISortable a[] = new ISortable[count+1]; + v.copyInto(a); + v.setSize(0); + + // Sort a. + sorter.sort(a, 0, count); + + // Flush new order of a into the ISortables. + parent.setChild(a[0]); + a[count] = null; + for (int i = 0; i < count; i++) + a[i].setNext(a[i+1]); + + // Repeat on the grandchildren. + for (int i = 0; i < count; i++) + sortMessageChildren_1(a[i]); + a = null; + } +} diff --git a/mozilla/grendel/view/TestThreader.java b/mozilla/grendel/view/TestThreader.java new file mode 100644 index 00000000000..764ef5201c4 --- /dev/null +++ b/mozilla/grendel/view/TestThreader.java @@ -0,0 +1,511 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 13 Jun 1995. + * Ported from C on 14 Aug 1997. + */ + +/** Test harness for Threader and Sorter. + @see Threader + @see Sorter + */ + +package grendel.view; + +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Date; +import calypso.util.ByteBuf; +import calypso.util.ArrayEnumeration; +import calypso.util.QSort; +import calypso.util.Comparer; +import calypso.util.NetworkDate; + +class TestMessageThread implements IThreadable, ISortable { + + TestMessageThread next; + TestMessageThread kid; + String subject; + String author; + long date; + Object id; + Object[] refs; + int message_number; + + private String subject2; + private boolean has_re; + + TestMessageThread() { + subject = null; // this means "dummy". + } + public TestMessageThread (TestMessageThread next, String subject, Object id, + Object references[]) { + this.next = next; + this.subject = subject; + this.id = id; + this.refs = references; + } + + public String toString() { + if (isDummy()) + return "[dummy]"; + + String s = "[ " + id + ": " + subject + " ("; + if (refs != null) + for (int i = 0; i < refs.length; i++) + s += " " + refs[i]; + if (date > 0) + s += " \"" + new Date(date) + "\""; + return s + " ) ]"; + } + + void simplifySubject() { + + int start = 0; + int L = subject.length(); + + boolean done = false; + while (!done) { + done = true; + + // skip whitespace. + while (subject.charAt(start) <= ' ') + start++; + + if (start < (L-2) && + (subject.charAt(start) == 'r' || subject.charAt(start) == 'R') && + (subject.charAt(start+1) == 'e' || subject.charAt(start+1) == 'e')) { + if (subject.charAt(start+2) == ':') { + start += 3; // Skip over "Re:" + has_re = true; // yes, we found it. + done = false; // keep going. + done = false; + + } else if (start < (L-2) && + (subject.charAt(start+2) == '[' || + subject.charAt(start+2) == '(')) { + int i = start+3; // skip over "Re[" or "Re(" + + // Skip forward over digits after the "[" or "(". + while (i < L && + subject.charAt(i) >= '0' && + subject.charAt(i) <= '9') + i++; + + // Now ensure that the following thing is "]:" or "):" + // Only if it is do we alter `start'. + if (i < (L-1) && + (subject.charAt(i) == ']' || + subject.charAt(i) == ')') && + subject.charAt(i+1) == ':') { + start = i+2; // Skip over "]:" + has_re = true; // yes, we found it. + done = false; // keep going. + } + } + } + + if (subject2 == "(no subject)") // #### i18n + subject2 = ""; + } + + int end = L; + // Strip trailing whitespace. + while (end > start && subject.charAt(end-1) < ' ') + end--; + + if (start == 0 && end == L) + subject2 = subject; + else + subject2 = subject.substring(start, end); + } + + void flushSubjectCache() { + subject2 = null; + } + + + // for IThreadable + + public synchronized Enumeration allElements() { + return new TestMessageThreadEnumeration(this, true); + } + + public Object messageThreadID() { + return id; + } + + public Object[] messageThreadReferences() { + return refs; + } + + public String simplifiedSubject() { + if (subject2 == null) simplifySubject(); + return subject2; + } + + public boolean subjectIsReply() { + if (subject2 == null) simplifySubject(); + return has_re; + } + + // Used by both IThreadable and ISortable + public void setNext (Object next) { + this.next = (TestMessageThread) next; + flushSubjectCache(); + } + + // Used by both IThreadable and ISortable + public void setChild (Object kid) { + this.kid = (TestMessageThread) kid; + flushSubjectCache(); + } + + public IThreadable makeDummy() { + return (IThreadable) new TestMessageThread(); + } + + public boolean isDummy() { + return (subject == null); + } + + // For ISortable + + public synchronized Enumeration children() { + return new TestMessageThreadEnumeration(this, false); + } +} + +class TestMessageThreadEnumeration implements Enumeration { + + TestMessageThread tail; + Enumeration kids; + boolean recursive_p; + + TestMessageThreadEnumeration(TestMessageThread thread, boolean recursive_p) { + this.recursive_p = recursive_p; + if (recursive_p) + tail = thread; + else + tail = thread.kid; + } + + public synchronized Object nextElement() { + if (kids != null) { + // if `kids' is non-null, then we've already returned a node, + // and we should now go to work on its children. + TestMessageThread result = (TestMessageThread) kids.nextElement(); + if (!kids.hasMoreElements()) + kids = null; + return result; + + } else if (tail != null) { + // Return `tail', but first note its children, if any. + // We will descend into them the next time around. + TestMessageThread result = tail; + if (recursive_p && tail.kid != null) + kids = new TestMessageThreadEnumeration(tail.kid, true); + tail = tail.next; + return result; + + } else { + throw new NoSuchElementException(); + } + } + + public synchronized boolean hasMoreElements() { + if (tail != null) + return true; + else if (kids != null && kids.hasMoreElements()) + return true; + else + return false; + } +} + +class TestThreader { + + private static TestMessageThread first, last; + private static int count = 0; + + private static final int SORT_NUMBER = 0; + private static final int SORT_DATE = 1; + private static final int SORT_SUBJECT = 2; + private static final int SORT_AUTHOR = 3; + + private static void make_thread(String subject, String id, String refs) { + TestMessageThread thread = new TestMessageThread(); + thread.subject = subject; + thread.id = id; + thread.message_number = ++count; + if (refs != null) { + StringTokenizer st = new StringTokenizer(refs); + Vector v = new Vector(5); + while (st.hasMoreTokens()) + v.addElement(st.nextToken()); + thread.refs = new Object[v.size()]; + for (int i = 0; i < v.size(); i++) + thread.refs[i] = (String) v.elementAt(i); + } + if (first == null) + first = thread; + else + last.next = thread; + last = thread; + } + + private static void make_thread2(String subject, String id, String date, + String refs) { + TestMessageThread thread = new TestMessageThread(); + thread.subject = subject; + thread.id = id; + thread.date = NetworkDate.parseLong(new ByteBuf(date), false); + thread.message_number = ++count; + if (refs != null) { + StringTokenizer st = new StringTokenizer(refs); + Vector v = new Vector(5); + while (st.hasMoreTokens()) + v.addElement(st.nextToken()); + thread.refs = new Object[v.size()]; + for (int i = 0; i < v.size(); i++) + thread.refs[i] = (String) v.elementAt(i); + } + if (first == null) + first = thread; + else + last.next = thread; + last = thread; + } + + public static void main(String[] args) { + + int sort_type = SORT_NUMBER; + + make_thread("A", "1", null); + make_thread("B", "2", "1"); + make_thread("C", "3", "1 2"); + make_thread("D", "4", "1"); + make_thread("E", "5", "3 x1 x2 x3"); + make_thread("F", "6", "2"); + make_thread("G", "7", "nonesuch"); + make_thread("H", "8", "nonesuch"); + + make_thread("Loop1", "loop1", "loop2 loop3"); + make_thread("Loop2", "loop2", "loop3 loop1"); + make_thread("Loop3", "loop3", "loop1 loop2"); + + make_thread("Loop4", "loop4", "loop5"); + make_thread("Loop5", "loop5", "loop4"); + + make_thread("Loop6", "loop6", "loop6"); + + make_thread("Loop7", "loop7", "loop8 loop9 loop10 loop8 loop9 loop10"); + make_thread("Loop8", "loop8", "loop9 loop10 loop7 loop9 loop10 loop7"); + make_thread("Loop8", "loop9", "loop10 loop7 loop8 loop10 loop7 loop8"); + make_thread("Loop10", "loop10","loop7 loop8 loop9 loop7 loop8 loop9"); + + make_thread("Ambig1", "ambig1", null); + make_thread("Ambig2", "ambig2", "ambig1"); + make_thread("Ambig3", "ambig3", "ambig1 ambig2"); + make_thread("Ambig4", "ambig4", "ambig1 ambig2 ambig3"); + make_thread("Ambig5a", "ambig5a", "ambig1 ambig2 ambig3 ambig4"); + make_thread("Ambig5b", "ambig5b", "ambig1 ambig3 ambig2 ambig4"); + + make_thread("dup", "dup", null); + make_thread("dup-kid", "dup-kid", "dup"); + make_thread("dup-kid", "dup-kid", "dup"); + make_thread("dup-kid-2", "dup-kid-2", "dup"); + make_thread("dup-kid-2", "dup-kid-2", "dup"); + make_thread("dup-kid-2", "dup-kid-2", "dup"); + + make_thread("same subject 1", "ss1.1", null); + make_thread("same subject 1", "ss1.2", null); + + make_thread("missingmessage", "missa", null); + make_thread("missingmessage", "missc", "missa missb"); + + make_thread2("liar 1", "", "", " "); + make_thread2("liar 2", "", "", " "); + + + make_thread2("liar2 1", "", "", " "); + make_thread2("liar2 2", "", "", " "); + + + make_thread2("xx", + "<331F7D61.2781@netscape.com>", + "Thu, 06 Mar 1997 18:28:50 -0800", + null); + make_thread2("lkjhlkjh", + "<3321E51F.41C6@netscape.com>", + "Sat, 08 Mar 1997 14:15:59 -0800", + null); + make_thread2("test 2", + "<3321E5A6.41C6@netscape.com>", + "Sat, 08 Mar 1997 14:18:14 -0800", + null); + make_thread2("enc", + "<3321E5C0.167E@netscape.com>", + "Sat, 08 Mar 1997 14:18:40 -0800", + null); + make_thread2("lkjhlkjh", + "<3321E715.15FB@netscape.com>", + "Sat, 08 Mar 1997 14:24:21 -0800", + null); + make_thread2("eng", + "<3321E7A4.59E2@netscape.com>", + "Sat, 08 Mar 1997 14:26:44 -0800", + null); + make_thread2("lkjhl", + "<3321E7BB.1CFB@netscape.com>", + "Sat, 08 Mar 1997 14:27:07 -0800", + null); + make_thread2("Re: certs and signed messages", + "<332230AA.41C6@netscape.com>", + "Sat, 08 Mar 1997 19:38:18 -0800", + "<33222A5E.ED4@netscape.com>"); + make_thread2("from dogbert", + "<3323546E.BEE44C78@netscape.com>", + "Sun, 09 Mar 1997 16:23:10 -0800", + null); + make_thread2("lkjhlkjhl", + "<33321E2A.1C849A20@netscape.com>", + "Thu, 20 Mar 1997 21:35:38 -0800", + null); + make_thread2("le:/u/jwz/mime/smi", + "<33323C9D.ADA4BCBA@netscape.com>", + "Thu, 20 Mar 1997 23:45:33 -0800", + null); + make_thread2("ile:/u/jwz", + "<33323F62.402C573B@netscape.com>", + "Thu, 20 Mar 1997 23:57:22 -0800", + null); + make_thread2("ljkljhlkjhl", + "<336FBAD0.864BC1F4@netscape.com>", + "Tue, 06 May 1997 16:12:16 -0700", + null); + make_thread2("lkjh", + "<336FBB46.A0028A6D@netscape.com>", + "Tue, 06 May 1997 16:14:14 -0700", + null); + make_thread2("foo", + "<337265C1.5C758C77@netscape.com>", + "Thu, 08 May 1997 16:46:09 -0700", + null); + make_thread2("Welcome to Netscape", + "<337AAB3D.C8BCE069@netscape.com>", + "Wed, 14 May 1997 23:20:45 -0700", + null); + make_thread2("Re: Welcome to Netscape", + "<337AAE46.903032E4@netscape.com>", + "Wed, 14 May 1997 23:33:45 -0700", + "<337AAB3D.C8BCE069@netscape.com>"); + make_thread2("[Fwd: enc/signed test 1]", + "<338B6EE2.BB26C74C@netscape.com>", + "Tue, 27 May 1997 16:31:46 -0700", + null); + + Threader t = new Threader(); + last = null; + first = (TestMessageThread) t.thread (first); + System.out.print("\n------- threaded:\n\n"); + printThread(first, 0); + +// System.out.print("\n------- pass 2\n\n"); +// t = new Threader(); +// first = (TestMessageThread) t.thread (first); +// printThread(first, 0); + +// System.out.print("\n------- pass 3\n\n"); +// t = new Threader(); +// first = (TestMessageThread) t.thread (first); +// printThread(first, 0); + + Comparer comparer; + + if (sort_type == SORT_DATE) { + comparer = new Comparer() { + public int compare(Object oa, Object ob) { + TestMessageThread ta = (TestMessageThread) oa; + TestMessageThread tb = (TestMessageThread) ob; + TestMessageThread a = (ta.subject == null ? ta.kid : ta); + TestMessageThread b = (tb.subject == null ? tb.kid : tb); + if (a.date == b.date) return 0; + else if (a.date < b.date) return -1; + else return 1; + } + }; + + } else if (sort_type == SORT_SUBJECT) { + comparer = new Comparer() { + public int compare(Object oa, Object ob) { + TestMessageThread ta = (TestMessageThread) oa; + TestMessageThread tb = (TestMessageThread) ob; + TestMessageThread a = (ta.subject == null ? ta.kid : ta); + TestMessageThread b = (tb.subject == null ? tb.kid : tb); + return a.simplifiedSubject().compareTo(b.simplifiedSubject()); + } + }; + + } else if (sort_type == SORT_AUTHOR) { + comparer = new Comparer() { + public int compare(Object oa, Object ob) { + TestMessageThread ta = (TestMessageThread) oa; + TestMessageThread tb = (TestMessageThread) ob; + TestMessageThread a = (ta.subject == null ? ta.kid : ta); + TestMessageThread b = (tb.subject == null ? tb.kid : tb); + return a.author.compareTo(b.author); + } + }; + + } else { // (sort_type == SORT_NUMBER) + + comparer = new Comparer() { + public int compare(Object oa, Object ob) { + TestMessageThread ta = (TestMessageThread) oa; + TestMessageThread tb = (TestMessageThread) ob; + TestMessageThread a = (ta.subject == null ? ta.kid : ta); + TestMessageThread b = (tb.subject == null ? tb.kid : tb); + + if (a.message_number == b.message_number) return 0; + else if (a.message_number < b.message_number) return -1; + else return 1; + } + }; + } + + Sorter s = new Sorter(comparer); + TestMessageThread dummy = new TestMessageThread(); + dummy.kid = first; + s.sortMessageChildren(dummy); + first = dummy.kid; + dummy = null; + + System.out.print("\n------- sorted:\n\n"); + printThread(first, 0); + } + + private static void printThread(TestMessageThread thread, int depth) { + for (int i = 0; i < depth; i++) System.out.print(" "); + System.out.println(thread.toString()); + if (thread.kid != null) printThread(thread.kid, depth+1); + if (thread.next != null) printThread(thread.next, depth); + } +} diff --git a/mozilla/grendel/view/Threader.java b/mozilla/grendel/view/Threader.java new file mode 100644 index 00000000000..d6cd058df30 --- /dev/null +++ b/mozilla/grendel/view/Threader.java @@ -0,0 +1,587 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Jamie Zawinski , 13 Jun 1995. + * Ported from C on 14 Aug 1997. + */ + +package grendel.view; + +import java.util.Hashtable; +import java.util.Enumeration; + +import calypso.util.Assert; + +/** + This arranges a set of messages into a thread hierarchy, by references. + The sets of messages are accessed via the IThreadable interface. + + @see IThreadable + @see Sorter + */ + +class Threader { + private ThreadContainer root_node; // has kids, and no next + private Hashtable id_table; // maps message IDs to ThreadContainers + private int bogus_id_count = 0; // tick of how many dup IDs we've seen + + /** Threads the set of messages indicated by threadable_root. + The IThreadable returned is the new first element of the root set. + @param threadable_root The start of the list. + */ + public IThreadable thread(IThreadable threadable_root) { + if (threadable_root == null) return null; + id_table = new Hashtable(); + + for (Enumeration e = threadable_root.allElements(); e.hasMoreElements();) { + IThreadable t = (IThreadable) e.nextElement(); + if (!t.isDummy()) + buildContainer(t); + } + + root_node = findRootSet(); + id_table.clear(); + id_table = null; + + pruneEmptyContainers(root_node); + + // We do this so to avoid flipping the input order each time through. + root_node.reverseChildren(); + + gatherSubjects(); + + if (root_node.next != null) + throw new Error("root node has a next?" + root_node); + + for (ThreadContainer r = root_node.child; r != null; r = r.next) { + // If this direct child of the root node has no threadable in it, + // manufacture a dummy container to bind its children together. + // Note that these dummies can only ever occur as elements of + // the root set. + if (r.threadable == null) + r.threadable = r.child.threadable.makeDummy(); + } + + IThreadable result = (root_node.child == null + ? null + : root_node.child.threadable); + + // Flush the tree structure of each element of the root set down into + // their underlying threadables. + root_node.flush(); + root_node = null; + return result; + } + + + // buildContainer() does three things: + // + // = It walks the tree of threadables, and wraps each in a + // ThreadContainer object. + // = It indexes each ThreadContainer object in the id_table, under + // the message ID of the contained IThreadable. + // = For each of the IThreadable's references, it ensures that there + // is a ThreadContainer in the table (an empty one, if necessary.) + // + private void buildContainer(IThreadable threadable) { + + Object id = threadable.messageThreadID(); + Assert.Assertion(id != null); + ThreadContainer c = (ThreadContainer) id_table.get (id); + + if (c != null) { + // There is already a ThreadContainer in the table for this ID. + // Under normal circumstances, there will be no IThreadable in it + // (since it was a forward reference from a References field.) + // + // If there is already a threadable in it, then that means there + // are two IThreadables with the same ID. Generate a new ID for + // this one, sigh... This ID is only used to cause the two entries + // in the hash table to not stomp each other. + // + if (c.threadable != null) { + id = ""; + c = null; + } else { + c.threadable = threadable; + } + } + + // Create a ThreadContainer for this IThreadable, and index it in + // the hash table. + // + if (c == null) { + c = new ThreadContainer(); + c.threadable = threadable; +// c.debug_id = id; + id_table.put (id, c); + } + + // Create ThreadContainers for each of the references which don't + // have them. Link each of the referenced messages together in the + // order implied by the references field, unless they are already + // linked. + ThreadContainer parent_ref = null; + { + Object refs[] = threadable.messageThreadReferences(); + int L = (refs == null ? 0 : refs.length); + for (int i = 0; i < L; i++) { + Object ref_string = refs[i]; + ThreadContainer ref = (ThreadContainer) id_table.get (ref_string); + + if (ref == null) { + ref = new ThreadContainer(); +// ref.debug_id = ref_string; + id_table.put (ref_string, ref); + } + + // If we have references A B C D, make D be a child of C, etc, + // except if they have parents already. + // + if (parent_ref != null && // there is a parent + ref.parent == null && // don't have a parent already + parent_ref != ref && // not a tight loop + !parent_ref.find_child(ref)) { // not a wide loop + // Ok, link it into the parent's child list. + ref.parent = parent_ref; + ref.next = parent_ref.child; + parent_ref.child = ref; + } + parent_ref = ref; + } + } + + // At this point `parent_ref' is set to the container of the last element + // in the references field. Make that be the parent of this container, + // unless doing so would introduce a circularity. + // + if (parent_ref != null && + (parent_ref == c || + c.find_child (parent_ref))) + parent_ref = null; + + if (c.parent != null) { + // If it has a parent already, that's there because we saw this message + // in a references field, and presumed a parent based on the other + // entries in that field. Now that we have the actual message, we can + // be more definitive, so throw away the old parent and use this new one. + // Find this container in the parent's child-list, and unlink it. + // + // Note that this could cause this message to now have no parent, if it + // has no references field, but some message referred to it as the + // non-first element of its references. (Which would have been some + // kind of lie...) + // + ThreadContainer rest, prev; + for (prev = null, rest = c.parent.child; + rest != null; + prev = rest, rest = rest.next) { + if (rest == c) + break; + } + if (rest == null) + throw new Error("didn't find " + c + " in parent " + c.parent); + + if (prev == null) + c.parent.child = c.next; + else + prev.next = c.next; + + c.next = null; + c.parent = null; + } + + // If we have a parent, link c into the parent's child list. + if (parent_ref != null) { + c.parent = parent_ref; + c.next = parent_ref.child; + parent_ref.child = c; + } + } + + + // Find the root set of the ThreadContainers (and return a root node.) + // A container is in the root set if it has no parents. + // + private ThreadContainer findRootSet() { + ThreadContainer root = new ThreadContainer(); +// root.debug_id = "((root))"; + for (Enumeration e = id_table.elements(); e.hasMoreElements(); ) { + ThreadContainer c = (ThreadContainer) e.nextElement(); + if (c.parent == null) { + if (c.next != null) + throw new Error("c.next is " + c.next.toString()); + c.next = root.child; + root.child = c; + } + } + return root; + } + + + // Walk through the threads and discard any empty container objects. + // After calling this, there will only be any empty container objects + // at depth 0, and those will all have at least two kids. + // + private void pruneEmptyContainers(ThreadContainer parent) { + ThreadContainer container, prev, next; + for (prev = null, container = parent.child, next = container.next; + container != null; + prev = container, container = next, + next = (container == null ? null : container.next)) { + + if (container.threadable == null && + container.child == null) { + // This is an empty container with no kids. Nuke it. + // + // Normally such containers won't occur, but they can show up when + // two messages have References lines that disagree. For example, + // assuming A and B are messages, and 1, 2, and 3 are references for + // messages we haven't seen: + // + // A has refs: 1 2 3 + // B has refs: 1 3 + // + // There is ambiguity as to whether 3 is a child of 1 or 2. So, + // depending on the processing order, we might end up with either + // + // -- 1 + // |-- 2 + // |-- 3 + // |-- A + // |-- B + // or + // -- 1 + // |-- 2 <--- non root childless container + // |-- 3 + // |-- A + // |-- B + // + if (prev == null) + parent.child = container.next; + else + prev.next = container.next; + + // Set container to prev so that prev keeps its same value + // the next time through the loop. + container = prev; + + } else if (container.threadable == null && // expired, and + container.child != null && // has kids, and + (container.parent != null || // not at root, or + container.child.next == null)) { // only one kid + + // Expired message with kids. Promote the kids to this level. + // Don't do this if we would be promoting them to the root level, + // unless there is only one kid. + + ThreadContainer tail; + ThreadContainer kids = container.child; + + // Remove this container from the list, replacing it with `kids'. + if (prev == null) + parent.child = kids; + else + prev.next = kids; + + // make each child's parent be this level's parent. + // make the last child's next be this container's next + // (splicing `kids' into the list in place of `container'.) + for (tail = kids; tail.next != null; tail = tail.next) + tail.parent = container.parent; + + tail.parent = container.parent; + tail.next = container.next; + + // Since we've inserted items in the chain, `next' currently points + // to the item after them (tail.next); reset that so that we process + // the newly promoted items the very next time around. + next = kids; + + // Set container to prev so that prev keeps its same value + // the next time through the loop. + container = prev; + + } else if (container.child != null) { + // A real message with kids. + // Iterate over its children, and try to strip out the junk. + + pruneEmptyContainers(container); + } + } + } + + + // If any two members of the root set have the same subject, merge them. + // This is so that messages which don't have References headers at all + // still get threaded (to the extent possible, at least.) + // + private void gatherSubjects() { + + int count = 0; + for (ThreadContainer c = root_node.child; c != null; c = c.next) + count++; + + // Make the hash table large enough to not need to be rehashed. + Hashtable subj_table = new Hashtable((int) (count * 1.2), (float) 0.9); + + count = 0; + for (ThreadContainer c = root_node.child; c != null; c = c.next) { + IThreadable threadable = c.threadable; + + // If there is no threadable, this is a dummy node in the root set. + // Only root set members may be dummies, and they always have at least + // two kids. Take the first kid as representative of the subject. + if (threadable == null) + threadable = c.child.threadable; + + String subj = threadable.simplifiedSubject(); + + if (subj == null || subj == "") + continue; + + ThreadContainer old = (ThreadContainer) subj_table.get(subj); + + // Add this container to the table if: + // - There is no container in the table with this subject, or + // - This one is a dummy container and the old one is not: the dummy + // one is more interesting as a root, so put it in the table instead. + // - The container in the table has a "Re:" version of this subject, + // and this container has a non-"Re:" version of this subject. + // The non-re version is the more interesting of the two. + // + if (old == null || + (c.threadable == null && old.threadable != null) || + (old.threadable != null && old.threadable.subjectIsReply() && + c.threadable != null && !c.threadable.subjectIsReply())) { + subj_table.put(subj, c); + count++; + } + } + + if (count == 0) // if the table is empty, we're done. + return; + + // The subj_table is now populated with one entry for each subject which + // occurs in the root set. Now iterate over the root set, and gather + // together the difference. + // + ThreadContainer prev, c, rest; + for (prev = null, c = root_node.child, rest = c.next; + c != null; + prev = c, c = rest, rest = (rest == null ? null : rest.next)) { + + IThreadable threadable = c.threadable; + if (threadable == null) // might be a dummy -- see above + threadable = c.child.threadable; + + String subj = threadable.simplifiedSubject(); + + // Don't thread together all subjectless messages; let them dangle. + if (subj == null || subj == "") + continue; + + ThreadContainer old = (ThreadContainer) subj_table.get(subj); + if (old == c) // oops, that's us + continue; + + // Ok, so now we have found another container in the root set with + // the same subject. There are a few possibilities: + // + // - If both are dummies, append one's children to the other, and remove + // the now-empty container. + // + // - If one container is a dummy and the other is not, make the non-dummy + // one be a child of the dummy, and a sibling of the other "real" + // messages with the same subject (the dummy's children.) + // + // - If that container is a non-dummy, and that message's subject does + // not begin with "Re:", but *this* message's subject does, then + // make this be a child of the other. + // + // - If that container is a non-dummy, and that message's subject begins + // with "Re:", but *this* message's subject does *not*, then make that + // be a child of this one -- they were misordered. (This happens + // somewhat implicitly, since if there are two messages, one with Re: + // and one without, the one without will be in the hash table, + // regardless of the order in which they were seen.) + // + // - Otherwise, make a new dummy container and make both messages be a + // child of it. This catches the both-are-replies and neither-are- + // replies cases, and makes them be siblings instead of asserting a + // hierarchical relationship which might not be true. + // + // (People who reply to messages without using "Re:" and without using + // a References line will break this slightly. Those people suck.) + // + // (It has occurred to me that taking the date or message number into + // account would be one way of resolving some of the ambiguous cases, + // but that's not altogether straightforward either.) + + + // Remove the "second" message from the root set. + if (prev == null) + root_node.child = c.next; + else + prev.next = c.next; + c.next = null; + + + if (old.threadable == null && c.threadable == null) { + // They're both dummies; merge them. + ThreadContainer tail; + for (tail = old.child; + tail != null && tail.next != null; + tail = tail.next) + ; + tail.next = c.child; + for (tail = c.child; tail != null; tail = tail.next) + tail.parent = old; + c.child = null; + + } else if (old.threadable == null || // old is empty, or + (c.threadable != null && + c.threadable.subjectIsReply() && // c has Re, and + !old.threadable.subjectIsReply())) { // old does not. + // Make this message be a child of the other. + c.parent = old; + c.next = old.child; + old.child = c; + + } else { + // Make the old and new messages be children of a new dummy container. + // We do this by creating a new container object for old->msg and + // transforming the old container into a dummy (by merely emptying it), + // so that the hash table still points to the one that is at depth 0 + // instead of depth 1. + + ThreadContainer newc = new ThreadContainer(); + newc.threadable = old.threadable; +// newc.debug_id = old.debug_id; + newc.child = old.child; + for (ThreadContainer tail = newc.child; tail != null; tail = tail.next) + tail.parent = newc; + + old.threadable = null; + old.child = null; +// old.debug_id = null; + + c.parent = old; + newc.parent = old; + + // old is now a dummy; make it have exactly two kids, c and newc. + old.child = c; + c.next = newc; + } + + // we've done a merge, so keep the same `prev' next time around. + c = prev; + } + + subj_table.clear(); + subj_table = null; + } +} + + +/* + The ThreadContainer object is used to encapsulate an IThreadable object + (it holds some intermediate state used while threading.) This is a + private class that doesn't escape from this module. + */ + +class ThreadContainer { + IThreadable threadable; + ThreadContainer parent; + ThreadContainer child; + ThreadContainer next; + +// Object debug_id; +// int id2; +// static int id2_pool = 0; + +// ThreadContainer() { id2 = ++id2_pool; } + +// public String toString() { return ""; } + +// public void debug_print(int depth) { +// for (int i = 0; i < depth; i++) System.out.print(" "); +// System.out.println(this); +// if (child != null) child.debug_print(depth+1); +// if (next != null) next.debug_print(depth); +// } + + // Copy the ThreadContainer tree structure down into the underlying + // IThreadable objects (that is, make the IThreadable tree look like + // the ThreadContainer tree.) + // + void flush() { + if (parent != null && threadable == null) + // Only the root_node is allowed to not have a threadable. + throw new Error("no threadable in " + this.toString()); + + parent = null; + + if (threadable != null) + threadable.setChild(child == null ? null : child.threadable); + + if (child != null) { + child.flush(); + child = null; + } + + if (threadable != null) + threadable.setNext(next == null ? null : next.threadable); + + if (next != null) { + next.flush(); + next = null; + } + + threadable = null; + } + + // Returns true if child is under self's tree. This is used for + // detecting circularities in the references header. + boolean find_child(ThreadContainer target) { + if (child == null) + return false; + else if (child == target) + return true; + else + return child.find_child(target); + } + + void reverseChildren() { + if (child != null) { + // nreverse the children (child through child.next.next.next...) + ThreadContainer kid, prev, rest; + for (prev = null, kid = child, rest = kid.next; + kid != null; + prev = kid, kid = rest, rest = (rest == null ? null : rest.next)) + kid.next = prev; + child = prev; + + // then do it for the kids + for (kid = child; kid != null; kid = kid.next) + kid.reverseChildren(); + } + } +} diff --git a/mozilla/grendel/view/ViewedFolder.java b/mozilla/grendel/view/ViewedFolder.java new file mode 100644 index 00000000000..7d55d1471da --- /dev/null +++ b/mozilla/grendel/view/ViewedFolder.java @@ -0,0 +1,96 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 2 Dec 1997. + */ + +package grendel.view; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Store; + +public interface ViewedFolder { + /** + * Returns the associated folder + */ + + public Folder getFolder(); + + /** + * Returns the ViewedFolder associated with the given folder. + * The Folder object inside the ViewedFolder may not be the + * same as the object passed in, but it will always represent + * the same folder + */ + + public ViewedFolder getViewedFolder(Folder aFolder) + throws MessagingException; + + /** + * Get cached message count data, since some protocols + * will hit the server for each call. + */ + + public int getMessageCount(); + + /** + * Get cached unread count, since some protocols will + * hit the server for each call. + */ + + public int getUnreadMessageCount(); + + /** + * Get cached undeleted message count, since some protocols will + * hit the server for each call. + */ + + public int getUndeletedMessageCount(); + + /** + * Returns the next folder at this level. + */ + + public ViewedFolder getNextFolder(); + + /** + * Returns the first subfolder of this folder. + */ + + public ViewedFolder getFirstSubFolder(); + + /** + * Returns the parent folder. Returns null for the default + * folder for a session. + */ + + public ViewedFolder getParentFolder(); + + /** + * Returns the associated session + */ + + public ViewedStore getViewedStore(); + + /** + * Returns whether this is an inbox or not + */ + + public boolean isInbox(); +} diff --git a/mozilla/grendel/view/ViewedFolderBase.java b/mozilla/grendel/view/ViewedFolderBase.java new file mode 100644 index 00000000000..3e94dcd66e8 --- /dev/null +++ b/mozilla/grendel/view/ViewedFolderBase.java @@ -0,0 +1,410 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 2 Dec 1997. + */ + +package grendel.view; + +import java.text.Collator; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.event.ConnectionEvent; +import javax.mail.event.ConnectionListener; +import javax.mail.event.FolderEvent; +import javax.mail.event.FolderListener; +import javax.mail.event.MessageChangedEvent; +import javax.mail.event.MessageChangedListener; +import javax.mail.event.MessageCountEvent; +import javax.mail.event.MessageCountListener; + +import calypso.util.Comparer; +import calypso.util.QSort; + +import grendel.storage.FolderExtraFactory; + +class ViewedFolderBase implements ViewedFolder { + + static final String kInbox = "INBOX"; + + Folder fFolder; + ViewedFolderBase fThis; + ViewedFolderBase fParent, fNext, fChild; + ViewedStoreBase fViewedStore; + boolean fBuilt = false; + boolean fOpened = false; + boolean fInbox = false; + int fMessageCount = -1; + int fUnreadCount = -1; + int fUndeletedCount = -1; + + Thread fUpdateThread; + + /** + * Constructor + */ + + public ViewedFolderBase(ViewedStoreBase aStore, ViewedFolderBase aParent, + Folder aFolder) { + fViewedStore = aStore; + fParent = aParent; + fThis = this; + + setFolder(aFolder); + } + + /** + * + */ + + protected void setFolder(Folder aFolder) { + fFolder = aFolder; + if (fFolder != null) { + FolderChangeListener l = new FolderChangeListener(); + fFolder.addConnectionListener(l); + fFolder.addFolderListener(l); + fFolder.addMessageCountListener(l); + fFolder.addMessageChangedListener(l); + fInbox = fFolder.getName().equalsIgnoreCase(kInbox); + } + } + + /** + * Returns the associated folder + */ + + public Folder getFolder() { + return fFolder; + } + + /** + * Returns whether we thing the folder is opened + */ + + public boolean isOpen() { + return fOpened; + } + + /** + * Returns the ViewedFolder associated with the given folder. + * The Folder object inside the ViewedFolder may not be the + * same as the object passed in, but it will always represent + * the same folder + */ + + public ViewedFolder getViewedFolder(Folder aFolder) + throws MessagingException { + Folder folder = getFolder(); + if (folder == null) { + return null; + } + String path1 = folder.getFullName(); + String path2 = aFolder.getFullName(); + if (path1.equals(path2)) { + return this; + } else { + if (!path2.startsWith(path1)) // We've gone down the wrong branch + throw new IllegalStateException(); + + String path = path2.substring(path1.length()); + char separator = folder.getSeparator(); + String root; + + int index = path.indexOf(separator); + if (index < 0) { + root = path; + } else { + root = path.substring(0, index); + } + ViewedFolder temp = getFirstSubFolder(); + while (temp != null) { + if (temp.getFolder().getName().equals(root)) { + return temp.getViewedFolder(aFolder); + } + temp = temp.getNextFolder(); + } + } + return null; + } + + /** + * Get cached message count data, since some protocols + * will hit the server for each call. + */ + + public int getMessageCount() { + if (fMessageCount < 0) { + updateMessageCount(); + } + return fMessageCount; + } + + /** + * Get cached unread count, since some protocols will + * hit the server for each call. + */ + + public int getUnreadMessageCount() { + if (fUnreadCount < 0) { + updateUnreadCount(); + } + return fUnreadCount; + } + + /** + * Get cached undeleted message count, since some protocols will + * hit the server for each call. + */ + + public int getUndeletedMessageCount() { + if (fUndeletedCount < 0) { + updateUndeletedCount(); + } + return fUndeletedCount; + } + + /** + * Returns the next folder at this level. + */ + + public ViewedFolder getNextFolder() { + return fNext; + } + + /** + * Returns the first subfolder of this folder. + */ + + public ViewedFolder getFirstSubFolder() { + Folder folder = getFolder(); + if (folder != null && !fBuilt) { + try { + fViewedStore.checkConnected(); + if ((folder.getType() & Folder.HOLDS_FOLDERS) != 0) { + Folder list[]; + if (fViewedStore.getVisible() == ViewedStore.kAll) { + list = folder.list(); + } else { + list = folder.listSubscribed(); + } + + if (list != null && list.length > 0) { + if (fViewedStore.isSorted()) { + QSort qsort = new QSort(new FolderComparer()); + qsort.sort(list); + } + fChild = new ViewedFolderBase(fViewedStore, this, list[0]); + ViewedFolderBase temp = fChild; + for (int i = 1; i < list.length; i++) { + ViewedFolderBase next = + new ViewedFolderBase(fViewedStore, this, list[i]); + temp.setNextFolder(next); + temp = next; + } + } + } + fBuilt = true; + } catch (MessagingException e) { + e.printStackTrace(); + } + } + return fChild; + } + + /** + * Returns the parent folder. Returns null for the default + * folder for a session. + */ + + public ViewedFolder getParentFolder() { + return fParent; + } + + /** + * Returns the associated store + */ + + public ViewedStore getViewedStore() { + return fViewedStore; + } + + /** + * Returns whether this is an inbox + */ + + public boolean isInbox() { + return fInbox; + } + + void setNextFolder(ViewedFolderBase aNext) { + fNext = aNext; + } + + void addSubFolder(ViewedFolderBase aFolder) { + if (fBuilt) { + if (fChild == null) { + fChild = aFolder; + } else { + ViewedFolderBase temp = fChild; + while (temp.fNext != null) { + temp = temp.fNext; + } + temp.fNext = aFolder; + } + } // else we don't care + } + + void removeSubFolder(ViewedFolderBase aFolder) { + ViewedFolderBase temp = fChild; + + if (temp == null) { + return; + } else if (aFolder == temp) { + fChild = temp.fNext; + } else { + while (temp.fNext != null) { + if (temp.fNext == aFolder) { + temp.fNext = temp.fNext.fNext; + aFolder.fNext = null; + return; + } + temp = temp.fNext; + } + } + } + + class FolderChangeListener implements MessageCountListener, + MessageChangedListener, + ConnectionListener, + FolderListener + { + public void messagesAdded(MessageCountEvent aEvent) { + updateCounts(); + fViewedStore.notifyFolderChanged(fThis); + } + + public void messagesRemoved(MessageCountEvent aEvent) { + updateCounts(); + fViewedStore.notifyFolderChanged(fThis); + } + + public void folderCreated(FolderEvent e) { + // It's unlikely we'll see this + } + + public void folderDeleted(FolderEvent e) { + if (fParent != null) { + fParent.removeSubFolder(fThis); + } + + fViewedStore.notifyFolderDeleted(fThis); + } + + public void folderRenamed(FolderEvent e) { + fViewedStore.notifyFolderChanged(fThis); + } + + public void messageChanged(MessageChangedEvent aEvent) { + updateCounts(); + fViewedStore.notifyFolderChanged(fThis); + } + + public void closed(ConnectionEvent aEvent) { + fOpened = false; + fViewedStore.notifyFolderChanged(fThis); + } + + public void disconnected(ConnectionEvent aEvent) { + fOpened = false; + fViewedStore.notifyFolderChanged(fThis); + } + + public void opened(ConnectionEvent aEvent) { + fOpened = true; + updateCounts(); + fViewedStore.notifyFolderChanged(fThis); + } + } + + void updateMessageCount() { + updateCounts(); + } + + void updateUnreadCount() { + updateCounts(); + } + + void updateUndeletedCount() { + updateCounts(); + } + + void updateCounts() { + try { + fViewedStore.checkConnected(); + fViewedStore.addFolderUpdate(fThis); + } catch (MessagingException e) { + e.printStackTrace(); + } + } + + void setCounts(int messages, int unread, int undeleted) { + if (messages != -1) { + fMessageCount = messages; + } + if (unread != -1) { + fUnreadCount = unread; + } + if (undeleted != -1) { + fUndeletedCount = undeleted; + } + fViewedStore.notifyFolderChanged(fThis); + } + + public String toString() { + return fFolder.getFullName(); + } + + class FolderComparer implements Comparer { + Collator fCollator; + + FolderComparer() { + fCollator = Collator.getInstance(); + fCollator.setStrength(Collator.SECONDARY); + } + + public int compare(Object a, Object b) { + int res = 0; + if (a instanceof Folder && b instanceof Folder) { + String folderA = ((Folder) a).getName(); + String folderB = ((Folder) b).getName(); + res = fCollator.compare(folderA, folderB); + if (res != 0) { + if (folderA.equalsIgnoreCase(kInbox)) { + res = -1; + } else if (folderB.equalsIgnoreCase(kInbox)) { + res = 1; + } + + } + } + return res; + } + } +} diff --git a/mozilla/grendel/view/ViewedMessage.java b/mozilla/grendel/view/ViewedMessage.java new file mode 100644 index 00000000000..2b053f2a8ec --- /dev/null +++ b/mozilla/grendel/view/ViewedMessage.java @@ -0,0 +1,59 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 25 Aug 1997. + */ + +package grendel.view; + +import javax.mail.Message; + +/** This is a message in a MessageSetView. It represents a message, and also + knows where it is in relationship with other messages in the same view. */ + +public interface ViewedMessage { + /** Gets the view that this message is a part of. */ + public MessageSetView getView(); + + /** Gets the message itself. You need to go through this to find out + stuff about the message (like its subject, author, etc.). */ + public Message getMessage(); + + /** Returns the parent of this message. (This is always null unless + threading is turned on in the view.) */ + public ViewedMessage getParent(); + + /** Returns the first child of this message. (This is always null unless + threading is turned on in the view.) */ + public ViewedMessage getChild(); + + /** Returns the next message. This is the next message that has the same + parent as this message.*/ + public ViewedMessage getNext(); + + /** This should return true of dummy messages, false otherwise. + It is legal to pass dummy messages in with the list returned by + elements(); the isDummy() method is the mechanism by which they are + noted and ignored. + */ + boolean isDummy(); + + /** Debugging hack. Dumps this message (and messages in its tree) out + to System.out. */ + public void dump(); +} diff --git a/mozilla/grendel/view/ViewedMessageBase.java b/mozilla/grendel/view/ViewedMessageBase.java new file mode 100644 index 00000000000..a2422016bc2 --- /dev/null +++ b/mozilla/grendel/view/ViewedMessageBase.java @@ -0,0 +1,236 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Terry Weissman , 25 Aug 1997. + */ + + +package grendel.view; + +import calypso.util.Assert; + +import grendel.storage.MessageExtra; +import grendel.storage.MessageExtraFactory; + +import javax.mail.Message; +import javax.mail.MessagingException; + +import java.util.Enumeration; +import java.util.NoSuchElementException; + +class ViewedMessageBase implements ViewedMessage, IThreadable, ISortable { + MessageSetView fView; + Message fMessage; + + ViewedMessage fParent; + ViewedMessage fChildren; + ViewedMessage fSibling; + + public int sortkey; // Used to index into cached info + // when sorting. + + public ViewedMessageBase(MessageSetView v, Message m) { + fView = v; + fMessage = m; + } + + public final MessageSetView getView() { + return fView; + } + + public final Message getMessage() { + return fMessage; + } + + // ******************* IThreadable methods **************** + + class AllEnumerator implements Enumeration { + + ViewedMessageBase tail; + Enumeration kids; + + AllEnumerator(ViewedMessageBase thread) { + tail = thread; + } + + public synchronized Object nextElement() { + if (kids != null) { + // if `kids' is non-null, then we've already returned a node, + // and we should now go to work on its children. + ViewedMessageBase result = (ViewedMessageBase) kids.nextElement(); + if (!kids.hasMoreElements()) { + kids = null; + } + return result; + + } else if (tail != null) { + // Return `tail', but first note its children, if any. + // We will descend into them the next time around. + ViewedMessageBase result = tail; + if (tail.fChildren != null) { + kids = new AllEnumerator((ViewedMessageBase) tail.fChildren); + } + tail = (ViewedMessageBase) tail.fSibling; + return result; + + } else { + throw new NoSuchElementException(); + } + } + + public synchronized boolean hasMoreElements() { + if (tail != null) + return true; + else if (kids != null && kids.hasMoreElements()) + return true; + else + return false; + } + } + + public Enumeration allElements() { + return new AllEnumerator(this); + } + + public Object messageThreadID() { + try { + return MessageExtraFactory.Get(fMessage).getMessageID(); + } catch (MessagingException e) { + return null; + } + } + + public Object[] messageThreadReferences() { + try { + return MessageExtraFactory.Get(fMessage).messageThreadReferences(); + } catch (MessagingException e) { + return null; + } + } + + public String simplifiedSubject() { + try { + return MessageExtraFactory.Get(fMessage).simplifiedSubject(); + } catch (MessagingException e) { + return ""; + } + } + + public boolean subjectIsReply() { + try { + return MessageExtraFactory.Get(fMessage).subjectIsReply(); + } catch (MessagingException e) { + return false; + } + } + + public Enumeration children() { + return new Enumeration() { + ViewedMessageBase cur = (ViewedMessageBase) fChildren; + public boolean hasMoreElements() { + return (cur != null); + } + public Object nextElement() { + Object result = cur; + cur = (ViewedMessageBase) cur.fSibling; + return result; + } + }; + } + + public void setNext(Object next) { + fSibling = (ViewedMessage) next; + } + + public void setChild(Object kid) { + fChildren = (ViewedMessage) kid; + } + + public void setParentPointers(ViewedMessage parent) { + fParent = parent; + ViewedMessageBase v = (ViewedMessageBase) fChildren; + if (v != null) v.setParentPointers(this); + v = (ViewedMessageBase) fSibling; + if (v != null) v.setParentPointers(parent); + } + + public IThreadable makeDummy() { + return new DummyThreadable(fView, fMessage); + } + + public boolean isDummy() { + return false; + } + + public void dump() { + dump(0); + } + + void dump(int indent) { + for (int i=0 ; i, 2 Dec 1997. + */ + +package grendel.view; + +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Store; + +public interface ViewedStore extends ViewedFolder { + /** + * Value for setVisible() for showing all folders + */ + + public static final int kAll = 0; + + /** + * Value for setVisible() for showing subscribed folders + */ + + public static final int kSubscribed = 1; + + /** + * Value for setVisible() for showing active folders + */ + + public static final int kActive = 2; + + /** + * Returns the associated store. + */ + + public Store getStore(); + + /** + * Returns the store's default folder wrapped in a ViewedFolder + * object. + */ + + public ViewedFolder getDefaultFolder() throws MessagingException; + + /** + * Returns the protocol used by this store. + */ + + public String getProtocol(); + + /** + * Returns the host for this store. Returns null for a local store. + */ + + public String getHost(); + + /** + * Returns the user name used to connect. May return null if + * no user name was used. + */ + + public String getUsername(); + + /** + * Returns the port used to connect. Returns -1 for the protocol default. + */ + + public int getPort(); + + /** + * Returns the connected state of this store + */ + + public boolean isConnected(); + + /** + * Sets which children to show for this store + */ + + public void setVisible(int aVisible); + + /** + * Returns which children are showing for this store + */ + + public int getVisible(); + + /** + * Adds a ViewedStoreListener + */ + + public void addViewedStoreListener(ViewedStoreListener l); + + /** + * Removes a ViewedStoreListener + */ + + public void removeViewedStoreListener(ViewedStoreListener l); +} diff --git a/mozilla/grendel/view/ViewedStoreBase.java b/mozilla/grendel/view/ViewedStoreBase.java new file mode 100644 index 00000000000..1d950ed3ce3 --- /dev/null +++ b/mozilla/grendel/view/ViewedStoreBase.java @@ -0,0 +1,360 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 2 Dec 1997. + */ + +package grendel.view; + +import java.util.Vector; + +import javax.mail.AuthenticationFailedException; +import javax.mail.Folder; +import javax.mail.MessagingException; +import javax.mail.Store; +import javax.mail.event.ConnectionEvent; +import javax.mail.event.ConnectionListener; +import javax.mail.event.FolderEvent; +import javax.mail.event.FolderListener; + +import com.sun.java.swing.JOptionPane; +import com.sun.java.swing.event.EventListenerList; + +import calypso.util.PreferencesFactory; + +import grendel.storage.FolderExtraFactory; + +public class ViewedStoreBase extends ViewedFolderBase implements ViewedStore { + + Vector fUpdateQueue; + Thread fUpdateThread; + + ViewedFolder fDefaultFolder; + Store fStore; + String fProto; + String fHost; + String fUser; + String fPrefBase; + int fPort; + ViewedStore fNext; + boolean fConnected; + boolean fSorted; + + int fVisible = kSubscribed; + + EventListenerList fListeners = new EventListenerList(); + + /** + * ViewedStoreBase constructor. This should be called before any + * attempt to connect so that it can reflect the correct connection + * state. + */ + + public ViewedStoreBase(Store aStore, String aProto, String aHost, + int aPort, String aUser) { + super(null, null, null); + + fViewedStore = this; + fStore = aStore; + fProto = aProto; + fHost = aHost; + fPort = aPort; + fUser = aUser; + + fPrefBase = "mail." + aProto + (aHost != null ? "-" + aHost : ""); + fSorted = PreferencesFactory.Get().getBoolean(fPrefBase + ".sort", false); + + fStore.addConnectionListener(new StoreConnectionListener()); + } + + /** + * Returns the associated folder + */ + + public Folder getFolder() { + try { + getDefaultFolder(); + return super.getFolder(); + } catch (MessagingException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Returns the associated store. + */ + + public Store getStore() { + return fStore; + } + + /** + * Returns the store's default folder wrapped in a ViewedFolder + * object. + */ + + public ViewedFolder getDefaultFolder() throws MessagingException { + if (fDefaultFolder == null) { + checkConnected(); + if (true) { + fDefaultFolder = new ViewedFolderBase(this, null, + fStore.getDefaultFolder()); + + setFolder(fDefaultFolder.getFolder()); + } + } + return fDefaultFolder; + } + + /** + * Returns the protocol used by this store. + */ + + public String getProtocol() { + return fProto; + } + + /** + * Returns the host for this store. Returns null for a local store. + */ + + public String getHost() { + return fHost; + } + + /** + * Returns the user name used to connect. May return null if + * no user name was used. + */ + + public String getUsername() { + return fUser; + } + + /** + * Returns the port used to connect. Returns -1 for the protocol default. + */ + + public int getPort() { + return fPort; + } + + /** + * Returns the connected state of this store + */ + + public boolean isConnected() { + return fConnected; + } + + /** + * Adds a ViewedStoreListener + */ + + public void addViewedStoreListener(ViewedStoreListener l) { + fListeners.add(ViewedStoreListener.class, l); + } + + /** + * Removes a ViewedStoreListener + */ + + public void removeViewedStoreListener(ViewedStoreListener l) { + fListeners.remove(ViewedStoreListener.class, l); + } + + void checkConnected() throws MessagingException { + if (!isConnected()) { + boolean success = false; + + try { + if (fStore != null) { + fStore.connect(getHost(), getUsername(), null); + } + success = true; + } catch (AuthenticationFailedException e) { + JOptionPane.showMessageDialog(null, + // labels.getString("loginFailedLabel"), + // labels.getString("errorDialogLabel"), + "Login failed", + "Grendel Error", + JOptionPane.ERROR_MESSAGE); + + } catch (MessagingException e) { + System.out.println("Got exception " + e + + " while connecting to " + this); + e.printStackTrace(); + } + } + } + + boolean isSorted() { + return fSorted; + } + + /** + * Sets which children to show for this store + */ + + public void setVisible(int aVisible) { + fVisible = aVisible; + } + + /** + * Returns which children are showing for this store + */ + + public int getVisible() { + return fVisible; + } + + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(fProto); + if (fHost != null) { + buffer.append(":"); + buffer.append(fHost); + } + return buffer.toString(); + } + + void addFolderUpdate(ViewedFolderBase aFolder) { + if (fUpdateQueue == null) { + fUpdateQueue = new Vector(); + } + + if (!fUpdateQueue.contains(aFolder)) { + fUpdateQueue.addElement(aFolder); + } + + if (fUpdateThread == null) { + fUpdateThread = new Thread(new UpdateCountThread(), "MsgCount"); + fUpdateThread.setPriority(Thread.MIN_PRIORITY); + fUpdateThread.start(); + } + } + + void notifyFolderChanged(ViewedFolderBase aFolder) { + Object[] listeners = fListeners.getListenerList(); + ViewedStoreEvent event = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + // Lazily create the event: + if (event == null) + event = new ViewedStoreEvent(this, aFolder); + ((ViewedStoreListener)listeners[i+1]).folderChanged(event); + } + } + + void notifyFolderDeleted(ViewedFolderBase aFolder) + throws MessagingException { + if (!aFolder.getFolder().exists()) { + Object[] listeners = fListeners.getListenerList(); + ViewedStoreEvent event = null; + for (int i = 0; i < listeners.length - 1; i += 2) { + // Lazily create the event: + if (event == null) + event = new ViewedStoreEvent(this, aFolder); + ((ViewedStoreListener)listeners[i+1]).folderChanged(event); + } + } else { + notifyFolderChanged(aFolder); + } + } + + void notifyFolderCreated(Folder aFolder) + throws MessagingException { + if (aFolder.getParent() != null) { + try { + ViewedFolderBase parent = + (ViewedFolderBase) getViewedFolder(aFolder.getParent()); + parent.addSubFolder(new ViewedFolderBase(this, parent, aFolder)); + } catch (MessagingException e) { + e.printStackTrace(); + } + } else { + throw new IllegalStateException("Can't create default folder?"); + } + } + + class StoreConnectionListener implements ConnectionListener { + public void closed(ConnectionEvent aEvent) { + fConnected = false; + } + + public void disconnected(ConnectionEvent aEvent) { + // XXX Maybe throw up dialog? + fConnected = false; + } + + public void opened(ConnectionEvent aEvent) { + fConnected = true; + } + } + + class StoreFolderListener implements FolderListener { + public void folderCreated(FolderEvent e) { + notifyFolderCreated(e.getFolder()); + } + + public void folderDeleted(FolderEvent e) { + // We'll let ViewedFolderBase handle this + } + + public void folderRenamed(FolderEvent e) { + // We'll let ViewedFolderBase handle this + } + } + + class UpdateCountThread implements Runnable { + void updateFolder(ViewedFolderBase viewedFolder) { + try { + Folder folder = viewedFolder.getFolder(); + if (folder != null) { + + if (!folder.isOpen()) { + folder.open(Folder.READ_WRITE); + } + int messageCount = folder.getMessageCount(); + int unreadCount = folder.getUnreadMessageCount(); + int undeletedCount = + FolderExtraFactory.Get(folder).getUndeletedMessageCount(); + viewedFolder.setCounts(messageCount, unreadCount, undeletedCount); + } + } catch (MessagingException e) { + } catch (IllegalStateException e) { + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void run() { + while (!Thread.interrupted()) { + if (fUpdateQueue.size() > 0) { + ViewedFolderBase folder = + (ViewedFolderBase) fUpdateQueue.firstElement(); + updateFolder(folder); + fUpdateQueue.removeElement(folder); + } + try { + Thread.sleep(100); + } catch (InterruptedException e) {} + } + } + } +} diff --git a/mozilla/grendel/view/ViewedStoreEvent.java b/mozilla/grendel/view/ViewedStoreEvent.java new file mode 100644 index 00000000000..b0ad343883d --- /dev/null +++ b/mozilla/grendel/view/ViewedStoreEvent.java @@ -0,0 +1,41 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 5 Dec 1997. + */ + +package grendel.view; + +import java.util.EventObject; + +public class ViewedStoreEvent extends EventObject { + ViewedFolder fFolder; + + public ViewedStoreEvent(ViewedStore aSource, ViewedFolder aFolder) { + super(aSource); + fFolder = aFolder; + } + + public ViewedStore getStore() { + return (ViewedStore) getSource(); + } + + public ViewedFolder getFolder() { + return fFolder; + } +} diff --git a/mozilla/grendel/view/ViewedStoreListener.java b/mozilla/grendel/view/ViewedStoreListener.java new file mode 100644 index 00000000000..303cebecb7d --- /dev/null +++ b/mozilla/grendel/view/ViewedStoreListener.java @@ -0,0 +1,31 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 5 Dec 1997. + */ + +package grendel.view; + +import java.util.EventListener; + +public interface ViewedStoreListener extends EventListener { + public void storeNotification(ViewedStoreEvent aEvent); + public void folderCreated(ViewedStoreEvent aEvent); + public void folderDeleted(ViewedStoreEvent aEvent); + public void folderChanged(ViewedStoreEvent aEvent); +} diff --git a/mozilla/grendel/widgets/Animation.java b/mozilla/grendel/widgets/Animation.java new file mode 100644 index 00000000000..e2033235907 --- /dev/null +++ b/mozilla/grendel/widgets/Animation.java @@ -0,0 +1,128 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 23 Oct 1997. + */ + +package grendel.widgets; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.text.MessageFormat; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; + +public class Animation extends Component implements Runnable +{ + Thread fThread; + Icon fGlyphs[]; + int fCurrent = 0; + + public void run() + { + while (true) + { + try + { + fThread.sleep(100); + paint(getGraphics()); + fCurrent++; + fCurrent %= fGlyphs.length; + if (fCurrent == 0) + { + fCurrent = 1; + } + } + catch (InterruptedException e) + { + fThread.stop(); + fThread = null; + } + } + } + + public void paint(Graphics g) + { + super.paint(g); + + Dimension size = getSize(); + int x = (size.width - fGlyphs[0].getIconWidth()) / 2; + int y = (size.height - fGlyphs[0].getIconHeight()) / 2; + fGlyphs[fCurrent].paintIcon(this, g, x, y); + } + + public Dimension getPreferredSize() + { + return new Dimension(fGlyphs[0].getIconWidth(), + fGlyphs[0].getIconHeight()); + } + + public Dimension getMaximumSize() + { + return getPreferredSize(); + } + public Dimension getMinimumSize() + { + return getPreferredSize(); + } + + // + // IShellAnimation interface impl.. + // + + public Component getComponent() + { + return this; + } + + public void setImageTemplate( String template, int iFrames ) + { + fGlyphs = new Icon[iFrames]; + for (int i = 0; i < iFrames; i++) + { + Integer arg[] = { new Integer(i) }; + String name = MessageFormat.format(template, arg); + fGlyphs[i] = new ImageIcon(getClass().getResource(name)); + } + } + + public synchronized void start() + { + if (fThread != null) { + fThread.stop(); + } + fThread = new Thread(this, "Animation"); + fThread.start(); + } + + public synchronized void stop() + { + if (fThread != null) { + fThread.stop(); + fThread = null; + } + fCurrent = 0; + repaint(); + } +} + + + + diff --git a/mozilla/grendel/widgets/CellEditor.java b/mozilla/grendel/widgets/CellEditor.java new file mode 100644 index 00000000000..e5381ee6522 --- /dev/null +++ b/mozilla/grendel/widgets/CellEditor.java @@ -0,0 +1,29 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.widgets; + +import java.util.EventObject; +import java.awt.Component; + +public interface CellEditor extends com.sun.java.swing.CellEditor { + void setValue(Object aObject, Object aData, boolean aSelected); + boolean startCellEditing(EventObject aEvent); + Component getCellEditorComponent(); +} diff --git a/mozilla/grendel/widgets/CellRenderer.java b/mozilla/grendel/widgets/CellRenderer.java new file mode 100644 index 00000000000..90d573c6604 --- /dev/null +++ b/mozilla/grendel/widgets/CellRenderer.java @@ -0,0 +1,29 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.Component; + +public interface CellRenderer { + void setValue(Object aObject, Object aData, boolean aSelected); + Component getComponent(); +} diff --git a/mozilla/grendel/widgets/Column.java b/mozilla/grendel/widgets/Column.java new file mode 100644 index 00000000000..22dcde0864e --- /dev/null +++ b/mozilla/grendel/widgets/Column.java @@ -0,0 +1,328 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.Vector; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.event.ChangeEvent; + +/** + * A Column is a vertical display unit for a table that maintains width, rendering, + * and header display data. A column has no data model, thus it is only useful + * in conjunction with another UI component that provides a one. + */ + +public class Column { + Icon fIcon; + String fTitle; + Object fID; + int fWidth, fMinWidth, fMaxWidth; + boolean fResizeable; + boolean fSelectable; + Vector fListeners = new Vector(); + + CellRenderer fCellRenderer = new DefaultCellRenderer(); + CellEditor fCellEditor; + HeaderRenderer fHeaderRenderer = new DefaultHeaderRenderer(); + + /** + * Constructs a column with the given ID and title + */ + + public Column(Object aID, String aTitle) { + this(aID); + fTitle = new String(aTitle); + } + + /** + * Contructs a column with the given ID and icon + */ + + public Column(Object aID, Icon aIcon) { + this(aID); + fIcon = aIcon; + } + + /** + * Constructs a column with the given ID + */ + + public Column(Object aID) { + fID = aID; + fMinWidth = 10; + fWidth = 100; + fMaxWidth = 1000; + fResizeable = true; + } + + /** + * Sets the width of the column. + * @see getWidth() + */ + + public void setWidth(int aWidth) { + if (fWidth != aWidth) { + fWidth = aWidth; + + notifyWidthChange(); + } + } + + /** + * Returns the current column width + * @see setWidth() + */ + + public int getWidth() { + return fWidth; + } + + /** + * Sets the minimum column width. This width + * is for reference only, it is not enforced + * @see getMinWidth() + */ + + public void setMinWidth(int aWidth) { + fMinWidth = aWidth; + } + + /** + * Returns the minimum column width. This width + * is for reference only, it is not enforced + * @see setMinWidth() + */ + + public int getMinWidth() { + return fMinWidth; + } + + /** + * Sets the maximum column width. This width + * is for reference only, it is not enforced + * @see getMaxWidth() + */ + + public void setMaxWidth(int aWidth) { + fMaxWidth = aWidth; + } + + /** + * returns the maximum column width. This width + * is for reference only, it is not enforced + * @see setMaxWidth() + */ + + public int getMaxWidth() { + return fMaxWidth; + } + + /** + * Sets whether or not the column is resizable. This + * attribute is for reference only, it is not enforced. + * @see isResizeable() + */ + + public void setResizeable(boolean aResizeable) { + fResizeable = aResizeable; + } + + /** + * Returns whether or not the column is resizable. This + * attribute is for reference only, it is not enforced. + * @see setResizeable() + */ + + public boolean isResizeable() { + return fResizeable; + } + + /** + * Sets whether or not the column is selectable. + * @see isSelectable() + */ + + public void setSelectable(boolean aSelectable) { + fSelectable = aSelectable; + } + + /** + * Returns whether or not the column is resizable. This + * attribute is for reference only, it is not enforced. + * @see setSelectable() + */ + + public boolean isSelectable() { + return fSelectable; + } + + /** + * Sets the column title. This is intended to be displayed + * in the header above the column. + * @see getTitle() + */ + + public void setTitle(String aTitle) { + fTitle = new String(aTitle); + } + + /** + * Returns the column title. + * @see setTitle() + */ + + public String getTitle() { + return fTitle; + } + + /** + * Sets the icon for the column. This is intended to be + * displayed in the header above the column + * + * @see getIcon() + */ + + public void setIcon(Icon aIcon) { + fIcon = aIcon; + } + + /** + * Returns the column icon + * + * @see setIcon() + */ + + public Icon getIcon() { + return fIcon; + } + + /** + * Sets the column's user defined id. The user can define + * whatever value they find meaningful for this purpose. + * @see getID() + */ + + public void setID(Object aID) { + fID = aID; + } + + /** + * Returns the column's user defined id. + * @see setID() + */ + + public Object getID() { + return fID; + } + + /** + * Sets the cell renderer for this column. This renderer + * is used for column data provided by another source. + * @see getCellRenderer() + * @see CellRenderer + */ + + public void setCellRenderer(CellRenderer aRenderer) { + fCellRenderer = aRenderer; + } + + /** + * Returns the cell renderer for this column. + * @see setCellRenderer() + * @see CellRenderer + */ + + public CellRenderer getCellRenderer() { + return fCellRenderer; + } + + /** + * Not implemented + */ + + public void setCellEditor(CellEditor aEditor) { + fCellEditor = aEditor; + } + + /** + * Not implemented + */ + + public CellEditor getCellEditor() { + return fCellEditor; + } + + /** + * Sets the column's header renderer. This renders the + * column caption in a space provided by that object + * @see getHeaderRenderer() + * @see com.sun.java.swing.CellRenderer + */ + + public void setHeaderRenderer(HeaderRenderer aRenderer) { + fHeaderRenderer = aRenderer; + } + + /** + * Returns the column's header renderer + * @see setHeaderRenderer() + */ + + public HeaderRenderer getHeaderRenderer() { + return fHeaderRenderer; + } + + /** + * Adds a ColumnChangeListener. The listener is notified whenever + * a significant attribute changes. Currently this only includes + * the column width, but this will expand. + * @see removeColumnListener() + * @see ColumnChangeListener + */ + + public void addColumnChangeListener(ColumnChangeListener aListener) { + fListeners.addElement(aListener); + } + + /** + * Removes a ColumnChangeListener. + * @see addColumnChangeListener. + */ + + public void removeColumnChangeListener(ColumnChangeListener aListener) { + fListeners.removeElement(aListener); + } + + /** + * Used internally + */ + + void notifyWidthChange() { + ChangeEvent event = new ChangeEvent(this); + for (int i = 0; i < fListeners.size(); i++) { + ColumnChangeListener listener = + (ColumnChangeListener) fListeners.elementAt(i); + listener.columnWidthChanged(event); + } + } +} diff --git a/mozilla/grendel/widgets/ColumnChangeListener.java b/mozilla/grendel/widgets/ColumnChangeListener.java new file mode 100644 index 00000000000..0b452eeec3b --- /dev/null +++ b/mozilla/grendel/widgets/ColumnChangeListener.java @@ -0,0 +1,30 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +public interface ColumnChangeListener extends EventListener { + public void columnWidthChanged(ChangeEvent aEvent); +} diff --git a/mozilla/grendel/widgets/ColumnHeader.java b/mozilla/grendel/widgets/ColumnHeader.java new file mode 100644 index 00000000000..700fdb6ea0b --- /dev/null +++ b/mozilla/grendel/widgets/ColumnHeader.java @@ -0,0 +1,432 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import java.util.Enumeration; + +import com.sun.java.swing.CellRendererPane; +import com.sun.java.swing.JComponent; +import com.sun.java.swing.UIManager; +import com.sun.java.swing.event.ChangeEvent; + +public class ColumnHeader extends JComponent implements ColumnModelListener { + ColumnModel fColumnModel; + CellRendererPane fCellRendererPane = new CellRendererPane(); + + static final int RESIZE_MARGIN = 4; + static final int DRAG_THRESHOLD = 4; + + boolean fDynamicUpdate = true; + + // Set by hit test + boolean fOverResize; + int fHitOffset; + Column fPressedColumn; + + HeaderMouseListener fMouseListener; + + // + // HeaderMouseListener + // + + class HeaderMouseListener implements MouseMotionListener, MouseListener { + boolean fResizing; + int fOverColumn; + int fDragOverColumn; + int fHitX; + int fHitColumn; + int fOldSize; + int fOldDragX = -1; + int fDragOffset; + boolean fDragging; + + // MouseMotionListener Interface + + public void mouseMoved(MouseEvent aEvent) { + fOverColumn = hitTest(aEvent.getX()); + if (fOverResize) { + setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); + } else { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + public void mouseDragged(MouseEvent aEvent) { + if (fHitColumn < 0) { + return; + } + int x = aEvent.getX(); + if (fResizing) { + if (fDynamicUpdate) { + updateColumnSize(x); + } else { + paintResizeLine(x); + } + } else if (Math.abs(x - fHitX) > DRAG_THRESHOLD) { + fDragging = true; + fDragOverColumn = hitTest(x); + if (fDynamicUpdate) { + int threshhold = 0; + if (fDragOverColumn != -1) { + if (fDragOverColumn < fHitColumn) { + threshhold = getColumnX(fDragOverColumn) + + fColumnModel.getColumn(fHitColumn).getWidth(); + } else { + threshhold = getColumnX(fDragOverColumn) + + fColumnModel.getColumn(fDragOverColumn).getWidth() - + fColumnModel.getColumn(fHitColumn).getWidth(); + } + } + + if (fDragOverColumn != -1 && + ((fDragOverColumn < fHitColumn && + aEvent.getX() < threshhold) || + (fDragOverColumn > fHitColumn && + aEvent.getX() > threshhold))) { + paintDraggedHeader(-1); + fColumnModel.moveColumn(fHitColumn, fDragOverColumn); + fHitColumn = fDragOverColumn; + } else { + paintDraggedHeader(aEvent.getX() + fDragOffset); + } + } else { + paintDraggedHeader(aEvent.getX() + fDragOffset); + } + } + } + + // MouseListener Interface + + public void mouseEntered(MouseEvent aEvent) { + + } + + public void mouseExited(MouseEvent aEvent) { + + } + + public void mouseClicked(MouseEvent aEvent) { + } + + public void mousePressed(MouseEvent aEvent) { + fHitX = aEvent.getX(); + fHitColumn = fOverColumn; + if (fHitColumn > -1) { + Column column = fColumnModel.getColumn(fHitColumn); + if (fOverResize) { + fResizing = true; + fOldSize = column.getWidth(); + } else { + fDragOffset = fHitOffset; + if (column.isSelectable()) { + fPressedColumn = column; + repaint(); + } + } + } + } + + public void mouseReleased(MouseEvent aEvent) { + if (fResizing && !fDynamicUpdate) { + paintResizeLine(-1); + updateColumnSize(aEvent.getX()); + } else if (fDragging) { + fDragging = false; + paintDraggedHeader(-1); + if (!fDynamicUpdate) { + if (fDragOverColumn != fHitColumn && fDragOverColumn != -1) { + fColumnModel.moveColumn(fHitColumn, fDragOverColumn); + } + } + } else { + if (fPressedColumn != null) { + fColumnModel.setSelectedColumn(fPressedColumn); + } + } + + fHitX = -1; + fHitColumn = -1; + fResizing = false; + fPressedColumn = null; + + repaint(); + } + + void updateColumnSize(int aMouseX) { + Column column = fColumnModel.getColumn(fHitColumn); + int newSize = fOldSize + (aMouseX - fHitX); + + if (newSize >= column.getMinWidth() && + newSize <= column.getMaxWidth()) { + column.setWidth(newSize); + } + } + + void paintResizeLine(int aMouseX) { + Graphics g = getGraphics(); + g.setXORMode(Color.white); + g.setColor(Color.black); + + if (fOldDragX > -1) { + g.drawLine(fOldDragX, 0, fOldDragX, getHeight()); + } + if (aMouseX > -1) { + g.drawLine(aMouseX, 0, aMouseX, getHeight()); + } + g.setPaintMode(); + fOldDragX = aMouseX; + } + + void paintDraggedHeader(int aMouseX) { + Graphics g = getGraphics(); + g.setXORMode(Color.white); + + Column column = fColumnModel.getColumn(fHitColumn); + + int w, h; + + w = column.getWidth(); + h = getHeight(); + + HeaderRenderer renderer = column.getHeaderRenderer(); + renderer.setValue(column, HeaderRenderer.DRAGGING); + Component component = renderer.getComponent(); + + if (fOldDragX > -1) { + paintComponent(g, component, fOldDragX, 0, w, h); + } + if (aMouseX > -1) { + paintComponent(g, component, aMouseX, 0, w, h); + } + + g.setPaintMode(); + fOldDragX = aMouseX; + } + + int getColumnX(int aIdx) { + int res = 0; + for (int i = 0; i < aIdx; i++) { + res += fColumnModel.getColumn(i).getWidth(); + } + return res; + } + } + + // + // Constructor + // + + /** + * Takes a ColumnModel. The ColumnModel is generally shared with whatever + * is the column header for. + */ + + public ColumnHeader(ColumnModel aColumnModel) { + add(fCellRendererPane); + + fColumnModel = aColumnModel; + fColumnModel.addColumnModelListener(this); + + updateUI(); + + fMouseListener = new HeaderMouseListener(); + addMouseListener(fMouseListener); + addMouseMotionListener(fMouseListener); + } + + /** + * Sets whether the UI repaints all columns as a column is + * being resized or dragged. Set to false for better performance. + */ + + public void setDynamicUpdate(boolean aDynamic) { + fDynamicUpdate = aDynamic; + } + + /** + * @return Whether the UI is repainting the columns as a column + * is being sized or dragged. + */ + + public boolean getDynamicUpdate() { + return fDynamicUpdate; + } + + // + // Utility methods + // + + int hitTest(int aX) { + if (aX < 0) { + return -1; + } + + int index = 0; + int w = 0; + + fOverResize = false; + + Enumeration columns = fColumnModel.getColumns(); + while (columns.hasMoreElements()) { + Column column = (Column) columns.nextElement(); + w += column.getWidth(); + if (aX < w) { + if (aX > w - RESIZE_MARGIN) { + fOverResize = true; + } + fHitOffset = w - column.getWidth() - aX; + return index; + } + index++; + } + + return -1; + } + + void paintComponent(Graphics g, Component component, + int x, int y, int w, int h) { + fCellRendererPane.paintComponent(g, component, this, x, y, w, h); + } + + // + // ColumnModelListener Interface + // + + public void columnAdded(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnRemoved(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnMoved(ColumnModelEvent e) { + repaint(); + } + + public void columnMarginChanged(ChangeEvent e) { + resizeAndRepaint(); + } + + public void columnWidthChanged(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnSelectionChanged(ChangeEvent e) { + repaint(); + } + + void resizeAndRepaint() { + setSize(getPreferredSize()); + repaint(); + } + + // + // Component Overloads + // + + public void paint(Graphics g) { + int margin = fColumnModel.getColumnMargin(); + int x = 0, y = 0; + int w = 0, h = getHeight(); + + Color bg = getBackground(); + for (Enumeration columnEnum = fColumnModel.getColumns(); + columnEnum.hasMoreElements(); ) { + Column column = (Column) columnEnum.nextElement(); + + // Easy Part + w = column.getWidth() + margin; + + if (bg != null) { + g.setColor(bg); + g.fillRect(x, y, w, h); + } + + // Hard Part + HeaderRenderer renderer = column.getHeaderRenderer(); + if (column == fPressedColumn) { + renderer.setValue(column, HeaderRenderer.DEPRESSED); + } else if (column == fColumnModel.getSelectedColumn()) { + renderer.setValue(column, HeaderRenderer.SELECTED); + } else { + renderer.setValue(column, HeaderRenderer.NORMAL); + } + Component component = renderer.getComponent(); + fCellRendererPane.paintComponent(g, component, this, x, y, w, h); + + x += w; + } + if (x < getWidth()) { + if (bg != null) { + g.setColor(bg); + g.fillRect(x, y, getWidth() - x, h); + } + } + } + + public Dimension getPreferredSize() { + Dimension res = new Dimension(); + int margin = fColumnModel.getColumnMargin(); + for (Enumeration columnEnum = fColumnModel.getColumns(); + columnEnum.hasMoreElements(); ) { + Column column = (Column) columnEnum.nextElement(); + + // Easy Part + res.width += column.getWidth() + margin; + + // Hard Part + HeaderRenderer renderer = column.getHeaderRenderer(); + renderer.setValue(column, HeaderRenderer.NORMAL); + Component component = renderer.getComponent(); + Dimension dim = component.getPreferredSize(); + if (dim.height > res.height) { + res.height = dim.height; + } + } + return res; + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public void updateUI() { + super.updateUI(); + + setBackground(UIManager.getColor("control")); + } +} diff --git a/mozilla/grendel/widgets/ColumnModel.java b/mozilla/grendel/widgets/ColumnModel.java new file mode 100644 index 00000000000..1dd103fd445 --- /dev/null +++ b/mozilla/grendel/widgets/ColumnModel.java @@ -0,0 +1,157 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.Enumeration; + +/** + * An interface for the data model that stores the layout of a set of columns. + * This interface is shared by the ColumnHeaders and the TreeTable to provide + * a common column layout for both. + */ + +public interface ColumnModel { + + /** + * Add a column to the model. The column is added at the end. + */ + + public void addColumn(Column aColumn); + + /** + * Add a column to the model at a specified index. An exception may be + * if the index is invalid. + */ + + public void addColumnAt(Column aColumn, int aIndex); + + /** + * Removes a column from the model. + */ + + public void removeColumn(Column aColumn); + + /** + * Moves a column at the source index to the destination index. Columns + * in between may be shifted. An exception may be thrown if either index + * is invalid. + */ + + public void moveColumn(int aSource, int aDest); + + /** + * @return The total number of columns in this model. + */ + + public int getColumnCount(); + + /** + * @return The column at the given index. Returns null if + * index is invalid. + */ + + public Column getColumn(int aIndex); + + /** + * @return The column with the given ID. Returns null if a column with the + * given ID is not in the model. + */ + + public Column getColumn(Object aID); + + /** + * @return An enumeration of all the columns in the model. + */ + + public Enumeration getColumns(); + + /** + * @return The index of the column in the model. Returns -1 if the column is + * not in the model. + */ + + public int getColumnIndex(Column aColumn); + + /** + * @return The index of the column at the given X location. Returns -1 if the + * given X does not fall over a column. + */ + + public int getColumnIndexAtX(int aX); + + /** + * @return The margin (gap) between columns. + */ + + public int getColumnMargin(); + + /** + * Sets the margin (gap) between columns. + */ + + public void setColumnMargin(int aMargin); + + /** + * @return The sum of all column widths and margins. + */ + + public int getTotalColumnWidth(); + + /** + * Adds a ColumnModelListener. + */ + + public void addColumnModelListener(ColumnModelListener aListener); + + /** + * Removes a ColumnModelListener. + */ + + public void removeColumnModelListener(ColumnModelListener aListener); + + + /** + * @return The currently selected column. + */ + + public Column getSelectedColumn(); + + /** + * Sets the currently selected column. Currently only single selection + * is supported. + */ + + public void setSelectedColumn(Column aColumn); + + /** + * @return A string representing the layout (order and widths) of the + * columns. + */ + + public String getPrefString(); + + /** + * Restores a column layout that was obtained from getPrefString(). + */ + + public void setPrefString(String aString); +} diff --git a/mozilla/grendel/widgets/ColumnModelEvent.java b/mozilla/grendel/widgets/ColumnModelEvent.java new file mode 100644 index 00000000000..fded45deab8 --- /dev/null +++ b/mozilla/grendel/widgets/ColumnModelEvent.java @@ -0,0 +1,44 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.EventObject; + +public class ColumnModelEvent extends EventObject { + int fFirst; + int fLast; + + public ColumnModelEvent(Object aSource, int aFirst, int aLast) { + super(aSource); + + fFirst = aFirst; + fLast = aLast; + } + + public int getFirst() { + return fFirst; + } + + public int getLast() { + return fLast; + } +} diff --git a/mozilla/grendel/widgets/ColumnModelImp.java b/mozilla/grendel/widgets/ColumnModelImp.java new file mode 100644 index 00000000000..05f971a51f4 --- /dev/null +++ b/mozilla/grendel/widgets/ColumnModelImp.java @@ -0,0 +1,285 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.AWTEventMulticaster; +import java.util.Vector; +import java.util.Enumeration; +import java.util.StringTokenizer; + +import com.sun.java.swing.event.ChangeEvent; + +public class ColumnModelImp implements ColumnModel, ColumnChangeListener { + Vector fColumns = new Vector(); + ColumnModelListener fListeners = null; + int fMargin = 0; + Column fSelectedColumn = null; + + public void addColumn(Column aColumn) { + addColumnAt(aColumn, fColumns.size()); + } + + public void addColumnAt(Column aColumn, int aIndex) { + aColumn.addColumnChangeListener(this); + fColumns.insertElementAt(aColumn, aIndex); + + if (fListeners != null) { + fListeners.columnAdded(new ColumnModelEvent(this, 0, aIndex)); + } + } + + public void removeColumn(Column aColumn) { + aColumn.removeColumnChangeListener(this); + int idx = getColumnIndex(aColumn); + if (idx > -1) { + fColumns.removeElementAt(idx); + if (fListeners != null) { + fListeners.columnRemoved(new ColumnModelEvent(this, idx, 0)); + } + } + } + + public void moveColumn(int aSource, int aDest) { + int i; + if (aSource < aDest) { + for (i = aSource; i < aDest; i++) { + Object a = fColumns.elementAt(i); + fColumns.removeElementAt(i); + fColumns.insertElementAt(a, i + 1); + } + if (fListeners != null) { + fListeners.columnMoved(new ColumnModelEvent(this, aSource, aDest)); + } + } else if (aDest < aSource) { + for (i = aSource; i > aDest; i--) { + Object a = fColumns.elementAt(i); + fColumns.removeElementAt(i); + fColumns.insertElementAt(a, i - 1); + } + if (fListeners != null) { + fListeners.columnMoved(new ColumnModelEvent(this, aSource, aDest)); + } + } // else same, do nothing + } + + public int getColumnCount() { + return fColumns.size(); + } + + public Column getColumn(int aIndex) { + return (Column) fColumns.elementAt(aIndex); + } + + public Column getColumn(Object aID) { + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + Column column = getColumn(i); + if (column.getID().equals(aID)) { + return column; + } + } + return null; + } + + private Column getColumn(String aID) { + int count = getColumnCount(); + for (int i = 0; i < count; i++) { + Column column = getColumn(i); + if (column.getID().toString().equals(aID)) { + return column; + } + } + return null; + } + + public Enumeration getColumns() { + return fColumns.elements(); + } + + public int getColumnIndex(Column aColumn) { + return fColumns.indexOf(aColumn); + } + + public int getColumnIndexAtX(int aX) { + if (aX < 0) { + return -1; + } + + int count = getColumnCount(); + int width = 0; + for (int i = 0; i < count; i++) { + Column column = getColumn(i); + width += column.getWidth() + fMargin; + if (aX <= width) { + return i; + } + } + return -1; + } + + public void setColumnMargin(int aMargin) { + fMargin = aMargin; + if (fListeners != null) { + fListeners.columnMarginChanged(new ChangeEvent(this)); + } + } + + public int getColumnMargin() { + return fMargin; + } + + public int getTotalColumnWidth() { + int i, res = 0; + for (i = 0; i < fColumns.size(); i++) { + res += getColumn(i).getWidth() + fMargin; + } + return res; + } + + public void setSelectedColumn(Column aColumn) { + fSelectedColumn = aColumn; + if (fListeners != null) { + fListeners.columnSelectionChanged(new ChangeEvent(this)); + } + } + + public Column getSelectedColumn() { + return fSelectedColumn; + } + + public synchronized String getPrefString() { + StringBuffer pref = new StringBuffer(); + Integer intVal = new Integer(0); + for (int i = 0; i < fColumns.size(); i++) { + pref.append(getColumn(i).getID().toString()); + pref.append(","); + pref.append(Integer.toString(getColumn(i).getWidth())); + if (i < fColumns.size() - 1) { + pref.append(","); + } + } + return pref.toString(); + } + + public synchronized void setPrefString(String aString) { + StringTokenizer tokens = new StringTokenizer(aString, ",", false); + int i = 0; + while (tokens.hasMoreTokens()) { + String id = tokens.nextToken(); + String width = tokens.nextToken(); + Column column = getColumn(id); + + if (column != null) { + column.setWidth(Integer.parseInt(width)); + moveColumn(getColumnIndex(column), i); + } + + i++; + } + } + + // + // Listener support + // + + public synchronized void addColumnModelListener(ColumnModelListener aListener) { + fListeners = ColumnModelMulticaster.add(fListeners, aListener); + } + + public synchronized void removeColumnModelListener(ColumnModelListener aListener) { + fListeners = ColumnModelMulticaster.remove(fListeners, aListener); + } + + // + // ColumnListener Interface + // + + public void columnWidthChanged(ChangeEvent aEvent) { + int idx = fColumns.indexOf(aEvent.getSource()); + if (fListeners != null) { + fListeners.columnWidthChanged(new ColumnModelEvent(this, 0, idx)); + } + } +} + +class ColumnModelMulticaster implements ColumnModelListener { + ColumnModelListener a, b; + + public ColumnModelMulticaster(ColumnModelListener a, ColumnModelListener b) { + this.a = a; + this.b = b; + } + + public static ColumnModelListener add(ColumnModelListener a, ColumnModelListener b) { + if (a == null) return b; + if (b == null) return a; + + return new ColumnModelMulticaster(a, b); + } + + public static ColumnModelListener remove(ColumnModelListener a, ColumnModelListener b) { + if (a == b || a == null) { + return null; + } else if (a instanceof ColumnModelMulticaster) { + return ((ColumnModelMulticaster)a).remove(b); + } else { + return a; // it's not here + } + } + + public ColumnModelListener remove(ColumnModelListener c) { + if (c == a) return b; + if (c == b) return a; + + ColumnModelListener a2 = remove(a, c); + ColumnModelListener b2 = remove(b, c); + if (a2 == a && b2 == b) { + return this; // it's not here + } + return add(a2, b2); + } + + public void columnAdded(ColumnModelEvent aEvent) { + a.columnAdded(aEvent); + b.columnAdded(aEvent); + } + public void columnRemoved(ColumnModelEvent aEvent) { + a.columnRemoved(aEvent); + b.columnRemoved(aEvent); + } + public void columnMoved(ColumnModelEvent aEvent) { + a.columnMoved(aEvent); + b.columnMoved(aEvent); + } + public void columnMarginChanged(ChangeEvent aEvent) { + a.columnMarginChanged(aEvent); + b.columnMarginChanged(aEvent); + } + public void columnSelectionChanged(ChangeEvent aEvent) { + a.columnSelectionChanged(aEvent); + b.columnSelectionChanged(aEvent); + } + public void columnWidthChanged(ColumnModelEvent aEvent) { + a.columnWidthChanged(aEvent); + b.columnWidthChanged(aEvent); + } +} diff --git a/mozilla/grendel/widgets/ColumnModelListener.java b/mozilla/grendel/widgets/ColumnModelListener.java new file mode 100644 index 00000000000..789ec2f09c7 --- /dev/null +++ b/mozilla/grendel/widgets/ColumnModelListener.java @@ -0,0 +1,36 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +public interface ColumnModelListener extends EventListener { + public void columnAdded(ColumnModelEvent e); + public void columnRemoved(ColumnModelEvent e); + public void columnMoved(ColumnModelEvent e); + public void columnMarginChanged(ChangeEvent e); + public void columnWidthChanged(ColumnModelEvent e); + public void columnSelectionChanged(ChangeEvent e); +} + diff --git a/mozilla/grendel/widgets/DefaultCellRenderer.java b/mozilla/grendel/widgets/DefaultCellRenderer.java new file mode 100644 index 00000000000..8e1d2c0f264 --- /dev/null +++ b/mozilla/grendel/widgets/DefaultCellRenderer.java @@ -0,0 +1,118 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 2 Oct 1997. + */ + +package grendel.widgets; + +import calypso.util.Assert; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Insets; + +import com.sun.java.swing.Icon; +import com.sun.java.swing.JComponent; +import com.sun.java.swing.UIManager; +import com.sun.java.swing.basic.BasicGraphicsUtils; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.border.Border; +import com.sun.java.swing.BorderFactory; + +/** + * The default TreeTable cell renderer. Handles text and not much + * else. + * @see TreeTable + * @see CellRenderer + */ + +public class DefaultCellRenderer extends JComponent implements CellRenderer { + protected Object fObject; + protected Object fData; + protected boolean fSelected; + + public DefaultCellRenderer() { +// setFont(BasicGraphicsUtils.controlFont); + setFont(Font.decode("SansSerif-12")); + } + + // HeaderRenderer Interface + + public void setValue(Object aObject, Object aData, boolean aSelected) { + fObject = aObject; + Assert.Assertion(aData != null); + fData = aData; + fSelected = aSelected; + + setSize(getPreferredSize()); + } + + public Component getComponent() { + return this; + } + + // Component overloads + + public Dimension getPreferredSize() { + Font font = getFont(); + String text = fData.toString(); + + Insets insets = getInsets(); + Dimension res = new Dimension(insets.left + insets.right, + insets.top + insets.bottom); + + if (font != null && text != null) { + FontMetrics fm = getToolkit().getFontMetrics(font); + res.height += 2 + fm.getHeight(); + res.width += 2 + fm.stringWidth(text); + } + return res; + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public void paint(Graphics g) { + Font font = getFont(); + String text = fData.toString(); + Insets insets = getInsets(); + + if (font != null && text != null) { + g.setFont(font); + FontMetrics fm = getToolkit().getFontMetrics(font); + int x = insets.left + 2; + int h = getHeight(); + int y = (h - fm.getHeight()) / 2 + fm.getAscent(); + + g.setColor(fSelected ? + UIManager.getColor("textHighlightText") : + UIManager.getColor("textText")); + Util.DrawTrimmedString(g, text, fm, Util.RIGHT, x, y, getWidth()); + } + } +} diff --git a/mozilla/grendel/widgets/DefaultHeaderRenderer.java b/mozilla/grendel/widgets/DefaultHeaderRenderer.java new file mode 100644 index 00000000000..5401fd29657 --- /dev/null +++ b/mozilla/grendel/widgets/DefaultHeaderRenderer.java @@ -0,0 +1,159 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 2 Oct 1997. + */ + +package grendel.widgets; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Insets; + +import com.sun.java.swing.BorderFactory; +import com.sun.java.swing.Icon; +import com.sun.java.swing.JComponent; +import com.sun.java.swing.UIManager; +import com.sun.java.swing.basic.BasicGraphicsUtils; +import com.sun.java.swing.border.BevelBorder; +import com.sun.java.swing.border.Border; +import com.sun.java.swing.border.EmptyBorder; + +/** + * The default renderer for column headers. Handles text headers and + * not much else. Aspires to handle icons. + */ + +public class DefaultHeaderRenderer extends JComponent implements HeaderRenderer { + Column fColumn; + int fState; + Border fRaisedBorder; + Border fLoweredBorder; + Border fEmptyBorder; + + public DefaultHeaderRenderer() { + fRaisedBorder = BorderFactory.createRaisedBevelBorder(); + fLoweredBorder = BorderFactory.createLoweredBevelBorder(); + + fEmptyBorder = new EmptyBorder(0,0,0,0); + + //setFont(Font.decode("Dialog-12")); + setFont(UIManager.getFont("Label.font")); + setBorder(fRaisedBorder); + } + + // HeaderRenderer Interface + + public void setValue(Column aColumn, int aState) { + fColumn = aColumn; + fState = aState; + + setSize(getPreferredSize()); + } + + public Component getComponent() { + return this; + } + + // Component overloads + + public Dimension getPreferredSize() { + Font font = getFont(); + String text = fColumn.getTitle(); + Icon icon = fColumn.getIcon(); + + Insets insets = getInsets(); + Dimension res = new Dimension(fColumn.getWidth(), 0); + + if (font != null && text != null) { + FontMetrics fm = getToolkit().getFontMetrics(font); + res.height = fm.getHeight(); + } + + if (icon != null && icon.getIconHeight() > res.height) { + res.height = icon.getIconHeight(); + } + + res.height += 2 + insets.top + insets.bottom; + + return res; + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public void paint(Graphics g) { + Dimension size = getSize(); + + if (fState == HeaderRenderer.DRAGGING) { + g.fillRect(0, 0, size.width, size.height); + setBorder(fEmptyBorder); + } else if (fState == HeaderRenderer.DEPRESSED) { + setBorder(fLoweredBorder); + } else { + setBorder(fRaisedBorder); + } + + Font font = getFont(); + String text = fColumn.getTitle(); + Icon icon = fColumn.getIcon(); + Insets insets = getInsets(); + + int x = insets.left + 2; + int y = 0; + int h = size.height; + + if (fState == HeaderRenderer.DEPRESSED) { + x++; + y++; + } + + if (icon != null) { + int iconY = y + (h - icon.getIconHeight()) / 2; + + icon.paintIcon(this, g, x, iconY); + + x += icon.getIconWidth() + 2; + } + + if (font != null && text != null) { + g.setFont(font); + FontMetrics fm = getToolkit().getFontMetrics(font); + int textY = y + (h - fm.getHeight()) / 2 + fm.getAscent(); + + if (fState == HeaderRenderer.SELECTED) { + g.setColor(UIManager.getColor("textHighlightText")); + } else { + g.setColor(UIManager.getColor("textText")); + } + + g.drawString(text, x, textY); + } + super.paint(g); + } +} + diff --git a/mozilla/grendel/widgets/HeaderRenderer.java b/mozilla/grendel/widgets/HeaderRenderer.java new file mode 100644 index 00000000000..af002aecbb9 --- /dev/null +++ b/mozilla/grendel/widgets/HeaderRenderer.java @@ -0,0 +1,36 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.Component; + +import com.sun.java.swing.Renderer; + +public interface HeaderRenderer { + public final static int NORMAL = 0; + public final static int DEPRESSED = 1; + public final static int SELECTED = 2; + public final static int DRAGGING = 3; + + public void setValue(Column aValue, int aState); + public Component getComponent(); +} diff --git a/mozilla/grendel/widgets/Makefile b/mozilla/grendel/widgets/Makefile new file mode 100644 index 00000000000..1bdb890d337 --- /dev/null +++ b/mozilla/grendel/widgets/Makefile @@ -0,0 +1,52 @@ +#!gmake +# +# The contents of this file are subject to the Mozilla Public License +# Version 1.0 (the "License"); you may not use this file except in +# compliance with the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. + +SRCS= \ + Animation.java \ + CellEditor.java \ + CellRenderer.java \ + Column.java \ + ColumnChangeListener.java \ + ColumnHeader.java \ + ColumnModel.java \ + ColumnModelEvent.java \ + ColumnModelImp.java \ + ColumnModelListener.java \ + DefaultCellRenderer.java \ + DefaultHeaderRenderer.java \ + HeaderRenderer.java \ + MultiSelectionManager.java \ + SelectionEvent.java \ + SelectionListener.java \ + SelectionManager.java \ + SingleSelectionManager.java \ + Splitter.java \ + StatusEvent.java \ + TextCellEditor.java \ + ToggleCellEditor.java \ + ToggleCellRenderer.java \ + TreePath.java \ + TreeTable.java \ + TreeTableDataModel.java \ + TreeTableModelBroadcaster.java \ + TreeTableModelEvent.java \ + TreeTableModelListener.java \ + Util.java \ + $(NULL) + +include ../rules.mk diff --git a/mozilla/grendel/widgets/MultiSelectionManager.java b/mozilla/grendel/widgets/MultiSelectionManager.java new file mode 100644 index 00000000000..588cfa42c1b --- /dev/null +++ b/mozilla/grendel/widgets/MultiSelectionManager.java @@ -0,0 +1,300 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 26 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.event.MouseEvent; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import com.sun.java.swing.event.ChangeEvent; + +public class MultiSelectionManager implements SelectionManager { + Vector fSelection = new Vector(); + Vector fListeners = new Vector(); + + /** + * Removes all objects from the selection + */ + + public synchronized void clearSelection() { + if (!fSelection.isEmpty()) { + Object removedArray[] = new Object[fSelection.size()]; + Object nullArray[] = new Object[0]; + Enumeration removedEnum = fSelection.elements(); + int idx = 0; + while (removedEnum.hasMoreElements()) { + removedArray[idx] = removedEnum.nextElement(); + idx++; + } + + fSelection.setSize(0); + notifySelectionChanged(nullArray, removedArray); + } + } + + /** + * Sets the selection to be a single object + */ + + public synchronized void setSelection(Object aObject) { + if (fSelection.size() != 1 || !fSelection.contains(aObject)) { + Object removedArray[]; + Object addedArray[]; + if (fSelection.contains(aObject)) { + removedArray = new Object[fSelection.size() - 1]; + addedArray = new Object[0]; + } else { + removedArray = new Object[fSelection.size()]; + addedArray = new Object[1]; + addedArray[0] = aObject; + } + + Enumeration removedEnum = fSelection.elements(); + int idx = 0; + while (removedEnum.hasMoreElements()) { + Object element = removedEnum.nextElement(); + if (!element.equals(aObject)) { + removedArray[idx] = element; + idx++; + } + } + + fSelection.setSize(0); + fSelection.addElement(aObject); + notifySelectionChanged(addedArray, removedArray); + } + } + + public synchronized void setSelection(Enumeration aObjects) { + Vector addedVector = new Vector(); + Vector removedVector = new Vector(); + Vector noopVector = new Vector(); + + // See which items are being removed + while (aObjects.hasMoreElements()) { + Object object = aObjects.nextElement(); + + if (fSelection.removeElement(object)) { + addedVector.insertElementAt(object, addedVector.size()); + } else { + noopVector.insertElementAt(object, noopVector.size()); + } + } + + // See which items were not being set + Enumeration removedEnum = fSelection.elements(); + while (removedEnum.hasMoreElements()) { + Object element = removedEnum.nextElement(); + removedVector.insertElementAt(element, removedVector.size()); + } + + // Remove everything + fSelection.setSize(0); + + // Set everything + int i; + + for (i = 0; i < addedVector.size(); i++) { + fSelection.insertElementAt(addedVector.elementAt(i), fSelection.size()); + } + for (i = 0; i < noopVector.size(); i++) { + fSelection.insertElementAt(noopVector.elementAt(i), fSelection.size()); + } + + // If there were deltas, notify + if (addedVector.size() > 0 || removedVector.size() > 0) { + Object removedArray[] = new Object[removedVector.size()]; + Object addedArray[] = new Object[addedVector.size()]; + + removedVector.copyInto(removedArray); + addedVector.copyInto(addedArray); + + notifySelectionChanged(addedArray, removedArray); + } + } + + /** + * Adds a single object to the selection. Notifies + * listeners if the selection changes. + */ + + public synchronized void addSelection(Object aObject) { + if (!fSelection.contains(aObject)) { + fSelection.insertElementAt(aObject, fSelection.size()); + Object addedArray[] = new Object[1]; + Object removedArray[] = new Object[0]; + addedArray[0] = aObject; + + notifySelectionChanged(addedArray, removedArray); + } + } + + /** + * NYI + */ + + public synchronized void addSelection(Enumeration aObjects) { + Vector addedVector = new Vector(); + while (aObjects.hasMoreElements()) { + Object object = aObjects.nextElement(); + if (!fSelection.contains(object)) { + fSelection.insertElementAt(object, fSelection.size()); + addedVector.insertElementAt(object, addedVector.size()); + } + } + + if (addedVector.size() > 0) { + Object addedArray[] = new Object[addedVector.size()]; + Object removedArray[] = new Object[0]; + + addedVector.copyInto(addedArray); + notifySelectionChanged(addedArray, removedArray); + } + } + + /** + * Removes a single object from the selection. Notifies + * listeners if the selection changes. + */ + + public synchronized void removeSelection(Object aObject) { + if (fSelection.removeElement(aObject)) { + Object addedArray[] = new Object[0]; + Object removedArray[] = new Object[1]; + removedArray[0] = aObject; + + notifySelectionChanged(addedArray, removedArray); + } + } + + public synchronized void removeSelection(Enumeration aObjects) { + Vector removedVector = new Vector(); + while (aObjects.hasMoreElements()) { + Object object = aObjects.nextElement(); + if (fSelection.removeElement(object)) { + removedVector.insertElementAt(object, removedVector.size()); + } + } + + if (removedVector.size() > 0) { + Object removedArray[] = new Object[removedVector.size()]; + Object addedArray[] = new Object[0]; + + removedVector.copyInto(removedArray); + notifySelectionChanged(addedArray, removedArray); + } + } + + /** + * Returns true if the object is in the selection. + */ + + public synchronized boolean isSelected(Object aObject) { + return fSelection.contains(aObject); + } + + /** + * Returns the number of objects in the selection + */ + + public synchronized int getSelectionCount() { + return fSelection.size(); + } + + /** + * Returns an enumeration of all objects in the selection + */ + + public synchronized Enumeration getSelection() { + return fSelection.elements(); + } + + /** + * Passed on to listeners + */ + + public void doubleClickSelection(MouseEvent aEvent) { + notifySelectionDoubleClicked(aEvent); + } + + /** + * Passed on to listeners + */ + + public void contextClickSelection(MouseEvent aEvent) { + notifySelectionContextClicked(aEvent); + } + + /** + * Passed on to listeners + */ + + public void dragSelection(MouseEvent aEvent) { + notifySelectionDragged(aEvent); + } + + /** + * Adds a selection listener + */ + + public synchronized void addSelectionListener(SelectionListener aListener) { + fListeners.addElement(aListener); + } + + /** + * Removes a selection listener + */ + + public synchronized void removeSelectionListener(SelectionListener aListener) { + fListeners.removeElement(aListener); + } + + void notifySelectionChanged(Object aAdded[], Object aRemoved[]) { + Enumeration listeners = fListeners.elements(); + SelectionEvent event = new SelectionEvent(this, aAdded, aRemoved); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionChanged(event); + } + } + + void notifySelectionDoubleClicked(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionDoubleClicked(aEvent); + } + } + + void notifySelectionContextClicked(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionContextClicked(aEvent); + } + } + + void notifySelectionDragged(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionDragged(aEvent); + } + } +} diff --git a/mozilla/grendel/widgets/SelectionEvent.java b/mozilla/grendel/widgets/SelectionEvent.java new file mode 100644 index 00000000000..fed653dbb0e --- /dev/null +++ b/mozilla/grendel/widgets/SelectionEvent.java @@ -0,0 +1,47 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 24 Sep 1997. + */ +package grendel.widgets; + +import java.util.EventObject; + +/** + * Event which represents a change in selection. + */ + +public class SelectionEvent extends EventObject { + Object fAdded[]; + Object fRemoved[]; + + public SelectionEvent(Object aSource, Object aAdded[], Object aRemoved[]) { + super(aSource); + + fAdded = aAdded; + fRemoved = aRemoved; + } + + public Object[] getAdded() { + return fAdded; + } + + public Object[] getRemoved() { + return fRemoved; + } +} diff --git a/mozilla/grendel/widgets/SelectionListener.java b/mozilla/grendel/widgets/SelectionListener.java new file mode 100644 index 00000000000..574e50f60ed --- /dev/null +++ b/mozilla/grendel/widgets/SelectionListener.java @@ -0,0 +1,34 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 26 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.event.MouseEvent; +import java.util.EventListener; + +import com.sun.java.swing.event.ChangeEvent; + +public interface SelectionListener extends EventListener { + void selectionChanged(SelectionEvent aEvent); + void selectionDoubleClicked(MouseEvent aEvent); + void selectionContextClicked(MouseEvent aEvent); + void selectionDragged(MouseEvent aEvent); +} diff --git a/mozilla/grendel/widgets/SelectionManager.java b/mozilla/grendel/widgets/SelectionManager.java new file mode 100644 index 00000000000..2b1194588c5 --- /dev/null +++ b/mozilla/grendel/widgets/SelectionManager.java @@ -0,0 +1,127 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 26 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.event.MouseEvent; +import java.util.Enumeration; + +/** + * Interface for simple selection management. + * @see MultiSelectionManager + * @see SingleSelectionManager + * @see SelectionListener + */ + +public interface SelectionManager { + /** + * Removes all objects from the selection + */ + public void clearSelection(); + + /** + * Sets the selection to be a single object + */ + + public void setSelection(Object aObject); + + /** + * Sets the selection to be the given array of objects + */ + + public void setSelection(Enumeration aObjects); + + /** + * Adds a single object to the selection. Notifies + * listeners if the selection changes. + */ + + public void addSelection(Object aObject); + + /** + * Adds an array of objects to the selection. Notifies + * listeners if the selection changes. + */ + + public void addSelection(Enumeration aObjects); + + /** + * Removes a single object from the selection. Notifies + * listeners if the selection changes. + */ + + public void removeSelection(Object aObject); + + /** + * Removes an array of objects from the selection. Notifies + * listeners if the selection changes. + */ + + public void removeSelection(Enumeration aObjects); + + /** + * Returns true if the object is in the selection. + */ + + public boolean isSelected(Object aObject); + + /** + * Returns the number of objects in the selection + */ + + public int getSelectionCount(); + + /** + * Returns an enumeration of all objects in the selection + */ + + public Enumeration getSelection(); + + /** + * Passed on to listeners + */ + + public void doubleClickSelection(MouseEvent aEvent); + + /** + * Passed on to listeners + */ + + public void contextClickSelection(MouseEvent aEvent); + + /** + * Passed on to listeners + */ + + public void dragSelection(MouseEvent aEvent); + + /** + * Adds a selection listener + */ + + public void addSelectionListener(SelectionListener aListener); + + /** + * Removes a selection listener + */ + + public void removeSelectionListener(SelectionListener aListener); +} diff --git a/mozilla/grendel/widgets/SingleSelectionManager.java b/mozilla/grendel/widgets/SingleSelectionManager.java new file mode 100644 index 00000000000..ae1170470c7 --- /dev/null +++ b/mozilla/grendel/widgets/SingleSelectionManager.java @@ -0,0 +1,243 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 4 Sep 1997. + */ + +package grendel.widgets; + +import java.awt.event.MouseEvent; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import com.sun.java.swing.event.ChangeEvent; + +public class SingleSelectionManager implements SelectionManager { + Object fSelection = null; + Vector fListeners = new Vector(); + + Object fNullArray[] = new Object[0]; + Object fAddedArray[] = new Object[1]; + Object fRemovedArray[] = new Object[1]; + + /** + * Removes all objects from the selection + */ + + public void clearSelection() { + if (fSelection != null) { + fRemovedArray[0] = fSelection; + fSelection = null; + notifySelectionChanged(fNullArray, fRemovedArray); + } + } + + /** + * Sets the selection to be a single object + */ + + public void setSelection(Object aObject) { + Object removed[] = fNullArray; + Object added[] = fNullArray; + + if (fSelection != null && !fSelection.equals(aObject)) { + fRemovedArray[0] = fSelection; + removed = fRemovedArray; + } + if (aObject != null && !aObject.equals(fSelection)) { + fAddedArray[0] = aObject; + added = fAddedArray; + } + + fSelection = aObject; + if (added != fNullArray || removed != fNullArray) { + notifySelectionChanged(added, removed); + } + } + + public void setSelection(Enumeration aObjects) { + if (aObjects.hasMoreElements()) { + setSelection(aObjects.nextElement()); + } + } + + /** + * Adds a single object to the selection. Overwrites + * previous selection if a selection already exists. Notifies + * listeners if the selection changes. + */ + + public void addSelection(Object aObject) { + if (fSelection != aObject && aObject != null) { + Object removed[]; + fAddedArray[0] = aObject; + if (fSelection != null) { + fRemovedArray[0] = fSelection; + removed = fRemovedArray; + } else { + removed = fNullArray; + } + + fSelection = aObject; + notifySelectionChanged(fAddedArray, removed); + } + } + + public void addSelection(Enumeration aObjects) { + if (aObjects.hasMoreElements()) { + addSelection(aObjects.nextElement()); + } + } + + /** + * Removes a single object from the selection. Notifies + * listeners if the selection changes. + */ + + public void removeSelection(Object aObject) { + if (fSelection == aObject) { + fRemovedArray[0] = aObject; + + fSelection = null; + notifySelectionChanged(fNullArray, fRemovedArray); + } + } + + public void removeSelection(Enumeration aObjects) { + if (aObjects.hasMoreElements()) { + removeSelection(aObjects.nextElement()); + } + } + + /** + * Returns true if the object is in the selection. + */ + + public boolean isSelected(Object aObject) { + return fSelection == aObject; + } + + /** + * Returns the number of objects in the selection + */ + + public int getSelectionCount() { + return fSelection != null ? 1 : 0; + } + + /** + * Returns an enumeration of all objects in the selection + */ + + public Enumeration getSelection() { + return new LameEnumeration(fSelection); + } + + /** + * Passed on to listeners + */ + + public void doubleClickSelection(MouseEvent aEvent) { + notifySelectionDoubleClicked(aEvent); + } + + /** + * Passed on to listeners + */ + + public void contextClickSelection(MouseEvent aEvent) { + notifySelectionContextClicked(aEvent); + } + + /** + * Passed on to listeners + */ + + public void dragSelection(MouseEvent aEvent) { + notifySelectionDragged(aEvent); + } + + /** + * Adds a selection listener + */ + + public synchronized void addSelectionListener(SelectionListener aListener) { + fListeners.addElement(aListener); + } + + /** + * Removes a selection listener + */ + + public synchronized void removeSelectionListener(SelectionListener aListener) { + fListeners.removeElement(aListener); + } + + void notifySelectionChanged(Object aAdded[], Object aRemoved[]) { + Enumeration listeners = fListeners.elements(); + SelectionEvent event = new SelectionEvent(this, aAdded, aRemoved); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionChanged(event); + } + } + + void notifySelectionDoubleClicked(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionDoubleClicked(aEvent); + } + } + + void notifySelectionContextClicked(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionContextClicked(aEvent); + } + } + + void notifySelectionDragged(MouseEvent aEvent) { + Enumeration listeners = fListeners.elements(); + while (listeners.hasMoreElements()) { + ((SelectionListener) listeners.nextElement()).selectionDragged(aEvent); + } + } +} + +// +// LameEnumeration class +// We only have one object, so the enumeration is trivial +// + +class LameEnumeration implements Enumeration { + Object fObject; + + public LameEnumeration(Object aObject) { + fObject = aObject; + } + + public boolean hasMoreElements() { + return fObject != null; + } + + public Object nextElement() { + Object res = fObject; + fObject = null; + return res; + } +} diff --git a/mozilla/grendel/widgets/Splitter.java b/mozilla/grendel/widgets/Splitter.java new file mode 100644 index 00000000000..5b1d7c0ebb1 --- /dev/null +++ b/mozilla/grendel/widgets/Splitter.java @@ -0,0 +1,514 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 26 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.LayoutManager2; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Vector; + +import com.sun.java.swing.JComponent; +import com.sun.java.swing.UIManager; + +/** + * This class is used to create a divided pane with moveable dividers (NYI) + * Components can be added with numeric weights that will determine how + * much of the pane they will occupy: + * add(aComponent, new Float(.5)) + * If no weight is given the default is 1. + *

+ * (Soon to be) Moveable separators can be created by the + * createSeparator() + * method. The width of the separator is fixed in pixels. + * + * @author Will Scullin + */ + +// +// Splitter Class +// + +public class Splitter extends JComponent { + Hashtable fWeights = new Hashtable(); + float fTotalWeight = 0.0f; + int fTotalSeparators = 0; + + int fOrientation; + + int fMinSize = 16; + + SplitterSeparatorListener fListener; + + /** + * Value to pass for a splitter that stacks components + * vertically. + */ + + public static final int VERTICAL = 0; + + /** + * Value to pass for a splitter that lays out components + * horizontally. + */ + + public static final int HORIZONTAL = 1; + + // + // Constructor + // + + /** + * Contructs a splitter container with the given orientation. + */ + + public Splitter(int aOrientation) { + super(); + + setLayout(new SplitterLayout()); + fOrientation = aOrientation; + + fListener = new SplitterSeparatorListener(); + + updateUI(); + } + + public void updateUI() { + super.updateUI(); + + setBackground(UIManager.getColor("control")); + } + + /** + * Creates a separator of a given pixel size. + */ + + public Component createSeparator(int aSize) { + return new Separator(fOrientation, aSize, false); + } + + /** + * Creates an separator of a given pixel size. If aFixed is + * true, the separator is not user movable. + */ + + public Component createSeparator(int aSize, boolean aFixed) { + return new Separator(fOrientation, aSize, aFixed); + } + + /** + * Returns the weight associated with a given component. This + * number can be used to save and restore layout preferences. + */ + + public Float getWeight(Component aComponent) { + return (Float) fWeights.get(aComponent); + } + + /** + * Sets the minimum size a component can be sized to. + * Should be greater than zero for best results. + */ + + public void setMinSize(int aMinSize) { + fMinSize = aMinSize; + } + + /** + * Returns the minimum size a component can be sized to. + */ + + public int getMinSize() { + return fMinSize; + } + + // + // SplitterLayout + // + + class SplitterLayout implements LayoutManager2 { + + public void addLayoutComponent(String aString, Component aComponent) { + addLayoutComponent(aComponent, new Float(1.0)); + } + + public void addLayoutComponent(Component aComponent, Object aObject) { + if (aComponent instanceof Separator) { + Separator sep = (Separator) aComponent; + sep.addSeparatorListener(fListener); + fTotalSeparators += sep.getSeparatorSize(); + } else { + Number weight = null; + if (aObject instanceof Number) { + weight = new Float(((Number) aObject).floatValue()); + } else { + weight = new Float(1.0f); + } + fTotalWeight += weight.floatValue(); + fWeights.put(aComponent, weight); + } + } + + public void removeLayoutComponent(Component aComponent) { + if (aComponent instanceof Separator) { + Separator sep = (Separator) aComponent; + sep.removeSeparatorListener(fListener); + fTotalSeparators -= sep.getSeparatorSize(); + } else { + Float weight = (Float) fWeights.get(aComponent); + fWeights.remove(aComponent); + fTotalWeight -= weight.floatValue(); + } + } + + public void layoutContainer(Container aParent) { + int i, count = aParent.getComponentCount(); + Component components[] = aParent.getComponents(); + + Dimension parentSize = aParent.getSize(); + Insets insets = aParent.getInsets(); + parentSize.width -= insets.left + insets.right; + parentSize.height -= insets.top + insets.bottom; + + Dimension availableSize = new Dimension(parentSize); + + if (fOrientation == Splitter.VERTICAL) { + availableSize.height -= fTotalSeparators; + } else { + availableSize.width -= fTotalSeparators; + } + + Point pos = new Point(insets.left, insets.right); + Dimension size = new Dimension(availableSize); + + for (i = 0; i < count; i++) { + + if (i == count - 1) { + if (fOrientation == Splitter.VERTICAL) { + size.height = parentSize.height - pos.y; + } else { + size.width = parentSize.width - pos.x; + } + } else { + if (components[i] instanceof Separator) { + if (fOrientation == Splitter.VERTICAL) { + size.height = ((Separator) components[i]).getSeparatorSize(); + } else { + size.width = ((Separator) components[i]).getSeparatorSize(); + } + } else { + float weight = ((Number) fWeights.get(components[i])).floatValue(); + if (fOrientation == Splitter.VERTICAL) { + size.height = (int)(parentSize.height * (weight / fTotalWeight)); + } else { + size.width = (int)(parentSize.width * (weight / fTotalWeight)); + } + } + } + + components[i].setLocation(pos); + components[i].setSize(size); + + if (fOrientation == Splitter.VERTICAL) { + pos.y += size.height; + } else { + pos.x += size.width; + } + } + } + + public float getLayoutAlignmentX(Container aParent) { + return 0.5f; + } + + public float getLayoutAlignmentY(Container aParent) { + return 0.5f; + } + + public void invalidateLayout(Container aParent) { + } + + public Dimension preferredLayoutSize(Container aParent) { + return aParent.getSize(); + } + + public Dimension minimumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + + public Dimension maximumLayoutSize(Container aParent) { + return preferredLayoutSize(aParent); + } + } + + class SplitterSeparatorListener implements SeparatorListener { + int fOldPos = -1; + + public boolean separatorMoving(Separator aSep, int aDelta) { + Dimension size = aSep.getParent().getSize(); + + Graphics g = getGraphics(); + int pos; + if (fOrientation == VERTICAL) { + pos = aSep.getY() + aDelta; + if (pos < fMinSize) + pos = fMinSize; + else if (pos > (size.height - aSep.getHeight() - fMinSize)) + pos = size.height - aSep.getHeight() - fMinSize; + } else { + pos = aSep.getX() + aDelta; + if (pos < fMinSize) + pos = fMinSize; + else if (pos > (size.width - aSep.getWidth() - fMinSize)) + pos = size.width - aSep.getWidth() - fMinSize; + } + drawDividerLine(g, pos, aSep.getSeparatorSize()); + return true; + } + + public void separatorMoved(Separator aSep, int aDelta) { + Graphics g = getGraphics(); + drawDividerLine(g, -1, aSep.getSeparatorSize()); + + Component components[] = getComponents(); + int count = getComponentCount(); + + float beforeValue = 0, afterValue = 0; + int beforeSize = 0, afterSize = 0; + boolean before = true; + + for (int i = 0; i < count; i++) { + if (components[i] instanceof Separator) { + if (components[i] == aSep) { + before = false; + } + } else { + Dimension dim = components[i].getSize(); + int size = fOrientation == VERTICAL ? + dim.height : + dim.width; + float value = ((Number) fWeights.get(components[i])).floatValue(); + if (before) { + beforeValue += value; + beforeSize += size; + } else { + afterValue += value; + afterSize += size; + } + } + } + + Dimension size = aSep.getParent().getSize(); + + int pos; + if (fOrientation == VERTICAL) { + pos = aSep.getY() + aDelta; + if (pos < fMinSize) + pos = fMinSize; + else if (pos > (size.height - aSep.getHeight() - fMinSize)) + pos = size.height - aSep.getHeight() - fMinSize; + + aDelta = pos - aSep.getY(); + } else { + pos = aSep.getX() + aDelta; + if (pos < fMinSize) + pos = fMinSize; + else if (pos > (size.width - aSep.getWidth() - fMinSize)) + pos = size.width - aSep.getWidth() - fMinSize; + + aDelta = pos - aSep.getX(); + } + + float beforeDelta = (float) (beforeSize + aDelta) / (float) beforeSize; + float afterDelta = (float) (afterSize - aDelta) / (float) afterSize; + + before = true; + fTotalWeight = 0; + for (int i = 0; i < count; i++) { + if (components[i] instanceof Separator) { + if (components[i] == aSep) { + before = false; + } + } else { + float value = ((Number) fWeights.get(components[i])).floatValue(); + if (before) { + value *= beforeDelta; + } else { + value *= afterDelta; + } + fWeights.put(components[i], new Float(value)); + fTotalWeight += value; + } + } + + invalidate(); + validate(); + } + + void drawDividerLine(Graphics g, int aPos, int aSize) { + g.setXORMode(Color.white); + g.setColor(Color.black); + if (fOldPos != -1) { + if (fOrientation == VERTICAL) { + g.fillRect(0, fOldPos, getWidth(), aSize); + } else { + g.fillRect(fOldPos, 0, aSize, getHeight()); + } + } + if (aPos != -1) { + if (fOrientation == VERTICAL) { + g.fillRect(0, aPos, getWidth(), aSize); + } else { + g.fillRect(aPos, 0, aSize, getHeight()); + } + } + + fOldPos = aPos; + g.setPaintMode(); + } + } +} + +// +// SeparatorListener +// + +interface SeparatorListener { + public boolean separatorMoving(Separator aSep, int aDelta); + public void separatorMoved(Separator aSep, int aDelta); +} + +// +// Separator Class +// + +class Separator extends JComponent { + int fOrientation; + int fSize; + SplitterMouseListener fMouseListener; + SeparatorListener fListener; + + class SplitterMouseListener extends MouseAdapter implements MouseMotionListener { + int hitX, hitY; + boolean fDragging; + int fDelta; + + public void mouseEntered(MouseEvent aEvent) { + setCursor(Cursor.getPredefinedCursor(fOrientation == Splitter.VERTICAL ? + Cursor.N_RESIZE_CURSOR : + Cursor.E_RESIZE_CURSOR)); + } + + public void mousePressed(MouseEvent aEvent) { + // XXX AWT bug work around + // mousePressed gets called again if the cursor leaves the + // frame. grr. + if (!fDragging) { + hitX = aEvent.getX(); + hitY = aEvent.getY(); + fDelta = 0; + + fDragging = true; + } + } + + public void mouseReleased(MouseEvent aEvent) { + notifyMoved(fDelta); + + fDragging = false; + } + + public void mouseMoved(MouseEvent aEvent) { + } + + public void mouseDragged(MouseEvent aEvent) { + int delta = fOrientation == Splitter.VERTICAL ? + aEvent.getY() - hitY : + aEvent.getX() - hitX; + + if (notifyMoving(delta)) { + fDelta = delta; + } + } + } + + public Separator(int aOrientation, int aSize, boolean aFixed) { + fOrientation = aOrientation; + fSize = aSize; + + if (!aFixed) { + fMouseListener = new SplitterMouseListener(); + addMouseListener(fMouseListener); + addMouseMotionListener(fMouseListener); + } + } + + public int getSeparatorSize() { + return fSize; + } + + public void paint(Graphics g) { + Color bg = getBackground(); + if (bg != null) { + Rectangle rect = getVisibleRect(); + + g.setColor(bg); + g.fillRect(rect.x, rect.y, rect.width, rect.height); + } + } + + public void addSeparatorListener(SeparatorListener aListener) { + fListener = aListener; + } + + public void removeSeparatorListener(SeparatorListener aListener) { + if (aListener == fListener) { + aListener = null; + } + } + + boolean notifyMoving(int aDelta) { + if (fListener != null) { + return fListener.separatorMoving(this, aDelta); + } + return true; + } + + void notifyMoved(int aDelta) { + if (fListener != null) { + fListener.separatorMoved(this, aDelta); + } + } +} diff --git a/mozilla/grendel/widgets/StatusEvent.java b/mozilla/grendel/widgets/StatusEvent.java new file mode 100644 index 00000000000..335caff2ce6 --- /dev/null +++ b/mozilla/grendel/widgets/StatusEvent.java @@ -0,0 +1,67 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 24 Oct 1997. + */ + +package grendel.widgets; + +import java.util.EventObject; + +/** + * Class for passing status messages to listeners + */ + +public class StatusEvent extends EventObject { + String fStatus; + + /** + * Creates a status event with the given status. The string + * is not copied for efficiency, use + * StatusEvent(Object aSource, String aStatus , boolean aCopy) + * for a constructor that copies the string. + */ + + public StatusEvent(Object aSource, String aStatus) { + super(aSource); + fStatus = aStatus; + } + + /** + * Creates a status event with the given string. If aCopy is + * true, a copy of the status string is stored. + */ + + public StatusEvent(Object aSource, String aStatus, boolean aCopy) { + super(aSource); + + if (aCopy) { + fStatus = aStatus.toString(); + } else { + fStatus = aStatus; + } + } + + /** + * @return The status string for this event. + */ + + public final String getStatus() { + return fStatus; + } +} diff --git a/mozilla/grendel/widgets/TextCellEditor.java b/mozilla/grendel/widgets/TextCellEditor.java new file mode 100644 index 00000000000..99456ccf7d0 --- /dev/null +++ b/mozilla/grendel/widgets/TextCellEditor.java @@ -0,0 +1,153 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 30 Oct 1997. + */ + +package grendel.widgets; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.util.EventObject; + +import com.sun.java.swing.CellEditorListener; +import com.sun.java.swing.JTextField; +import com.sun.java.swing.KeyStroke; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.DocumentEvent; +import com.sun.java.swing.event.DocumentListener; + +public class TextCellEditor extends JTextField implements CellEditor, +DocumentListener { + CellEditorListener fListener; + boolean fSelected, fStopping; + Dimension fSize = new Dimension(); + + public TextCellEditor() { + addActionListener(new StopAction()); + addFocusListener(new MyFocusListener()); + + registerKeyboardAction(new CancelAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + WHEN_FOCUSED); + getDocument().addDocumentListener(this); + setFont(Font.decode("SansSerif-12")); + } + + public void cancelCellEditing() { + if (fListener != null) { + fListener.editingCanceled(new ChangeEvent(this)); + } + } + + public void setValue(Object aObject, Object aData, boolean aSelected) { + setText(aData.toString()); + fSelected = aSelected; + fSize = getPreferredSize(); + } + + public Object getCellEditorValue() { + return getText(); + } + + public boolean isCellEditable(EventObject aEvent) { + if (aEvent instanceof MouseEvent) { + return (((MouseEvent) aEvent).getID() == MouseEvent.MOUSE_RELEASED && + fSelected); + } + return false; + } + + public boolean shouldSelectCell(EventObject aEvent) { + return !fSelected; + } + + public boolean stopCellEditing() { + if (fListener != null && !fStopping) { + fStopping = true; + fListener.editingStopped(new ChangeEvent(this)); + fStopping = false; + } + return true; + } + + public boolean startCellEditing(EventObject aEvent) { + updateSize(getPreferredSize()); + requestFocus(); + return true; + } + + public Component getCellEditorComponent() { + return this; + } + + public void addCellEditorListener(CellEditorListener aListener) { + fListener = aListener; + } + + public void removeCellEditorListener(CellEditorListener aListener) { + if (fListener == aListener) { + fListener = null; + } + } + + protected void updateSize(Dimension aSize) { + int w = aSize.width > fSize.width ? aSize.width : fSize.width; + int h = aSize.height > fSize.height ? aSize.height : fSize.height; + + setSize(w, h); + } + + public void changedUpdate(DocumentEvent aEvent) { + updateSize(getPreferredSize()); + } + + public void insertUpdate(DocumentEvent aEvent) { + updateSize(getPreferredSize()); + } + + public void removeUpdate(DocumentEvent aEvent) { + updateSize(getPreferredSize()); + } + + class CancelAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + cancelCellEditing(); + } + } + + class StopAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + stopCellEditing(); + } + } + + class MyFocusListener extends FocusAdapter { + public void focusLost(FocusEvent aEvent) { + stopCellEditing(); + } + } +} diff --git a/mozilla/grendel/widgets/ToggleCellEditor.java b/mozilla/grendel/widgets/ToggleCellEditor.java new file mode 100644 index 00000000000..583a42ff728 --- /dev/null +++ b/mozilla/grendel/widgets/ToggleCellEditor.java @@ -0,0 +1,119 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.widgets; + +import java.awt.AWTEvent; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.util.EventObject; + +import com.sun.java.swing.CellEditorListener; +import com.sun.java.swing.JCheckBox; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.UIManager; + +public class ToggleCellEditor implements CellEditor, ActionListener { + JCheckBox fToggle; + CellEditorListener fListener; + + public ToggleCellEditor() { + fToggle = new JCheckBox() { + public void requestFocus() { + } + }; + fToggle.addActionListener(this); + } + + public boolean isCellEditable(EventObject aEvent) { + if (aEvent instanceof MouseEvent) { + return (((MouseEvent) aEvent).getClickCount() == 1); + } + return false; + } + + public boolean shouldSelectCell(EventObject aEvent) { + return false; + } + + public boolean startCellEditing(EventObject aEvent) { + if (aEvent instanceof AWTEvent) { + fToggle.dispatchEvent((AWTEvent) aEvent); + } + return true; + } + + public boolean stopCellEditing() { + if (fListener != null) { + fListener.editingStopped(new ChangeEvent(this)); + } + return true; + } + + public void cancelCellEditing() { + if (fListener != null) { + fListener.editingCanceled(new ChangeEvent(this)); + } + } + + public void setValue(Object aObject, Object aData, boolean aSelected) { + if (aSelected) { + fToggle.setBackground(UIManager.getColor("textHighlight")); + fToggle.setForeground(UIManager.getColor("textHighlightText")); + } else { + fToggle.setBackground(UIManager.getColor("window")); + fToggle.setForeground(UIManager.getColor("textText")); + } + if (aData instanceof Boolean) { + fToggle.setSelected(((Boolean) aData).booleanValue()); + } else { + fToggle.setSelected(false); + } + } + + public Component getCellEditorComponent() { + return fToggle; + } + + public Object getCellEditorValue() { + return new Boolean(fToggle.isSelected()); + } + + public void addCellEditorListener(CellEditorListener aListener) { + fListener = aListener; + } + + public void removeCellEditorListener(CellEditorListener aListener) { + if (fListener == aListener) { + fListener = null; + } + } + + public void actionPerformed(ActionEvent e) { + if (fListener != null) { + fListener.editingStopped(new ChangeEvent(this)); + } + } + + public JCheckBox getCheckBox() { + return fToggle; + } +} diff --git a/mozilla/grendel/widgets/ToggleCellRenderer.java b/mozilla/grendel/widgets/ToggleCellRenderer.java new file mode 100644 index 00000000000..3125ccdfe8c --- /dev/null +++ b/mozilla/grendel/widgets/ToggleCellRenderer.java @@ -0,0 +1,52 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + */ + +package grendel.widgets; + +import java.awt.Component; + +import com.sun.java.swing.JCheckBox; +import com.sun.java.swing.UIManager; + +public class ToggleCellRenderer implements CellRenderer { + JCheckBox fToggle = new JCheckBox(); + + public void setValue(Object aObject, Object aData, boolean aSelected) { + if (aSelected) { + fToggle.setBackground(UIManager.getColor("textHighlight")); + fToggle.setForeground(UIManager.getColor("textHighlightText")); + } else { + fToggle.setBackground(UIManager.getColor("window")); + fToggle.setForeground(UIManager.getColor("textText")); + } + if (aData instanceof Boolean) { + fToggle.setSelected(((Boolean) aData).booleanValue()); + } else { + fToggle.setSelected(false); + } + } + + public Component getComponent() { + return fToggle; + } + + public JCheckBox getCheckBox() { + return fToggle; + } +} diff --git a/mozilla/grendel/widgets/TreePath.java b/mozilla/grendel/widgets/TreePath.java new file mode 100644 index 00000000000..9a783fe1459 --- /dev/null +++ b/mozilla/grendel/widgets/TreePath.java @@ -0,0 +1,106 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 28 Aug 1997. + */ + +package grendel.widgets; + +import java.util.Vector; + +/** + * Object that represents the path through a tree to a node. + * Provides equals and hash methods to ensure expected behaviors. + * + * @author Will Scullin + */ + +public class TreePath { + Object fPath[]; + + /** + * Constructs a TreePath from an array of nodes + */ + + public TreePath(Object aPath[]) { + fPath = aPath; + } + + /** + * Constructs a TreePath from a Vector of nodes + */ + + public TreePath(Vector aPath) { + fPath = new Object[aPath.size()]; + aPath.copyInto(fPath); + } + + /** + * Returns the tree path associated with this object. + */ + + public Object[] getPath() { + return fPath; + } + + /** + * Returns the last node in the path + */ + + public Object getTip() { + return fPath[fPath.length - 1]; + } + + /** + * Returns the length of the path + */ + + public int getLength() { + return fPath.length; + } + + /** + * Overloaded so that different objects with same contents + * are considered equal. + */ + + public boolean equals(Object aObject) { + if (aObject instanceof TreePath) { + Object path[] = ((TreePath) aObject).getPath(); + int length = path.length; + if (length == fPath.length) { + for (int i = length - 1; i >= 0; i--) { + if (path[i] != fPath[i]) { + return false; + } + } + return true; + } + } + return false; + } + + /** + * Overloaded so different objects with the same contents hash + * to the same value + */ + + public int hashCode() { + return getTip().hashCode(); + } +} diff --git a/mozilla/grendel/widgets/TreeTable.java b/mozilla/grendel/widgets/TreeTable.java new file mode 100644 index 00000000000..eedc5f8730f --- /dev/null +++ b/mozilla/grendel/widgets/TreeTable.java @@ -0,0 +1,1942 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseMotionListener; +import java.awt.event.MouseEvent; + +import java.util.Enumeration; +import java.util.Vector; + +import com.sun.java.swing.CellEditorListener; +import com.sun.java.swing.CellRendererPane; +import com.sun.java.swing.Icon; +import com.sun.java.swing.ImageIcon; +import com.sun.java.swing.JComponent; +import com.sun.java.swing.JLabel; +import com.sun.java.swing.JViewport; +import com.sun.java.swing.KeyStroke; +import com.sun.java.swing.Scrollable; +import com.sun.java.swing.SwingConstants; +import com.sun.java.swing.SwingUtilities; +import com.sun.java.swing.UIManager; +import com.sun.java.swing.basic.BasicGraphicsUtils; +import com.sun.java.swing.event.ChangeEvent; +import com.sun.java.swing.event.ListSelectionEvent; + +import grendel.dnd.DropTarget; +import grendel.dnd.DropTargetComponent; +import grendel.dnd.DragSource; + +/** + * A outliner view similar to those used in previous versions of + * mail and news. + */ + +public class TreeTable extends JComponent implements Scrollable, + DropTargetComponent +{ + // For synchronization + TreeTable fTreeTable; + + CellRendererPane fCellRendererPane = new CellRendererPane(); + ColumnModel fColumnModel; + ColumnHeader fColumnHeader; + TreeTableDataModel fDataModel; + int fHeight; + TreeNode fRoot; + TreeModelListener fDataModelListener = new TreeModelListener(); + + // Selection/focus + TreeNode fCaret; + TreeNode fAnchor; + SelectionManager fSelection = new MultiSelectionManager(); + int fHitX, fHitY; + Point fHitPoint; + boolean fDragging = false; + Point fTooltipPoint = null; + + // Editing + boolean fEditing = false; + CellEditor fEditor; + TreeNode fEditedNode; + Column fEditedColumn; + boolean fMousinessAfoot = false; + boolean fJustSelected = false; + DropTarget fDropTarget = null; + + // Attributes + Object fTreeColumn; + int fIndentLevel = 16; + int fRowHeight = 0; + int fRowMargin = 0; + boolean fFixed = true; + static final int kGap = 4; + + // Properties + boolean fDrawPipes = false; + + // Images + Icon fPlusIcon; + Icon fMinusIcon; + + Color fWindowColor; + Color fHighlightColor; + + // + // Constructors + // + + /** + * Constructs a TreeTable with the given data model. + */ + + public TreeTable(TreeTableDataModel aDataModel) { + fTreeTable = this; + + setLayout(null); + add(fCellRendererPane); + + fColumnModel = new ColumnModelImp(); + fColumnModel.addColumnModelListener(new TreeColumnModelListener()); + fColumnHeader = new ColumnHeader(fColumnModel); + + TreeMouseListener mouseListener = new TreeMouseListener(); + addMouseListener(mouseListener); + addMouseMotionListener(mouseListener); + addFocusListener(new TreeFocusListener()); + + fSelection = new MultiSelectionManager(); + fSelection.addSelectionListener(new TreeSelectionListener()); + + updateUI(); + + initializeKeys(); + + if (aDataModel != null) { + updateDataModel(aDataModel); + } + } + + // + // Accessor functions + // + + /** + * Sets the current data model. A null value (should be) + * acceptable. + */ + + public synchronized void setDataModel(TreeTableDataModel aDataModel) { + if (fDataModel != null) { + clearDataModel(); + } + updateDataModel(aDataModel); + + resizeAndRepaint(); + } + + /** + * Returns the current data model. + * + * @see TreeTableDataModel + */ + + public synchronized TreeTableDataModel getDataModel() { + return fDataModel; + } + + /** + * Retuns the column model + * + * @see ColumnModel + */ + + public synchronized ColumnModel getColumnModel() { + return fColumnModel; + } + + /** + * Adds a column to the tree table. The column is added at the end. + */ + + public void addColumn(Column aColumn) { + fColumnModel.addColumn(aColumn); + } + + /** + * Adds a column at the given position. Existing columns may be + * shifted to the right. + */ + + public void addColumnAt(Column aColumn, int aIndex) { + fColumnModel.addColumnAt(aColumn, aIndex); + } + + /** + * Returns the ColumnHeader widget that allows the user to + * view and manipulate column properties. + */ + + public ColumnHeader getColumnHeader() { + return fColumnHeader; + } + + /** + * Returns the last object on which the user clicked. This + * object is used for keyboard navigation. + */ + + public TreePath getCaret() { + return buildTreePath(fCaret); + } + + void setCaret(TreeNode aNode) { + if (fCaret != null) { + repaint(fCaret); + } + fCaret = aNode; + if (fCaret != null) { + repaint(fCaret); + } + } + + /** + * Sets the last object on which the user clicked. This + * object is used for keyboard navigation. + */ + + public void setCaret(TreePath aPath) { + TreeNode node = findTreeNode(aPath); + if (node != null) { + setCaret(node); + } + } + + /** + * Returns the interface for the tree table's selection manager + */ + + public SelectionManager getSelectionManager() { + return fSelection; + } + + // + // Attributes + // + + /** + * Sets which column in which the tree indention and (eventually) + * icons appear. + */ + + public void setTreeColumn(Object aID) { + fTreeColumn = aID; + } + + /** + * Returns which column in which the tree indention and (eventually) + * icons appear. + */ + + public Object getTreeColumn() { + return fTreeColumn; + } + + /** + * Sets the number of pixels each level of the tree is indented + * (16 or more is currently recommended.) + */ + + public void setIndentLevel(int aIndent) { + fIndentLevel = aIndent; + } + + /** + * Returns the number of pixels each level of the tree is indented. + */ + + public int getIndentLevel() { + return fIndentLevel; + } + + /** + * Sets a fixed row height. Overrides the per-row sizing feature. + * A height of 0 enables the per-row sizing feature. + */ + + public void setRowHeight(int aHeight) { + fRowHeight = aHeight; + } + + /** + * Returns the fixed row height + * @see setRowHeight() + */ + + public int getRowHeight() { + return fRowHeight; + } + + /** + * Sets whether all rows have the same height. Saves a tremendous + * amount of time if true. Defaults to true. + * @see isFixedHeight() + */ + + public void setFixedHeight(boolean aFixed) { + fFixed = aFixed; + } + + /** + * Returns whether all rows have the same height. Saves a tremendous + * amount of time if true. + * @see setFixedHeight() + */ + + public boolean isFixedHeight() { + return fFixed; + } + + /** + * Does nothing, yet. + */ + + public void setRowMargin(int aMargin) { + fRowMargin = aMargin; + } + + /** + * Does nothing, yet. + */ + + public int getRowMargin() { + return fRowMargin; + } + + // + // Selection functions + // + + Enumeration getShiftRange(TreePath aPath) { + if (fAnchor == null) { + fAnchor = getVisibleRoot(); + } + + TreeNode node = findTreeNode(aPath); + int anchorY = getNodeY(fAnchor); + int nodeY = getNodeY(node); + + TreeNode first; + TreeNode last; + + if (anchorY < nodeY) { + first = fAnchor; + last = node; + } else { + first = node; + last = fAnchor; + } + + Vector range = new Vector(); + boolean done = false; + while (!done && first != null) { + range.insertElementAt(buildTreePath(first), range.size()); + + if (first == last) { + done = true; + } else { + TreeNode next = null; + + if (!first.isCollapsed() && !first.isLeaf()) { + first = first.getFirstChild(); + } else { + next = first.getNextSibling(); + if (next == null) { + TreeNode parent = first.getParent(); + while (next == null && parent != null) { + next = parent.getNextSibling(); + parent = parent.getParent(); + } + } + first = next; + } + } + } + return range.elements(); + } + + void select(TreePath aPath, int aModifiers) { + boolean shiftKey = (aModifiers & KeyEvent.SHIFT_MASK) != 0; + boolean controlKey = (aModifiers & KeyEvent.CTRL_MASK) != 0; + boolean rightMouse = (aModifiers & KeyEvent.BUTTON3_MASK) != 0; + + // Complex heuristic for selecting based on windows. + // Probably wrong for other platforms. + // Maybe we need a plugable UI after all. + + if (controlKey) { + if (!rightMouse) { + if (fSelection.isSelected(aPath)) { + if (shiftKey) { + fSelection.removeSelection(getShiftRange(aPath)); + } else { + fSelection.removeSelection(aPath); + } + } else { + if (shiftKey) { + fSelection.addSelection(getShiftRange(aPath)); + } else { + fSelection.addSelection(aPath); + } + } + } + } else { + if (rightMouse) { + if (!fSelection.isSelected(aPath)) { + fSelection.setSelection(aPath); + } + } else { + if (shiftKey) { + fSelection.setSelection(getShiftRange(aPath)); + } else { + fSelection.setSelection(aPath); + } + } + } + + if (!shiftKey) { + fAnchor = findTreeNode(aPath); + } + } + + /** + * Selects the given TreePath. + */ + + public void select(TreePath aPath) { + select(aPath, 0); + } + + // + // Navigation functions + // + + /** + * Navigation used for up arrow key. Does reverse in-order + * traversal, skipping children of collapsed nodes. + */ + + void navigateUp(int aModifiers) { + if (fCaret != null) { + if (fCaret != getVisibleRoot()) { + TreeNode prev, last; + prev = fCaret.getPrevSibling(); + if (prev == null) { + setCaret(fCaret.getParent()); + } else { + setCaret((TreeNode) null); + while (prev != null && fCaret == null) { + if (!prev.isCollapsed() && prev.getFirstChild() != null) { + prev = prev.getLastChild(); + } else { + setCaret(prev); + } + } + } + } + } + + if (fCaret == null) { + setCaret(getVisibleRoot()); + } + + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + /** + * Navigation used for down arrow key. Does in-order + * traversal, skipping children of collapsed nodes. + */ + + void navigateDown(int aModifiers) { + if (fCaret != null) { + TreeNode next = null; + + if (!fCaret.isCollapsed()) { + next = fCaret.getFirstChild(); + } + + if (next == null) { + next = fCaret.getNextSibling(); + if (next == null) { + TreeNode parent = fCaret.getParent(); + while (next == null && parent != null) { + next = parent.getNextSibling(); + parent = parent.getParent(); + } + } + } + + if (next != null) { + setCaret(next); + } + } else { + setCaret(getVisibleRoot()); + } + + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + /** + * Navigation used for left arrow key. First collapses node, + * then moves to parent + */ + + void navigateLeft(int aModifiers) { + if (fCaret != null) { + if (fCaret.isLeaf() || fCaret.isCollapsed()) { + fCaret = fCaret.getParent(); + } else { + collapse(fCaret); + } + } + + if (fCaret == null) { + setCaret(getVisibleRoot()); + } + + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + /** + * Navigation used for right arrow key. First expands node, + * then moves to first child + */ + + void navigateRight(int aModifiers) { + if (fCaret != null) { + if (!fCaret.isLeaf()) { + if (!fCaret.isCollapsed()) { + fCaret = fCaret.getFirstChild(); + } else { + expand(fCaret); + } + } + } + + if (fCaret == null) { + setCaret(getVisibleRoot()); + } + + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + void navigateHome(int aModifiers) { + setCaret(getVisibleRoot()); + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + TreeNode findLastVisible() { + TreeNode node = getVisibleRoot(); + TreeNode res = null; + + while (node != null) { + res = node; + if (node.getNextSibling() != null) { + node = node.getNextSibling(); + } else { + node = node.getFirstChild(); + } + } + return res; + } + + void navigateEnd(int aModifiers) { + setCaret(findLastVisible()); + if (fCaret == null) { + setCaret(getVisibleRoot()); + } + select(buildTreePath(fCaret), aModifiers); + ensureVisible(fCaret); + } + + + /** + * Moves the caret up one position, and selects that object. + * (currently doesn't support scrolling) + */ + + public void navigateUp() { + navigateUp(0); + } + + /** + * Moves the caret down one position, and selects that object. + * (currently doesn't support scrolling) + */ + + public void navigateDown() { + navigateDown(0); + } + + /** + * Returns whether or not the give path is visible. If the path + * points to the root, and the root is not shown, the method + * still returns true. + */ + + public boolean isVisible(TreePath aPath) { + return (findTreeNode(aPath) != null); + } + + /** + * Attempts to tweek parent into displaying the given node. + */ + + void ensureVisible(TreeNode aNode) { + ensureVisible(buildTreePath(aNode)); + } + + public void ensureVisible(TreePath aPath) { + int y = getNodeY(aPath); + int h = getNodeHeight(aPath); + + if (y != -1) { + Container parent = getParent(); + if (parent != null && parent instanceof JViewport) { + JViewport parentView = (JViewport) parent; + Dimension viewSize = parentView.getExtentSize(); + Point viewPos = parentView.getViewPosition(); + + if (y < viewPos.y) { + viewPos.y = y; + parentView.setViewPosition(viewPos); + } else if ((y + h) > (viewPos.y + viewSize.height)) { + viewPos.y = y - viewSize.height + h; + parentView.setViewPosition(viewPos); + } + } + } + } + + int getNodeY(TreeNode aNode) { + return getNodeY(buildTreePath(aNode)); + } + + /** + * Returns the Y location of the give node + */ + + public int getNodeY(TreePath aPath) { + Object path[] = aPath.getPath(); + int length = aPath.getLength(); + int index = 0; + int y = 0; + TreeNode traverse = fRoot; + + if (!fDataModel.showRoot()) { + index = 1; + if (traverse != null) { + traverse = traverse.getFirstChild(); + } + } + + while (index < length) { + while (traverse != null && traverse.getData() != path[index]) { + y += traverse.getTotalHeight(); + traverse = traverse.getNextSibling(); + } + + if (traverse == null) { + return -1; + } + + index++; + if (index < length) { + y += traverse.getNodeHeight(); + traverse = traverse.getFirstChild(); + } + } + + return y; + } + + public int getNodeHeight(TreePath aPath) { + if(fRowHeight > 0) { + return fRowHeight; + } + + TreeNode node = findTreeNode(aPath); + if (node == null) { + return -1; + } + return node.getNodeHeight(); + } + + public int getColumnX(Column column) { + int res = -1; + if (column != null) { + int idx = fColumnModel.getColumnIndex(column); + int margin = fColumnModel.getColumnMargin(); + + res = 0; + for (int i = 0; i < idx; i++) { + res += fColumnModel.getColumn(i).getWidth() + margin; + } + } + + return res; + } + + // + // Utility functions + // + + TreeNode getVisibleRoot() { + if (fDataModel == null) { + return null; + } + + if (fDataModel.showRoot()) { + return fRoot; + } else { + return fRoot.getFirstChild(); + } + } + + TreePath buildTreePath(TreeNode aNode) { + if (aNode == null) { + return null; + } + + int depth = aNode.getDepth(); + Object path[] = new Object[depth + 1]; + + while(aNode != null) { + path[depth] = aNode.getData(); + aNode = aNode.getParent(); + depth--; + } + + return new TreePath(path); + } + + TreeNode findTreeNodeRecurse(TreeNode aNode, Object aPath[], int aFirst, int aLast) { + while (aNode != null) { + if (aNode.getData() == aPath[aFirst]) { + if (aFirst == aLast) { + return aNode; + } else { + return findTreeNodeRecurse(aNode.getFirstChild(), aPath, aFirst + 1, aLast); + } + } else { + aNode = aNode.getNextSibling(); + } + } + return null; + } + + /** + * Finds a TreeNode given a path. If null is returned, either + * the path is invalid, or the path isn't fully visible. + */ + + synchronized TreeNode findTreeNode(TreePath aPath) { + return findTreeNodeRecurse(fRoot, aPath.getPath(), 0, aPath.getLength() - 1); + } + + synchronized void resizeAndRepaint() { + setSize(getPreferredSize()); + repaint(); + } + + synchronized void resizeAndRepaintFrom(TreePath aPath) { + setSize(getPreferredSize()); + repaintFrom(aPath); + } + + synchronized void clearDataModel() { + fSelection.clearSelection(); + setCaret((TreeNode) null); + + fDataModel.removeTreeTableModelListener(fDataModelListener); + fDataModel = null; + + TreeNode node = fRoot; + while (node != null) { + TreeNode next = node.getNextSibling(); + node.detachSelf(); + node = next; + } + fRoot = null; + } + + synchronized void updateDataModel(TreeTableDataModel aDataModel) { + fDataModel = aDataModel; + + if (fDataModel != null) { + fDataModel.addTreeTableModelListener(fDataModelListener); + } + + reload(); + } + + /** + * Forces the tree to rebuild itself from scratch. + * Doesn't repaint. + */ + + public void reload() { + if (fDataModel != null) { + Enumeration children = fDataModel.getChildren(null); + if (children != null) { + if (children.hasMoreElements()) { + // First case where children are given as enumerations + Object node = children.nextElement(); + fRoot = new TreeNode(fDataModel, null, node); + TreeNode prev = fRoot; + + while (children.hasMoreElements()) { + node = children.nextElement(); + prev.addSiblingAfter(new TreeNode(fDataModel, null, node)); + prev = prev.getNextSibling(); + } + } else { + fRoot = null; + } + } else { + // Second case where children are give by getFirstChild, + // getNextSibling + Object node = fDataModel.getRoot(); + if (node != null) { + fRoot = new TreeNode(fDataModel, null, node); + TreeNode prev = fRoot; + + node = fDataModel.getNextSibling(node); + while (node != null) { + prev.addSiblingAfter(new TreeNode(fDataModel, null, node)); + prev = prev.getNextSibling(); + node = fDataModel.getNextSibling(node); + } + } else { + fRoot = null; + } + } + } + initializeCached(); + } + + void initializeCached() { + TreeNode node = getVisibleRoot(); + fHeight = 0; + + while (node != null) { + fHeight += node.getTotalHeight(); + node = node.getNextSibling(); + } + } + + void initializeKeys() { + registerKeyboardAction(new NavigateUpAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), + WHEN_FOCUSED); + registerKeyboardAction(new NavigateDownAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), + WHEN_FOCUSED); + registerKeyboardAction(new NavigateLeftAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), + WHEN_FOCUSED); + registerKeyboardAction(new NavigateRightAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), + WHEN_FOCUSED); + registerKeyboardAction(new NavigateHomeAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), + WHEN_FOCUSED); + registerKeyboardAction(new NavigateEndAction(0), + KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), + WHEN_FOCUSED); + registerKeyboardAction(new OpenAction(), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + WHEN_FOCUSED); + } + + void expand(TreeNode aNode) { + fDataModel.setCollapsed(buildTreePath(aNode), false); + } + + void collapse(TreeNode aNode) { + fDataModel.setCollapsed(buildTreePath(aNode), true); + } + + int getIndent(Column aColumn, TreeNode aNode) { + int indent = 0; + if (aColumn.getID().equals(fTreeColumn)) { + Icon icon = fDataModel.getIcon(aNode.getData()); + indent = aNode.getVisibleDepth() * fIndentLevel + fIndentLevel + + (icon != null ? icon.getIconWidth() : 0) + kGap; + } + return indent; + } + + int drawPipes(Graphics g, TreeNode aNode, int x, int y, int w, int h) { + TreeNode node = aNode; + int depth = aNode.getVisibleDepth(); + x += depth * fIndentLevel; + + int midY = y + h / 2; + + g.setColor(Color.black); + + int iconWidth = 0; + Icon icon = fDataModel.getIcon(aNode.getData()); + + if (icon != null) { + iconWidth = icon.getIconWidth(); + int iconY = midY - icon.getIconHeight() / 2; + + icon.paintIcon(this, g, x + fIndentLevel, iconY); + + Icon overlayIcon = fDataModel.getOverlayIcon(aNode.getData()); + if (overlayIcon != null) { + overlayIcon.paintIcon(this, g, x + fIndentLevel, iconY); + } + } + + while (node != null && node.getVisibleDepth() >= 0) { + int midX = x + fIndentLevel / 2; + + if (fDrawPipes) { + if (node.getNextSibling() != null) { + g.drawLine(midX, y, midX, y + h); + } + + if (aNode == node) { + g.drawLine(midX, midY, x + fIndentLevel, midY); + if (node.getParent() != null) { + g.drawLine(midX, midY, midX, y); + } + } + } + + if (node == aNode && !node.isLeaf()) { + if (node.isCollapsed()) { + fPlusIcon.paintIcon(this, g, x, y); + } else { + fMinusIcon.paintIcon(this, g, x, y); + } + } + + node = node.getParent(); + x -= fIndentLevel; + } + + return fIndentLevel + depth * fIndentLevel + iconWidth + kGap; + } + + /** + * Recursive part of paint(). + * @see #paint + */ + + static int fPaintedRows; + + void paintRecurse(Graphics g, TreeNode aRoot, int y) { + int x; + int w, h; + boolean selected = false; + boolean done = false; + TreeNode node = aRoot; + + int columnMargin = fColumnModel.getColumnMargin(); + + Rectangle rect = g.getClipBounds(); + + while (node != null && !done) { + TreePath path = buildTreePath(node); + + h = node.getNodeHeight(); + x = 0; + selected = fSelection.isSelected(path); + + if (y + h >= rect.y) { // Paint only visible stuff + if (y < rect.y + rect.height) { // ditto + + fPaintedRows++; + + Enumeration columns = fColumnModel.getColumns(); + + g.setColor(selected ? fHighlightColor : fWindowColor); + g.fillRect(0, y, getWidth(), h); + + while (columns.hasMoreElements()) { + Column column = (Column) columns.nextElement(); + CellRenderer renderer = column.getCellRenderer(); + w = column.getWidth(); + + if (x + w >= rect.x && x < rect.x + rect.width) { + renderer.setValue(node.getData(), + fDataModel.getData(node.getData(), column.getID()), + selected); + + Component component = renderer.getComponent(); + + int dx = x; + int dw = w; + if (column.getID().equals(fTreeColumn)) { + int pipeWidth = drawPipes(g, node, x, y, w, h); + dx = x + pipeWidth; + dw = w - pipeWidth; + } + + fCellRendererPane.paintComponent(g, component, this, dx, y, dw, h); + } + + x += w + columnMargin; + } + + if (hasFocus() && node == fCaret) { + g.setXORMode(Color.white); + BasicGraphicsUtils.drawDashedRect(g, 0, y, getWidth(), h); + g.setPaintMode(); + } + + } else { + done = true; + } + } + + if (!done) { + y += h; + + if (node.getFirstChild() != null && !node.isCollapsed()) { + paintRecurse(g, node.getFirstChild(), y); + y += node.getChildrenHeight(); + } + node = node.getNextSibling(); + } + } + } + + /** + * Recursive part of hitTestNode() + * @see #hitTestNode + */ + + TreeNode hitTestNodeRecurse(TreeNode aFirst, int aTop, int aY) { + TreeNode node = aFirst; + int nodeHeight, totalHeight; + + while (node != null) { + nodeHeight = node.getNodeHeight(); + totalHeight = node.getTotalHeight(); + if (aY < aTop + totalHeight) { // hit node or child + if (aY < aTop + nodeHeight) { // hit node + return node; + } else { // hit child + return hitTestNodeRecurse(node.getFirstChild(), aTop + nodeHeight, aY); + } + } + node = node.getNextSibling(); + aTop += totalHeight; + } + return null; + } + + /** + * Determines what node is under the given coordinate + */ + + synchronized TreeNode hitTestNode(int aY) { + return hitTestNodeRecurse(getVisibleRoot(), 0, aY); + } + + /** + * Determines what column is under the given coordinate + */ + + Column hitTestColumn(int aX) { + int columnMargin = fColumnModel.getColumnMargin(); + int w, x = 0; + fHitX = -1; + + Enumeration columns = fColumnModel.getColumns(); + while (columns.hasMoreElements()) { + Column column = (Column) columns.nextElement(); + w = column.getWidth() + columnMargin; + + if (aX < x + w) { + fHitX = aX - x; + return column; + } + x += w; + } + + return null; + } + + // + // Component Overloads + // + + public synchronized void paintComponent(Graphics g) { + fPaintedRows = 0; + + paintRecurse(g, getVisibleRoot(), 0); + +// System.out.println("Painted " + fPaintedRows + " Rows"); + + if (fHeight < getHeight()) { + Rectangle rect = new Rectangle(0, fHeight, getWidth(), getHeight() - fHeight). + intersection(g.getClipBounds()); + g.setColor(fWindowColor); + g.fillRect(rect.x, rect.y, rect.width, rect.height); + } + } + + void repaint(TreeNode aNode) { + int y = getNodeY(aNode); + int h = aNode.getNodeHeight(); + repaint(0, y, getWidth(), h); + } + + public void repaint(TreePath aPath) { + int y = getNodeY(aPath); + int h = getNodeHeight(aPath); + repaint(0, y, getWidth(), h); + } + + public void repaintFrom(TreePath aPath) { + int y = getNodeY(aPath); + repaint(0, y, getWidth(), getHeight() - y); + } + + public Dimension getPreferredSize() { + return new Dimension(fColumnModel.getTotalColumnWidth(), fHeight); + } + + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + public void updateUI() { + super.updateUI(); + + fPlusIcon = new ImageIcon(getClass().getResource("images/plus.gif")); + fMinusIcon = new ImageIcon(getClass().getResource("images/minus.gif")); + + fWindowColor = UIManager.getColor("window"); + fHighlightColor = UIManager.getColor("textHighlight"); + } + public boolean isOpaque() { + return true; + } + + public boolean getAutoscrolls() { + return true; + } + + public Point getToolTipLocation(MouseEvent aEvent) { + return fTooltipPoint; + } + + public String getToolTipText(MouseEvent aEvent) { + TreeNode node = hitTestNode(aEvent.getY()); + if (node != null) { + Column column = hitTestColumn(aEvent.getX()); + + if (column != null) { + int indent = getIndent(column, node); + CellRenderer renderer = column.getCellRenderer(); + Component component = renderer.getComponent(); + Object data = fDataModel.getData(node.getData(), column.getID()); + renderer.setValue(node.getData(), data, false); + Dimension size = component.getSize(); + if (size.width + indent > column.getWidth()) { + fTooltipPoint = + new Point(getColumnX(column) + indent, getNodeY(node)); + return data.toString(); + } + } + } + fTooltipPoint = null; + return null; + } + + // + // Scrollable interface + // + + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + public int getScrollableBlockIncrement(Rectangle aVisible, int aOrient, + int aDirection) { + if (aOrient == SwingConstants.VERTICAL) { + return aVisible.height; + } else { + return aVisible.width; + } + } + + public int getScrollableUnitIncrement(Rectangle aVisible, int aOrient, + int aDirection) { + if (aOrient == SwingConstants.VERTICAL) { + if (fRowHeight != 0) { + return fRowHeight + fRowMargin; + } + } + return 10; + } + + public boolean getScrollableTracksViewportHeight() { + return false; + } + + public boolean getScrollableTracksViewportWidth() { + return false; + } + + // + // DropTargetComponentInterface + // + + public void setDropTarget(DropTarget dt) throws IllegalArgumentException { + fDropTarget = dt; + } + + public DropTarget getDropTarget() { + return fDropTarget; + } + + // + // TreeMouseListener class + // + + class TreeMouseListener extends MouseAdapter implements MouseMotionListener { + + boolean isContextClick(MouseEvent aEvent) { + return (aEvent.isPopupTrigger() || + (aEvent.getModifiers() & MouseEvent.BUTTON3_MASK) != 0); + } + + MouseEvent convertMouseEvent(MouseEvent aEvent) { + return SwingUtilities.convertMouseEvent(fTreeTable, aEvent, + fEditor.getCellEditorComponent()); + } + + void dispatchMouseEvent(MouseEvent aEvent) { + fEditor.getCellEditorComponent().dispatchEvent(convertMouseEvent(aEvent)); + } + + void checkEditor(MouseEvent aEvent, Column column, TreeNode node) { + fEditor = column != null ? column.getCellEditor() : null; + fEditing = false; + + if (fEditor != null) { + TreePath path = buildTreePath(node); + fEditor.setValue(node.getData(), + fDataModel.getData(node.getData(),column.getID()), + fSelection.isSelected(path)); + if (fEditor.isCellEditable(aEvent)) { + fEditedColumn = column; + fEditedNode = node; + fEditor.addCellEditorListener(new TreeCellEditorListener()); + Component editorComponent = fEditor.getCellEditorComponent(); + Dimension preferredSize = editorComponent.getPreferredSize(); + add(editorComponent); + int x = getColumnX(column); + int w = column.getWidth(); + int indent = getIndent(column, node); + x += indent; + w -= indent; + + int y = getNodeY(path); + int h = getNodeHeight(path); + editorComponent.setLocation(x, y); + editorComponent.setSize(w, h); + + fEditing = true; + fEditor.startCellEditing(convertMouseEvent(aEvent)); + } + } + } + + public void mousePressed(MouseEvent aEvent) { + fJustSelected = false; + fMousinessAfoot = true; + fHitPoint = new Point(aEvent.getPoint()); + fDragging = false; + + TreeNode node = hitTestNode(aEvent.getY()); + if (node != null) { + Column column = hitTestColumn(aEvent.getX()); + + if (column != null && !isContextClick(aEvent)) { + if (column.getID().equals(fTreeColumn)) { + int plusX = node.getVisibleDepth() * fIndentLevel; + + if (fHitX > plusX && fHitX < (plusX + fIndentLevel)) { + if (node.isCollapsed()) { + expand(node); + } else { + collapse(node); + } + requestFocus(); + fJustSelected = true; // Pretend we did a selection to + // Avoid triggering editor + return; // Don't do selection + } + } + } + + if (fEditing) { + if (fEditedColumn == column && fEditedNode == node) { + dispatchMouseEvent(aEvent); + return; + } else { + fEditor.cancelCellEditing(); + fEditing = false; + } + } + + checkEditor(aEvent, column, node); + + if (!fEditing) { + requestFocus(); + setCaret(node); + select(buildTreePath(fCaret), aEvent.getModifiers()); + } + } else { + requestFocus(); + } + } + + public void mouseReleased(MouseEvent aEvent) { + fDragging = false; + if (fEditing) { + dispatchMouseEvent(aEvent); + } else if (isContextClick(aEvent)) { + fSelection.contextClickSelection(aEvent); + } else if (!fJustSelected) { + TreeNode node = hitTestNode(aEvent.getY()); + if (node != null) { + Column column = hitTestColumn(aEvent.getX()); + checkEditor(aEvent, column, node); + } + } + fMousinessAfoot = false; + fJustSelected = false; + fHitPoint = null; + } + + public void mouseClicked(MouseEvent aEvent) { + if (fEditing) { + dispatchMouseEvent(aEvent); + } else if (aEvent.getClickCount() == 2) { + fSelection.doubleClickSelection(aEvent); + } + } + + public void mouseMoved(MouseEvent aEvent) { + if (fEditing) { + dispatchMouseEvent(aEvent); + } + } + + public void mouseDragged(MouseEvent aEvent) { + if (fEditing) { + dispatchMouseEvent(aEvent); + } else if (!fDragging && fHitPoint != null) { + Rectangle box = + DragSource.getDragSource(fTreeTable).getDragThresholdBBox(fTreeTable, + fHitPoint); + + if (!box.contains(aEvent.getPoint())) { + fSelection.dragSelection(aEvent); + } + fDragging = true; + } + } + } + + // + // TreeCellEditorListener class + // + + class TreeCellEditorListener implements CellEditorListener { + public void editingStarted(ChangeEvent aEvent) { + fEditing = true; + } + + public void editingStopped(ChangeEvent aEvent) { + fEditing = false; + fDataModel.setData(fEditedNode.getData(), fEditedColumn.getID(), fEditor.getCellEditorValue()); + Rectangle edRect = fEditor.getCellEditorComponent().getBounds(); + remove(fEditor.getCellEditorComponent()); + repaint(edRect); + fEditor.removeCellEditorListener(this); + } + + public void editingCanceled(ChangeEvent aEvent) { + fEditing = false; + Rectangle edRect = fEditor.getCellEditorComponent().getBounds(); + remove(fEditor.getCellEditorComponent()); + repaint(edRect); + fEditor.removeCellEditorListener(this); + } + } + + // + // TreeColumnModelListener Class + // + + class TreeColumnModelListener implements ColumnModelListener { + public void columnAdded(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnRemoved(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnMoved(ColumnModelEvent e) { + repaint(); + } + + public void columnMarginChanged(ChangeEvent e) { + resizeAndRepaint(); + } + + public void columnWidthChanged(ColumnModelEvent e) { + resizeAndRepaint(); + } + + public void columnSelectionChanged(ChangeEvent e) { + } + } + + class TreeModelListener implements TreeTableModelListener { + public void nodeExpanded(TreeTableModelEvent aEvent) { + synchronized (fTreeTable) { + // We take the leap of faith here that we're not + // expanding an already expanded node + TreeNode node = findTreeNode(aEvent.getPath()); + fHeight += node.expand(); + } + resizeAndRepaintFrom(aEvent.getPath()); + } + + public void nodeCollapsed(TreeTableModelEvent aEvent) { + synchronized (fTreeTable) { + // We take the leap of faith here that we're not + // collapsing an already collapsed node + TreeNode node = findTreeNode(aEvent.getPath()); + + if (fCaret != null && fCaret.isChildOf(node)) { + if (fSelection.isSelected(buildTreePath(fCaret))) { + select(buildTreePath(node), 0); + } + setCaret(node); + } + + fHeight += node.collapse(); + } + + resizeAndRepaintFrom(aEvent.getPath()); + } + + public void nodeInserted(TreeTableModelEvent aEvent) { + synchronized (fTreeTable) { + TreePath path = aEvent.getPath(); + if (path.getPath().length == 0) { // something at the root changed + reload(); + resizeAndRepaint(); + } else { + TreeNode node = findTreeNode(path); + if (node != null) { // visible node changed + node.reload(); // Redo everything for now + resizeAndRepaintFrom(aEvent.getPath()); + } + } + } + } + + public void nodeDeleted(TreeTableModelEvent aEvent) { + synchronized (fTreeTable) { + TreePath path = aEvent.getPath(); + if (path.getPath().length == 0) { // something at the root changed + reload(); + resizeAndRepaint(); + } else { + TreeNode node = findTreeNode(path); + if (node != null) { // visible node changed + node.reload(); // Redo everything for now + resizeAndRepaintFrom(aEvent.getPath()); + } + } + } + } + + public void nodeChanged(TreeTableModelEvent aEvent) { + repaint(aEvent.getPath()); + } + } + + // + // TreeSelectionListener class + // + + class TreeSelectionListener implements SelectionListener { + public void selectionChanged(SelectionEvent aEvent) { +// repaint(); + int i; + Object added[] = aEvent.getAdded(); + Object removed[] = aEvent.getRemoved(); + + if (added != null) { + if (fMousinessAfoot) + fJustSelected = true; + + for (i = 0; i < added.length; i++) { + repaint((TreePath) added[i]); + } + } + if (removed != null) { + for (i = 0; i < removed.length; i++) { + repaint((TreePath) removed[i]); + } + } + } + + public void selectionDoubleClicked(MouseEvent aEvent) { + } + public void selectionContextClicked(MouseEvent aEvent) { + } + public void selectionDragged(MouseEvent aEvent) { + } + } + + // + // TreeFocusListener class + // + + class TreeFocusListener implements FocusListener { + public void focusGained(FocusEvent aEvent) { + repaint(); + } + public void focusLost(FocusEvent aEvent) { + repaint(); + } + } + + // + // ActionListener Classes + // + + class NavigateUpAction implements ActionListener { + int fModifiers; + + NavigateUpAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateUp(fModifiers); + } + } + + class NavigateDownAction implements ActionListener { + int fModifiers; + + NavigateDownAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateDown(fModifiers); + } + } + + class NavigateLeftAction implements ActionListener { + int fModifiers; + + NavigateLeftAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateLeft(fModifiers); + } + } + + class NavigateRightAction implements ActionListener { + int fModifiers; + + NavigateRightAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateRight(fModifiers); + } + } + + class NavigateHomeAction implements ActionListener { + int fModifiers; + + NavigateHomeAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateHome(fModifiers); + } + } + + class NavigateEndAction implements ActionListener { + int fModifiers; + + NavigateEndAction(int aModifiers) { + fModifiers = aModifiers; + } + + public void actionPerformed(ActionEvent aEvent) { + navigateEnd(fModifiers); + } + } + + class OpenAction implements ActionListener { + public void actionPerformed(ActionEvent aEvent) { + fSelection.doubleClickSelection(null); + } + } + + // + // TreeNode Class + // + + class TreeNode { + Object fData; + + TreeNode fParent; + TreeNode fFirstChild; + TreeNode fNextSibling; + + boolean fBuilt = false; + boolean fFreeOnCollapse = true; + boolean fInvalid = true; + boolean fIsLeaf = false; + int fChildrenHeight = 0; + int fDepth = -1; + int fNodeHeight = 0; + + // + // Constructor + // + + public TreeNode(TreeTableDataModel aDataModel, TreeNode aParent, Object aData) { + fDataModel = aDataModel; + fData = aData; + fParent = aParent; + fIsLeaf = aDataModel.isLeaf(fData); + + if (!fIsLeaf && !isCollapsed()) { + expand(); + } + } + + // + // Building + // + + final void addChild(TreeNode aNode) { + addChildAfter(getLastChild(), aNode); + } + + final void addChildAfter(TreeNode aPrev, TreeNode aNode) { + aNode.fParent = this; + + if (aPrev == null) { // add at beginning + aNode.fNextSibling = fFirstChild; + fFirstChild = aNode; + } else { + aPrev.addSiblingAfter(aNode); + } + } + + final void addSiblingAfter(TreeNode aNode) { + aNode.fNextSibling = fNextSibling; + fNextSibling = aNode; + } + + // + // Unbuilding + // + + final int remove() { + if (fParent != null) { + fParent.removeChild(this); + } else { + TreeNode prev = getPrevSibling(); + prev.fNextSibling = fNextSibling; + } + return -getHeight(); + } + + final int removeChild(TreeNode aNode) { + if (fFirstChild == aNode && aNode != null) { + fFirstChild = aNode.fNextSibling; + } + aNode.fParent = null; + return aNode.remove(); + } + + /** + * Make garbage collection possible + */ + + final void detachChildren() { + TreeNode node = fFirstChild; + while (node != null) { + TreeNode next = node.fNextSibling; + node.detachSelf(); + node = next; + } + fFirstChild = null; + } + + final void detachSelf() { + detachChildren(); + fParent = null; + fNextSibling = null; + } + + void invalidateBranch() { + TreeNode node = this; + while (node != null) { + fInvalid = true; + node = node.getParent(); + } + } + + public final int collapse() { + int res = -getChildrenHeight(); + + if (fFreeOnCollapse) { + detachChildren(); + fBuilt = false; + } + invalidateBranch(); + + return res; + } + + public final int expand() { + if (!fBuilt) { + build(); + } + + invalidateBranch(); + + return getChildrenHeight(); + } + + public final int reload() { + fIsLeaf = fDataModel.isLeaf(fData); + + if (fIsLeaf || isCollapsed()) { + return 0; + } + int oldChildHeight = getChildrenHeight(); + + detachChildren(); + build(); + + invalidateBranch(); + + return getChildrenHeight() - oldChildHeight; + } + + final void build() { + if (!fIsLeaf) { + Enumeration children = fDataModel.getChildren(fData); + if (children == null) { + Object node = fDataModel.getChild(fData); + while (node != null) { + addChild(new TreeNode(fDataModel, this, node)); + node = fDataModel.getNextSibling(node); + } + } else { + while (children.hasMoreElements()) { + addChild(new TreeNode(fDataModel, this, children.nextElement())); + } + } + } + } + + public final boolean isCollapsed() { + return fDataModel.isCollapsed(buildTreePath(this)); + } + + public final boolean isLeaf() { + return fIsLeaf; + } + + public final int getVisibleDepth() { + return fDataModel.showRoot() ? getDepth() : getDepth() - 1; + } + + public final int getDepth() { + if (fDepth < 0) { + if (getParent() != null) { + fDepth = getParent().getDepth() + 1; + } else { + fDepth = 0; + } + } + return fDepth; + } + + public final TreeNode getParent() { + return fParent; + } + + public final TreeNode getPrevSibling() { + TreeNode node = (fParent != null) ? fParent.getFirstChild() : fRoot; + while (node != null) { + if (node.fNextSibling == this) { + return node; + } + node = node.fNextSibling; + } + return null; + } + + public final TreeNode getNextSibling() { + return fNextSibling; + } + + public final TreeNode getFirstChild() { + return fFirstChild; + } + + public final TreeNode getLastChild() { + TreeNode node = fFirstChild; + while (node != null && node.fNextSibling != null) { + node = node.fNextSibling; + } + return node; + } + + public final boolean isChildOf(TreeNode aNode) { + TreeNode parent = fParent; + while (parent != null) { + if (parent == aNode) { + return true; + } + parent = parent.fParent; + } + return false; + } + + public final Object getData() { + return fData; + } + + public final int getNodeHeight() { + if (fRowHeight != 0) { + return fRowHeight; + } + if (fNodeHeight == 0) { + Enumeration columns = fColumnModel.getColumns(); + while (columns.hasMoreElements()) { + Column column = (Column) columns.nextElement(); + CellRenderer renderer = column.getCellRenderer(); + + renderer.setValue(fData, + fDataModel.getData(fData, column.getID()), + false); + + Component component = renderer.getComponent(); + Dimension size = component.getPreferredSize(); + if (size.height > fNodeHeight) { + fNodeHeight = size.height; + } + } + Icon icon = fDataModel.getIcon(fData); + if (icon != null && icon.getIconHeight() > fNodeHeight) { + fNodeHeight = icon.getIconHeight(); + } + } + + // If we're fixed, we can avoid this calculation + if (fFixed) { + fRowHeight = fNodeHeight; + } + + return fNodeHeight; + } + + public final int getChildrenHeight() { + if (fInvalid) { + fChildrenHeight = 0; + TreeNode node = getFirstChild(); + while (node != null) { + fChildrenHeight += node.getTotalHeight(); + node = node.getNextSibling(); + } + } + return fChildrenHeight; + } + + public final int getTotalHeight() { + return getNodeHeight() + (isCollapsed() ? 0 : getChildrenHeight()); + } + } + +} diff --git a/mozilla/grendel/widgets/TreeTableDataModel.java b/mozilla/grendel/widgets/TreeTableDataModel.java new file mode 100644 index 00000000000..828caa87d04 --- /dev/null +++ b/mozilla/grendel/widgets/TreeTableDataModel.java @@ -0,0 +1,205 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.Enumeration; + +import com.sun.java.swing.Icon; + +/** + * The TreeTableDataModel interface is used to mediate + * between the underlying data store and the TreeTable. The interface + * supports two major mechanisms for retrieving tree data: as enumerations + * or by getting the first child/getting the next sibling of a node. + * The model only needs to implement one. After initially setting the data + * model for the TreeTable, the model should notify listeners + * of changes using the TreeTableModelListener interface. + * + * @author Will Scullin + * + * @see TreeTable + * @see TreeTableModelListener + */ + +public interface TreeTableDataModel { + + /** + * Indicates whether the root node is visible. An invisible + * root node should not have siblings. + * + * @return true if the root node should be displayed. + */ + + public boolean showRoot(); + + /** + * Retrieves the children of a given node. If aNode + * is null then the enumeration returned should represent + * the nodes at the root level. An implementation that supports this + * method does not need to implement the getRoot(), + * getChild() or getNextSibling() methods + * (they can return null). + * + * @return An enumeration of objects representing the children of the + * given node. + * + * @see #getRoot + * @see #getChild + * @see #getNextSibling + */ + + public Enumeration getChildren(Object aNode); + + /** + * Retrieves the root node of the tree. An implementation that + * supports this method should implement the getChild() and + * getNextSibling() methods and does not need to implement + * the getChildren() method (it can return + * null). + * + * @return An object representing the root of the data model being + * displayed. + * + * @see #getChildren + * @see #getChild + * @see #getNextSibling + */ + + public Object getRoot(); + + /** + * Retrieves the first child of the given node. Should return + * null if the node has no children. An implementation that + * supports this method should implement the getRoot() and + * getNextSibling() methods and does not need to implement + * the getChildren() method (it can return + * null). + * + * @return An object representing the first child of the given node. + * + * @see #getChildren + * @see #getRoot + * @see #getNextSibling + */ + + public Object getChild(Object aNode); + + /** + * Retrieves the next sibling of the given node. Should return + * null if the node has no next sibling. An implementation + * thatsupports this method should implement the getChild() + * and getNextSibling() methods and does not need to + * implement the getChildren() method (it can return + * null). + * + * @return An object representing the next sibing of the given node. + * + * @see #getChildren + * @see #getRoot + * @see #getChild + */ + + public Object getNextSibling(Object aNode); + + /** + * Indicates whether the given node is a leaf. The values + * returned by isLeaf() and getChild() + * or getChildren() should be consistant. + * + * @return true if the given node is a leaf. + */ + + public boolean isLeaf(Object aNode); + + /** + * Indicates whether the given node is collapsed. + * + * @return true if the given node is collapsed. + */ + + public boolean isCollapsed(TreePath aPath); + + /** + * Method to set the collapsed state of a node. + */ + + public void setCollapsed(TreePath aPath, boolean aCollapsed); + + /** + * Retrieves the data for the given node at the column with the given id. + * The data will be passed on to the CellRenderer of the column + * for rendering. + * + * @return An Object representing the data at the + * given column for the given node. + * + * @see CellRenderer + * @see Column + */ + + public Object getData(Object aNode, Object aID); + + /** + * Method for setting the data for the given node at the column with the + * given id. The data is retrieved from the CellEditor of the + * column. + * + * @see CellEditor + * @see Column + */ + + public void setData(Object aNode, Object aID, Object aValue); + + /** + * Retrieves an icon representing the given node. The return value + * can be null if no icon is to be drawn. + * + * @return An Icon representing the given node. + */ + + public Icon getIcon(Object aNode); + + /** + * Retrieves an icon representing a modified state for a given node. + * The return value can be null if no overlay icon is to + * be drawn. The overlay icon should be transparent and the same size + * as the returned icon. + * + * @return An Icon representing the given node state. + * + * @see #getIcon + */ + + public Icon getOverlayIcon(Object aNode); + + /** + * Adds a TreeTableModelListener. + */ + + public void addTreeTableModelListener(TreeTableModelListener aListener); + + /** + * Removes a TreeTableModelListener. + */ + + public void removeTreeTableModelListener(TreeTableModelListener aListener); +} diff --git a/mozilla/grendel/widgets/TreeTableModelBroadcaster.java b/mozilla/grendel/widgets/TreeTableModelBroadcaster.java new file mode 100644 index 00000000000..16879c5e986 --- /dev/null +++ b/mozilla/grendel/widgets/TreeTableModelBroadcaster.java @@ -0,0 +1,85 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 18 Sep 1997. + */ + +package grendel.widgets; + +public class TreeTableModelBroadcaster implements TreeTableModelListener { + TreeTableModelListener a, b; + + public TreeTableModelBroadcaster(TreeTableModelListener a, TreeTableModelListener b) { + this.a = a; + this.b = b; + } + + public static TreeTableModelListener add(TreeTableModelListener a, TreeTableModelListener b) { + if (a == null) return b; + if (b == null) return a; + + return new TreeTableModelBroadcaster(a, b); + } + + public static TreeTableModelListener remove(TreeTableModelListener a, TreeTableModelListener b) { + if (a == b || a == null) { + return null; + } else if (a instanceof TreeTableModelBroadcaster) { + return ((TreeTableModelBroadcaster)a).remove(b); + } else { + return a; // it's not here + } + } + + public TreeTableModelListener remove(TreeTableModelListener c) { + if (c == a) return b; + if (c == b) return a; + + TreeTableModelListener a2 = remove(a, c); + TreeTableModelListener b2 = remove(b, c); + if (a2 == a && b2 == b) { + return this; // it's not here + } + return add(a2, b2); + } + + public void nodeExpanded(TreeTableModelEvent aEvent) { + a.nodeExpanded(aEvent); + b.nodeExpanded(aEvent); + } + + public void nodeCollapsed(TreeTableModelEvent aEvent) { + a.nodeCollapsed(aEvent); + b.nodeCollapsed(aEvent); + } + + public void nodeInserted(TreeTableModelEvent aEvent) { + a.nodeInserted(aEvent); + b.nodeInserted(aEvent); + } + + public void nodeDeleted(TreeTableModelEvent aEvent) { + a.nodeDeleted(aEvent); + b.nodeDeleted(aEvent); + } + + public void nodeChanged(TreeTableModelEvent aEvent) { + a.nodeChanged(aEvent); + b.nodeChanged(aEvent); + } +} diff --git a/mozilla/grendel/widgets/TreeTableModelEvent.java b/mozilla/grendel/widgets/TreeTableModelEvent.java new file mode 100644 index 00000000000..072ce2e9b28 --- /dev/null +++ b/mozilla/grendel/widgets/TreeTableModelEvent.java @@ -0,0 +1,72 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 17 Sep 1997. + */ + +package grendel.widgets; + +import java.util.EventObject; + +/** + * A TreeTableDataModel event. Used for reporting which node and possibly + * which child nodes were involved in an event. + * + * @see TreeTableDataModel + * @see TreeTableModelListener + */ + +public class TreeTableModelEvent extends EventObject { + TreePath fPath; + Object fChildren[]; + + /** + * Constructor for event with a TreePath. + */ + + public TreeTableModelEvent(Object aSource, TreePath aPath) { + this(aSource, aPath, null); + } + + /** + * Constructor for event with a TreePath and children. + */ + + public TreeTableModelEvent(Object aSource, TreePath aPath, + Object aChildren[]) { + super(aSource); + fPath = aPath; + fChildren = aChildren; + } + + /** + * @return The TreePath associated with this event + */ + + public TreePath getPath() { + return fPath; + } + + /** + * @return The node children associated with this event. Can be null. + */ + + public Object[] getChildren() { + return fChildren; + } +} diff --git a/mozilla/grendel/widgets/TreeTableModelListener.java b/mozilla/grendel/widgets/TreeTableModelListener.java new file mode 100644 index 00000000000..178c088b6d5 --- /dev/null +++ b/mozilla/grendel/widgets/TreeTableModelListener.java @@ -0,0 +1,32 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 21 Aug 1997. + */ + +package grendel.widgets; + +import java.util.EventListener; + +public interface TreeTableModelListener extends EventListener { + public void nodeExpanded(TreeTableModelEvent aEvent); + public void nodeCollapsed(TreeTableModelEvent aEvent); + public void nodeInserted(TreeTableModelEvent aEvent); + public void nodeDeleted(TreeTableModelEvent aEvent); + public void nodeChanged(TreeTableModelEvent aEvent); +} diff --git a/mozilla/grendel/widgets/Util.java b/mozilla/grendel/widgets/Util.java new file mode 100644 index 00000000000..5930af9e728 --- /dev/null +++ b/mozilla/grendel/widgets/Util.java @@ -0,0 +1,298 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * The contents of this file are subject to the Mozilla Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See + * the License for the specific language governing rights and limitations + * under the License. + * + * The Original Code is the Grendel mail/news client. + * + * The Initial Developer of the Original Code is Netscape Communications + * Corporation. Portions created by Netscape are Copyright (C) 1997 + * Netscape Communications Corporation. All Rights Reserved. + * + * Created: Will Scullin , 9 Sep 1997. + */ + +package grendel.widgets; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Frame; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.io.UnsupportedEncodingException; +import java.util.Vector; + +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.MimeUtility; + +import netscape.orion.toolbars.NSButton; +import netscape.orion.toolbars.NSToolbar; +import netscape.orion.uimanager.AbstractUICmd; +import netscape.orion.uimanager.IUICmd; + +import com.sun.java.swing.JComponent; +import com.sun.java.swing.JPopupMenu; +import com.sun.java.swing.JScrollBar; +import com.sun.java.swing.JScrollPane; +import com.sun.java.swing.KeyStroke; + +public class Util { + public static final int LEFT = 0; + public static final int CENTER = 0; + public static final int RIGHT = 0; + + // Recycled character array + private static char fChars[] = new char[128]; + + public static void DrawTrimmedString(Graphics g, String aString, + FontMetrics aFM, int aWhere, + int x, int y, int aWidth) { + int w = aFM.stringWidth(aString); + if (w <= aWidth) { + g.drawString(aString, x, y); + return; + } + + int i; + int first = 0; + int length = aString.length(); + if (length > fChars.length) { + fChars = new char[length]; + } + aString.getChars(0, length, fChars, 0); + + if (aWhere == RIGHT) { + fChars[length - 3] = '.'; + fChars[length - 2] = '.'; + fChars[length - 1] = '.'; + } else { // Only left cut for now + fChars[0] = '.'; + fChars[1] = '.'; + fChars[2] = '.'; + } + + w = aFM.charsWidth(fChars, first, length); + + if (aWhere == RIGHT) { + while (w > aWidth) { + length--; + w -= aFM.charWidth(fChars[length - 3]); + fChars[length - 3] = '.'; + } + } else { // Only left cut for now + while (w > aWidth) { + length--; + first++; + w -= aFM.charWidth(fChars[first + 2]); + fChars[first + 2] = '.'; + } + } + g.drawChars(fChars, first, length, x, y); + } + + static IUICmd FindAction(Vector aVector, String aAction) { + for (int i = 0; i < aVector.size(); i++) { + IUICmd action = (IUICmd) aVector.elementAt(i); + if (action.getText(IUICmd.NAME).equals(aAction)) { + return action; + } + } + return null; + } + + static public IUICmd[] MergeActions(IUICmd aActions1[], IUICmd aActions2[]) { + Vector resVector = new Vector(); + int i; + if (aActions1 != null) { + for (i = 0; i < aActions1.length; i++) { + resVector.addElement(aActions1[i]); + } + } + if (aActions2 != null) { + for (i = 0; i < aActions2.length; i++) { + if (FindAction(resVector, aActions2[i].getText(IUICmd.NAME)) == null) { + resVector.addElement(aActions2[i]); + } + } + } + IUICmd res[] = new IUICmd[resVector.size()]; + resVector.copyInto(res); + return res; + } + + static public NSToolbar MergeToolBars(NSToolbar aBar1, NSToolbar aBar2) { + NSToolbar res = new NSToolbar(); + int count1 = aBar1.getItemCount(); + int count2 = aBar2.getItemCount(); + int i = 0, j = 0, k, l; + + while (i < count1) { + NSButton button1 = (NSButton) aBar1.getItemAt(i); + if (j < count2) { + NSButton button2 = (NSButton) aBar2.getItemAt(j); + if (button1.getActionCommand().equals(button2.getActionCommand())) { + res.addItem(button1); + i++; + j++; + } else { + boolean merge = false; + for (k = j; k < count2; k++) { + button2 = (NSButton) aBar2.getItemAt(k); + if (button1.getActionCommand().equals(button2.getActionCommand())) { + merge = true; + while (j < k) { + NSButton button3 = (NSButton) aBar2.getItemAt(j); + res.addItem(button3); + j++; + } + break; + } + } + if (merge) { + res.addItem(button1); + j++; + } else { + res.addItem(button1); + } + i++; + } + } else { + res.addItem(button1); + i++; + } + } + + while (j < count2) { + NSButton button2 = (NSButton) aBar2.getItemAt(j); + res.addItem(button2); + j++; + } + + return res; + } + + public static void RegisterScrollingKeys(JScrollPane aScrollPane) { + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_UP), + KeyStroke.getKeyStroke(KeyEvent.VK_UP, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_DOWN), + KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_LEFT), + KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_RIGHT), + KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_PAGE_UP), + KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_PAGE_DOWN), + KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_HOME), + KeyStroke.getKeyStroke(KeyEvent.VK_HOME, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + aScrollPane.registerKeyboardAction(new ScrollAction(aScrollPane, KeyEvent.VK_END), + KeyStroke.getKeyStroke(KeyEvent.VK_END, + KeyEvent.CTRL_MASK), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); + + } + + static public Frame GetParentFrame(Component aComponent) { + Component parent = aComponent.getParent(); + while (parent != null && !(parent instanceof Frame)) { + if (parent instanceof JPopupMenu) { + parent = ((JPopupMenu) parent).getInvoker(); + } else { + parent = parent.getParent(); + } + } + return (Frame) parent; + } + + static String GetSubject(Message aMessage) { + String result = ""; + try { + String rawvalue = null; + try { + if ((rawvalue = aMessage.getSubject()) != null) + result = MimeUtility.decodeText(rawvalue); + } catch (UnsupportedEncodingException e) { + System.err.println(e + ":" + rawvalue); + // Don't care + result = rawvalue; + } + } catch (MessagingException e) { + } + return result; + } +} + +class ScrollAction extends AbstractUICmd { + JScrollPane fScrollPane; + int fAction; + + ScrollAction(JScrollPane aScrollPane, int aAction) { + super(""); + fScrollPane = aScrollPane; + fAction = aAction; + } + + public void actionPerformed(ActionEvent aEvent) { + JScrollBar vScroll = fScrollPane.getVerticalScrollBar(); + JScrollBar hScroll = fScrollPane.getHorizontalScrollBar(); + + switch (fAction) { + case KeyEvent.VK_UP: + vScroll.setValue(vScroll.getValue() - vScroll.getUnitIncrement()); + break; + case KeyEvent.VK_DOWN: + vScroll.setValue(vScroll.getValue() + vScroll.getUnitIncrement()); + break; + case KeyEvent.VK_LEFT: + hScroll.setValue(hScroll.getValue() - hScroll.getUnitIncrement()); + break; + case KeyEvent.VK_RIGHT: + hScroll.setValue(hScroll.getValue() + hScroll.getUnitIncrement()); + break; + case KeyEvent.VK_PAGE_UP: + vScroll.setValue(vScroll.getValue() - vScroll.getVisibleAmount()); + break; + case KeyEvent.VK_PAGE_DOWN: + vScroll.setValue(vScroll.getValue() + vScroll.getVisibleAmount()); + break; + case KeyEvent.VK_HOME: + vScroll.setValue(vScroll.getMinimum()); + break; + case KeyEvent.VK_END: + vScroll.setValue(vScroll.getMaximum() - vScroll.getVisibleAmount()); + break; + } + } +} diff --git a/mozilla/grendel/widgets/images/README b/mozilla/grendel/widgets/images/README new file mode 100644 index 00000000000..fd9524084fe --- /dev/null +++ b/mozilla/grendel/widgets/images/README @@ -0,0 +1,16 @@ +# +# The contents of this directory are subject to the Mozilla Public +# License Version 1.0 (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License +# at http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# The Original Code is the Grendel mail/news client. +# +# The Initial Developer of the Original Code is Netscape Communications +# Corporation. Portions created by Netscape are Copyright (C) 1997 +# Netscape Communications Corporation. All Rights Reserved. diff --git a/mozilla/grendel/widgets/images/minus.gif b/mozilla/grendel/widgets/images/minus.gif new file mode 100644 index 0000000000000000000000000000000000000000..e4854c6172b3183a43390767de19303f3f2bf4e0 GIT binary patch literal 122 zcmZ?wbhEHb6krfw*vtR|4Pe{=rW$~}hK2(N4m310{0EBvXZR1MfHaT=1d2ad7#SFN z8FYY>KobNQ7+BOdob+71*CP5r_5K6Oef$iQ7VliRD5_PiW|N-iCF3anjm!TYSRf$G H$Y2csV-G6p literal 0 HcmV?d00001 diff --git a/mozilla/grendel/widgets/images/plus.gif b/mozilla/grendel/widgets/images/plus.gif new file mode 100644 index 0000000000000000000000000000000000000000..0cb61ce52637b6e08915aaba31b02abc14b0c078 GIT binary patch literal 123 zcmZ?wbhEHb6krfw*vtR|4Pe{=rW$~}hK2(N4m310{0EBvXZR1MfHaT=1d2ad7#SFN z8FYY>KobNQ7+5qmob+71*CP5r_5K6Oef$i43p4UGs;VAFEnRwg)i