From aedc34d55462a75e329bbf342251ff6504cd117e Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Sun, 19 May 2024 17:56:33 -0400 Subject: Initial import from SVN --- projects/net.wotonomy.all/.classpath | 5 + projects/net.wotonomy.all/.cvsignore | 1 + projects/net.wotonomy.all/.project | 23 + projects/net.wotonomy.all/pom.xml | 72 + projects/net.wotonomy.all/src/site/apt/index.apt | 9 + projects/net.wotonomy.all/src/site/apt/license.apt | 461 +++ projects/net.wotonomy.all/src/site/build.xml | 175 + projects/net.wotonomy.all/src/site/fml/faq.fml | 145 + .../src/site/resources/doc/control-guide.txt | 53 + .../src/site/resources/doc/ui-guide.html | 469 +++ .../src/site/resources/doc/ui-guide.txt | 341 ++ .../src/site/resources/images/standard-mvc.gif | Bin 0 -> 11657 bytes .../src/site/resources/images/wotonomy-er.gif | Bin 0 -> 8073 bytes .../src/site/resources/images/wotonomy-mvc.gif | Bin 0 -> 10332 bytes .../src/site/resources/index-old.html | 105 + projects/net.wotonomy.all/src/site/site.xml | 37 + projects/net.wotonomy.datastore/.classpath | 8 + projects/net.wotonomy.datastore/.cvsignore | 2 + projects/net.wotonomy.datastore/.project | 23 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../.settings/org.eclipse.jdt.ui.prefs | 3 + projects/net.wotonomy.datastore/pom.xml | 45 + .../java/net/wotonomy/datastore/DataIndex.java | 111 + .../main/java/net/wotonomy/datastore/DataKey.java | 142 + .../main/java/net/wotonomy/datastore/DataSoup.java | 257 ++ .../java/net/wotonomy/datastore/DataStore.java | 94 + .../main/java/net/wotonomy/datastore/DataView.java | 111 + .../net/wotonomy/datastore/DefaultComparator.java | 145 + .../net/wotonomy/datastore/DefaultDataIndex.java | 239 ++ .../net/wotonomy/datastore/DefaultDataView.java | 557 ++++ .../java/net/wotonomy/datastore/DuplicateList.java | 49 + .../main/java/net/wotonomy/datastore/FileSoup.java | 468 +++ .../net/wotonomy/datastore/SerializedFileSoup.java | 120 + .../wotonomy/datastore/UniquelyIdentifiable.java | 40 + .../java/net/wotonomy/datastore/XMLFileSoup.java | 130 + .../main/java/net/wotonomy/datastore/package.html | 19 + projects/net.wotonomy.foundation/.classpath | 9 + projects/net.wotonomy.foundation/.cvsignore | 2 + projects/net.wotonomy.foundation/.gitignore | 1 + projects/net.wotonomy.foundation/.project | 23 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../.settings/org.eclipse.jdt.ui.prefs | 3 + projects/net.wotonomy.foundation/pom.xml | 48 + .../main/java/net/wotonomy/foundation/NSArray.java | 661 ++++ .../java/net/wotonomy/foundation/NSBundle.java | 418 +++ .../main/java/net/wotonomy/foundation/NSCoder.java | 113 + .../java/net/wotonomy/foundation/NSCoding.java | 371 +++ .../java/net/wotonomy/foundation/NSComparator.java | 104 + .../main/java/net/wotonomy/foundation/NSData.java | 263 ++ .../main/java/net/wotonomy/foundation/NSDate.java | 217 ++ .../java/net/wotonomy/foundation/NSDictionary.java | 332 ++ .../java/net/wotonomy/foundation/NSDisposable.java | 35 + .../wotonomy/foundation/NSForwardException.java | 157 + .../net/wotonomy/foundation/NSKeyValueCoding.java | 431 +++ .../foundation/NSKeyValueCodingAdditions.java | 213 ++ .../foundation/NSKeyValueCodingSupport.java | 228 ++ .../main/java/net/wotonomy/foundation/NSLock.java | 108 + .../java/net/wotonomy/foundation/NSLocking.java | 59 + .../main/java/net/wotonomy/foundation/NSLog.java | 489 +++ .../net/wotonomy/foundation/NSMultiReaderLock.java | 159 + .../net/wotonomy/foundation/NSMutableArray.java | 429 +++ .../net/wotonomy/foundation/NSMutableData.java | 167 + .../wotonomy/foundation/NSMutableDictionary.java | 166 + .../net/wotonomy/foundation/NSMutableRange.java | 116 + .../net/wotonomy/foundation/NSNotification.java | 164 + .../wotonomy/foundation/NSNotificationCenter.java | 671 ++++ .../wotonomy/foundation/NSNotificationQueue.java | 345 ++ .../main/java/net/wotonomy/foundation/NSNull.java | 104 + .../net/wotonomy/foundation/NSNumberFormatter.java | 57 + .../foundation/NSPropertyListSerialization.java | 275 ++ .../main/java/net/wotonomy/foundation/NSRange.java | 351 ++ .../net/wotonomy/foundation/NSRecursiveLock.java | 148 + .../java/net/wotonomy/foundation/NSRunLoop.java | 522 +++ .../java/net/wotonomy/foundation/NSSelector.java | 391 +++ .../main/java/net/wotonomy/foundation/NSSet.java | 212 ++ .../java/net/wotonomy/foundation/NSTimeZone.java | 272 ++ .../java/net/wotonomy/foundation/NSTimestamp.java | 285 ++ .../wotonomy/foundation/NSTimestampFormatter.java | 65 + .../wotonomy/foundation/internal/Duplicator.java | 299 ++ .../wotonomy/foundation/internal/Introspector.java | 941 ++++++ .../foundation/internal/IntrospectorException.java | 32 + .../internal/MissingPropertyException.java | 32 + .../foundation/internal/NetworkClassLoader.java | 368 +++ .../internal/NullPrimitiveException.java | 32 + .../foundation/internal/PropertyComparator.java | 100 + .../foundation/internal/PropertyListParser.java | 546 +++ .../net/wotonomy/foundation/internal/QueueMap.java | 538 +++ .../foundation/internal/URLResourceReader.java | 207 ++ .../foundation/internal/ValueConverter.java | 718 ++++ .../foundation/internal/WotonomyException.java | 134 + .../main/java/net/wotonomy/foundation/package.html | 18 + .../net/wotonomy/foundation/xml/XMLDecoder.java | 68 + .../net/wotonomy/foundation/xml/XMLEncoder.java | 61 + .../src/test/java/AllTests.java | 17 + .../src/test/java/TestBundle.java | 21 + .../java/net/wotonomy/foundation/AllTests.java | 17 + .../java/net/wotonomy/foundation/NSArrayTest.java | 638 ++++ .../java/net/wotonomy/foundation/NSBundleTest.java | 179 + .../src/test/resources/README.txt | 0 .../.classpath | 7 + .../.cvsignore | 2 + .../net.wotonomy.persistence.adapter.jdbc/.project | 23 + .../net.wotonomy.persistence.adapter.jdbc/pom.xml | 45 + .../java/net/wotonomy/jdbcadaptor/JDBCAdaptor.java | 123 + .../wotonomy/jdbcadaptor/JDBCAdaptorException.java | 68 + .../java/net/wotonomy/jdbcadaptor/JDBCChannel.java | 449 +++ .../java/net/wotonomy/jdbcadaptor/JDBCContext.java | 146 + .../net/wotonomy/jdbcadaptor/JDBCExpression.java | 73 + .../jdbcadaptor/JDBCExpressionFactory.java | 65 + projects/net.wotonomy.persistence/.classpath | 8 + projects/net.wotonomy.persistence/.cvsignore | 3 + projects/net.wotonomy.persistence/.project | 23 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../.settings/org.eclipse.jdt.ui.prefs | 3 + projects/net.wotonomy.persistence/pom.xml | 45 + .../wotonomy/access/EOAccessArrayFaultHandler.java | 71 + .../net/wotonomy/access/EOAccessFaultHandler.java | 66 + .../access/EOAccessGenericFaultHandler.java | 75 + .../java/net/wotonomy/access/EOAccessLock.java | 60 + .../main/java/net/wotonomy/access/EOAdaptor.java | 269 ++ .../java/net/wotonomy/access/EOAdaptorChannel.java | 244 ++ .../java/net/wotonomy/access/EOAdaptorContext.java | 118 + .../net/wotonomy/access/EOAdaptorOperation.java | 120 + .../main/java/net/wotonomy/access/EOAttribute.java | 381 +++ .../main/java/net/wotonomy/access/EODatabase.java | 267 ++ .../net/wotonomy/access/EODatabaseChannel.java | 138 + .../net/wotonomy/access/EODatabaseContext.java | 568 ++++ .../net/wotonomy/access/EODatabaseOperation.java | 48 + .../main/java/net/wotonomy/access/EOEntity.java | 637 ++++ .../wotonomy/access/EOEntityClassDescription.java | 185 ++ .../wotonomy/access/EOGeneralAdaptorException.java | 62 + .../src/main/java/net/wotonomy/access/EOJoin.java | 56 + .../src/main/java/net/wotonomy/access/EOModel.java | 396 +++ .../java/net/wotonomy/access/EOModelGroup.java | 198 ++ .../main/java/net/wotonomy/access/EOProperty.java | 46 + .../wotonomy/access/EOPropertyListEncoding.java | 53 + .../wotonomy/access/EOQualifierSQLGeneration.java | 241 ++ .../java/net/wotonomy/access/EORelationship.java | 319 ++ .../java/net/wotonomy/access/EOSQLExpression.java | 697 ++++ .../wotonomy/access/EOSQLExpressionFactory.java | 131 + .../net/wotonomy/access/EOStoredProcedure.java | 141 + .../net/wotonomy/control/AbstractObjectStore.java | 762 +++++ .../main/java/net/wotonomy/control/ArrayFault.java | 219 ++ .../java/net/wotonomy/control/ChildDataSource.java | 187 ++ .../java/net/wotonomy/control/EOAndQualifier.java | 167 + .../net/wotonomy/control/EOClassDescription.java | 601 ++++ .../wotonomy/control/EOCooperatingObjectStore.java | 82 + .../java/net/wotonomy/control/EOCustomObject.java | 673 ++++ .../java/net/wotonomy/control/EODataSource.java | 164 + .../net/wotonomy/control/EODatabaseDataSource.java | 339 ++ .../net/wotonomy/control/EODeferredFaulting.java | 48 + .../net/wotonomy/control/EODelayedObserver.java | 152 + .../wotonomy/control/EODelayedObserverQueue.java | 323 ++ .../net/wotonomy/control/EOEditingContext.java | 3247 ++++++++++++++++++ .../net/wotonomy/control/EOEnterpriseObject.java | 219 ++ .../java/net/wotonomy/control/EOFaultHandler.java | 102 + .../main/java/net/wotonomy/control/EOFaulting.java | 79 + .../net/wotonomy/control/EOFetchSpecification.java | 565 ++++ .../java/net/wotonomy/control/EOGenericRecord.java | 151 + .../main/java/net/wotonomy/control/EOGlobalID.java | 83 + .../wotonomy/control/EOIntegralKeyGlobalID.java | 72 + .../wotonomy/control/EOKeyComparisonQualifier.java | 151 + .../java/net/wotonomy/control/EOKeyGlobalID.java | 146 + .../net/wotonomy/control/EOKeyValueArchiver.java | 106 + .../net/wotonomy/control/EOKeyValueArchiving.java | 41 + .../net/wotonomy/control/EOKeyValueCoding.java | 129 + .../control/EOKeyValueCodingAdditions.java | 176 + .../wotonomy/control/EOKeyValueCodingSupport.java | 235 ++ .../net/wotonomy/control/EOKeyValueQualifier.java | 233 ++ .../net/wotonomy/control/EOKeyValueUnarchiver.java | 165 + .../java/net/wotonomy/control/EONotQualifier.java | 129 + .../java/net/wotonomy/control/EONullValue.java | 113 + .../java/net/wotonomy/control/EOObjectStore.java | 320 ++ .../wotonomy/control/EOObjectStoreCoordinator.java | 204 ++ .../net/wotonomy/control/EOObserverCenter.java | 547 ++++ .../java/net/wotonomy/control/EOObserverProxy.java | 104 + .../java/net/wotonomy/control/EOObserving.java | 51 + .../java/net/wotonomy/control/EOOrQualifier.java | 169 + .../java/net/wotonomy/control/EOQualifier.java | 680 ++++ .../wotonomy/control/EOQualifierEvaluation.java | 42 + .../control/EORelationshipManipulation.java | 76 + .../java/net/wotonomy/control/EOSortOrdering.java | 406 +++ .../net/wotonomy/control/EOTemporaryGlobalID.java | 219 ++ .../java/net/wotonomy/control/EOValidation.java | 72 + .../net/wotonomy/control/EOVectorKeyGlobalID.java | 79 + .../java/net/wotonomy/control/EditingContext.java | 283 ++ .../wotonomy/control/KeyValueCodingUtilities.java | 740 +++++ .../java/net/wotonomy/control/ObservableArray.java | 346 ++ .../net/wotonomy/control/OrderedDataSource.java | 57 + .../net/wotonomy/control/PropertyDataSource.java | 549 ++++ .../net/wotonomy/control/internal/Surrogate.java | 259 ++ .../main/java/net/wotonomy/control/package.html | 6 + .../java/net/wotonomy/access/EOEntityTest.java | 14 + projects/net.wotonomy.test/.classpath | 7 + projects/net.wotonomy.test/.cvsignore | 1 + projects/net.wotonomy.test/.project | 23 + projects/net.wotonomy.test/pom.xml | 53 + .../java/net/wotonomy/test/BindingController.java | 35 + .../main/java/net/wotonomy/test/BindingPanel.java | 172 + .../src/main/java/net/wotonomy/test/DataKeyID.java | 88 + .../java/net/wotonomy/test/DataObjectStore.java | 483 +++ .../java/net/wotonomy/test/EditController.java | 214 ++ .../src/main/java/net/wotonomy/test/EditPanel.java | 61 + .../net/wotonomy/test/InspectorController.java | 137 + .../src/main/java/net/wotonomy/test/Test.java | 120 + .../java/net/wotonomy/test/TestController.java | 395 +++ .../java/net/wotonomy/test/TestDataSource.java | 115 + .../src/main/java/net/wotonomy/test/TestMap.java | 164 + .../main/java/net/wotonomy/test/TestObject.java | 362 ++ .../net/wotonomy/test/TestObjectClassDesc.java | 31 + .../java/net/wotonomy/test/TestObjectStore.java | 258 ++ .../src/main/java/net/wotonomy/test/TestPanel.java | 110 + .../java/net/wotonomy/test/TreeController.java | 285 ++ .../net/wotonomy/test/TreeInspectorController.java | 184 ++ .../src/main/java/net/wotonomy/test/TreePanel.java | 39 + projects/net.wotonomy.ui.swing/.classpath | 7 + projects/net.wotonomy.ui.swing/.cvsignore | 1 + projects/net.wotonomy.ui.swing/.project | 23 + projects/net.wotonomy.ui.swing/pom.xml | 45 + .../net/wotonomy/ui/swing/ActionAssociation.java | 335 ++ .../wotonomy/ui/swing/AdjustableAssociation.java | 327 ++ .../net/wotonomy/ui/swing/ButtonAssociation.java | 444 +++ .../net/wotonomy/ui/swing/ComboBoxAssociation.java | 700 ++++ .../net/wotonomy/ui/swing/DateAssociation.java | 613 ++++ .../ui/swing/DisplayGroupActionAssociation.java | 134 + .../wotonomy/ui/swing/DisplayGroupInspector.java | 120 + .../net/wotonomy/ui/swing/DisplayGroupNode.java | 1518 +++++++++ .../net/wotonomy/ui/swing/ListAssociation.java | 368 +++ .../wotonomy/ui/swing/MutableDisplayGroupNode.java | 216 ++ .../wotonomy/ui/swing/NotificationInspector.java | 333 ++ .../wotonomy/ui/swing/RadioPanelAssociation.java | 457 +++ .../net/wotonomy/ui/swing/ReferenceInspector.java | 283 ++ .../net/wotonomy/ui/swing/SliderAssociation.java | 419 +++ .../net/wotonomy/ui/swing/TableAssociation.java | 927 ++++++ .../wotonomy/ui/swing/TableColumnAssociation.java | 708 ++++ .../net/wotonomy/ui/swing/TextAssociation.java | 1212 +++++++ .../wotonomy/ui/swing/TimedTextAssociation.java | 1029 ++++++ .../net/wotonomy/ui/swing/TreeAssociation.java | 582 ++++ .../wotonomy/ui/swing/TreeColumnAssociation.java | 331 ++ .../wotonomy/ui/swing/TreeModelAssociation.java | 1751 ++++++++++ .../ui/swing/components/AbsoluteLayout.java | 74 + .../ui/swing/components/AlphaTextField.java | 335 ++ .../components/AlternatingRowCellRenderer.java | 129 + .../ui/swing/components/BetterFlowLayout.java | 515 +++ .../ui/swing/components/BetterRootLayout.java | 274 ++ .../ui/swing/components/BetterTableUI.java | 123 + .../wotonomy/ui/swing/components/ButtonPanel.java | 610 ++++ .../ui/swing/components/CheckButtonPanel.java | 272 ++ .../ui/swing/components/ColorCellEditor.java | 84 + .../ui/swing/components/ColorCellRenderer.java | 81 + .../ui/swing/components/ComboBoxCellRenderer.java | 57 + .../ui/swing/components/DateTextField.java | 630 ++++ .../ui/swing/components/FormattedCellRenderer.java | 284 ++ .../ui/swing/components/IconCellRenderer.java | 845 +++++ .../wotonomy/ui/swing/components/ImagePanel.java | 104 + .../wotonomy/ui/swing/components/InfoPanel.java | 1693 ++++++++++ .../ui/swing/components/KeyDelayTimer.java | 188 ++ .../ui/swing/components/KeyableCellEditor.java | 350 ++ .../ui/swing/components/LineWrappingRenderer.java | 154 + .../ui/swing/components/MultiLineLabel.java | 135 + .../ui/swing/components/NumericTextField.java | 434 +++ .../ui/swing/components/PropertyEditorTable.java | 572 ++++ .../swing/components/PropertyEditorTableModel.java | 418 +++ .../ui/swing/components/RadioButtonPanel.java | 174 + .../ui/swing/components/SmartPasswordField.java | 274 ++ .../ui/swing/components/SmartTextField.java | 244 ++ .../ui/swing/components/StatusButtonPanel.java | 276 ++ .../ui/swing/components/TintedImageFilter.java | 100 + .../wotonomy/ui/swing/components/TreeChooser.java | 727 ++++ .../ui/swing/components/TreeTableCellRenderer.java | 224 ++ .../net/wotonomy/ui/swing/components/package.html | 26 + .../main/java/net/wotonomy/ui/swing/package.html | 21 + .../net/wotonomy/ui/swing/util/ClassGrabber.java | 126 + .../ui/swing/util/ComponentHighlighter.java | 160 + .../net/wotonomy/ui/swing/util/GIFEncoder.java | 520 +++ .../wotonomy/ui/swing/util/ObjectInspector.java | 226 ++ .../wotonomy/ui/swing/util/PositionComparator.java | 89 + .../ui/swing/util/StackTraceInspector.java | 457 +++ .../ui/swing/util/TextInputRangeChecker.java | 368 +++ .../net/wotonomy/ui/swing/util/WindowGrabber.java | 203 ++ .../wotonomy/ui/swing/util/WindowUtilities.java | 521 +++ .../java/net/wotonomy/ui/swing/util/package.html | 6 + projects/net.wotonomy.ui/.classpath | 7 + projects/net.wotonomy.ui/.cvsignore | 2 + projects/net.wotonomy.ui/.project | 23 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../.settings/org.eclipse.jdt.ui.prefs | 3 + projects/net.wotonomy.ui/pom.xml | 45 + .../java/net/wotonomy/ui/DebuggingDelegate.java | 333 ++ .../main/java/net/wotonomy/ui/DelegateAdapter.java | 257 ++ .../main/java/net/wotonomy/ui/DisplayGroup.java | 300 ++ .../main/java/net/wotonomy/ui/EOAssociation.java | 565 ++++ .../main/java/net/wotonomy/ui/EODisplayGroup.java | 2359 +++++++++++++ .../java/net/wotonomy/ui/GenericAssociation.java | 373 +++ .../net/wotonomy/ui/MasterDetailAssociation.java | 406 +++ .../net/wotonomy/ui/MirrorDetailAssociation.java | 106 + .../main/java/net/wotonomy/ui/ObservableArray.java | 349 ++ .../src/main/java/net/wotonomy/ui/package.html | 14 + projects/net.wotonomy.web/.classpath | 8 + projects/net.wotonomy.web/.cvsignore | 2 + projects/net.wotonomy.web/.project | 23 + .../.settings/org.eclipse.jdt.core.prefs | 12 + .../.settings/org.eclipse.jdt.ui.prefs | 3 + projects/net.wotonomy.web/pom.xml | 70 + .../java/net/wotonomy/web/ObservableArray.java | 351 ++ .../src/main/java/net/wotonomy/web/URI.java | 3464 ++++++++++++++++++++ .../java/net/wotonomy/web/WOActionResults.java | 51 + .../main/java/net/wotonomy/web/WOActionURL.java | 57 + .../main/java/net/wotonomy/web/WOActiveImage.java | 81 + .../main/java/net/wotonomy/web/WOApplication.java | 1193 +++++++ .../main/java/net/wotonomy/web/WOAssociation.java | 168 + .../src/main/java/net/wotonomy/web/WOBody.java | 121 + .../src/main/java/net/wotonomy/web/WOCheckBox.java | 81 + .../main/java/net/wotonomy/web/WOComponent.java | 1312 ++++++++ .../java/net/wotonomy/web/WOComponentContent.java | 45 + .../wotonomy/web/WOComponentRequestHandler.java | 229 ++ .../main/java/net/wotonomy/web/WOConditional.java | 99 + .../src/main/java/net/wotonomy/web/WOContext.java | 572 ++++ .../src/main/java/net/wotonomy/web/WOCookie.java | 203 ++ .../main/java/net/wotonomy/web/WODirectAction.java | 317 ++ .../wotonomy/web/WODirectActionRequestHandler.java | 221 ++ .../main/java/net/wotonomy/web/WODisplayGroup.java | 2455 ++++++++++++++ .../java/net/wotonomy/web/WODynamicElement.java | 208 ++ .../src/main/java/net/wotonomy/web/WOElement.java | 97 + .../src/main/java/net/wotonomy/web/WOForm.java | 124 + .../src/main/java/net/wotonomy/web/WOFrame.java | 60 + .../java/net/wotonomy/web/WOGenericContainer.java | 61 + .../java/net/wotonomy/web/WOGenericElement.java | 95 + .../main/java/net/wotonomy/web/WOHiddenField.java | 42 + .../main/java/net/wotonomy/web/WOHyperlink.java | 249 ++ .../src/main/java/net/wotonomy/web/WOImage.java | 150 + .../main/java/net/wotonomy/web/WOImageButton.java | 58 + .../src/main/java/net/wotonomy/web/WOInput.java | 102 + .../main/java/net/wotonomy/web/WOMailDelivery.java | 54 + .../src/main/java/net/wotonomy/web/WOMessage.java | 333 ++ .../java/net/wotonomy/web/WOParentElement.java | 178 + .../java/net/wotonomy/web/WOPasswordField.java | 20 + .../main/java/net/wotonomy/web/WOPopUpButton.java | 135 + .../main/java/net/wotonomy/web/WORadioButton.java | 20 + .../main/java/net/wotonomy/web/WORepetition.java | 179 + .../src/main/java/net/wotonomy/web/WORequest.java | 586 ++++ .../java/net/wotonomy/web/WORequestHandler.java | 68 + .../main/java/net/wotonomy/web/WOResetButton.java | 50 + .../java/net/wotonomy/web/WOResourceManager.java | 489 +++ .../net/wotonomy/web/WOResourceRequestHandler.java | 122 + .../main/java/net/wotonomy/web/WOResourceURL.java | 31 + .../src/main/java/net/wotonomy/web/WOResponse.java | 184 ++ .../net/wotonomy/web/WOServletSessionStore.java | 188 ++ .../src/main/java/net/wotonomy/web/WOSession.java | 528 +++ .../main/java/net/wotonomy/web/WOSessionStore.java | 93 + .../java/net/wotonomy/web/WOStaticElement.java | 70 + .../src/main/java/net/wotonomy/web/WOString.java | 136 + .../main/java/net/wotonomy/web/WOSubmitButton.java | 70 + .../java/net/wotonomy/web/WOSwitchComponent.java | 112 + .../src/main/java/net/wotonomy/web/WOText.java | 72 + .../main/java/net/wotonomy/web/WOTextField.java | 59 + .../src/main/java/net/wotonomy/web/package.html | 11 + .../net/wotonomy/web/util/BrowserLauncher.java | 665 ++++ .../java/net/wotonomy/web/xml/XMLRPCDecoder.java | 114 + .../net/wotonomy/web/xml/XMLRPCDecoderHelper.java | 534 +++ .../java/net/wotonomy/web/xml/XMLRPCEncoder.java | 526 +++ .../java/net/wotonomy/web/xml/XMLRPCReceiver.java | 72 + .../java/net/wotonomy/web/xml/XMLRPCSelector.java | 238 ++ .../java/net/wotonomy/web/xml/XMLRPCServlet.java | 283 ++ .../main/java/net/wotonomy/web/xml/package.html | 30 + .../net/wotonomy/web/xml/XMLRPCSelectorTest.java | 83 + projects/pom.xml | 40 + 367 files changed, 93631 insertions(+) create mode 100644 projects/net.wotonomy.all/.classpath create mode 100644 projects/net.wotonomy.all/.cvsignore create mode 100644 projects/net.wotonomy.all/.project create mode 100644 projects/net.wotonomy.all/pom.xml create mode 100644 projects/net.wotonomy.all/src/site/apt/index.apt create mode 100644 projects/net.wotonomy.all/src/site/apt/license.apt create mode 100644 projects/net.wotonomy.all/src/site/build.xml create mode 100644 projects/net.wotonomy.all/src/site/fml/faq.fml create mode 100644 projects/net.wotonomy.all/src/site/resources/doc/control-guide.txt create mode 100644 projects/net.wotonomy.all/src/site/resources/doc/ui-guide.html create mode 100644 projects/net.wotonomy.all/src/site/resources/doc/ui-guide.txt create mode 100644 projects/net.wotonomy.all/src/site/resources/images/standard-mvc.gif create mode 100644 projects/net.wotonomy.all/src/site/resources/images/wotonomy-er.gif create mode 100644 projects/net.wotonomy.all/src/site/resources/images/wotonomy-mvc.gif create mode 100644 projects/net.wotonomy.all/src/site/resources/index-old.html create mode 100644 projects/net.wotonomy.all/src/site/site.xml create mode 100644 projects/net.wotonomy.datastore/.classpath create mode 100644 projects/net.wotonomy.datastore/.cvsignore create mode 100644 projects/net.wotonomy.datastore/.project create mode 100644 projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.core.prefs create mode 100644 projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.ui.prefs create mode 100644 projects/net.wotonomy.datastore/pom.xml create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataIndex.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataKey.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataSoup.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataStore.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataView.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultComparator.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataIndex.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataView.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DuplicateList.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/FileSoup.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/SerializedFileSoup.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/UniquelyIdentifiable.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/XMLFileSoup.java create mode 100644 projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/package.html create mode 100644 projects/net.wotonomy.foundation/.classpath create mode 100644 projects/net.wotonomy.foundation/.cvsignore create mode 100644 projects/net.wotonomy.foundation/.gitignore create mode 100644 projects/net.wotonomy.foundation/.project create mode 100644 projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.core.prefs create mode 100644 projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.ui.prefs create mode 100644 projects/net.wotonomy.foundation/pom.xml create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSArray.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSBundle.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoder.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoding.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSComparator.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDictionary.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDisposable.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSForwardException.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCoding.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingAdditions.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingSupport.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLock.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLocking.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLog.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMultiReaderLock.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableArray.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableData.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableDictionary.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableRange.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotification.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationCenter.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationQueue.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNull.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNumberFormatter.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRange.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRecursiveLock.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRunLoop.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSelector.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSet.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimeZone.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestamp.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestampFormatter.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Duplicator.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Introspector.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/IntrospectorException.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/MissingPropertyException.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NetworkClassLoader.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NullPrimitiveException.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyComparator.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyListParser.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/QueueMap.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/URLResourceReader.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ValueConverter.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/WotonomyException.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/package.html create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLDecoder.java create mode 100644 projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLEncoder.java create mode 100644 projects/net.wotonomy.foundation/src/test/java/AllTests.java create mode 100644 projects/net.wotonomy.foundation/src/test/java/TestBundle.java create mode 100644 projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/AllTests.java create mode 100644 projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSArrayTest.java create mode 100644 projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSBundleTest.java create mode 100644 projects/net.wotonomy.foundation/src/test/resources/README.txt create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/.classpath create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/.cvsignore create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/.project create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/pom.xml create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptor.java create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptorException.java create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCChannel.java create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCContext.java create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpression.java create mode 100644 projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpressionFactory.java create mode 100644 projects/net.wotonomy.persistence/.classpath create mode 100644 projects/net.wotonomy.persistence/.cvsignore create mode 100644 projects/net.wotonomy.persistence/.project create mode 100644 projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.core.prefs create mode 100644 projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.ui.prefs create mode 100644 projects/net.wotonomy.persistence/pom.xml create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessArrayFaultHandler.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessFaultHandler.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessGenericFaultHandler.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessLock.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptor.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorChannel.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorContext.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorOperation.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAttribute.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabase.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseChannel.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseContext.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseOperation.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntity.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntityClassDescription.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOGeneralAdaptorException.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOJoin.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModel.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModelGroup.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOProperty.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOPropertyListEncoding.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOQualifierSQLGeneration.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EORelationship.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpression.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpressionFactory.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOStoredProcedure.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ArrayFault.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ChildDataSource.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOAndQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOClassDescription.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCooperatingObjectStore.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCustomObject.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODataSource.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODatabaseDataSource.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODeferredFaulting.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserver.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserverQueue.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEditingContext.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEnterpriseObject.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaultHandler.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaulting.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFetchSpecification.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGenericRecord.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGlobalID.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOIntegralKeyGlobalID.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyComparisonQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyGlobalID.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiver.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiving.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCoding.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingAdditions.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingSupport.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueUnarchiver.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONotQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONullValue.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStore.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStoreCoordinator.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverCenter.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverProxy.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserving.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOOrQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifier.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifierEvaluation.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EORelationshipManipulation.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOSortOrdering.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOTemporaryGlobalID.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOValidation.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOVectorKeyGlobalID.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EditingContext.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ObservableArray.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/OrderedDataSource.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/PropertyDataSource.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/internal/Surrogate.java create mode 100644 projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/package.html create mode 100644 projects/net.wotonomy.persistence/src/test/java/net/wotonomy/access/EOEntityTest.java create mode 100644 projects/net.wotonomy.test/.classpath create mode 100644 projects/net.wotonomy.test/.cvsignore create mode 100644 projects/net.wotonomy.test/.project create mode 100644 projects/net.wotonomy.test/pom.xml create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingPanel.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataKeyID.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataObjectStore.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditPanel.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/InspectorController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/Test.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestDataSource.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestMap.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObject.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectClassDesc.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectStore.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestPanel.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeInspectorController.java create mode 100644 projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreePanel.java create mode 100644 projects/net.wotonomy.ui.swing/.classpath create mode 100644 projects/net.wotonomy.ui.swing/.cvsignore create mode 100644 projects/net.wotonomy.ui.swing/.project create mode 100644 projects/net.wotonomy.ui.swing/pom.xml create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java create mode 100644 projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html create mode 100644 projects/net.wotonomy.ui/.classpath create mode 100644 projects/net.wotonomy.ui/.cvsignore create mode 100644 projects/net.wotonomy.ui/.project create mode 100644 projects/net.wotonomy.ui/.settings/org.eclipse.jdt.core.prefs create mode 100644 projects/net.wotonomy.ui/.settings/org.eclipse.jdt.ui.prefs create mode 100644 projects/net.wotonomy.ui/pom.xml create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DebuggingDelegate.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DelegateAdapter.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DisplayGroup.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EOAssociation.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EODisplayGroup.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/GenericAssociation.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MasterDetailAssociation.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MirrorDetailAssociation.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/ObservableArray.java create mode 100644 projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/package.html create mode 100644 projects/net.wotonomy.web/.classpath create mode 100644 projects/net.wotonomy.web/.cvsignore create mode 100644 projects/net.wotonomy.web/.project create mode 100644 projects/net.wotonomy.web/.settings/org.eclipse.jdt.core.prefs create mode 100644 projects/net.wotonomy.web/.settings/org.eclipse.jdt.ui.prefs create mode 100644 projects/net.wotonomy.web/pom.xml create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java create mode 100644 projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html create mode 100644 projects/net.wotonomy.web/src/test/java/net/wotonomy/web/xml/XMLRPCSelectorTest.java create mode 100644 projects/pom.xml (limited to 'projects') diff --git a/projects/net.wotonomy.all/.classpath b/projects/net.wotonomy.all/.classpath new file mode 100644 index 0000000..5b3f131 --- /dev/null +++ b/projects/net.wotonomy.all/.classpath @@ -0,0 +1,5 @@ + + + + + diff --git a/projects/net.wotonomy.all/.cvsignore b/projects/net.wotonomy.all/.cvsignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/projects/net.wotonomy.all/.cvsignore @@ -0,0 +1 @@ +target diff --git a/projects/net.wotonomy.all/.project b/projects/net.wotonomy.all/.project new file mode 100644 index 0000000..eb5e918 --- /dev/null +++ b/projects/net.wotonomy.all/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.all + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.all/pom.xml b/projects/net.wotonomy.all/pom.xml new file mode 100644 index 0000000..330062f --- /dev/null +++ b/projects/net.wotonomy.all/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + pom + Wotonomy - Parent Project + Wotonomy - A free system for creating great database-oriented swing or web applications. + http://wotonomy.sourceforge.net + + ../net.wotonomy.datastore + ../net.wotonomy.foundation + ../net.wotonomy.persistence + ../net.wotonomy.persistence.adapter.jdbc + ../net.wotonomy.ui + ../net.wotonomy.ui.swing + ../net.wotonomy.web + ../net.wotonomy.test + + + continuum + + + + + israfil + Israfil Deploy + scp://mikail.israfil.net/var/www/vhosts/www.israfil.net/htdocs/maven2 + + + + + israfil + Israfil Snapshot + scp://mikail.israfil.net/var/www/vhosts/www.israfil.net/htdocs/maven2 + + + + website + scp://cgruber@shell.sourceforge.net/home/groups/w/wo/wotonomy/htdocs + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.all + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.all + + + + Christian Gruber + cgruber@israfil.net + http://www.israfil.net/ + + Israfil Consulting Services Corporation + + + Admin + Developer + Build Manager + + -5 + + + Micheal Powers + mpowers@users.sourceforge.net + http://sourceforge.net/users/mpowers/ + + Admin + Developer + + + + diff --git a/projects/net.wotonomy.all/src/site/apt/index.apt b/projects/net.wotonomy.all/src/site/apt/index.apt new file mode 100644 index 0000000..51005b5 --- /dev/null +++ b/projects/net.wotonomy.all/src/site/apt/index.apt @@ -0,0 +1,9 @@ + -------------------------------- + Wotonomy + -------------------------------- + + +Wotonomy + + Wotonomy is a pure java user interface framework for building both web and gui applications. It is heavily inspired by the NeXT and OpenStep APIs and it includes clean-room implementations of portions of the WebObjects and JavaClient frameworks. + diff --git a/projects/net.wotonomy.all/src/site/apt/license.apt b/projects/net.wotonomy.all/src/site/apt/license.apt new file mode 100644 index 0000000..e6be04b --- /dev/null +++ b/projects/net.wotonomy.all/src/site/apt/license.apt @@ -0,0 +1,461 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + + diff --git a/projects/net.wotonomy.all/src/site/build.xml b/projects/net.wotonomy.all/src/site/build.xml new file mode 100644 index 0000000..a71e2fb --- /dev/null +++ b/projects/net.wotonomy.all/src/site/build.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/net.wotonomy.all/src/site/fml/faq.fml b/projects/net.wotonomy.all/src/site/fml/faq.fml new file mode 100644 index 0000000..4f3f7cd --- /dev/null +++ b/projects/net.wotonomy.all/src/site/fml/faq.fml @@ -0,0 +1,145 @@ + + + About the Project + + + What is Wotonomy? + + +

Wotonomy is a pure-java open-source application framework. It contains + an "Object/Relational" mapping framework (such as TopLink or hibernate). + It contains a Web Application framework that is J2EE compliant. It is, + in particular, a clean-room re-implementation of Apple's WebObjects + platform, originally written by NeXT Software, Inc.

+
+
+ + + Why is it called Wotonomy? + + +

Wotonomy is a play on WebObjects and autonomy. One of the early project + goals was to make sure that projects that relied on WebObjects would not + be jepardized by WebObjects being discontinued. At the time, Apple had + recently purchased NeXT, and their plan for WebObjects' long-term + continuity was unclear. Therefore, using Wotonomy gives a project + autonomy from WebObjects. Simple...

+
+
+ + + Do I need to run Apple hardware/software to use Wotonomy? + + +

Wotonomy is pure-java and open-source. As such, it does not rely on + any commercial software. + Wotonomy should run on any standard J2SE platform, and web-applications + written with Wotonomy should deploy to J2EE 1.2 compliant application + servers.

+

* Wotonomy currently relies on xml code from + www.jclark.com, but this is being refactored to avoid any licensing + concerns.

+
+
+ + + Is this project active? Why have there been no releases for over a year? + + +

Yes, the project is active, but all of the updates have been to CVS. + There have been few formal packaged updates, and the project has + undergone a change in active contributors.

+

CVS updates have been tapering off because the ui and + control libraries have become vey mature. (They've verifiably + saved at least one startup from extinction) Another alpha release + has been cut so that the non-CVS-using audience can take advantage + of the current state.

+
+
+ + + Do you need help? How can I help? + + +

One word: yes. Grab the source, start hacking, and send in your diffs.

+
+
+
+ + Audience + + + I'm a Java developer: what's in it for me? + + +

Java developers will find the design patterns in wotonomy useful for + greatly reducing the complexities of data-driven application development. + Swing developers will additionally find a wealth of reusable components + that help you "go the extra mile" in creating usable and polished Swing + applications.

+
+
+ + + I'm an OpenStep developer: what's in it for me? + + +

OpenStep developers will find that they can use familiar APIs while still + writing pure java applications. Wotonomy provides clean-room and + near-API-compatible implementations of the EOInterface/EOControl, + WebObjects, and Foundation frameworks that interoperate with and + extend the Java Swing, Servlet, and Collections packages respectively. + Wotonomy tries to be as API-compatible as possible while still tightly + integrating with Java. Wotonomy wants to make you feel at home.

+
+
+
+ + + Miscellaneous + + + Why not use GNUStep? + + +

GNUStep is awesome, but it's an Objective-C framework. The Java + bridge is great, but even Apple is backpedaling on that approach + with their pure-Java version of WebObjects, due to performance + problems and conceptual difficulties in development. Additionally, + GNUStep may not be available or stable on your platform.

+ +

Wotonomy has the same license (GNU Lesser Public License), and is + written completely in Java.

+
+
+ + + WebObjects is pure-java, so why would I need Wotonomy? + + +

You don't need Wotonomy: WO5 is what wotonomy wants to be, and $499 + is an absolute steal. If your project needs this capabilities in a + complete and mature product, you should run, not walk, to your + nearest web-browser and buy a license.

+ +

However, some people cannot or will not develop on Apple hardware or + software, and some people prefer to use Free Software. Moreover, + wotonomy integrates more tightly with java. For example, Foundation's + collections extend J2SE 1.3+ java.util collections, which makes coding + much nicer. There's also a lot of benefit in having source code to + see what's going on behind the scenes. The developers have learned a + heck of a lot about WebObjects by trying to implement it.

+ +

Perhaps if enough people adopt wotonomy for their Java projects, + they'll see the advantage of investing in Apple technology. + Additionally, applications using wotonomy will be extremely + portable to Apple platforms.

+
+
+
+
+ diff --git a/projects/net.wotonomy.all/src/site/resources/doc/control-guide.txt b/projects/net.wotonomy.all/src/site/resources/doc/control-guide.txt new file mode 100644 index 0000000..53d6547 --- /dev/null +++ b/projects/net.wotonomy.all/src/site/resources/doc/control-guide.txt @@ -0,0 +1,53 @@ +*Title: wotonomy user guide: control package + +*TOC + +* Introduction + +** Scope + +This document is intended to serve as a Java developer's introduction to the wotonomy control framework and how to use it to back an application built with the wotonomy ui or web frameworks with data from a database or other persistent storage mechanism. + +** Overview + +The wotonomy classes contained in the net.wotonomy.control package provide nestable editing buffers that decouple the model portion of your application from any specific persistence mechanism. + +*** Where does it fit in? + +The control package supports both the ui and web packages. Both ui and web packages can be used without the control package, but the control package enhances both significantly. The point of contact is the EODataSource, which can be used to populate both EODisplayGroups and WODisplayGroups. + +But aside from the EODataSource, the ui and control packages work in parallel. The data model objects that are observed by a display group are also observed by an editing context. The edi + + - EOObjectStore + - abstract class: encapsulates something to save changes to + - while you will work almost exclusively with editing contexts, most of the methods you will call are defined in object store. + - getting data + + - data objects are specific to the type of object store + - faulting + + - EOEditingContext + - parent is object store + - supports all methods of object store + - additional methods + - saveChanges + - hasChanges + - insertedObjects + - updatedObjects + - deletedObjects + + - object graph / edit buffer / in-place editing + - versioning of objects + - observes objects / maintains lists + + - NSNotificationCenter + - Notifications + - Subscribing + - Posting + - Center + - Queue + + + + - EODataSource + - bridge between ui and control diff --git a/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.html b/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.html new file mode 100644 index 0000000..992eefa --- /dev/null +++ b/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.html @@ -0,0 +1,469 @@ + + + +wotonomy ui user guide + + + + + +

wotonomy ui user guide

+ + +

Introduction

+

Scope

+

+This document is intended to serve as a Java developer's introduction to the wotonomy user interface framework and how to use it to build a graphical user interface application with Swing. +

+ +

Overview

+

+The wotonomy user interface package contained in net.wotonomy.ui provides a design pattern that further abstracts and decouples the model, view, and controller portions of a graphical user interface application. It lets you do more with less code. +

+

Where does it fit in?

+

+Let's begin by looking at the standard model-view-controller (MVC) architecture, and then we'll look at how the design changes with wotonomy. +

+ +

A Typical MVC Java application

+

+A typical Java application can be described in a very high-level way as simply reading a bunch of data objects into memory from the file system or JDBC or elsewhere and rendering a graphical representation of the data for the user. Additionally, the user may be allowed to edit some or all the data represented, and in that case the modifications are copied back from whence they were read. +

+ +

+In MVC parlance, the model is an object-oriented representation of your data and business logic. In the typical application example, the data objects are the model. To afford maximum reuse, these data objects have no knowledge of and no dependencies on the view or controller portions of your application. In the Enterprise Java Bean (EJB) world, entity beans are usually good examples of data objects. +

+

+The view is the user interface of your application, where the data objects are represented on screen using labels, text fields, sliders, and the like. While you might need to know a little bit about the data you are trying to visualize when constructing the user interface, the view classes have no code dependencies on the data object classes in the model. And the view similarly has no code dependencies on the controller. To afford maximum flexibility, the view consists only of the Swing components arranged in the proper layouts and nothing more. +

+

+The controller, on the other hand, contains the logic specific to the Java Swing application you are writing, and it is tightly coupled with and has deep code dependencies on the model and view. +

+

+The controller is responsible for fetching the data objects that will be viewed or edited, and is responsible for populating each user interface component in the view with data from the data objects. This means that the controller directly calls methods on both the model's data objects and the view's user interface components. +

+

+If the application supports editing, the controller is responsible for even more. It must detect changes to the view, like edited text fields or moved sliders, and update the model to reflect the changes. And it must detect changes in the model and update the view components accordingly (especially for side-effect changes like a task's status changing in the data model when the user assigns a worker). These responsibilities deepen the couplings with both the model and the view. +

+

+Furthermore, the controller must track commits and reverts of sets of changes to the data model, and it must track lists of edited, added, and deleted objects to efficiently update the persistent data store. And these responsibilities deepen the complexity of the logic contained in the controller. +

+ +

A Typical wotonomy Application

+

+Because so much code resides in the controller portion of an application, and because the controller is the least generic and therefore least reusable code in an application, wotonomy is designed to abstract and simplify the responsibilities of the controller to reduce the cost of writing and maintaining it. +

+ +

+With wotonomy, the only requirement on the model portion of your application is that the data objects adhere to Java Bean naming conventions for properties. Wotonomy further decouples your model from the rest of your application by working with it only through the use of bean properties. +

+

+The view portion of your application remains the same. The controller portion, however, is vastly simplified. +

+

+The fetched data objects are placed into an EditingContext. The editing context monitors the objects for changes, and tracks insertions and deletions, and handles commits and reverts. The controller is only responsible for providing the glue to allow the editing context to talk to whatever persistent object storage is used for the application. If your data is read only or if your data model is fairly simple, then the editing context can be omitted and your job is even easier. +

+

+A DisplayGroup then retrieves some or all of those objects from the EditingContext. A display group maintains an ordered collection of objects, monitoring them for changes. The display group additionally tracks the concept of selection: a subset of objects in the display group are selected, and the selection might contain zero, one, many, or all objects in the group. The display group broadcasts notifications of changes to the data object or to the selection to interested listeners. The controller is only responsible for creating the display group and deciding what objects from the editing context should be placed in the group. +

+

+An Association is then created for each user interface component in the view. Each association is bound to the display group as a listener for change notifications. Different types of associations correspond to different kinds of user interface objects: a JTextField with a TextAssociation will display the value of a property for the selected object in the bound display group, while a TableColumn with a TableColumnAssociation will display the value of a property for all objects in the bound display group, highlighting those objects that are selected. The controller is only responsible for creating the associations, connecting them to the user interface components, and binding them to the display group. +

+

+Your controller will not need to directly touch any data objects, and will only need to access the view to get references to the user interface components and pass them on to the associations. After all the pieces are connected, they'll just work and it's pretty much fire and forget. +

+ +

Expanded Framework

+

+On the whole, an application may have one or more editing contexts, each of which may be used by one or more display groups, each of which is observed by one or more associations, each of which is connected to a single user interface component. But there's a little more to this story. +

+ +

+As the diagram shows, a display group actually has a data source which has an editing context, and editing contexts have object stores which can in turn be editing contexts. This means that editing contexts can be nested within one another. And there are associations that can control a display group instead of a Swing component, which means that display groups can also be nested. +

+

+However, you can write a fully-functional application using just display groups and associations, skipping the editing contexts and data sources completely. +

+

+Wotonomy makes the simple things easy while still making complex things possible. +

+ + + +

Using wotonomy

+

+Wotonomy is a design pattern as well as a Java library, so you'll want to understand how to structure your application to best leverage the framework. +

+

Designing Data Objects

+

+In short, any objects that have getter and setter methods can be used in a display group. Your objects probably have those already. Even better are objects that use Java Bean naming conventions to define properties. You probably do that too. For change notifications, extending java.util.Observable is helpful, but since your objects probably don't already do that, there is an alternate way to post change notifications. Or you can choose to not post notifications at all. The requirements of wotonomy on your data model are minimal. +

+

Java Bean Conventions

+

+A java bean property is defined by having a method of the form getProperty() where Property is the name of the property, for example, a name property is defined by the method getName(). The get method can have no parameters and the return type defines the type of the property. (The alternate form for getters that omits the get in the method name, e.g. property(), is also supported.) +

+

+An editable property is defined by additionally having a method of the form setProperty( Object aProperty ) where the return value is void and the type of the parameter matches the type of the getter method. (When a property has a getter method but no setter method, that property is considered-read only.) +

+

+An indexed property works the same way but the type is either an Array (like Object[] or String[]) or a Collection (like Set or List). Indexed properties typically define one-to-many relationships in your data model. Indexed properties are useful for nested display groups and recursive tree associations. +

+ +

Change Notification

+

+The Java way for objects to post change notifications is to extend java.util.Observable and call notifyObservers() after your change has taken place. If your data object extends Observable, DisplayGroups will register themselves as observers and expect notifications for changes that occur to the object, whether from user interaction or from external influence. +

+

+The Observable approach may be undesirable if you need to subclass a third party object that does not extend Observable or if you don't want a strong reference to an observer from your data object that may need to be manually cleared later. +

+

+For objects that do not extend Observer, you can get much the same effect by calling the static method EOObserverCenter.notifyObserversObjectWillChange() before your change takes place. +

+

+Wotonomy uses EOObserverCenter exclusively within the framework and it presents a preferrable alternative to Observable because it uses weak references to track objects and it relaxes the requirement that observed objects extend a particular class by requiring that observing objects implement the EOObserving interface. The EOObserverCenter can also coalesce change events if the observing object extends EODelayedObserver. +

+

+Please note that if your object does not post change notifications, most of wotonomy will continue to work. The only problem will then be that changes external to wotonomy - changes made to your data model by your data model or by a timed refresh or something similar - will not be automatically reflected in the user interface. Manually calling EODisplayGroup.updateDisplayedObjects() can refresh the display. +

+ + +

Display Groups

+

+Display groups are objects that manage an ordered list of data objects. Of this list, a subset are considered to be "displayed" objects that are visible to the user. Of the displayed objects, a subset are considered "selected". In common usage, all of the objects are displayed, but only one object and sometimes no objects are selected. +

+

+Display groups monitor changes to the list, changes to what objects are in the list, changes to which objects are selected, as well as changes to the properties of the objects themselves, and notify any bound associations of these changes. +

+

+If the display group has a delegate, the delegate is informed of these changes and more so that the delegate can fine-tune the behavior of the display group. +

+

Creating a Display Group

+

+A display group is created very simply with the default constructor. +This creates a display group with no data source, no delegate, and no objects to manage. +

+
+    EODisplayGroup displayGroup = new EODisplayGroup();
+
+ +

Populating a Display Group

+

+You can populate the display group with objects in two ways. +

+

+The direct way is easiest. Simply pass in a List containing the objects to be managed in the order they should appear. +

+
+    List objectList = new LinkedList();
+    objectList.add( new TestObject() );
+    displayGroup.setObjectArray( objectList );
+
+

+Alternately, you can specify a data source for the display group to use to retrieve objects. +

+

+EODataSource is an abstract class that you will extend with methods to create, insert, and delete objects from whatever persistent object store you are using. This class is particularly useful for rendering recursive tree-like object relationships that the data objects do not explicitly model. +

+

+If you will be using an editing context, you will need to use a data source. +

+
+    EODataSource dataSource = new MyDataSource();
+    displayGroup.setDataSource( dataSource );
+    displayGroup.fetch();
+
+ +

Qualifying Displayed Objects

+

+The displayed objects are a subset of the list of objects that the display group manages. By default all objects are displayed. +

+

+You can change this behavior by specifying a qualifier for the display group. This is an instance of EOQualifier and it will have evaluateWithObject called for every object in the display group. Only those objects for which the return value is true will be in the displayed objects list. Call updateDisplayedObjects() on the display group to apply the qualifier. +

+

+You would typically use this feature when you want to preload a large number of data objects into the display group so that the user can quickly and repeatedly apply different filters to sift through the data. +

+
+    // show only Powers in San Antonio
+
+    Map matchValues = new HashMap();
+    matchValues.put( "lastName", "Powers" );
+    matchValues.put( "city", "San Antonio" );
+
+    EOQualifier qualifier = 
+	EOQualifier.qualifierToMatchAllValues( matchValues );
+    displayGroup.setQualifier( qualifier );
+    displayGroup.updateDisplayedObjects();
+
+ +

Handling Selection

+

+Display groups also track which of the displayed objects are selected. By default no objects are selected, but you can change the selection programmatically. Certain associations allow the user to change the selection as well. +

+

+If you do not provide the user with an association that can change the selection, you should make sure to set the selection to some object. +

+
+    // select all objects
+    displayGroup.setSelectedObjects( 
+	displayGroup.displayedObjects() );
+
+    // select the third object
+    List selectionList = new LinkedList();
+    selectionList.add( new Integer( 2 ) );
+    displayGroup.setSelectedIndexes( selectionList );
+
+    // select the next object
+    displayGroup.selectNext();
+
+    // clear the selection
+    displayGroup.clearSelection();
+
+ +

Handling Insertions and Deletions

+

+You can programmatically insert or delete objects from a display group, although only if they are displayed. Any associations that display all of the displayed objects (like TableColumns) will be updated automatically. +

+

+Deleted objects that were selected are removed from the selection. Inserted objects are not selected, so you will need to select them manually if desired. Default values for inserted objects can be specified with setInsertedObjectDefaultValues(). +

+

+If a data source is used, the data source will be informed of the insertion or deletion, which will notify the editing context if it has one, which would update the persistent store on commit. +

+

+Also, if a data source is used, the display group can be asked to simply create an object to be inserted into the group, which will call createObject() on the data source. +

+
+    // insert an object
+    displayGroup.insertObjectAtIndex( new TestObject(), 0 );
+
+    // delete that object
+    displayGroup.deleteObjectAtIndex();
+
+    // select the next object
+    displayGroup.selectNext();
+
+    // delete that object
+    displayGroup.deleteSelection();
+
+    // insert a new object
+    displayGroup.insertNewObjectAtIndex( 0 );
+
+ +

Delegate

+

+Display groups check with their delegate before performing almost any operation, you can easily customize the behavior of a display group by providing a class that implements the DisplayGroup.Delegate interface. +

+

+Display groups ask their delegate for permission to fetch, insert, delete, edit, create, and change selection, and then notify their delegate after each of these operations has taken place. Wotonomy provides a DelegateAdapter class that you can easily subclass to implement the required interface. +

+ + +

Associations

+

+Associations are the bridge between the controller and the view. An association has exactly one "controlled object", which is usually a user interface component. They also have one or sometimes more "aspects", each of which has a name, like ValueAspect or EnabledAspect. +You bind an aspect of an association with a display group and/or a key. Then you establish the connection between the association and the controlled object and you're done and you can forget about it. The association will keep the user interface object synchronized with the objects in the display group. +

+

Creating Associations

+

+You create an association by calling the constructor of the subclass of EOAssociation that is appropriate for the controlled object. Most associations are applicable to many kinds of objects, so the constructor is expected to take an Object. (This means that if you specify the wrong kind of object for the association, you won't hear about it until runtime.) +

+
+    EOAssociation association = new TextAssociation( textField );
+
+ +

Kinds of Associations

+

+Associations fall into one of two categories: overview associations and detail associations. Almost all associations are used to control some kind of user interface component. +

+

+Overview associations show some property of all the items in the display group. Overview associations usually allow the user to select and deselect objects in the display group. +

+

+For example, a JList might be used with a ListAssociation to show the "name" property of all of the displayed objects in the display group, in the same order. Clicking the fourth name in the list would cause the fourth object in the display group to be selected, and - depending on the JList's selection model - cause whatever was previously selected to become unselected. +

+

+Detail associations show some property of only the first selected object in the display group. If there is no selection, the detail association causes the controlled component to be blank or otherwise empty. If multiple objects are selected, the detail association shows the property of only the first object in the selected objects list. +

+

+In our example, a JTextField might be used with a TextAssociation to display the "email" property of the selected item in the display group. Clicking the fourth name in the list causes the fourth item in the display group to be selected, which populates the text field with the value of the email property for that object. Changing the value in the text field changes the value of that object's email property automatically. +

+ +

Master-Detail Associations

+

+While nearly all associations control user interface components, the MasterDetailAssociation is worthy of note because it does not. +

+

+A MasterDetailAssociation controls a display group and binds to an indexed property of the objects in another display group. It is a detail association, with the bound display group as the "master" and the controlled display group as the "detail" component. +

+

+Again using our example, another JList might be used with a ListAssociation to show all the dependents of the person selected in the first JList. +

+

+First we would create a new display group and then create a MasterDetailAssociation with our new display group as the parameter to the constructor. Then we would bind that association to our original master display group with the "dependents" property (assuming there is a method on our data object called getDependents() that returns an array or a collection of objects). Then we would create our new ListAssociation controlling our new JList, and bind that to our new display group with the "name" property. Seeing the source might make this clearer - that's coming in the next section. +

+

+Clicking on a name in the name list would then display the email address in the text field and a list of names of all the dependents. +

+ +

Binding Aspects and Establishing Connections

+

+An association has one or more aspects. Most of the time, you'll just need one aspect, sometimes called the "primary aspect". Aspects are referred to by name, like ValueAspect, and the names of an association's aspects are static members of that association's class. +

+

+The primary aspect is usually responsible for populating the controlled object with data from the display group. The other aspects are useful, but they'll do different things depending on the nature of the association, so you should consult the documentation for the association you will be using. +

+

+To bind an aspect, you call bindAspect() with the name of the aspect, a display group and a string "key". +

+

+Usually, all three are specified, and the key is a property name. The association will display or edit the value of this property for the objects in the display group. +

+

+The key may be an empty string, and this is considered the "identity property", and the value of the actual object in the display group will be used instead of a property on that object. This is sometimes useful for view components like JTrees for which you want to write custom renderers: with the identity property, the renderer receives the value of the actual object. +

+

+The display group may be left null, in which case the key is treated as a value, acting as if all objects in the display group for the given property returned that value. This is useful for certain aspects of certain associations, like the EnabledAspect of TextAssociation, or the EditableAspect of TableColumnAssociation. +

+

+After all the aspects have been bound, you need to call establishConnection() to put the association to work. Importantly, you do not need to retain a reference to the associations you create - they will exist as long as their controlled components exist, and when the controlled component is garbage collected, the association will be garbage collected. +

+
+    // set up the master display group
+    EODisplayGroup masterGroup = new EODisplayGroup();
+    masterGroup.setObjectArray( getMyUsers() );
+    
+    EOAssociation association;
+
+    // set up names list
+    association = new ListAssociation( usersList );
+    association.bindAspect( 
+	EOAssociation.TitlesAspect, masterGroup, "name" );
+    association.establishConnection();
+
+    // set up email field and for fun make it non-editable
+    association = new TextAssociation( emailField );
+    association.bindAspect( 
+	EOAssociation.ValueAspect, masterGroup, "email" );
+    association.bindAspect( 
+	EOAssociation.EditableAspect, null, "false" );
+    association.establishConnection();
+
+    // set up detail display group
+    EODisplayGroup detailGroup = new EODisplayGroup();
+    association = new MasterDetailAssociation( detailGroup );
+    association.bindAspect( 
+	EOAssociation.ParentAspect, masterGroup, "dependents" );
+    association.establishConnection();
+
+    // set up dependents' names list
+    association = new ListAssociation( dependentsList );
+    association.bindAspect( 
+	EOAssociation.TitlesAspect, detailGroup, "name" );
+    association.establishConnection();
+
+    // we're done!
+
+ + +

Editing Contexts

+

+Editing contexts allow multiple display groups to manipulate the same set of objects, such that objects edited in one display group are updated in the associations of all other display groups that contain that object. +

+

+Editing contexts monitor all inserts, edits, and deletes in the display groups that use them, allowing these sets of changes to be committed or rolled-back as a single transaction. +

+

+Furthermore, editing contexts can be nested, so that a new editing context will reflect the existing state of another editing context, but all changes made to the new context are not applied to the parent context until they have been committed in the child context. +

+

Object Stores

+

+Two concepts are critical to an understanding of editing contexts: (1) An editing context must have an object store, and (2) an editing context is an object store. +

+

+Because you need an object store to instantiate an editing context, there must be some implementation of EOObjectStore that is not an editing context. That object store will be responsible for directly reading and writing to the persistent storage. +

+

+Because editing contexts do implement object stores, you can then create other editing contexts that use the first editing context, which uses the first object store. This is what allows editing contexts to be nest. +

+ +

Creating an Editing Context

+

+To use an editing context in your application, you must create an object store. Your implementation of EOObjectStore will act as the glue between wotonomy and whatever persistent storage mechanism your application uses. +

+

+You will then create your primary editing context, specifying your object store as the parameter to the constructor. You will probably have only one object store per application, and one primary editing context that uses it. Any other editing contexts you create will use your primary context as their object store. +

+
+    // initial application setup
+    EOObjectStore objectStore = new MyObjectStore();
+    EOEditingContext mainContext = new EOEditingContext( objectStore );
+
+    // set up a child display group
+    EOEditingContext childContext = new EOEditingContext( mainContext );
+    EODataSource dataSource = new MyDataSource( childContext );
+    EODisplayGroup displayGroup = new EODisplayGroup();
+    displayGroup.setDataSource( dataSource );
+    displayGroup.fetch();
+
+ + + +

What's next?

+

+This document gives you a very basic overview and brief introduction to the major players in the wotonomy user interface framework. You'll probably want to check out the API Reference + next +

+ +
+
+
+This document was generated using AFT v5.06 +
+ + + diff --git a/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.txt b/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.txt new file mode 100644 index 0000000..d9d0bb1 --- /dev/null +++ b/projects/net.wotonomy.all/src/site/resources/doc/ui-guide.txt @@ -0,0 +1,341 @@ +*Title: wotonomy user guide: ui package + +*TOC + +* Introduction + +** Scope + +This document is intended to serve as a Java developer's introduction to the wotonomy user interface framework and how to use it to build a graphical user interface application with Swing. + +** Overview + +The wotonomy user interface package contained in net.wotonomy.ui provides a design pattern that further abstracts and decouples the model, view, and controller portions of a graphical user interface application. It lets you do more with less code. + +*** Where does it fit in? + +Let's begin by looking at the standard model-view-controller (MVC) architecture, and then we'll look at how the design changes with wotonomy. + +*** A Typical MVC Java application + +A typical Java application can be described in a very high-level way as simply reading a bunch of data objects into memory from the file system or JDBC or elsewhere and rendering a graphical representation of the data for the user. Additionally, the user may be allowed to edit some or all the data represented, and in that case the modifications are copied back from whence they were read. + +*Image: images/standard-mvc.gif + +In MVC parlance, the model is an object-oriented representation of your data and business logic. In the typical application example, the data objects are the model. To afford maximum reuse, these data objects have no knowledge of and no dependencies on the view or controller portions of your application. In the Enterprise Java Bean (EJB) world, entity beans are usually good examples of data objects. + +The view is the user interface of your application, where the data objects are represented on screen using labels, text fields, sliders, and the like. While you might need to know a little bit about the data you are trying to visualize when constructing the user interface, the view classes have no code dependencies on the data object classes in the model. And the view similarly has no code dependencies on the controller. To afford maximum flexibility, the view consists only of the Swing components arranged in the proper layouts and nothing more. + +The controller, on the other hand, contains the logic specific to the Java Swing application you are writing, and it is tightly coupled with and has deep code dependencies on the model and view. + +The controller is responsible for fetching the data objects that will be viewed or edited, and is responsible for populating each user interface component in the view with data from the data objects. This means that the controller directly calls methods on both the model's data objects and the view's user interface components. + +If the application supports editing, the controller is responsible for even more. It must detect changes to the view, like edited text fields or moved sliders, and update the model to reflect the changes. And it must detect changes in the model and update the view components accordingly (especially for side-effect changes like a task's status changing in the data model when the user assigns a worker). These responsibilities deepen the couplings with both the model and the view. + +Furthermore, the controller must track commits and reverts of sets of changes to the data model, and it must track lists of edited, added, and deleted objects to efficiently update the persistent data store. And these responsibilities deepen the complexity of the logic contained in the controller. + +*** A Typical wotonomy Application + +Because so much code resides in the controller portion of an application, and because the controller is the least generic and therefore least reusable code in an application, wotonomy is designed to abstract and simplify the responsibilities of the controller to reduce the cost of writing and maintaining it. + +*Image: images/wotonomy-mvc.gif + +With wotonomy, the only requirement on the model portion of your application is that the data objects adhere to Java Bean naming conventions for properties. Wotonomy further decouples your model from the rest of your application by working with it only through the use of bean properties. + +The view portion of your application remains the same. The controller portion, however, is vastly simplified. + +The fetched data objects are placed into an EditingContext. The editing context monitors the objects for changes, and tracks insertions and deletions, and handles commits and reverts. The controller is only responsible for providing the glue to allow the editing context to talk to whatever persistent object storage is used for the application. If your data is read only or if your data model is fairly simple, then the editing context can be omitted and your job is even easier. + +A DisplayGroup then retrieves some or all of those objects from the EditingContext. A display group maintains an ordered collection of objects, monitoring them for changes. The display group additionally tracks the concept of selection: a subset of objects in the display group are selected, and the selection might contain zero, one, many, or all objects in the group. The display group broadcasts notifications of changes to the data object or to the selection to interested listeners. The controller is only responsible for creating the display group and deciding what objects from the editing context should be placed in the group. + +An Association is then created for each user interface component in the view. Each association is bound to the display group as a listener for change notifications. Different types of associations correspond to different kinds of user interface objects: a JTextField with a TextAssociation will display the value of a property for the selected object in the bound display group, while a TableColumn with a TableColumnAssociation will display the value of a property for all objects in the bound display group, highlighting those objects that are selected. The controller is only responsible for creating the associations, connecting them to the user interface components, and binding them to the display group. + +Your controller will not need to directly touch any data objects, and will only need to access the view to get references to the user interface components and pass them on to the associations. After all the pieces are connected, they'll just work and it's pretty much fire and forget. + +*** Expanded Framework + +On the whole, an application may have one or more editing contexts, each of which may be used by one or more display groups, each of which is observed by one or more associations, each of which is connected to a single user interface component. But there's a little more to this story. + +*Image: images/wotonomy-er.gif + +As the diagram shows, a display group actually has a data source which has an editing context, and editing contexts have object stores which can in turn be editing contexts. This means that editing contexts can be nested within one another. And there are associations that can control a display group instead of a Swing component, which means that display groups can also be nested. + +However, you can write a fully-functional application using just display groups and associations, skipping the editing contexts and data sources completely. + +Wotonomy makes the simple things easy while still making complex things possible. + +* Using wotonomy + +Wotonomy is a design pattern as well as a Java library, so you'll want to understand how to structure your application to best leverage the framework. + +** Designing Data Objects + +In short, any objects that have getter and setter methods can be used in a display group. Your objects probably have those already. Even better are objects that use Java Bean naming conventions to define properties. You probably do that too. For change notifications, extending java.util.Observable is helpful, but since your objects probably don't already do that, there is an alternate way to post change notifications. Or you can choose to not post notifications at all. The requirements of wotonomy on your data model are minimal. + +*** Java Bean Conventions + +A java bean property is defined by having a method of the form getProperty() where Property is the name of the property, for example, a name property is defined by the method getName(). The get method can have no parameters and the return type defines the type of the property. (The alternate form for getters that omits the get in the method name, e.g. property(), is also supported.) + +An editable property is defined by additionally having a method of the form setProperty( Object aProperty ) where the return value is void and the type of the parameter matches the type of the getter method. (When a property has a getter method but no setter method, that property is considered-read only.) + +An indexed property works the same way but the type is either an Array (like Object[] or String[]) or a Collection (like Set or List). Indexed properties typically define one-to-many relationships in your data model. Indexed properties are useful for nested display groups and recursive tree associations. + +*** Change Notification + +The Java way for objects to post change notifications is to extend java.util.Observable and call notifyObservers() after your change has taken place. If your data object extends Observable, DisplayGroups will register themselves as observers and expect notifications for changes that occur to the object, whether from user interaction or from external influence. + +The Observable approach may be undesirable if you need to subclass a third party object that does not extend Observable or if you don't want a strong reference to an observer from your data object that may need to be manually cleared later. + +For objects that do not extend Observer, you can get much the same effect by calling the static method {-EOObserverCenter.notifyObserversObjectWillChange()@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EOObserverCenter.html#notifyObserversObjectWillChange-} before your change takes place. + +Wotonomy uses {-EOObserverCenter@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EOObserverCenter.html-} exclusively within the framework and it presents a preferrable alternative to Observable because it uses weak references to track objects and it relaxes the requirement that observed objects extend a particular class by requiring that observing objects implement the EOObserving interface. The EOObserverCenter can also coalesce change events if the observing object extends {-EODelayedObserver@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EODelayedObserver.html-}. + +Please note that if your object does not post change notifications, most of wotonomy will continue to work. The only problem will then be that changes external to wotonomy - changes made to your data model by your data model or by a timed refresh or something similar - will not be automatically reflected in the user interface. Manually calling {-EODisplayGroup.updateDisplayedObjects()@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/EODisplayGroup.html#updateDisplayedObjects-} can refresh the display. + +** Display Groups + +{-Display groups@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/EODisplayGroup.html-} are objects that manage an ordered list of data objects. Of this list, a subset are considered to be "displayed" objects that are visible to the user. Of the displayed objects, a subset are considered "selected". In common usage, all of the objects are displayed, but only one object and sometimes no objects are selected. + +Display groups monitor changes to the list, changes to what objects are in the list, changes to which objects are selected, as well as changes to the properties of the objects themselves, and notify any bound associations of these changes. + +If the display group has a delegate, the delegate is informed of these changes and more so that the delegate can fine-tune the behavior of the display group. + +*** Creating a Display Group + +A display group is created very simply with the default constructor. +This creates a display group with no data source, no delegate, and no objects to manage. + +^<< + EODisplayGroup displayGroup = new EODisplayGroup(); +^>> + +*** Populating a Display Group + +You can populate the display group with objects in two ways. + +The direct way is easiest. Simply pass in a List containing the objects to be managed in the order they should appear. + +^<< + List objectList = new LinkedList(); + objectList.add( new TestObject() ); + displayGroup.setObjectArray( objectList ); +^>> + +Alternately, you can specify a data source for the display group to use to retrieve objects. + +{-EODataSource@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EODataSource.html-} is an abstract class that you will extend with methods to create, insert, and delete objects from whatever persistent object store you are using. This class is particularly useful for rendering recursive tree-like object relationships that the data objects do not explicitly model. + +If you will be using an editing context, you will need to use a data source. + +^<< + EODataSource dataSource = new MyDataSource(); + displayGroup.setDataSource( dataSource ); + displayGroup.fetch(); +^>> + +*** Qualifying Displayed Objects + +The displayed objects are a subset of the list of objects that the display group manages. By default all objects are displayed. + +You can change this behavior by specifying a qualifier for the display group. This is an instance of {-EOQualifier@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EOQualifier.html-} and it will have evaluateWithObject called for every object in the display group. Only those objects for which the return value is true will be in the displayed objects list. Call updateDisplayedObjects() on the display group to apply the qualifier. + +You would typically use this feature when you want to preload a large number of data objects into the display group so that the user can quickly and repeatedly apply different filters to sift through the data. + +^<< + // show only Powers in San Antonio + + Map matchValues = new HashMap(); + matchValues.put( "lastName", "Powers" ); + matchValues.put( "city", "San Antonio" ); + + EOQualifier qualifier = + EOQualifier.qualifierToMatchAllValues( matchValues ); + displayGroup.setQualifier( qualifier ); + displayGroup.updateDisplayedObjects(); +^>> + +*** Handling Selection + +Display groups also track which of the displayed objects are selected. By default no objects are selected, but you can change the selection programmatically. Certain associations allow the user to change the selection as well. + +If you do not provide the user with an association that can change the selection, you should make sure to set the selection to some object. + +^<< + // select all objects + displayGroup.setSelectedObjects( + displayGroup.displayedObjects() ); + + // select the third object + List selectionList = new LinkedList(); + selectionList.add( new Integer( 2 ) ); + displayGroup.setSelectedIndexes( selectionList ); + + // select the next object + displayGroup.selectNext(); + + // clear the selection + displayGroup.clearSelection(); +^>> + +*** Handling Insertions and Deletions + +You can programmatically insert or delete objects from a display group, although only if they are displayed. Any associations that display all of the displayed objects (like TableColumns) will be updated automatically. + +Deleted objects that were selected are removed from the selection. Inserted objects are not selected, so you will need to select them manually if desired. Default values for inserted objects can be specified with setInsertedObjectDefaultValues(). + +If a data source is used, the data source will be informed of the insertion or deletion, which will notify the editing context if it has one, which would update the persistent store on commit. + +Also, if a data source is used, the display group can be asked to simply create an object to be inserted into the group, which will call createObject() on the data source. + +^<< + // insert an object + displayGroup.insertObjectAtIndex( new TestObject(), 0 ); + + // delete that object + displayGroup.deleteObjectAtIndex(); + + // select the next object + displayGroup.selectNext(); + + // delete that object + displayGroup.deleteSelection(); + + // insert a new object + displayGroup.insertNewObjectAtIndex( 0 ); +^>> + +*** Delegate + +Display groups check with their delegate before performing almost any operation, you can easily customize the behavior of a display group by providing a class that implements the {-DisplayGroup.Delegate@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/EODisplayGroup.Delegate.html-} interface. + +Display groups ask their delegate for permission to fetch, insert, delete, edit, create, and change selection, and then notify their delegate after each of these operations has taken place. Wotonomy provides a DelegateAdapter class that you can easily subclass to implement the required interface. + +** Associations + +{-Associations@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/EOAssociation.html-} are the bridge between the controller and the view. An association has exactly one "controlled object", which is usually a user interface component. They also have one or sometimes more "aspects", each of which has a name, like ValueAspect or EnabledAspect. +You bind an aspect of an association with a display group and/or a key. Then you establish the connection between the association and the controlled object and you're done and you can forget about it. The association will keep the user interface object synchronized with the objects in the display group. + +*** Creating Associations + +You create an association by calling the constructor of the subclass of EOAssociation that is appropriate for the controlled object. Most associations are applicable to many kinds of objects, so the constructor is expected to take an Object. (This means that if you specify the wrong kind of object for the association, you won't hear about it until runtime.) + +^<< + EOAssociation association = new TextAssociation( textField ); +^>> + +*** Kinds of Associations + +Associations fall into one of two categories: overview associations and detail associations. Almost all associations are used to control some kind of user interface component. + +Overview associations show some property of all the items in the display group. Overview associations usually allow the user to select and deselect objects in the display group. + +For example, a JList might be used with a {-ListAssociation@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/swing/ListAssociation.html-} to show the "name" property of all of the displayed objects in the display group, in the same order. Clicking the fourth name in the list would cause the fourth object in the display group to be selected, and - depending on the JList's selection model - cause whatever was previously selected to become unselected. + +Detail associations show some property of only the first selected object in the display group. If there is no selection, the detail association causes the controlled component to be blank or otherwise empty. If multiple objects are selected, the detail association shows the property of only the first object in the selected objects list. + +In our example, a JTextField might be used with a {-TextAssociation@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/swing/TextAssociation.html-} to display the "email" property of the selected item in the display group. Clicking the fourth name in the list causes the fourth item in the display group to be selected, which populates the text field with the value of the email property for that object. Changing the value in the text field changes the value of that object's email property automatically. + +*** Master-Detail Associations + +While nearly all associations control user interface components, the {-MasterDetailAssociation@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/MasterDetailAssociation.html-} is worthy of note because it does not. + +A MasterDetailAssociation controls a display group and binds to an indexed property of the objects in another display group. It is a detail association, with the bound display group as the "master" and the controlled display group as the "detail" component. + +Again using our example, another JList might be used with a ListAssociation to show all the dependents of the person selected in the first JList. + +First we would create a new display group and then create a MasterDetailAssociation with our new display group as the parameter to the constructor. Then we would bind that association to our original master display group with the "dependents" property (assuming there is a method on our data object called getDependents() that returns an array or a collection of objects). Then we would create our new ListAssociation controlling our new JList, and bind that to our new display group with the "name" property. Seeing the source might make this clearer - that's coming in the next section. + +Clicking on a name in the name list would then display the email address in the text field and a list of names of all the dependents. + +*** Binding Aspects and Establishing Connections + +An association has one or more aspects. Most of the time, you'll just need one aspect, sometimes called the "primary aspect". Aspects are referred to by name, like ValueAspect, and the names of an association's aspects are static members of that association's class. + +The primary aspect is usually responsible for populating the controlled object with data from the display group. The other aspects are useful, but they'll do different things depending on the nature of the association, so you should consult the documentation for the association you will be using. + +To bind an aspect, you call bindAspect() with the name of the aspect, a display group and a string "key". + +Usually, all three are specified, and the key is a property name. The association will display or edit the value of this property for the objects in the display group. + +The key may be an empty string, and this is considered the "identity property", and the value of the actual object in the display group will be used instead of a property on that object. This is sometimes useful for view components like JTrees for which you want to write custom renderers: with the identity property, the renderer receives the value of the actual object. + +The display group may be left null, in which case the key is treated as a value, acting as if all objects in the display group for the given property returned that value. This is useful for certain aspects of certain associations, like the EnabledAspect of {-TextAssociation@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/swing/TextAssociation.html-}, or the EditableAspect of {-TableColumnAssociation@http://wotonomy.sourceforge.net/docs/net/wotonomy/ui/swing/TableColumnAssociation.html-}. + +After all the aspects have been bound, you need to call establishConnection() to put the association to work. Importantly, you do not need to retain a reference to the associations you create - they will exist as long as their controlled components exist, and when the controlled component is garbage collected, the association will be garbage collected. + +^<< + // set up the master display group + EODisplayGroup masterGroup = new EODisplayGroup(); + masterGroup.setObjectArray( getMyUsers() ); + + EOAssociation association; + + // set up names list + association = new ListAssociation( usersList ); + association.bindAspect( + EOAssociation.TitlesAspect, masterGroup, "name" ); + association.establishConnection(); + + // set up email field and for fun make it non-editable + association = new TextAssociation( emailField ); + association.bindAspect( + EOAssociation.ValueAspect, masterGroup, "email" ); + association.bindAspect( + EOAssociation.EditableAspect, null, "false" ); + association.establishConnection(); + + // set up detail display group + EODisplayGroup detailGroup = new EODisplayGroup(); + association = new MasterDetailAssociation( detailGroup ); + association.bindAspect( + EOAssociation.ParentAspect, masterGroup, "dependents" ); + association.establishConnection(); + + // set up dependents' names list + association = new ListAssociation( dependentsList ); + association.bindAspect( + EOAssociation.TitlesAspect, detailGroup, "name" ); + association.establishConnection(); + + // we're done! +^>> + +** Editing Contexts + +{-Editing contexts@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EOEditingContext.html-} allow multiple display groups to manipulate the same set of objects, such that objects edited in one display group are updated in the associations of all other display groups that contain that object. + +Editing contexts monitor all inserts, edits, and deletes in the display groups that use them, allowing these sets of changes to be committed or rolled-back as a single transaction. + +Furthermore, editing contexts can be nested, so that a new editing context will reflect the existing state of another editing context, but all changes made to the new context are not applied to the parent context until they have been committed in the child context. + +*** Object Stores + +Two concepts are critical to an understanding of editing contexts: (1) An editing context must have an object store, and (2) an editing context is an object store. + +Because you need an object store to instantiate an editing context, there must be some implementation of {-EOObjectStore@http://wotonomy.sourceforge.net/docs/net/wotonomy/control/EOObjectStore.html-} that is not an editing context. That object store will be responsible for directly reading and writing to the persistent storage. + +Because editing contexts do implement object stores, you can then create other editing contexts that use the first editing context, which uses the first object store. This is what allows editing contexts to be nest. + +*** Creating an Editing Context + +To use an editing context in your application, you must create an object store. Your implementation of EOObjectStore will act as the glue between wotonomy and whatever persistent storage mechanism your application uses. + +You will then create your primary editing context, specifying your object store as the parameter to the constructor. You will probably have only one object store per application, and one primary editing context that uses it. Any other editing contexts you create will use your primary context as their object store. + +^<< + // initial application setup + EOObjectStore objectStore = new MyObjectStore(); + EOEditingContext mainContext = new EOEditingContext( objectStore ); + + // set up a child display group + EOEditingContext childContext = new EOEditingContext( mainContext ); + EODataSource dataSource = new MyDataSource( childContext ); + EODisplayGroup displayGroup = new EODisplayGroup(); + displayGroup.setDataSource( dataSource ); + displayGroup.fetch(); +^>> + +* What's next? + +This document gives you a very basic overview and brief introduction to the major players in the wotonomy user interface framework. You'll probably want to check out the {-API Reference@http://wotonomy.sourceforge.net/docs-} + next. \ No newline at end of file diff --git a/projects/net.wotonomy.all/src/site/resources/images/standard-mvc.gif b/projects/net.wotonomy.all/src/site/resources/images/standard-mvc.gif new file mode 100644 index 0000000..5528e07 Binary files /dev/null and b/projects/net.wotonomy.all/src/site/resources/images/standard-mvc.gif differ diff --git a/projects/net.wotonomy.all/src/site/resources/images/wotonomy-er.gif b/projects/net.wotonomy.all/src/site/resources/images/wotonomy-er.gif new file mode 100644 index 0000000..48560cb Binary files /dev/null and b/projects/net.wotonomy.all/src/site/resources/images/wotonomy-er.gif differ diff --git a/projects/net.wotonomy.all/src/site/resources/images/wotonomy-mvc.gif b/projects/net.wotonomy.all/src/site/resources/images/wotonomy-mvc.gif new file mode 100644 index 0000000..52eb211 Binary files /dev/null and b/projects/net.wotonomy.all/src/site/resources/images/wotonomy-mvc.gif differ diff --git a/projects/net.wotonomy.all/src/site/resources/index-old.html b/projects/net.wotonomy.all/src/site/resources/index-old.html new file mode 100644 index 0000000..d21e8c1 --- /dev/null +++ b/projects/net.wotonomy.all/src/site/resources/index-old.html @@ -0,0 +1,105 @@ + + + +wotonomy + + + + + +

wotonomy

+

+Wotonomy is a pure java user interface framework for building both web and gui applications. It is heavily inspired by the NeXT and OpenStep APIs and it includes clean-room implementations of portions of the WebObjects and JavaClient frameworks. +

+

Useful links

+ + + + +

Frequently Asked Questions

+

I'm a Java developer: what's in it for me?

+

+Java developers will find the design patterns in wotonomy useful for reducing the complexities of data-driven application development. Swing developers will additionally find a number of useful Swing components and utilities. +

+ +

I'm an OpenStep developer: what's in it for me?

+

+OpenStep developers will find that they can use familiar APIs while still writing pure java applications. Wotonomy provides clean-room and API-compatible implementations of the EOInterface/EOControl, WebObjects, and Foundation frameworks that interoperate with and extend the Java Swing, Servlet, and Collections packages respectively. Wotonomy tries to be as API-compatible as possible while still tightly integrating with Java. +

+ +

What's the current status?

+

+The current status of the framework is alpha. The user interface package is suitable for simple to moderate application development and has working associations for all Swing components and a display group that supports data sources, sorting, and delegates. Editing contexts are in development. The web package is a proof-of-concept implementation that currently only supports WOString. The foundation package has pure java ports of all classes needed by the ui and web packages, including NSArray and NSDictionary classes that implement the List and Map interfaces respectively. +

+ +

Why not use GNUStep?

+

+GNUStep is awesome, but it's an Objective-C framework. The new Java bridge is great, but even Apple is backpedaling on that approach with their pure-Java version of WebObjects, due to performance problems and conceptual difficulties in development. +

+

+Wotonomy has the same license (GNU Lesser Public License), and is written completely in Java. +

+ +

Why not use WebObjects?

+

+Yes, you should if at all possible. Then you'll get tools like InterfaceBuilder and EOModeler and ProjectBuilder and you'll be even more productive. But some of us have to deploy applications on Windows or Linux or MacOS9. +

+

+It's not an issue of free beer: WebObjects is worth at least ten times the price (which is currently way cheap at ~$500). It's the restrictions on deployment that create the problem. +

+

+The other problem is political. Perhaps if enough people adopt wotonomy for their Java projects, they'll see the advantage of investing in Apple technology. Additionally, applications using wotonomy will be extremely portable to Apple platforms. +

+ +

Did you say MacOS9?

+

+Yes, it should be possible to get wotonomy running on JDK1.1, although it doesn't work right now. The collections packages would have to be renamed and the weak references would have to be looked at, but it looks do-able. +

+

+If your application used only NSArrays and NSDictionaries and no java2 collections, you could just link your app against a version of the wotonomy library built for 1.1 and it will just work. +

+
+ + + +
+
+
+This document was generated using AFT v5.06 +
+ + + diff --git a/projects/net.wotonomy.all/src/site/site.xml b/projects/net.wotonomy.all/src/site/site.xml new file mode 100644 index 0000000..65ae393 --- /dev/null +++ b/projects/net.wotonomy.all/src/site/site.xml @@ -0,0 +1,37 @@ + + + + ${project.name} + + + http://sourceforge.net/sflogo.php?group_id=12936&type=1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/net.wotonomy.datastore/.classpath b/projects/net.wotonomy.datastore/.classpath new file mode 100644 index 0000000..f47e1ff --- /dev/null +++ b/projects/net.wotonomy.datastore/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/projects/net.wotonomy.datastore/.cvsignore b/projects/net.wotonomy.datastore/.cvsignore new file mode 100644 index 0000000..e519f48 --- /dev/null +++ b/projects/net.wotonomy.datastore/.cvsignore @@ -0,0 +1,2 @@ +target +cobertura.ser diff --git a/projects/net.wotonomy.datastore/.project b/projects/net.wotonomy.datastore/.project new file mode 100644 index 0000000..d45d043 --- /dev/null +++ b/projects/net.wotonomy.datastore/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.datastore + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.core.prefs b/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..2bd49d4 --- /dev/null +++ b/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Sun Feb 19 13:09:52 EST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.ui.prefs b/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..89db986 --- /dev/null +++ b/projects/net.wotonomy.datastore/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Sun Feb 19 13:09:52 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/projects/net.wotonomy.datastore/pom.xml b/projects/net.wotonomy.datastore/pom.xml new file mode 100644 index 0000000..41acf69 --- /dev/null +++ b/projects/net.wotonomy.datastore/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-datastore + Wotonomy - Object Datastore + jar + + + ${project.groupId} + wotonomy-web + ${project.version} + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.datastore + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.datastore + + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataIndex.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataIndex.java new file mode 100644 index 0000000..89fa1eb --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataIndex.java @@ -0,0 +1,111 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.Serializable; +import java.util.List; + +/** +* A DataIndex maintains a list of objects associated with values. +* The objects can then be retrieved based on the values. This class +* should not be much more complex than a simple map or list because +* the DataSoup is responsible for populating it. +*/ +public interface DataIndex extends Serializable +{ + /** + * Gets the name of this index. The DataSoup uses this to + * uniquely refer to this index. + * @return The name of this index. + */ + public String getName(); + + /** + * The property managed by this index. This is the property + * used when the DataSoup builds and rebuilds this index. + * @return The property managed by this index. + */ + public String getProperty(); + + /** + * Adds an object to be associated with the specified value. + * @param anObject A data object, usually but not always a DataKey. + * @param newValue The property value to be associated with the data object. + * @return The data object that was inserted, or null if an error occurred. + */ + public Object addObject( Object anObject, Object newValue ); + + /** + * Updates an object previously associated with the specified + * value to be associated with the specified new value. + * @param anObject A data object, usually but not always a DataKey. + * @param oldValue The value currently associated with the data object. + * @param newValue The value to be associated with the data object. + * @return The data object that was updated, or null if an error occurred. + */ + public Object updateObject( Object anObject, + Object oldValue, Object newValue ); + + /** + * Removes an object from the index. + * @param anObject A data object, usually but not always a DataKey. + * @param oldValue The value currently associated with the data object. + * @return The data object that was removed, or null if not found or error. + */ + public Object removeObject( Object anObject, Object oldValue ); + + /** + * Removes all objects from the index. Usually called before + * rebuilding the index. + */ + public void clear(); + + /** + * Returns all objects in the index whose associated values fall + * between the two specified values, inclusive. + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A List of the matching objects, ordered in increasing + * value, or null for invalid query parameters or other error. + */ + public List query( Object beginValue, Object endValue ); + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:46:50 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataKey.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataKey.java new file mode 100644 index 0000000..6fe574f --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataKey.java @@ -0,0 +1,142 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.Serializable; + +import net.wotonomy.foundation.internal.ValueConverter; + +public class DataKey implements Comparable, Serializable, Cloneable +{ + static final long serialVersionUID = 8421127539579065705L; + + Long key; + + public DataKey() + { + key = new Long( 0 ); + } + + /** + * Converts string representation to new object. + */ + public DataKey( String aString ) + { + this(); + setKeyString( aString ); + } + + public int hashCode() + { + return key.intValue(); + } + + public void increment() + { + key = new Long( key.longValue() + 1 ); + } + + public Object clone() + { + return new DataKey( this.toString() ); + } + + public String toString() + { + return key.toString(); + } + + public String getKeyString() + { + return key.toString(); + } + + public void setKeyString( String aString ) + { + Long parsed = ValueConverter.getLong( aString ); + if ( parsed != null ) key = parsed; + } + + public boolean equals( Object anObject ) + { + if ( anObject instanceof String ) + { + if ( toString().equals( anObject ) ) + { + return true; + } + } + if ( ! ( anObject instanceof DataKey ) ) return false; + return key.equals( ((DataKey)anObject).key ); + } + + public int compareTo( Object anObject ) + { + if ( anObject instanceof String ) + { + if ( toString().equals( anObject ) ) + { + return 0; + } + } + if ( ! ( anObject instanceof DataKey ) ) + { + Long converted = (Long) ValueConverter.getLong( anObject ); + if ( converted != null ) + { + return (int) ( key.longValue() - converted.longValue() ); + } + return 0; + }; + return (int) ( key.longValue() - ((DataKey)anObject).key.longValue() ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/14 19:29:38 chochos + * minor cleanup (imports, static method calls, etc) + * + * Revision 1.3 2001/02/23 23:44:44 mpowers + * Fixes for hashcode to ensure proper key comparison. + * + * Revision 1.2 2001/02/15 21:12:41 mpowers + * Added accessors for key throughout the api. This breaks compatibility. + * insertObject now returns the permanent key for the newly created object. + * The old way returned a copy of the object which was an additional read + * that was often ignored. Now you can read it only if you need it. + * Furthermore, there was not other way of getting the permanent key. + * + * Revision 1.1.1.1 2000/12/21 15:47:04 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataSoup.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataSoup.java new file mode 100644 index 0000000..3c37d43 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataSoup.java @@ -0,0 +1,257 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.util.Collection; + +public interface DataSoup +{ + /** + * Adds the specified object to the soup and returns the key + * for the new object by which it may be subsequently retrieved. + * Null indicates an error, probably due to serialization. + * @param anObject Object to be added to soup. + * @return The unique identifier used for the new object. + */ + public DataKey addObject( Object anObject ); + + /** + * Removes the specified object from the soup and returns + * the removed object as read from the soup (which is the + * original copy of the object). Null indicates object not found. + * @param aKey A key for an object to be removed. + * @return The object that was removed, or null if not found or error. + */ + public Object removeObject( DataKey aKey ); + + /** + * Updates the specified object and returns the object + * as updated. Null indicates an error writing the object. + * @param aKey A key for an object to be updated. + * @param aKey The new object for that key. + * @return A copy of the updated object, possibly updated, + * or null if not found or error. + */ + public Object updateObject( DataKey aKey, Object updatedObject ); + + /** + * Gets object from data store whose identifier is equal + * to the specified object. + * @param aKey A key for an object to be retrieved. + * @return The corresponding object from the soup. + */ + public Object getObjectByKey( DataKey aKey ); + + /** + * Registers an object that may or may not be created + * later, returning a temporary but uniquely identifiable + * key. The key will be replaced with a permanent key when + * the object is created with addObject(). + * @param anObject An object to be registered. + * @return A temporary key for this object. + */ + public DataKey registerTemporaryObject( Object anObject ); + + // index management + + /** + * Adds an index to the soup. + * @param aName The string identifier for this index. + * @param aProperty The property on which this index will be based. + */ + public void addIndex( String aName, String aProperty ); + + /** + * Deletes the specified index from the soup. + * @param aName The string identifier for the index to be removed. + */ + public void removeIndex( String aName ); + + /** + * Gets a collection of all indices in this soup. + * @return A collection of all indices in this soup. + */ + public Collection getAllIndices(); + + // relationship management + + /** + * Adds a relation to entries in another soup. + * @param aProperty The property on which this relation will be based. + * @param aSoup The name of the soup to be related in this store. + */ +// public void addRelation( String aProperty, String aSoup ); + + /** + * Deletes the specified relation to entries in another soup. + * @param aProperty The property on which this relation will be based. + * @param aSoup The name of the soup to be related in this store. + */ +// public void removeRelation( String aProperty, String aSoup ); + + /** + * Gets a collection of all relations in this soup. + * @return A collection of all relation in this soup. + */ +// public Collection getAllRelations(); + + // queries + + /** + * Returns an empty data view, suitable for creating + * new entries in the soup. + * @return A DataView containing no entries. + */ + public DataView createView(); + + /** + * Queries by the specified pre-generated index, if it exists. + * Will return objects whose values for the indexed property + * fall between the two values inclusive. + * Otherwise, falls through to queryByProperty. + * @param anIndexName The index to be queried. + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView queryByIndex( + String anIndexName, Object beginKey, Object endKey ); + + /** + * Generates an index based on the specified property + * and then executes the query. + * Will return objects whose values for the specified property + * fall between the two values inclusive. + * @param aPropertyName The property to be queried. If null, + * will query the objects directly with queryObjects(). + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView queryByProperty( + String aPropertyName, Object beginKey, Object endKey ); + + /** + * Generates an index based on the values of the objects themselves + * and then executes the query. + * Will return objects whose values fall between the two values inclusive. + * @param beginValue The beginning value, or null for all values + * up to and including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView queryObjects( Object beginKey, Object endKey ); + + /** + * Returns a view containing the objects for the specified keys. + * @param aKeyList A collection of keys to be placed in the view. + * @return A DataView containing the objects for the corresponding + * keys, in the order in which the keys are returned from the collection. + */ + public DataView queryByKeys( Collection aKeyList ); + + /** + * As queryByIndex, but with objects returned in reverse order. + * @param anIndexName The index to be queried. + * @param beginValue The beginning value, or null for all values + * up to and including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryByIndex( + String anIndexName, Object beginKey, Object endKey ); + + /** + * As queryByProperty, but with objects returned in reverse order. + * @param aPropertyName The property to be queried. If null, + * will query the objects directly with queryObjects(). + * @param beginValue The beginning value, or null for all values + * up to and including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryByProperty( + String aPropertyName, Object beginKey, Object endKey ); + + /** + * As queryObjects, but with objects returned in reverse order. + * @param beginValue The beginning value, or null for all values + * up to and including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryObjects( Object beginKey, Object endKey ); + +// public void addIndex( String aName, DataIndex anIndex ) {} +// public void removeIndex( String aName ) {} +// public void addTaggedObject( String aKey, Serializable anObject ) +// public Object getTaggedObject( String aKey ); +// public void removeTaggedObject( String aKey ); +// public void setMetaData( +// String aMetaProperty, Serializable aValue, Serializable anObject ); + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/14 19:29:38 chochos + * minor cleanup (imports, static method calls, etc) + * + * Revision 1.3 2001/03/05 22:12:11 mpowers + * Created the control package for a datastore-specific implementation + * of EOObjectStore. + * + * Revision 1.2 2001/02/15 21:12:41 mpowers + * Added accessors for key throughout the api. This breaks compatibility. + * insertObject now returns the permanent key for the newly created object. + * The old way returned a copy of the object which was an additional read + * that was often ignored. Now you can read it only if you need it. + * Furthermore, there was not other way of getting the permanent key. + * + * Revision 1.1.1.1 2000/12/21 15:47:05 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataStore.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataStore.java new file mode 100644 index 0000000..28fbc90 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataStore.java @@ -0,0 +1,94 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.File; +import java.io.Serializable; +import java.util.Iterator; + +public class DataStore implements Serializable +{ + protected File homeDirectory; + + public DataStore( String aPath ) + { + homeDirectory = new File( aPath ); + + // if specified directory does not exist + if ( ! homeDirectory.exists() ) + { + homeDirectory.mkdirs(); + } + + // if existing path is a file, exit with error + if ( homeDirectory.isDirectory() ) + { + new RuntimeException( "DataStore: Specified directory is a file." ); + } + } + + public File getHomeDirectory() + { + return homeDirectory; + } + + + public DataSoup getSoupForName( String aName ) + { + return null; + } + public void removeSoup( DataSoup aSoup ) + { + // FIXME + } + public Iterator getAllSoups() + { + return null; + } + + public static void main( String[] argv ) + { + new DataStore( "/Local/Users/michael/Projects/test/data" ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/03/05 22:12:11 mpowers + * Created the control package for a datastore-specific implementation + * of EOObjectStore. + * + * Revision 1.1.1.1 2000/12/21 15:47:05 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataView.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataView.java new file mode 100644 index 0000000..fec9cfc --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DataView.java @@ -0,0 +1,111 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.util.List; +import java.util.Observer; + +public interface DataView extends List +{ +// public void newQuery( String aProperty, Object beginKey, Object endKey ); +// public void addQuery( String aProperty, Object beginKey, Object endKey ); +// public void removeQuery( String aProperty, Object beginKey, Object endKey ); +// public void retainQuery( String aProperty, Object beginKey, Object endKey ); + + /** + * This method is called to notify the DataView + * that one of its objects has been modified and + * should be updated when the view is committed. + */ + public void update(Object o); + + /** + * This method is called commit all changes to + * the DataView to its data store. The list + * elements may be refreshed from the datastore, + * although the list itself should remain unchanged. + * @return True if the commit was successful, + * otherwise false. + */ + public boolean commit(); + + /** + * Called to add the specified observer to the + * list of observers that should receive notifications + * when the view if modified. DataViews notify + * when objects are added, updated, or deleted, + * passing the affected object as the parameter + * to the Observer's notify method. + * @param o The observer to add. + */ + public void addObserver(Observer o); + + /** + * Called to remove the specified observer from the + * list of observers that should receive notifications + * when the view if modified. + * @param o The observer to delete. + */ + public void deleteObserver(Observer o); + + /** + * Called to clear the list of observers that should + * receive notifications when the view if modified. + */ + public void deleteObservers(); + + /** + * Returns the key for the specified object. + * If the object is not in the view, returns null. + */ + public DataKey getKeyForObject( Object anObject ); + + /** + * Returns the object for the specified key. + * If the key is not in the view, returns null. + */ + public Object getObjectForKey( DataKey aKey ); +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/15 21:12:41 mpowers + * Added accessors for key throughout the api. This breaks compatibility. + * insertObject now returns the permanent key for the newly created object. + * The old way returned a copy of the object which was an additional read + * that was often ignored. Now you can read it only if you need it. + * Furthermore, there was not other way of getting the permanent key. + * + * Revision 1.1.1.1 2000/12/21 15:47:05 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultComparator.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultComparator.java new file mode 100644 index 0000000..b97b8a1 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultComparator.java @@ -0,0 +1,145 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.Serializable; +import java.util.Comparator; + +import net.wotonomy.foundation.internal.ValueConverter; + +/** +* DefaultComparator exists to compare basic java +* primitive wrappers, since these classes don't +* implement Comparable in jdk 1.1.x. Also uses +* ValueConverter to try to match types for comparison. +*/ +public class DefaultComparator implements Comparator, Serializable +{ + public int compare(Object o1, Object o2) + { +// System.out.println( "compare: " + o1 + " : " + o1.getClass() + " : " + o2 + " : " + o2.getClass() ); +/* + if ( ( o1 instanceof Comparable ) && ( o2 instanceof Comparable ) ) + { + return ((Comparable)o1).compareTo( o2 ); + } +*/ + if ( ( o1 instanceof Number ) && ( o2 instanceof Number ) ) + { + // TODO: special case for each type would be faster + return (int) + ( ((Number)o1).doubleValue() - ((Number)o2).doubleValue() ); + } + + if ( o1 instanceof StringBuffer ) + { + o1 = o1.toString(); + } + if ( o2 instanceof StringBuffer ) + { + o2 = o2.toString(); + } + + if ( ( o1 instanceof String ) && ( o2 instanceof String ) ) + { + return ((String)o1).compareTo( ((String)o2) ); + } + + if ( ( o1 instanceof Character ) && ( o2 instanceof Character ) ) + { + return (int) + ((Character)o1).charValue() - ((Character)o2).charValue(); + } + + if ( ( o1 instanceof Byte ) && ( o2 instanceof Byte ) ) + { + return (int) + ((Byte)o1).byteValue() - ((Byte)o2).byteValue(); + } + + if ( ( o1 instanceof Boolean ) && ( o2 instanceof Boolean ) ) + { + if ( o1.equals( o2 ) ) return 0; + + // presumably TRUE is greater than FALSE + if ( o1.equals( Boolean.TRUE ) ) return 1; + return -1; + } + + // handle all NULL cases: NULL is less than anything else. + if ( ( o1 == null ) && ( o2 == null ) ) return 0; + if ( ( o1 == null ) && ( o2 != null ) ) return -1; + if ( ( o2 == null ) && ( o1 != null ) ) return 1; + + if ( o1.getClass() != o2.getClass() ) + { + Object convertedValue; + + if ( ! ( o2 instanceof String ) ) + // (string should be lowest common demoninator, if possible) + { + // convert first to second's type + convertedValue = + ValueConverter.convertObjectToClass( o1, o2.getClass() ); + if ( convertedValue != null ) + { + return compare( convertedValue, o2 ); + } + } + + // convert second to first's type + convertedValue = + ValueConverter.convertObjectToClass( o2, o1.getClass() ); + if ( convertedValue != null ) + { + return -1 * compare( convertedValue, o1 ); // reverse result + } + } + + // we tried really hard, but these values are incomparable: + // we'll consider them equal. + return 0; + } + + public boolean equals(Object obj) + { + return (obj == this); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:08 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataIndex.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataIndex.java new file mode 100644 index 0000000..a8ede78 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataIndex.java @@ -0,0 +1,239 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeMap; + +/** +* This implementation of DataIndex wraps a TreeMap and +* adds the ability to contain objects with duplicate keys. +*/ +public class DefaultDataIndex implements DataIndex +{ + static final long serialVersionUID = -3759982714240822885L; + + protected String name; + protected String property; + private TreeMap treeMap; + private Comparator comparator; + + public DefaultDataIndex() + { + comparator = new DefaultComparator(); + setTreeMap( new TreeMap( new DefaultComparator() ) ); + } + + public DefaultDataIndex( String aName, String aProperty ) + { + this(); + setName( aName ); + setProperty( aProperty ); + } + + // included for xml serialization + public Comparator getComparator() { return comparator; } + public void setComparator( Comparator aComparator ) + { + comparator = aComparator; + // set comparator and copy contents + TreeMap map = getTreeMap(); + setTreeMap( new TreeMap( comparator ) ); + getTreeMap().putAll( map ); + + } + + public String getName() { return name; }; + public void setName( String aName ) { name = aName; } + public String getProperty() { return property; }; + public void setProperty( String aProperty ) { property = aProperty; } + public TreeMap getTreeMap() { return treeMap; } + public void setTreeMap( TreeMap aMap ) { treeMap = aMap; } + + public List query( Object beginValue, Object endValue ) + { +//System.out.println( "DefaultDataIndex.query: " + beginValue + " : " + endValue ); + List result = new LinkedList(); + if ( endValue == null ) + { + if ( beginValue == null ) + { + // begin and end are null, return entire set + populateListFromIterator( result, treeMap.values().iterator() ); + return result; + } + + // only end is null, return all starting from beginValue + populateListFromIterator( result, + treeMap.tailMap( beginValue ).values().iterator() ); + return result; + } + else + if ( beginValue == null ) + { + // only begin is null, return all ending with endValue + populateListFromIterator( result, + treeMap.headMap( endValue ).values().iterator() ); + } + else + { + // begin and end are specified, return all inclusive + populateListFromIterator( result, + treeMap.subMap( beginValue, endValue ).values().iterator() ); + } + + // append endValue results, so it's inclusive + Object o = treeMap.get( endValue ); + if ( o != null ) + { + if ( o instanceof DuplicateList ) + { + populateListFromIterator( result, + ((DuplicateList)o).iterator() ); + } + else + { + result.add( o ); + } + } + + // return complete result + return result; + } + + protected void populateListFromIterator( List aList, Iterator it ) + { + Object o; + while ( it.hasNext() ) + { + o = it.next(); + if ( o instanceof DuplicateList ) + { + populateListFromIterator( + aList, ((DuplicateList)o).iterator() ); + } + else + { + aList.add( o ); + } + } + } + + public Object addObject( Object anObject, Object newValue ) + { + Object o = treeMap.get( newValue ); + if ( o != null ) + { + if ( o instanceof DuplicateList ) + { + ((DuplicateList)o).add( anObject ); + return anObject; + } + + DuplicateList list = new DuplicateList(); + list.add( o ); + list.add( anObject ); + anObject = list; + + } +if ( anObject == null ) new RuntimeException().printStackTrace(); + + treeMap.put( newValue, anObject ); + return anObject; + } + + public Object updateObject( Object anObject, + Object oldValue, Object newValue ) + { + removeObject( anObject, oldValue ); + return addObject( anObject, newValue ); + } + + public Object removeObject( Object anObject, Object oldValue ) + { + Object o = treeMap.get( oldValue ); + if ( o != null ) + { + if ( o instanceof DuplicateList ) + { + // remove this item from the list + DuplicateList list = (DuplicateList) o; + list.remove( anObject ); + + // if there are still duplicates, return + if ( list.size() > 1 ) + return anObject; + + // else, list size must be one + if ( list.size() == 0 ) + { + System.out.println( "DefaultDataIndex.deleteObject: " + oldValue + + " : list size is 1 : this should never happen." ); + return null; + } + + // replace existing list with remaining item from list + treeMap.remove( oldValue ); + treeMap.put( oldValue, list.getFirst() ); + return anObject; + } + + // otherwise, proceed normally + treeMap.remove( oldValue ); + } + return anObject; + } + + public void clear() + { + treeMap.clear(); + } + + public String toString() + { + return "DefaultDataIndex: " + name + " : " + property + " : " + treeMap.toString(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/14 19:29:38 chochos + * minor cleanup (imports, static method calls, etc) + * + * Revision 1.1.1.1 2000/12/21 15:47:11 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataView.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataView.java new file mode 100644 index 0000000..ca76252 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DefaultDataView.java @@ -0,0 +1,557 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Observable; + +public class DefaultDataView extends Observable + implements DataView +{ + protected DataSoup backingSoup; + protected List objectList; + protected List keyList; + protected List addedObjectList; + protected List removedObjectList; + protected List addedKeyList; + protected List removedKeyList; + protected Collection updatedObjects; + protected boolean fullyLoaded; + + DefaultDataView( DataSoup aSoup, Collection aKeyList ) + { + backingSoup = aSoup; + objectList = new ArrayList(); + keyList = new ArrayList(); + addedObjectList = new ArrayList(); + removedObjectList = new ArrayList(); + addedKeyList = new ArrayList(); + removedKeyList = new ArrayList(); + updatedObjects = new LinkedList(); + fullyLoaded = false; + + setKeyList( aKeyList ); + } + + void setKeyList( Collection aCollection ) + { + fullyLoaded = false; + addedObjectList.clear(); + removedObjectList.clear(); + addedKeyList.clear(); + removedKeyList.clear(); + updatedObjects.clear(); + keyList.clear(); + objectList.clear(); + if ( ( aCollection == null ) || ( aCollection.size() == 0 ) ) + { + return; + } + keyList.addAll( aCollection ); + for ( int i = 0; i < keyList.size(); i++ ) + { + objectList.add( null ); + } + } + + public Object get( int i ) + { + if ( i > keyList.size() ) return null; + + Object o = objectList.get( i ); + if ( o == null ) + { + Object key = keyList.get( i ); + if ( key == null ) return null; // FIXME!! + + //NOTE: this is the gateway for getting object from the soup + o = backingSoup.getObjectByKey( (DataKey) key ); + + objectList.set( i, o ); + } + return o; + } + + public int indexOf( Object o ) + { + if ( o == null ) return -1; + for ( int i = 0; i < size(); i++ ) + { + if ( o.equals( get( i ) ) ) + { + return i; + } + } + return -1; + } + + private int indexOfIdenticalObject( Object o ) + { + if ( o == null ) return -1; + for ( int i = 0; i < size(); i++ ) + { + if ( o == get( i ) ) + { + return i; + } + } + return -1; + } + + public int lastIndexOf( Object o ) + { + if ( o == null ) return -1; + int lastIndex = -1; + for ( int i = 0; i < size(); i++ ) + { + if ( o.equals( get( i ) ) ) + { + lastIndex = i; + } + } + return lastIndex; + } + + protected void loadAllObjects() + { + if ( fullyLoaded ) return; + for ( int i = 0; i < keyList.size(); i++ ) + { + get( i ); + } + fullyLoaded = true; + } + + // convenience to return the first object, or null. + public Object getObject() + { + return get( 0 ); + } + + // marked object as updated + public void update( Object anObject ) + { + if ( contains( anObject ) ) + { + if ( ! addedObjectList.contains( anObject ) ) + { + updatedObjects.add( anObject ); + } + } + + // notification + setChanged(); + notifyObservers( anObject ); + } + + // DefaultDataViews know their parent soup to perform the query + // and take the subset + public DataView query( + String aProperty, Object beginKey, Object endKey ) { return this; } + + public boolean commit() + { + int index; + Object o; + DataKey key; + for ( int i = 0; i < addedObjectList.size(); i++ ) + { + o = addedObjectList.get(i); + key = backingSoup.addObject( o ); + index = indexOfIdenticalObject( o ); + keyList.set( index, key ); + } + addedObjectList.clear(); + addedKeyList.clear(); + for ( int i = 0; i < removedObjectList.size(); i++ ) + { + backingSoup.removeObject( (DataKey) removedKeyList.get(i) ); + } + removedObjectList.clear(); + removedKeyList.clear(); + + int i; + Iterator it = updatedObjects.iterator(); + while ( it.hasNext() ) + { + i = objectList.indexOf( it.next() ); + backingSoup.updateObject( + (DataKey) keyList.get(i), objectList.get(i) ); + } + updatedObjects.clear(); + + // notification + setChanged(); + notifyObservers( this ); + + return true; + } + + public DataKey getKeyForObject( Object anObject ) + { + int index = indexOfIdenticalObject( anObject ); + if ( index == -1 ) return null; + return (DataKey) keyList.get( index ); + } + + public Object getObjectForKey( DataKey aKey ) + { + int index = keyList.indexOf( aKey ); + if ( index == -1 ) return null; + return get( index ); + } + + // interface Collection + + public int size () { return keyList.size(); } + public boolean isEmpty () { return keyList.isEmpty(); } + public void clear () { setKeyList( null ); }; + public int hashCode() { return keyList.hashCode(); }; + + public boolean equals (Object o) + { + if ( ! ( o instanceof DefaultDataView ) ) return false; + return keyList.equals( ((DefaultDataView)o).keyList ); + } + + public boolean contains (Object o) + { + loadAllObjects(); + return objectList.contains(o); + } + + public boolean containsAll (Collection c) + { + loadAllObjects(); + return objectList.containsAll( c ); + } + + public boolean add (Object o) + { + // if previously removed, restore to list + if ( removedObjectList.contains( o ) ) + { + int index = removedObjectList.indexOf( o ); + removedObjectList.remove( index ); + Object key = removedKeyList.remove( index ); + objectList.add( o ); + keyList.add( key ); + + // notification + setChanged(); + notifyObservers( o ); + return true; + } + + // register and add to lists + Object key = backingSoup.registerTemporaryObject( o ); + addedObjectList.add( o ); + addedKeyList.add( key ); + objectList.add( o ); + keyList.add( key ); + + // notification + setChanged(); + notifyObservers( o ); + return true; + } + + public Object remove( int index ) + { + Object result = get( index ); + if ( remove( result ) ) + { + return result; + } + return null; + } + + public boolean remove (Object o) + { + loadAllObjects(); + + int index = objectList.indexOf( o ); + if ( index == -1 ) return false; + + objectList.remove( index ); + Object key = keyList.remove( index ); + + if ( updatedObjects.contains( o ) ) + { + updatedObjects.remove( o ); + } + + // if not previously added, track removal + if ( ! ( removedObjectList.contains( o ) ) ) + { + removedObjectList.add( o ); + removedKeyList.add( key ); + } + + // notification + setChanged(); + notifyObservers( o ); + + return true; + } + + /** + * Set completely replaces the object at the specified + * index with the specified object. The new object is not + * marked as inserted, and the old object is not marked + * as deleted: the new object will be stored in the soup + * with the same key. The old object is returned. + */ + public Object set( int index, Object element ) + { + Object result = objectList.set( index, element ); + update( element ); + return result; + } + + public void add( int index, Object o ) + { + // if previously removed, restore to list + if ( removedObjectList.contains( o ) ) + { + int i = removedObjectList.indexOf( o ); + removedObjectList.remove( i ); + Object key = removedKeyList.remove( i ); + objectList.add( index, o ); + keyList.add( index, key ); + + // notification + setChanged(); + notifyObservers( o ); + return; + } + + // register and add to lists + Object key = backingSoup.registerTemporaryObject( o ); + addedObjectList.add( o ); + addedKeyList.add( key ); + objectList.add( index, o ); + keyList.add( index, key ); + + // notification + setChanged(); + notifyObservers( o ); + } + + public boolean addAll (Collection c) + { + boolean result = true; + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + result = result && add( it.next() ); + } + return result; + } + + public boolean addAll (int index, Collection c) + { + int originalSize = size(); + boolean result = true; + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + add( index, it.next() ); + } + return ( originalSize + c.size() == size() ); + } + + public boolean removeAll (Collection c) + { + boolean result = true; + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + result = result && remove( it.next() ); + } + return result; + } + + public boolean retainAll (Collection c) + { + removeAll( new ArrayList( objectList ) ); + return addAll( c ); + } + + public List subList( int fromIndex, int toIndex ) + { + List result = new LinkedList(); + for ( int i = fromIndex; i < toIndex; i++ ) + { + result.add( get( i ) ); + } + return result; + } + + public Iterator iterator() + { + loadAllObjects(); + return objectList.iterator(); + +/* // uncomment to enable on-demand loading + return new Iterator() + { + int index = 0; + public boolean hasNext() { return ( index + 1 < keyList.size() ); } + public Object next() { + return get( index++ ); } + public void remove() + { + Object o = get( index ); + if ( o != null ) DefaultDataView.this.remove( o ); + } + }; +*/ + } + + public ListIterator listIterator() + { + return new DataViewIterator( this ); + } + + public ListIterator listIterator( int index ) + { + return new DataViewIterator( this, index ); + } + + public Object[] toArray () + { + loadAllObjects(); + return objectList.toArray(); + } + public java.lang.Object[] toArray (Object[] array) + { + loadAllObjects(); + return objectList.toArray( array ); + } + + protected class DataViewIterator implements ListIterator + { + DataView theView; + int currentIndex; + + //TODO: should track current object in addition to index + // to track external changes to the view. (or should be listener) + Object currentObject; + + public DataViewIterator( DataView aView ) + { + this( aView, 0 ); + } + + public DataViewIterator( DataView aView, int index ) + { + theView = aView; + if ( theView.size() > index ) + { + currentIndex = index; + currentObject = theView.get( currentIndex ); + } + else + { + index = -1; + currentObject = null; + } + } + + public void add( Object o ) + { + currentIndex++; + theView.add( currentIndex, o ); + } + + public boolean hasNext() + { + return ( theView.size() > currentIndex + 1 ); + } + + public boolean hasPrevious() + { + return ( currentIndex > -1 ); + } + + public Object next() + { + return theView.get( ++currentIndex ); + } + + public int nextIndex() + { + return currentIndex + 1; + } + + public Object previous() + { + return theView.get( currentIndex-- ); + } + + public int previousIndex() + { + return currentIndex; + } + + public void remove() + { + theView.remove( currentIndex-- ); + } + + public void set( Object o ) + { + theView.set( currentIndex, o ); + } + + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/15 21:12:41 mpowers + * Added accessors for key throughout the api. This breaks compatibility. + * insertObject now returns the permanent key for the newly created object. + * The old way returned a copy of the object which was an additional read + * that was often ignored. Now you can read it only if you need it. + * Furthermore, there was not other way of getting the permanent key. + * + * Revision 1.1.1.1 2000/12/21 15:47:14 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DuplicateList.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DuplicateList.java new file mode 100644 index 0000000..ed47b24 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/DuplicateList.java @@ -0,0 +1,49 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.util.LinkedList; + +/** +* DuplicateList is a marker class used to store values +* with duplicate keys in the DataIndex TreeMap. +*/ +public class DuplicateList extends LinkedList +{ +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:14 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/FileSoup.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/FileSoup.java new file mode 100644 index 0000000..45cc9d8 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/FileSoup.java @@ -0,0 +1,468 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.internal.Introspector; + +abstract public class FileSoup implements DataSoup +{ + public static final String INDEX_SUFFIX = ".idx"; + public static final String MAP_SUFFIX = ".map"; + private static final String ID_ID = "id"; + private static final String IDENTITY_PROPERTY = "__SELF__"; + + protected DataKey nextUniqueIdentifier; + protected File homeDirectory; + protected Map indices; + + public FileSoup( String aPath ) + { + homeDirectory = new File( aPath ); + + // if specified directory does not exist + if ( ! homeDirectory.exists() ) + { + homeDirectory.mkdirs(); + } + + // if existing path is a file, exit with error + if ( homeDirectory.isDirectory() ) + { + new RuntimeException( "DataStore: Specified directory is a file." ); + } + + // read indices + DataIndex index; + indices = new HashMap(); + String[] files = getHomeDirectory().list(); + for ( int i = 0; i < files.length; i++ ) + { + if ( files[i].endsWith( INDEX_SUFFIX ) ) + { + index = (DataIndex) readFile( files[i] ); + if ( index != null ) + { + indices.put( index.getName(), index ); + } + } + } + + // read unique identifier + nextUniqueIdentifier = (DataKey) readFile( ID_ID ); + if ( nextUniqueIdentifier == null ) + nextUniqueIdentifier = new DataKey( "1" ); + + } + + public File getHomeDirectory() + { + return homeDirectory; + } + + // index management + + public void addIndex( String aName, String aProperty ) + { + DataIndex index = new DefaultDataIndex( aName, aProperty ); + indices.put( index.getName(), index ); + buildIndex( index ); + writeIndices(); + } + + public void removeIndex( String aName ) + { + indices.remove( aName ); + writeIndices(); + } + + public Collection getAllIndices() + { + return indices.values(); + } + +// public void addIndex( String aName, Index anIndex ) {} +// public void removeIndex( String aName ) {} +// public void addTaggedObject( String aKey, Serializable anObject ) +// public Object getTaggedObject( String aKey ); +// public void removeTaggedObject( String aKey ); +// public void setMetaData( +// String aMetaProperty, Serializable aValue, Serializable anObject ); + + protected void buildIndex( DataIndex anIndex ) + { +//System.out.print( "FileSoup.buildIndex: " + anIndex.getName() + " : " ); +long millis = System.currentTimeMillis(); + + anIndex.clear(); + + int count = 0; + DataKey key; + Object o; + Object value; + String property = anIndex.getProperty(); + + String[] files = getHomeDirectory().list(); + for ( int i = 0; i < files.length; i++ ) + { + if ( ( ! files[i].equals( ID_ID.toString() ) + && ( ! files[i].endsWith( INDEX_SUFFIX ) ) ) ) + { + key = new DataKey( files[i] ); + o = getObjectByKey( key ); + value = getValueFromObject( o, property ); + anIndex.addObject( key, value ); + count++; + } + } + +//System.out.print( count + " objects: " ); +//System.out.println( System.currentTimeMillis() - millis + " milliseconds" ); + } + protected void writeIndices() + { + DataIndex index; + Iterator it = getAllIndices().iterator(); + while ( it.hasNext() ) + { + index = (DataIndex) it.next(); + writeFile( index.getName() + INDEX_SUFFIX, index ); + } + } + + // object management + + // this implementation currently uses up a valid key increment + public DataKey registerTemporaryObject( Object anObject ) + { + DataKey id = getNextKey(); + + if ( anObject instanceof UniquelyIdentifiable ) + { + ((UniquelyIdentifiable)anObject).setUniqueIdentifier( id ); + } + + return id; + } + + /** + * Adds the specified object to the soup and returns the key + * for the new object by which it may be subsequently retrieved. + * Null indicates an error, probably due to serialization. + */ + public DataKey addObject( Object anObject ) + { + DataKey id = getNextKey(); + + if ( anObject instanceof UniquelyIdentifiable ) + { // set id if necessary + ((UniquelyIdentifiable)anObject).setUniqueIdentifier( id ); + } + + writeFile( id.toString(), anObject ); + + // update indices + DataIndex index; + Iterator it = indices.values().iterator(); + while ( it.hasNext() ) + { + index = (DataIndex)it.next(); + index.addObject( id, + getValueFromObject( anObject, index.getProperty() ) ); + } + + writeIndices(); + + return id; + } + + /** + * Removes the specified object from the soup and returns + * the removed object as read from the soup (which is the + * original copy of the object). Null indicates object not found. + */ + public Object removeObject( DataKey aKey ) + { + Object existing = getObjectByKey( aKey ); + if ( existing != null ) + { + if ( ! deleteFile( aKey.toString() ) ) + { + existing = null; // return error + } + else + { + // update indices + DataIndex index; + Iterator it = indices.values().iterator(); + while ( it.hasNext() ) + { + index = (DataIndex)it.next(); + index.removeObject( aKey, + getValueFromObject( existing, index.getProperty() ) ); + } + + writeIndices(); + } + } + + return existing; + } + + /** + * Updates the specified object and returns the object + * as updated. Null indicates an error writing the object. + */ + public Object updateObject( DataKey aKey, Object updatedObject ) + { + Object existing = getObjectByKey( aKey ); + if ( existing == null ) + { + System.err.println( "FileSoup.updateObject: " + + "existing object could not be found with id: " + aKey ); + return null; + } + + Object result = null; + if ( updatedObject instanceof UniquelyIdentifiable ) + { + // update key if changed + ((UniquelyIdentifiable)updatedObject).setUniqueIdentifier( aKey ); + } + + if ( writeFile( aKey.toString(), updatedObject ) ) + { + result = updatedObject; + + // update indices + DataIndex index; + Iterator it = indices.values().iterator(); + while ( it.hasNext() ) + { + index = (DataIndex)it.next(); + index.updateObject( aKey, + getValueFromObject( existing, index.getProperty() ), + getValueFromObject( updatedObject, index.getProperty() ) ); + } + + writeIndices(); + } + +//System.out.println( "FileSoup.updateObject: " + updatedObject + " -> " + result ); + return getObjectByKey( aKey ); + } + + protected DataKey getNextKey() + { + DataKey id = (DataKey) nextUniqueIdentifier.clone(); + // while ( id isn't yet in use by the soup ) increment(); + nextUniqueIdentifier.increment(); + writeFile( ID_ID.toString(), nextUniqueIdentifier ); + return id; + } + + protected DataKey getNextTempKey() + { + return getNextKey(); + } + + /** + * Gets object from data store whose identifier is equal + * to the specified object. + */ + public Object getObjectByKey( DataKey aKey ) + { + return readFile( aKey.toString() ); + } + + // queries + + /** + * Returns an empty data view, suitable for creating + * new entries in the soup. + * @return A DataView containing no entries. + */ + public DataView createView() + { + return new DefaultDataView( this, new LinkedList() ); + } + + /** + * Queries by the specified pre-generated index, if it exists. + * Otherwise, falls through to queryByProperty. + */ + public DataView queryByIndex( + String anIndexName, Object beginKey, Object endKey ) + { + DataIndex index = (DataIndex) indices.get( anIndexName ); + + if ( index == null ) + { + return queryByProperty( anIndexName, beginKey, endKey ); + } + + return queryByKeys( index.query( beginKey, endKey ) ); + } + + /** + * Generates an index based on the specified property + * and then executes the query. + */ + public DataView queryByProperty( + String aPropertyName, Object beginKey, Object endKey ) + { + if ( aPropertyName == null ) aPropertyName = IDENTITY_PROPERTY; + DataIndex index = new DefaultDataIndex( "temporary", aPropertyName ); + buildIndex( index ); + return queryByKeys( index.query( beginKey, endKey ) ); + } + + /** + * Generates an index based on the specified property + * and then executes the query. + */ + public DataView queryObjects( Object beginKey, Object endKey ) + { + return queryByProperty( IDENTITY_PROPERTY, beginKey, endKey ); + } + + /** + * Returns a view containing the objects for the specified keys. + */ + public DataView queryByKeys( Collection aKeyList ) + { + return new DefaultDataView( this, aKeyList ); + } + + /** + * As queryByIndex, but with objects returned in reverse order. + * @param anIndexName The index to be queried. + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryByIndex( + String anIndexName, Object beginKey, Object endKey ) + { + DataIndex index = (DataIndex) indices.get( anIndexName ); + + if ( index == null ) + { + return reverseQueryByProperty( anIndexName, beginKey, endKey ); + } + + List items = index.query( endKey, beginKey ); + Collections.reverse( items ); + return queryByKeys( items ); + } + + /** + * As queryByProperty, but with objects returned in reverse order. + * @param aPropertyName The property to be queried. If null, + * will query the objects directly with queryObjects(). + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryByProperty( + String aPropertyName, Object beginKey, Object endKey ) + { + if ( aPropertyName == null ) aPropertyName = IDENTITY_PROPERTY; + DataIndex index = new DefaultDataIndex( "temporary", aPropertyName ); + buildIndex( index ); + List items = index.query( endKey, beginKey ); + Collections.reverse( items ); + return queryByKeys( items ); + } + + /** + * As queryObjects, but with objects returned in reverse order. + * @param beginValue The beginning value, or null for all values + * up to an including the end key. + * @param endValue The ending value, or null for all values + * since and including the begin key. + * @return A DataView containing the query results, or null + * for invalid query parameters. + */ + public DataView reverseQueryObjects( Object beginKey, Object endKey ) + { + return queryByProperty( IDENTITY_PROPERTY, beginKey, endKey ); + } + + public Object getValueFromObject( Object anObject, String aProperty ) + { + if ( IDENTITY_PROPERTY.equals( aProperty ) ) return anObject; + return Introspector.getValueForObject( anObject, aProperty ); + } + + // file access methods + + abstract protected boolean writeFile( String name, Object anObject ); + abstract protected Object readFile( String name ); + abstract protected boolean deleteFile( String name ); + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/14 19:29:38 chochos + * minor cleanup (imports, static method calls, etc) + * + * Revision 1.3 2001/03/05 22:12:11 mpowers + * Created the control package for a datastore-specific implementation + * of EOObjectStore. + * + * Revision 1.2 2001/02/15 21:12:41 mpowers + * Added accessors for key throughout the api. This breaks compatibility. + * insertObject now returns the permanent key for the newly created object. + * The old way returned a copy of the object which was an additional read + * that was often ignored. Now you can read it only if you need it. + * Furthermore, there was not other way of getting the permanent key. + * + * Revision 1.1.1.1 2000/12/21 15:47:20 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:36 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/SerializedFileSoup.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/SerializedFileSoup.java new file mode 100644 index 0000000..e466c78 --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/SerializedFileSoup.java @@ -0,0 +1,120 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public class SerializedFileSoup extends FileSoup +{ + public SerializedFileSoup( String aPath ) + { + super( aPath ); + } + + // file access methods + + protected boolean writeFile( String name, Object anObject ) + { + try + { + File f = new File( getHomeDirectory(), name ); + ObjectOutputStream oos = new ObjectOutputStream( + new FileOutputStream( f ) ); + oos.writeObject( anObject ); + oos.flush(); + oos.close(); + return true; + } + catch ( Exception exc ) + { + System.err.println( "SerializedFileSoup.writeFile: " + exc ); + } + + return false; + } + + protected Object readFile( String name ) + { + Object result = null; + + try + { + File f = new File( getHomeDirectory(), name ); + ObjectInputStream ois = new ObjectInputStream( + new FileInputStream( f ) ); + result = ois.readObject(); + ois.close(); + } + catch ( FileNotFoundException exc ) + { + result = null; + } + catch ( Exception exc ) + { + System.err.println( "SerializedFileSoup.readFile: " + exc ); + exc.printStackTrace(); + } + return result; + } + + protected boolean deleteFile( String name ) + { + try + { + File f = new File( getHomeDirectory(), name ); + if ( f.exists() ) + { + f.delete(); + return true; + } + } + catch ( Exception exc ) + { + System.err.println( "SerializedFileSoup.deleteFile: " + exc ); + } + + return false; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:20 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:37 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/UniquelyIdentifiable.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/UniquelyIdentifiable.java new file mode 100644 index 0000000..104932e --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/UniquelyIdentifiable.java @@ -0,0 +1,40 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +public interface UniquelyIdentifiable +{ + Object getUniqueIdentifier(); + void setUniqueIdentifier( Object id ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:20 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:37 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/XMLFileSoup.java b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/XMLFileSoup.java new file mode 100644 index 0000000..94d85fb --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/XMLFileSoup.java @@ -0,0 +1,130 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.datastore; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; + +import net.wotonomy.web.xml.XMLRPCDecoder; +import net.wotonomy.web.xml.XMLRPCEncoder; + +public class XMLFileSoup extends FileSoup +{ + public XMLFileSoup( String aPath ) + { + super( aPath ); + } + + // file access methods + + protected boolean writeFile( String name, Object anObject ) + { +//System.out.print( "writeFile: " + name + "..." ); + try + { + File f = new File( getHomeDirectory(), name ); + FileOutputStream fos = new FileOutputStream( f ); + XMLRPCEncoder encoder = new XMLRPCEncoder(); + encoder.encode( anObject, fos ); + fos.flush(); + fos.close(); + } + catch ( Exception exc ) + { + System.err.println( "XMLFileSoup.writeFile: " + exc ); + return false; + } +//System.out.println( "done." ); + return true; + } + + protected Object readFile( String name ) + { +//System.out.print( "readFile: " + name + "..." ); + Object result = null; + + try + { + File f = new File( getHomeDirectory(), name ); + FileInputStream fis = new FileInputStream( f ); + XMLRPCDecoder decoder = new XMLRPCDecoder(); + result = decoder.decode( fis, f.getAbsolutePath(), f.toURL() ); + fis.close(); + } + catch ( MalformedURLException exc ) + { + result = null; + } + catch ( IOException exc ) + { + result = null; + } +//System.out.println( "done." ); + return result; + } + + protected boolean deleteFile( String name ) + { + try + { + File f = new File( getHomeDirectory(), name ); + if ( f.exists() ) + { + f.delete(); + return true; + } + } + catch ( Exception exc ) + { + System.err.println( "XMLFileSoup.deleteFile: " + exc ); + } + + return false; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 16:26:19 cgruber + * Move non-unit-test code to tests project + * Fix up code to work with proper imports + * Fix maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/14 19:29:38 chochos + * minor cleanup (imports, static method calls, etc) + * + * Revision 1.2 2001/02/07 19:26:28 mpowers + * XML classes are in new package. + * + * Revision 1.1.1.1 2000/12/21 15:47:23 mpowers + * Contributing wotonomy. + * + * Revision 1.6 2000/12/20 16:25:37 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/package.html b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/package.html new file mode 100644 index 0000000..7ca944c --- /dev/null +++ b/projects/net.wotonomy.datastore/src/main/java/net/wotonomy/datastore/package.html @@ -0,0 +1,19 @@ + +

+A "low technology" object database, suitable +for simple application persistence. The design +goal is to simply store, retrieve, and query +arbitrary java objects that need no knowledge of +how they are to be persisted. +

+

+DataSoup is the primary interface, with +SerializedFileSoup and XMLFileSoup being the +primary implementations. Both implementations +persist arbitrary java objects to the file system, +with some limitations. +

+

+This package has dependencies on the util package. +

+ diff --git a/projects/net.wotonomy.foundation/.classpath b/projects/net.wotonomy.foundation/.classpath new file mode 100644 index 0000000..0b70c8a --- /dev/null +++ b/projects/net.wotonomy.foundation/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/projects/net.wotonomy.foundation/.cvsignore b/projects/net.wotonomy.foundation/.cvsignore new file mode 100644 index 0000000..e519f48 --- /dev/null +++ b/projects/net.wotonomy.foundation/.cvsignore @@ -0,0 +1,2 @@ +target +cobertura.ser diff --git a/projects/net.wotonomy.foundation/.gitignore b/projects/net.wotonomy.foundation/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/projects/net.wotonomy.foundation/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/projects/net.wotonomy.foundation/.project b/projects/net.wotonomy.foundation/.project new file mode 100644 index 0000000..6177a08 --- /dev/null +++ b/projects/net.wotonomy.foundation/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.foundation + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.core.prefs b/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..4a07d6d --- /dev/null +++ b/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Wed Feb 15 21:39:02 EST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.ui.prefs b/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..3ea503d --- /dev/null +++ b/projects/net.wotonomy.foundation/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Wed Feb 15 21:39:02 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/projects/net.wotonomy.foundation/pom.xml b/projects/net.wotonomy.foundation/pom.xml new file mode 100644 index 0000000..acb6815 --- /dev/null +++ b/projects/net.wotonomy.foundation/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + net.wotonomy + wotonomy-root + 1 + + wotonomy-foundation + Wotonomy - Foundation Classes + jar + + + junit + junit + 3.8.1 + test + + + concurrent + concurrent + 1.3.4 + + + ognl + ognl + 2.6.7 + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + org.codehaus.mojo + cobertura-maven-plugin + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.foundation + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.foundation + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSArray.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSArray.java new file mode 100644 index 0000000..491722c --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSArray.java @@ -0,0 +1,661 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** +* NSArray is an unmodifiable List. +* Calling the mutator methods of the List interface (add, addAll, +* set, etc.) on an instance of NSArray will throw an Unsupported +* Operation exception: use a NSMutableArray instead. This is to +* simulate Objective-C's pattern of exposing mutator methods only +* on mutable subinterface, which is wonderful for communicating +* via interface the contract on returned collections (whether you +* may modify return values) as well as implementing array faults. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 929 $ +*/ +public class NSArray implements List, Serializable +{ + /** + * Actual list that backs this instance. + */ + List list; + + /** + * Return value when array index is not found. + */ + public static final int NotFound = -1; + + /** + * A constant representing an empty array. + */ + public static final NSArray EmptyArray = new NSArray(); + + /** + * Returns an NSArray backed by the specified List. + * This is useful to "protect" an internal representation + * that is returned by a method of return type NSArray. + */ + public static NSArray arrayBackedByList( List aList ) + { + return new NSArray( aList, null ); + } + + /** + * A constructor that uses the provided list as the backing + * list object. This is unlike ArrayList and other + * java.util.Collection types insofar as that API requires + * that the provided Collection be copied into the newly constructed + * Collection. + * + * TODO: See if this signature can be reasonably changed, as having a no-op parameter is a little counter-intuitive. + * @param aList A list that the caller wishes to become the backing list for the NSArray. + * @param ignored This parameter is entirely ignored, and is only there to distinguish the API. + */ + NSArray( List aList, Object ignored ) // differentiates + { + list = aList; + } + + /** + * Constructor with a size hint, used by NSMutableArray. + */ + NSArray( int aSize ) + { + list = new ArrayList( aSize ); + } + + /** + * Default constructor returns an empty array. + */ + public NSArray () + { + list = new ArrayList(); + } + + /** + * Produces an array containing only the specified object. + */ + public NSArray (Object anObject) + { + this(); + list.add( anObject ); + } + + /** + * Produces an array containing the specified objects. + */ + public NSArray (Object[] anArray) + { + this(); + for ( int i = 0; i < anArray.length; i++ ) + { + list.add( anArray[i] ); + } + } + + /** + * Produces an array containing the objects in the specified collection. + */ + public NSArray (Collection aCollection) + { + this(); + Iterator i = aCollection.iterator(); + while ( i.hasNext() ) list.add( i.next() ); + } + + /** + * Returns the number of items in this array. + */ + public int count () + { + return list.size(); + } + + /** + * Returns an array containing all objects in this array + * plus the specified object. + */ + public NSArray arrayByAddingObject (Object anObject) + { + NSArray result = new NSArray( this ); + result.protectedAdd( anObject ); + return result; + } + + /** + * Returns an array containing all objects in this array + * plus all objects in the specified list. + */ + public NSArray arrayByAddingObjectsFromArray (Collection aCollection) + { + NSArray result = new NSArray( this ); + result.protectedAddAll( aCollection ); + return result; + } + + /** + * Returns a string containing the string representations of + * each element in this array, with each element separated from + * each neighboring element by the specified string. + */ + public String componentsJoinedByString (String separator) + { + StringBuffer buf = new StringBuffer(); + Iterator it = list.iterator(); + if ( it.hasNext() ) + { + buf.append( it.next().toString() ); + } + while ( it.hasNext() ) + { + buf.append( separator ); + buf.append( String.valueOf(it.next()) ); + } + return buf.toString(); + } + + /** + * Returns whether an equivalent object is contained in this array. + */ + public boolean containsObject (Object anObject) + { + return list.contains( anObject ); + } + + /** + * Returns the first object in this array that is equivalent to + * an object in the specified list, or null if no objects are + * in common. + */ + public Object firstObjectCommonWithArray (Collection aCollection) + { + if ( aCollection == null ) return null; + + Object o; + Iterator it = list.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( aCollection.contains( o ) ) return o; + } + return null; + } + + /** + * Returns whether the specified list contains elements equivalent + * to those in this array in the same order. + */ + public boolean isEqualToArray (List aList) + { + return list.equals( aList ); + } + + /** + * Returns the last object in this array, or null if the array is empty. + */ + public Object lastObject () + { + int i; + if ( (i = list.size()) == 0 ) return null; + return list.get( i - 1 ); + } + + /** + * + */ +/* + public NSArray sortedArrayUsingSelector (NSSelector); +*/ + + /** + * Returns an array comprised of only those elements whose + * indices fall within the specified range. + */ + public NSArray subarrayWithRange (NSRange aRange) + { + //TODO: Test this logic. + NSArray result = new NSArray(); + if ( aRange == null ) return result; + + int loc = aRange.location(); + int max = aRange.maxRange(); + int count = count(); + for ( int i = loc; i <= max && i < count; i++ ) + { + result.protectedAdd( list.get( i ) ); + } + return result; + } + + /** + * Returns an enumeration over the the elements of the array. + */ + public Enumeration objectEnumerator () + { + //TODO: Test this logic. + return new Enumeration() + { + Iterator it = NSArray.this.iterator(); + public boolean hasMoreElements() + { + return it.hasNext(); + } + public Object nextElement() + { + return it.next(); + } + }; + } + + /** + * Returns an enumeration over the elements of the array in reverse order. + */ + public java.util.Enumeration reverseObjectEnumerator () + { + return new java.util.Enumeration() + { + ListIterator it = null; + public ListIterator getIterator() + { + if ( it == null ) + { + it = NSArray.this.listIterator(); + // zoom to end + while ( it.hasNext() ) it.next(); + } + return it; + } + public boolean hasMoreElements() + { + return getIterator().hasPrevious(); + } + public Object nextElement() + { + return getIterator().previous(); + } + }; + } + + /** + * Copies the elements of this array into the specified object array + * as the array's capacity permits. + */ + public void getObjects (Object[] anArray) + { + getObjects(anArray,null); + } + + /** + * Copies the elements of this array that fall within the specified range + * into the specified object array as the array's capacity permits. This + * method must not overflow, even in the face of a null range, an over or + * under-sized array, or a bad range. It may underflow and fail to + * entirely populate the array, if the array is larger than the data to + * be copied. + * + * TODO: Check whether in WebObjects the range supposed to be measured against the parameter or the NSArray itself??? -ceg + * + * @param anArray An object array to be filled by this method. + * @param range An NSRange object representing the range of data in the NSArray to be copied. + */ + public void getObjects (Object[] array, NSRange range) + { + if ( array == null ) return; + if ( range == null) range = new NSRange(0,array.length); + int limit = Math.min(Math.min(array.length,range.length()),(count()-range.location())); + for ( int i = 0; i < limit ; i++ ) { + //anArray[ i-aRange.location() ] = objectAtIndex( i ); + array[ i ] = objectAtIndex( range.location() + i ); + } + } + + /** + * Returns the index of the first object in the array equivalent + * to the specified object. Returns NotFound if the item is not found. + */ + public int indexOfObject (Object anObject) + { + int result = list.indexOf( anObject ); + if ( result == -1 ) return NotFound; // in case this changes + return result; + } + + /** + * Returns the index of the first object in the array + * within the specified range equivalent to the specified object. + * Returns NotFound if the item is not found. + */ + public int indexOfObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return NotFound; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( list.get(i) ) ) + { + return i; + } + } + return NotFound; + } + + /** + * Returns the index of the specified object if it exists + * in the array, comparing by reference. + * Returns NotFound if the item is not found. + */ + public int indexOfIdenticalObject (Object anObject) + { + int size = list.size(); + for ( int i = 0; i < size; i++ ) + { + if ( anObject == list.get(i) ) + { + return i; + } + } + return NotFound; + } + + /** + * Returns the index of the first object in the array + * within the specified range equivalent to the specified object. + */ + public int indexOfIdenticalObject (Object anObject, NSRange aRange) + { + if ( aRange == null ) return NotFound; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == list.get(i) ) + { + return i; + } + } + return NotFound; + } + + /** + * Returns the object at the specified index. Throws an + * IndexOutOfRange exception if the index is out of range. + */ + public Object objectAtIndex (int anIndex) + { + return list.get( anIndex ); + } + + /** + * Returns an array consisting of strings within the specified string + * as delimited by the specified separator characters. + */ + public static NSArray componentsSeparatedByString + (String aString, String aSeparator) + { + NSArray result = new NSArray(); + if ( aString == null ) return result; + if ( aSeparator == null ) return new NSArray( aString ); + + //FIXME: The spec probably considers the whole + // string as a separator, unlike string tokenizer. + java.util.StringTokenizer tokens = + new java.util.StringTokenizer( aString, aSeparator ); + while ( tokens.hasMoreTokens() ) + { + result.protectedAdd( tokens.nextToken() ); + } + + return result; + } + + public Object clone() + { + return new NSArray( list ); + } + + public NSArray immutableClone() + { + return this; + } + + public NSMutableArray mutableClone() + { + return new NSMutableArray( this ); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(NSPropertyListSerialization.TOKEN_BEGIN[NSPropertyListSerialization.PLIST_ARRAY]); + for (int i = 0; i < count(); i++) { + Object x = objectAtIndex(i); + buf.append(NSPropertyListSerialization.stringForPropertyList(x)); + if (i < count() - 1) + buf.append(", "); + } + buf.append(NSPropertyListSerialization.TOKEN_END[NSPropertyListSerialization.PLIST_ARRAY]); + return buf.toString(); + } + + // interface List: accessors + + public boolean contains(Object o) { return list.contains(o); } + public boolean containsAll(Collection c) { return list.containsAll(c); } + public boolean equals(Object o) { return list.equals(o); } + public Object get(int index) { return list.get(index); } + public int hashCode() { + int code = 19; + code *= getClass().hashCode(); + code *= list.hashCode(); + return code; + } + public int indexOf(Object o) { return list.indexOf(o); } + public boolean isEmpty() { return list.isEmpty(); } + public int lastIndexOf(Object o) { return list.lastIndexOf(o); } + public int size() { return list.size(); } + public Object[] toArray() { return list.toArray(); } + public Object[] toArray(Object[] a) { return list.toArray(a); } + + // interface List: mutators + + + public void add(int index, Object element) + { + this.list.add(index,element); + } + + public boolean add(Object o) + { + return this.list.add(o); + } + + public boolean addAll(Collection coll) + { + return this.list.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + return this.list.addAll(index,c); + } + + public void clear() + { + this.list.clear(); + } + + public Iterator iterator() + { + // make a copy to avoid ConcurrentModificationExceptions + final Iterator i = new LinkedList( list ).iterator(); + return new Iterator() + { + public boolean hasNext() {return i.hasNext();} + public Object next() {return i.next();} + public void remove() { throw new UnsupportedOperationException(); } + }; + } + + public ListIterator listIterator() { return listIterator(0); } + + public ListIterator listIterator(final int index) + { + // make a copy to avoid ConcurrentModificationExceptions + final ListIterator i = new LinkedList( list ).listIterator(index); + return new ListIterator() + { + public boolean hasNext() {return i.hasNext();} + public Object next() {return i.next();} + public boolean hasPrevious() {return i.hasPrevious();} + public Object previous() {return i.previous();} + public int nextIndex() {return i.nextIndex();} + public int previousIndex() {return i.previousIndex();} + public void remove() { throw new UnsupportedOperationException(); } + public void set(Object o) { throw new UnsupportedOperationException(); } + public void add(Object o) { throw new UnsupportedOperationException(); } + }; + } + + public Object remove(int index) + { + return this.list.remove(index); + } + + public boolean remove(Object o) + { + return this.list.remove(o); + } + + public boolean removeAll(Collection coll) + { + return this.list.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + return this.list.retainAll(coll); + } + + public Object set(int index, Object element) + { + return this.list.set(index,element); + } + + public List subList(int fromIndex, int toIndex) + { + return Collections.unmodifiableList(list.subList(fromIndex, toIndex)); + } + + /** + * Provided for the use of subclasses like ArrayFault. + */ + protected boolean protectedAdd( Object o ) + { + return list.add( o ); + } + + /** + * Provided for the use of subclasses like ArrayFault. + */ + protected boolean protectedAddAll( Collection coll ) + { + return list.addAll( coll ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/03/10 00:52:27 cgruber + * Add tests for NSArray and fix some problems that became obvious as a result. + * + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.16 2005/07/13 14:12:44 cgruber + * Add mutableClone() and immutableClone() per. WebObjects 5.3 conformance. + * + * Revision 1.15 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.14 2003/08/05 00:48:56 chochos + * use NSPropertyListSerialization to get the opening and closing tokens for the string representation + * + * Revision 1.13 2003/08/04 20:26:10 chochos + * use NSPropertyListSerialization inside toString() + * + * Revision 1.12 2003/08/04 18:18:43 chochos + * toString() yields strings in the same format as Apple's NSArray + * + * Revision 1.11 2003/01/28 19:44:20 mpowers + * Fixed reverse enumerator. + * + * Revision 1.10 2003/01/18 23:49:55 mpowers + * Added mutableClone(). + * + * Revision 1.9 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.8 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.7 2003/01/10 19:16:40 mpowers + * Implemented support for page caching. + * + * Revision 1.6 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.5 2002/10/24 18:16:30 mpowers + * Now enforcing NSArray's immutable nature. + * + * Revision 1.4 2002/03/08 19:02:54 mpowers + * Long-overdue speed optimization of indexOfIdenticalObject. + * + * Revision 1.3 2002/02/13 22:02:56 mpowers + * Fixed: bug in componentsSeparatedByString when separator is null + * (thanks to Cedrik LIME). + * + * Revision 1.2 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.1.1.1 2000/12/21 15:47:26 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:37 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSBundle.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSBundle.java new file mode 100644 index 0000000..b735404 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSBundle.java @@ -0,0 +1,418 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2005 Israfil Consulting Services Corporation + Copyright (C) 2005 Christian Gruber + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.foundation; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; +import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarInputStream; + +import net.wotonomy.foundation.internal.NetworkClassLoader; + +/** + * An implementation of NSBundle. Unlike the standard WebObjects NSBundle, this + * implementation loads bundles dynamically. This means that all bundles do not + * need to exist in the classpath at startup time. Practically, this means that + * NSBundle has a custom classloader. + * + * This behaviour is not supported in Apple's WebObjects, and should be used + * only if compatibility is not desired. It is largely intended for internal use + * within this framework, but is exposed through bundleForURL() + * + * Another difference between Wotonomy's NSBundle and Apple's implementation is + * the ability to initialize the application with a custom resource lookup + * "path". + * + * @author cgruber@israfil.net + * @author $Author: cgruber $ + * @version $Revision: 892 $ + * + */ +public class NSBundle { + + /* Class variables */ + + public static final String BundleDidLoadNotification = "NSBundleDidLoadNotification"; + + public static final String LoadedClassesNotification = "NSLoadedClassesNotification"; + + private static final NSMutableArray _allBundles = new NSMutableArray(); + + private static final NSMutableArray _allFrameworks = new NSMutableArray(); + + private static NSMutableDictionary _languageCodes = new NSMutableDictionary(); + + private static NSBundle _mainBundle = null; + + protected static NetworkClassLoader _classLoader = new NetworkClassLoader(ClassLoader.getSystemClassLoader()); + + /* Instance variables */ + + protected String name; + + protected NSMutableDictionary info = null; + + protected String path; + + protected NSMutableArray classNames = new NSMutableArray(); + + protected NSMutableArray packages = new NSMutableArray(); + + protected Properties properties; + + protected boolean isFramework = false; + + protected boolean loaded = false; + + protected Class principalClass; + + /* Constructors */ + + /** + * The default constructor, which is only public to support other framework + * functionality, and to be API compatible with Apple's WebObjects. + * Generally, framework users should use bundleForXXXX() methods. + */ + public NSBundle() { + } + + /* Methods */ + + /** + * @deprecated use mainBundle() to access the application bundle and + * frameworkBundles() to access any frameworks. + */ + public static synchronized NSArray allBundles() { + return _allBundles.immutableClone(); + } + + /** + * @deprecated use frameworkBundles() to access any frameworks. + */ + public static NSArray allFrameworks() { + return frameworkBundles(); + } + + /** + * Returns the bundle that contains the provided class, if any. Otherwise, + * it returns null. Because NSBundles have a specialized class-loader, if + * any two bundles contain duiplicates of the same class, the second will + * fail to load. TODO: Determine if class-load scoping of duplicate classes + * is appropriate. + * + * @param class1 + * @return NSBundle + */ + public static synchronized NSBundle bundleForClass(Class class1) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + /** + * @deprecated Apple's WebObjects says you should not load from arbitrary + * path. + * @param path + * @return + */ + public static synchronized NSBundle bundleWithPath(String path) { + try { + return bundleWithURL(new File(path).toURI().toURL()); + } catch (MalformedURLException e) { + NSLog.err.appendln("Bundle path is invalid: " + path); + return null; + } + } + + /** + * Note:This method is only in Wotonomy. + * + * This method returns a bundle at a given URL, registering that bundle as + * well. If the bundle has already been loaded/registered, it is simply + * returned from the cache. + * + * @param url + * @return + */ + public static synchronized NSBundle bundleWithURL(URL url) { + NSBundle result = null; + String sep = System.getProperty("file.separator"); + String protocol = url.getProtocol(); + if (protocol.equals("file")) { + File f = new File(url.getPath()); + if (!f.exists()) { + NSLog.err.appendln("Bundle not found: " + url); + return null; + } + StringBuffer filename = new StringBuffer(f.getName()); + int extensionIndex = filename.lastIndexOf("."); + if (extensionIndex == -1) { + NSLog.err + .appendln("Named URL does not point to a bundle with an extension: " + + url); + return null; + } + String basename = filename.substring(0, extensionIndex); + String extension = filename.substring(extensionIndex + 1, filename + .length()); + System.out.println("basename: " + basename); + System.out.println("extension: " + extension); + result = new NSBundle(); + result.name = basename; + result.isFramework = extension.equals("framework"); + if (f.isDirectory()) { + try { + File javadir = new File(f.getCanonicalPath() + sep + "Contents" + + sep + "Resources" + sep + "Java"); + System.out.println(javadir); + System.out.println(javadir.exists()); + File[] jars = javadir.listFiles(); + + } catch (IOException e) { } + } else { + throw new RuntimeException( + "Compressed bundle files not currently supported."); + } + throw new RuntimeException("Method not finished."); + } else { + try { + JarInputStream j = new JarInputStream(url.openStream()); + JarEntry entry1 = j.getNextJarEntry(); + + JarFile f; + throw new RuntimeException("Method not finished."); + } catch (IOException e) { + NSLog.err + .appendln("IOException loading framework jar from URL " + + url + " - message: " + + e.getLocalizedMessage()); + StringWriter stacktrace = new StringWriter(); + e.printStackTrace(new PrintWriter(stacktrace)); + NSLog.err.appendln(stacktrace); + return null; + } + } + } + + /** + * This method returns a bundle, either from cache, or if it doesn't exist + * yet, it attempts to look it up - first from the classpath, then from the + * resource path. TODO: Determine if the lookup order is the desired + * semantic. + * + * @param name + * @return + */ + public static synchronized NSBundle bundleForName(String name) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public static synchronized NSArray frameworkBundles() { + return _allFrameworks.immutableClone(); + } + + /** + * Used to set the "Main" application bundle, in which primary resources are + * loaded for GUI applications. This is mostly only relevant for + * XXApplication objects. This should therefore not be generally used by + * consumers of the framework. + * + * @param aBundle + */ + public static void setMainBundle(NSBundle aBundle) { + _mainBundle = aBundle; + } + + public static NSBundle mainBundle() { + return _mainBundle; + } + + /** + * Get the default prefix for locale. TODO: This really needs to be made + * dynamic somehow. + * + * @return + */ + protected static String defaultLocalePrefix() { + String language = (String) _languageCodes.objectForKey(Locale + .getDefault().getLanguage()); + return language + ".lproj"; + } + + protected static synchronized NSBundle findOrCreateBundleWithPath(String s) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + /** + * TODO: figure out what this does. + * + * @return + */ + public NSArray bundleClassPackageNames() { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public String bundlePath() { + return this.path; + } + + /** + * Returns a byte array for the given resource path. TODO: Lookup semantics + * in WebObjects javadocs. + * + * @param path + * @return + */ + public byte[] bytesForResourcePath(String path) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public NSArray bundleClassNames() { + return classNames.immutableClone(); + } + + public NSDictionary infoDictionary() { + return info; + } + + /** + * Returns an input stream for a given resource path. TODO: Lookup semantics + * in WebObjects javadocs. + * + * @param path + * @return + */ + public InputStream inputStreamForResourcePath(String path) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public boolean isFramework() { + return isFramework; + } + + public boolean load() { + return loaded; + } + + public String name() { + return name; + } + + /** + * @deprecated Don't use this method, use + * resourcePathForLocalizedResourceNamed() instead. + */ + + public String pathForResource(String aName, String anExtension) { + return this.resourcePathForLocalizedResourceNamed(aName, null); + } + + /** + * @deprecated Don't use this method, use + * resourcePathForLocalizedResourceNamed() instead. + */ + public String pathForResource(String aName, String anExtension, + String subDir) { + return this.resourcePathForLocalizedResourceNamed(aName, subDir); + } + + /** + * @deprecated Don't use this method, use resourcePathsForResources() + * instead. + */ + public NSArray pathsForResources(String aName, String anExtension) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public Class principalClass() { + return principalClass; + } + + public Properties properties() { + return properties; + } + + /** + * @deprecated Resources are now accessed using the bytesForResourcePath() + * and inputStreamForResourcePath() methods. + */ + public String resourcePath() { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public String resourcePathForLocalizedResourceNamed(String aName, + String subDir) { + throw new UnsupportedOperationException("Method not yet implemented."); + // TODO: Implement. + } + + public NSArray resourcePathsForDirectories(String extension, + String subdirPath) { + throw new UnsupportedOperationException("Method not yet implemented."); + } + + public NSArray resourcePathsForLocalizedResources(String extension, + String subdirPath) { + throw new UnsupportedOperationException("Method not yet implemented."); + } + + public NSArray resourcePathsForResources(String extension, String subdirPath) { + throw new UnsupportedOperationException("Method not yet implemented."); + + } + + public String toString() { + int i = 0; + if (classNames != null) + i = classNames.count(); + return "<" + getClass().getName() + " name:'" + name + "' bundlePath:'" + + path + "' packages:'" + packages + "' " + i + " classes >"; + } + + /* Static initialization code */ + + private static void _initLanguages() { + _languageCodes.setObjectForKey("de", "German"); + _languageCodes.setObjectForKey("en", "English"); + _languageCodes.setObjectForKey("eo", "Esperanto"); + _languageCodes.setObjectForKey("es", "Spanish"); + _languageCodes.setObjectForKey("fr", "French"); + _languageCodes.setObjectForKey("ja", "Japanese"); + } + + static { + NSBundle._initLanguages(); + + } + +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoder.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoder.java new file mode 100644 index 0000000..ca32dd5 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoder.java @@ -0,0 +1,113 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSCoder.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.io.InputStream; +import java.io.OutputStream; + +/** +* A class that defines a simple encode/decode paradigm. Subclasses +* would handle the target format. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public abstract class NSCoder { + + public NSCoder() { + } + + public abstract void encodeBoolean(boolean flag); + + public abstract void encodeByte(byte byte0); + + public abstract void encodeBytes(byte abyte0[]); + + public abstract void encodeChar(char c); + + public abstract void encodeShort(short word0); + + public abstract void encodeInt(int i); + + public abstract void encodeLong(long l); + + public abstract void encodeFloat(float f); + + public abstract void encodeDouble(double d); + + public abstract void encodeObject(Object obj); + + public abstract void encodeClass(Class class1); + + public abstract void encodeObjects(Object aobj[]); + + public abstract boolean decodeBoolean(); + + public abstract byte decodeByte(); + + public abstract byte[] decodeBytes(); + + public abstract char decodeChar(); + + public abstract short decodeShort(); + + public abstract int decodeInt(); + + public abstract long decodeLong(); + + public abstract float decodeFloat(); + + public abstract double decodeDouble(); + + public abstract Object decodeObject(); + + public abstract Class decodeClass(); + + public abstract Object[] decodeObjects(); + + public void prepareForWriting(OutputStream outputstream) { + } + + public void prepareForReading(InputStream inputstream) { + } + + public void finishCoding() { + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.2 2002/06/25 19:03:02 cgruber + * Internal documentation fixes. + * + * Revision 1.1 2002/06/25 07:52:57 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoding.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoding.java new file mode 100644 index 0000000..56e4124 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSCoding.java @@ -0,0 +1,371 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSCoding.java 892 2006-02-16 12:47:16Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.math.BigInteger; + +/** +* A helper interface supporting the NSCoder APIs. At present it +* very confusing to me how this even works from a structural +* perspective, but to be consistent with WebObjects APIs, this will +* have to be properly implemented. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ + +public interface NSCoding { + + /** Not yet implemented */ + public static class _BigDecimalSupport extends _BigIntegerSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _BigDecimalSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _BigIntegerSupport extends Support { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + protected static void _encodeBigInteger( + NSCoder nscoder, + BigInteger biginteger) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + protected static BigInteger _decodeBigInteger(NSCoder nscoder, int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _BigIntegerSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _DoubleSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _DoubleSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _FloatSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _FloatSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _LongSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _LongSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _IntegerSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _IntegerSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _ShortSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _ShortSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _ByteSupport extends _NumberSupport { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _ByteSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _NumberSupport extends Support { + + /** Not yet implemented */ + public Class classForCoder(Object obj) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _NumberSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _CharacterSupport extends Support { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _CharacterSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _DateSupport extends Support { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _DateSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _StringSupport extends Support { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _StringSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Not yet implemented */ + public static class _BooleanSupport extends Support { + + /** Not yet implemented */ + public void encodeWithCoder(Object obj, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** Not yet implemented */ + public _BooleanSupport() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + /** Helper class for NSCoding. */ + public static abstract class Support { + + private static NSMutableDictionary classSupportMap = + new NSMutableDictionary(16); + + public static Support supportForClass(Class aClass) { + Support support = null; + Class realClass = aClass; + while (support == null && realClass != null) { + support = (Support) classSupportMap.objectForKey(realClass); + if (support == null) + realClass.getSuperclass(); + // Cache if we had to look to a superclass. + else if (aClass != realClass) + classSupportMap.setObjectForKey(support, aClass); + } + return support; + } + + public static void setSupportForClass(Support support, Class class1) { + classSupportMap.setObjectForKey(support, class1); + } + + /** Return the class of a given object. It boggles the mind + * as to why this is not a static, but in the original, it's + * not. <sigh> + */ + public Class classForCoder(Object obj) { + return obj.getClass(); + } + + public abstract void encodeWithCoder(Object obj, NSCoder nscoder); + + public abstract Object decodeObject(NSCoder nscoder); + + protected static void _encodeUTF8(String s, NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected static String _decodeUTF8(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + static { + setSupportForClass(new _StringSupport(), java.lang.String.class); + setSupportForClass(new _BooleanSupport(), java.lang.Boolean.class); + setSupportForClass(new _NumberSupport(), null); + setSupportForClass(new _ByteSupport(), java.lang.Byte.class); + setSupportForClass(new _ShortSupport(), java.lang.Short.class); + setSupportForClass(new _IntegerSupport(), java.lang.Integer.class); + setSupportForClass(new _LongSupport(), java.lang.Long.class); + setSupportForClass(new _FloatSupport(), java.lang.Float.class); + setSupportForClass(new _DoubleSupport(), java.lang.Double.class); + setSupportForClass( + new _BigIntegerSupport(), + java.math.BigInteger.class); + setSupportForClass( + new _BigDecimalSupport(), + java.math.BigDecimal.class); + setSupportForClass(new _DateSupport(), java.util.Date.class); + setSupportForClass( + new _CharacterSupport(), + java.lang.Character.class); + } + + public Support() { + } + } + + //CEG: I'm not sure why these are here, since NSCoding is an interface. + // It doesn't seem to even be used anywhere. + //CEG: I'm not even sure why this compiles!! + //public abstract Class classForCoder(); + + //CEG: I'm not sure why these are here, since NSCoding is an interface. + // It doesn't seem to even be used anywhere. + //CEG: I'm not even sure why this compiles!! + //public abstract void encodeWithCoder(NSCoder nscoder); + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.2 2002/06/25 19:03:02 cgruber + * Internal documentation fixes. + * + * Revision 1.1 2002/06/25 07:52:56 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSComparator.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSComparator.java new file mode 100644 index 0000000..287a59b --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSComparator.java @@ -0,0 +1,104 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSComparator.java 913 2006-03-10 00:52:27Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.util.Comparator; + + +/** +* An object that compares two other objects. As a convenience, it +* also implements java.util.Comparator. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 913 $ +*/ + +public abstract class NSComparator implements Comparator { + protected static class _NSSelectorComparator extends NSComparator { + + public int compare(Object obj, Object obj1) throws ComparisonException { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public _NSSelectorComparator(NSSelector nsselector) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + private static class StandInComparator extends NSComparator { + + public StandInComparator() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int compare(Object obj, Object obj1) throws ComparisonException { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + } + + public static class ComparisonException extends ClassCastException { + + public ComparisonException(String s) { + super(s); + } + } + + + public static final NSComparator AscendingStringComparator = new StandInComparator(); + public static final NSComparator DescendingStringComparator = new StandInComparator(); + public static final NSComparator AscendingCaseInsensitiveStringComparator = new StandInComparator(); + public static final NSComparator DescendingCaseInsensitiveStringComparator = new StandInComparator(); + public static final NSComparator AscendingNumberComparator = new StandInComparator(); + public static final NSComparator DescendingNumberComparator = new StandInComparator(); + public static final NSComparator AscendingTimestampComparator = new StandInComparator(); + public static final NSComparator DescendingTimestampComparator = new StandInComparator(); + public static final int OrderedAscending = -1; + public static final int OrderedSame = 0; + public static final int OrderedDescending = 1; + + public NSComparator() { + } + + public abstract int compare(Object obj, Object obj1) throws ClassCastException; + + public static int _compareObjects(Comparable comparable, Comparable comparable1) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + +} +/* + * $Log$ + * Revision 1.2 2006/03/10 00:52:27 cgruber + * Add tests for NSArray and fix some problems that became obvious as a result. + * + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.1 2002/06/25 07:52:56 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java new file mode 100644 index 0000000..36c527c --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSData.java @@ -0,0 +1,263 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** +* A pure java implementation of NSData, which +* is basically a wrapper on a byte array. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSData +{ + public static final NSData EmptyData = new NSData(); + + protected byte[] bytes; + + /** + * Default constructor creates a zero-data object. + */ + public NSData () + { + bytes = new byte[0]; + } + + /** + * Creates an object containing a copy of the specified bytes. + */ + public NSData (byte[] data) + { + this( data, 0, data.length ); + } + + /** + * Creates an object containing a copy of the bytes from the specified + * array within the specified range. + */ + public NSData (byte[] data, int start, int length) + { + bytes = new byte[ length ]; + for ( int i = 0; i < length; i++ ) + { + bytes[i] = data[ start+i ]; + } + } + + /** + * Creates an object containing the bytes of the specified string. + */ + public NSData (String aString) + { + this( aString.getBytes() ); + } + + /** + * Creates an object containing the contents of the specified file. + * Errors reading the file will produce an empty or partially blank array. + */ + public NSData (File aFile) + { + int len = (int) aFile.length(); + byte[] data = new byte[ len ]; + try + { + new java.io.FileInputStream( aFile ).read( data ); + } + catch ( Exception exc ) + { + // produce an empty or partially blank array + } + bytes = data; + } + + /** + * Creates an object containing the contents of the specified URL. + */ + public NSData (java.net.URL aURL) + { + throw new RuntimeException( "Not Implemented" ); + } + + /** + * Creates an object containing a copy of the contents of the + * specified NSData object. + */ + public NSData (NSData aData) + { + this( aData.bytes() ); + } + + /** + * Creates a new NSData object from the bytes in the input stream. + * The input stream is read fully and is not closed. + * @param stream The stream to read from. + * @param chunkSize The buffer size used to read from the stream. + * @throws IOException if the stream cannot be read from. + */ + public NSData(InputStream stream, int chunkSize) throws IOException { + super(); + byte[] b = new byte[chunkSize]; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + int read = 0; + do { + read = stream.read(b); + if (read > 0) + bout.write(b, 0, read); + } while (read > 0); + bytes = bout.toByteArray(); + } + + /** + * Returns the length of the contained data. + */ + public int length () + { + return bytes.length; + } + + /** + * Returns whether the specified data is equivalent to these data. + */ + public boolean isEqualToData (NSData aData) + { + if (length() != aData.length()) + return false; + byte[] a = bytes(); + byte[] b = aData.bytes(); + + for ( int i = 0; i < a.length; i++ ) { + if ( a[i] != b[i] ) + return false; + } + return true; + } + + /** + * Return the bytes within the data that fall within the specified range. + */ + public NSData subdataWithRange (NSRange aRange) + { + int loc = aRange.location(); + byte[] src = bytes(); + byte[] data = new byte[ aRange.length() ]; + System.arraycopy(src, loc, data, 0, data.length); + return new NSData( data ); + } + + /** + * Writes the contents of this data to the specified URL. + * If atomically is true, then the data is written to a temporary + * file and then renamed to the name specified by the URL when + * the data transfer is complete. + */ + public boolean writeToURL (java.net.URL aURL, boolean atomically) + { + throw new RuntimeException( "Not Implemented" ); + } + + /** + * Convenience to return the contents of the specified file. + */ + public static NSData dataWithContentsOfMappedFile (java.io.File aFile) + { + return new NSData( aFile ); + } + + /** + * Returns a copy of the bytes starting at the specified location + * and ranging for the specified length. + */ + public byte[] bytes (int location, int length) + { + byte[] data = new byte[ length ]; + for ( int i = 0; i < length; i++ ) + { + data[i] = bytes[ location + i ]; + } + return data; + } + + /** + * Returns a copy of the bytes backing this data object. + * NOTE: This method is not in the NSData spec and is + * included for convenience only. + */ + public byte[] bytes() + { + return bytes( 0, length() ); + } + + public String toString() { + String hex = "0123456789ABCDEF"; + StringBuffer buf = new StringBuffer(); + buf.append(NSPropertyListSerialization.TOKEN_BEGIN[NSPropertyListSerialization.PLIST_DATA]); + for (int i = 0; i < bytes.length; i++) { + byte b = bytes[i]; + buf.append(hex.charAt((b & 0xf0) >> 4)); + buf.append(hex.charAt(b & 0x0f)); + if (i % 5 == 4) + buf.append(' '); + } + buf.append(NSPropertyListSerialization.TOKEN_END[NSPropertyListSerialization.PLIST_DATA]); + return buf.toString(); + } + + public boolean isEqual(Object obj) { + if (obj == this) + return true; + if (obj instanceof NSData) + return isEqualToData((NSData)obj); + return false; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/19 01:53:52 chochos + * added constructor with an InputStream + * + * Revision 1.4 2003/08/05 00:51:31 chochos + * get the enclosing tokens from NSPropertyListSerialization + * + * Revision 1.3 2003/08/04 22:45:47 chochos + * toString() prints out the bytes in hex (in property list format) + * + * Revision 1.2 2003/08/02 01:52:00 chochos + * added EmptyData + * + * Revision 1.1.1.1 2000/12/21 15:47:26 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java new file mode 100644 index 0000000..d5b6f61 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDate.java @@ -0,0 +1,217 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** +* A pure java implementation of NSDate that extends +* java.util.Date for greater java compatibility. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ +public class NSDate extends Date +{ + // NSComparisonResult compatibility + + public static final int NSOrderedAscending = -1; + public static final int NSOrderedSame = 0; + public static final int NSOrderedDescending = 1; + +// public static final double TimeIntervalSince1970; +// public static final NSDate DateFor1970; + + /** + * Default constructor represents the current date. + */ + public NSDate () + { + super(); + } + + /** + * Represents the specified number of seconds from the current date. + */ + public NSDate (double seconds) + { + super( (long) new NSDate().getTime() + + timeIntervalToMilliseconds(seconds) ); + } + + /** + * Represents the specified number of seconds from the specified date. + */ + public NSDate (double seconds, Date sinceDate) + { + super( (long) sinceDate.getTime() + + timeIntervalToMilliseconds(seconds) ); + } + + /** + * Returns the interval between this date and 1 January 2001 GMT. + */ + public double timeIntervalSinceReferenceDate () + { + GregorianCalendar referenceDate = + new GregorianCalendar( TimeZone.getTimeZone( "GMT" ) ); + referenceDate.set( 2001, 0, 0, 0, 0, 0 ); + return timeIntervalSinceDate( referenceDate.getTime() ); + } + + /** + * Returns the interval between this date and the specified date + * in seconds. + */ + public double timeIntervalSinceDate (Date aDate) + { + return millisecondsToTimeInterval( + this.getTime() - aDate.getTime() ); + } + + /** + * Returns the interval between this date and the current date + * in seconds. + */ + public double timeIntervalSinceNow () + { + return timeIntervalSinceDate( new NSDate() ); + } + + /** + * Compares this date to the specified date and returns the + * earlier date. Unspecified which is returned if both are equal. + */ + public NSDate earlierDate (NSDate aDate) + { + if ( aDate == null ) return this; + if ( after( aDate ) ) return aDate; + return this; + } + + /** + * Compares this date to the specified date and returns the + * later date. Unspecified which is returned if both are equal. + */ + public NSDate laterDate (NSDate aDate) + { + if ( aDate == null ) return this; + if ( before( aDate ) ) return aDate; + return this; + } + + /** + * Returns a negative value if the specified date is later than + * this date, a positive value if the specified date is earlier + * than this date, or zero if the dates are equal. The return + * values are compatible with type NSComparisonResult. + */ + public int compare (Date aDate) + { + if ( before( aDate ) ) return NSOrderedAscending; + if ( after( aDate ) ) return NSOrderedDescending; + return NSOrderedSame; + } + + /** + * Returns whether the this date is equal to the specified date, + * per the result of equals(). + */ + public boolean isEqualToDate (Date aDate) + { + return equals( aDate ); + } + + /** + * Returns a date that differs from this date by the specified + * number of seconds. + */ + public NSDate dateByAddingTimeInterval (double seconds) + { + return new NSDate( seconds, this ); + } + + /** + * Returns the number of seconds between now and the reference date. + */ + public static double currentTimeIntervalSinceReferenceDate () + { + return new NSDate().timeIntervalSinceReferenceDate(); + } + + /** + * Converts seconds to milliseconds. Included for compatibility. + */ + public static long timeIntervalToMilliseconds (double seconds) + { + return (long) seconds*1000; + } + + /** + * Converts milliseconds to seconds. Included for compatibility. + */ + public static double millisecondsToTimeInterval (long millis) + { + return millis/1000.0; + } + + /** + * Returns a date that is greater than all representable dates. + */ + public static NSDate distantFuture () + { + NSDate result = new NSDate(); + result.setTime( Long.MAX_VALUE ); + return result; + } + + /** + * Returns a date that is less than all representable dates. + */ + public static NSDate distantPast () + { + NSDate result = new NSDate(); + result.setTime( Long.MIN_VALUE ); + return result; + } + +// inherited from java.util.Date +// public java.lang.String toString (); +// public boolean equals (java.lang.Object); +// public int hashCode (); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:28 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDictionary.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDictionary.java new file mode 100644 index 0000000..235a7bb --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDictionary.java @@ -0,0 +1,332 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** +* A pure java implementation of NSDictionary that +* implements Map for greater java interoperability. +* +* @author michael@mpowers.net +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSDictionary extends HashMap implements NSKeyValueCoding +{ + public static final NSDictionary EmptyDictionary = new NSDictionary(); + + /** + * Default constructor produces an empty dictionary. + */ + public NSDictionary () + { + super(); + } + + /** + * Constructor produces an empty dictionary with an initial capacity. + */ + public NSDictionary (int initialCapacity) + { + super(initialCapacity); + } + + /** + * Produces a dictionary that contains one key referencing one value. + */ + public NSDictionary (Object key, Object value) + { + super(); + put( key, value ); + } + + /** + * Produces a dictionary containing the specified keys and values. + * An IllegalArgumentException is thrown if the arrays are not + * of the same length. + */ + public NSDictionary (Object[] objects, Object[] keys) + { + super(); + if ( keys.length != objects.length ) + { + throw new IllegalArgumentException( "Array lengths do not match." ); + } + + for ( int i = 0; i < keys.length; i++ ) + { + put( keys[i], objects[i] ); + } + } + + /** + * Produces a dictionary that is a copy of the specified map (or dictionary). + */ + public NSDictionary (Map aMap) + { + super( aMap ); + } + + /** + * Returns a count of the key-value pairs in this dictionary. + */ + public int count () + { + return size(); + } + + /** + * Returns an NSArray containing all keys in this dictionary. + */ + public NSArray allKeys () + { + return new NSArray( keySet() ); + + } + + /** + * Returns an NSArray containing all keys that reference the + * specified value. + */ + public NSArray allKeysForObject (Object value) + { + NSMutableArray result = new NSMutableArray(); + Map.Entry entry; + Iterator it = entrySet().iterator(); + + while ( it.hasNext() ) + { + entry = (Map.Entry) it.next(); + + // handle null values + if ( ( value == null ) && ( entry.getValue() == null ) + || ( value.equals( entry.getValue() ) ) ) + { + // if match, add to result set + result.addObject( entry.getKey() ); + } + } + + return result; + } + + /** + * Returns an NSArray containing all values in this dictionary. + */ + public NSArray allValues () + { + return new NSArray( values() ); + } + + /** + * Returns whether the specified dictionary has the same or + * equivalent key-value pairs as this dictionary. + */ + public boolean isEqualToDictionary (NSDictionary aDictionary) + { + return equals( aDictionary ); + } + + /** + * Returns an array of objects for the specified array of keys. + * If a key isn't found, the marker parameter will be placed + * in the corresponding index(es) in the returned array. + */ + public NSArray objectsForKeys (NSArray anArray, Object aMarker) + { + NSMutableArray result = new NSMutableArray(); + if ( anArray == null ) return result; + + Object value; + Enumeration enumeration = anArray.objectEnumerator(); + while ( enumeration.hasMoreElements() ) + { + value = objectForKey( enumeration.nextElement() ); + if ( value == null ) + { + value = aMarker; + } + result.addObject( value ); + } + return result; + } + + /** + * Returns an enumeration over the keys in this dictionary. + */ + public java.util.Enumeration keyEnumerator () + { + return new java.util.Enumeration() + { + Iterator it = NSDictionary.this.keySet().iterator(); + public boolean hasMoreElements() + { + return it.hasNext(); + } + public Object nextElement() + { + return it.next(); + } + }; + } + + /** + * Returns an enumeration over the values in this dictionary. + */ + public java.util.Enumeration objectEnumerator () + { + return new java.util.Enumeration() + { + Iterator it = NSDictionary.this.values().iterator(); + public boolean hasMoreElements() + { + return it.hasNext(); + } + public Object nextElement() + { + return it.next(); + } + }; + } + + /** + * Returns the value for the specified key, or null + * if the key is not found. + */ + public Object objectForKey (Object aKey) + { + return get( aKey ); + } + + // interface NSKeyValueCoding + + public Object valueForKey (String aKey) + { // System.out.println( "valueForKey: " + aKey + "->" + this ); + Object result = objectForKey( aKey ); + if ( result == null ) + result = NSKeyValueCodingSupport.valueForKey( this, aKey ); + return result; + } + + public void takeValueForKey (Object aValue, String aKey) + { // System.out.println( "takeValueForKey: " + aKey + " : " + aValue + "->" + this ); + put( aKey, aValue ); //FIXME: technically cheating since this is a read-only class + } + + public Object storedValueForKey (String aKey) + { + Object result = objectForKey( aKey ); + if ( result == null ) + result = NSKeyValueCodingSupport.storedValueForKey( this, aKey ); + return result; + } + + public void takeStoredValueForKey (Object aValue, String aKey) + { + put( aKey, aValue ); //FIXME: technically cheating since this is a read-only class + } + + public Object handleQueryWithUnboundKey (String aKey) + { + return NSKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + public void handleTakeValueForUnboundKey (Object aValue, String aKey) + { + NSKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + public void unableToSetNullForKey (String aKey) + { + NSKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + public Object validateTakeValueForKeyPath (Object aValue, String aKey) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + Enumeration enumeration = keyEnumerator(); + boolean quote = false; + buf.append(NSPropertyListSerialization.TOKEN_BEGIN[NSPropertyListSerialization.PLIST_DICTIONARY]); + while (enumeration.hasMoreElements()) { + if (buf.length() == 1) + buf.append(' '); + Object k = enumeration.nextElement(); + buf.append(NSPropertyListSerialization.stringForPropertyList(k)); + buf.append(" = "); + k = objectForKey(k); + buf.append(NSPropertyListSerialization.stringForPropertyList(k)); + buf.append("; "); + } + buf.append(NSPropertyListSerialization.TOKEN_END[NSPropertyListSerialization.PLIST_DICTIONARY]); + return buf.toString(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.8 2003/08/05 00:50:14 chochos + * use NSPropertyListSerialization to get the tokens to enclose the string description + * + * Revision 1.7 2003/08/04 20:26:10 chochos + * use NSPropertyListSerialization inside toString() + * + * Revision 1.6 2003/08/04 18:49:38 chochos + * NSDictionary(Object[], Object[]) was taking the parameters in the wrong order; for compatibility with Apple's NSDictionary, objects comes first, then keys. + * + * Revision 1.5 2003/08/04 18:26:19 chochos + * fixed opening '{' + * + * Revision 1.4 2003/01/28 22:11:30 mpowers + * Now implements NSKeyValueCoding. + * + * Revision 1.3 2002/06/30 17:58:06 mpowers + * Add a capacity constructor and static empty dictionary: thanks cgruber. + * + * Revision 1.2 2001/02/23 23:43:41 mpowers + * Removed ill-advised this. + * + * Revision 1.1.1.1 2000/12/21 15:47:31 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDisposable.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDisposable.java new file mode 100644 index 0000000..46df7d2 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSDisposable.java @@ -0,0 +1,35 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSDisposable.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +/** +* Interface for objects that respond to dispose(); +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface NSDisposable { + + public abstract void dispose(); + +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSForwardException.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSForwardException.java new file mode 100644 index 0000000..1db0ad2 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSForwardException.java @@ -0,0 +1,157 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; + +/** +* Serves to wrap an exception inside of a RuntimeException, +* which is not required to be declared in a throws statement. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class NSForwardException extends RuntimeException +{ + protected String message; + protected Throwable wrappedThrowable; + + /** + * Default constructor. + */ + public NSForwardException() + { + super(); + message = null; + wrappedThrowable = null; + } + + /** + * Standard constructor with message. + */ + public NSForwardException( String aMessage ) + { + super( aMessage ); + message = aMessage; + wrappedThrowable = null; + } + + /** + * Specifies a throwable to wrap. + */ + public NSForwardException( Throwable aThrowable ) + { + super(); + message = null; + wrappedThrowable = aThrowable; + } + + /** + * Specifies a message and a throwable to wrap. + */ + public NSForwardException( Throwable aThrowable, String aMessage ) + { + super( aMessage ); + message = aMessage; + wrappedThrowable = aThrowable; + } + + /** + * Returns the wrapped throwable. + */ + public Throwable originalException() + { + return wrappedThrowable; + } + + public void printStackTrace(PrintWriter s) + { + if ( message != null ) + { + s.println( toString() ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace( s ); + return; + } + super.printStackTrace( s ); + } + + public void printStackTrace(PrintStream s) + { + if ( message != null ) + { + s.println( toString() ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace( s ); + return; + } + super.printStackTrace( s ); + } + + public void printStackTrace() + { + if ( message != null ) + { + System.err.println( toString() ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace(); + return; + } + super.printStackTrace(); + } + + public String stackTrace() + { + StringWriter writer = new StringWriter(); + PrintWriter printWriter = new PrintWriter( writer ); + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace( printWriter ); + } + else + { + super.printStackTrace( printWriter ); + } + printWriter.flush(); + printWriter.close(); + return writer.toString(); + } + + public String toString() + { + String result = message; + if ( result == null ) result = ""; + if ( wrappedThrowable != null ) + { + result = wrappedThrowable.toString() + " : " + result; + } + return result; + } + +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCoding.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCoding.java new file mode 100644 index 0000000..5792303 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCoding.java @@ -0,0 +1,431 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.IntrospectorException; +import net.wotonomy.foundation.internal.MissingPropertyException; +import net.wotonomy.foundation.internal.NullPrimitiveException; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* NSKeyValueCoding defines an interface for classes that +* need to have more control over the wotonomy's property +* introspection facilities.

+* +* On an object that implements this interface, wotonomy +* will call these methods, and otherwise use the static +* methods on NSKeyValueCodingSupport.

+* +* NSKeyValueCodingSupport implements the default behaviors +* for each of these methods, so classes implementing this +* interface can call those methods to acheive the same +* behavior.

+* +* valueForKey and takeValueForKey are called in response +* to user actions, like viewing an object or updating its +* value in a user interface. These should call the public +* getter and setter methods on the object itself and the +* operations should be subject to validation.

+* +* storedValueForKey and takeStoredValueForKey are called +* in response to wotonomy actions, like snapshotting, +* faulting, commits, and reverts. These operations should +* bypass the public methods and directly modify the internal +* state of the object without validation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface NSKeyValueCoding +{ + + public static final Null NullValue = new Null(); + + /** + * Returns the value for the specified property. + * If the property does not exist, this method should + * call handleQueryWithUnboundKey. + */ + Object valueForKey( String aKey ); + + /** + * Sets the property to the specified value. + * If the property does not exist, this method should + * call handleTakeValueForUnboundKey. + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey. + */ + void takeValueForKey( Object aValue, String aKey ); + + /** + * Returns the value for the private field that + * corresponds to the specified property. + */ + Object storedValueForKey( String aKey ); + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value. + */ + void takeStoredValueForKey( Object aValue, String aKey ); + + /** + * Called by valueForKey when the specified key is + * not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + Object handleQueryWithUnboundKey( String aKey ); + + /** + * Called by takeValueForKey when the specified key + * is not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + void handleTakeValueForUnboundKey( Object aValue, String aKey ); + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types. Implementing + * classes should handle this case appropriately + * or otherwise throw an exception. + */ + void unableToSetNullForKey( String aKey ); + + /** + * Static utility methods that + * call the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + public class Utility + { + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public Object valueForKey( + Object anObject, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + return ((NSKeyValueCoding)anObject).valueForKey( aKey ); + } + return DefaultImplementation.valueForKey( anObject, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public void takeValueForKey( + Object anObject, Object aValue, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).takeValueForKey( aValue, aKey ); + } + DefaultImplementation.takeValueForKey( anObject, aValue, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public Object storedValueForKey( + Object anObject, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + return ((NSKeyValueCoding)anObject).storedValueForKey( aKey ); + } + return DefaultImplementation.storedValueForKey( anObject, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public void takeStoredValueForKey( + Object anObject, Object aValue, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).takeStoredValueForKey( aValue, aKey ); + } + DefaultImplementation.takeStoredValueForKey( anObject, aValue, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public Object handleQueryWithUnboundKey( + Object anObject, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + return ((NSKeyValueCoding)anObject).handleQueryWithUnboundKey( aKey ); + } + return DefaultImplementation.handleQueryWithUnboundKey( anObject, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public void handleTakeValueForUnboundKey( + Object anObject, Object aValue, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).handleTakeValueForUnboundKey( aValue, aKey ); + } + DefaultImplementation.handleTakeValueForUnboundKey( anObject, aValue, aKey ); + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCoding, otherwise calls the method + * on DefaultImplementation. + */ + static public void unableToSetNullForKey( + Object anObject, String aKey ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).unableToSetNullForKey( aKey ); + } + DefaultImplementation.unableToSetNullForKey( anObject, aKey ); + } + } + + public class DefaultImplementation + { + /** + * Returns the value for the specified property key + * on the specified object.

+ * + * If the property does not exist, this method calls + * handleQueryWithUnboundKey on the object if it + * implements NSKeyValueCoding, otherwise calls + * handleQueryWithUnboundKey on this class.

+ */ + static public Object valueForKey( + Object anObject, String aKey ) + { + if ( anObject == null ) return null; + //TODO: may need to handle "." nesting here so + // that handleQueryWithUnboundKey gets called for + // for the nested object, not the parent object + + //Correction: need to handle key paths in + // KeyValueCodingAdditionsSupport. + + try + { + return Introspector.get( anObject, aKey ); + } + catch ( IntrospectorException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + return ((NSKeyValueCoding)anObject).handleQueryWithUnboundKey( aKey ); + } + return handleQueryWithUnboundKey( anObject, aKey ); + } + } + + /** + * Sets the property to the specified value on + * the specified object. + * + * If the property does not exist, this method calls + * handleTakeValueForUnboundKey on the object if it + * implements NSKeyValueCoding, otherwise calls + * handleTakeValueForUnboundKey on this class. + * + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey + * on the object if it implements NSKeyValueCoding, + * otherwise calls unableToSetNullForKey on this class. + */ + static public void takeValueForKey( + Object anObject, Object aValue, String aKey ) + { + if ( anObject == null ) return; + //TODO: may need to handle "." nesting here so + // that handleTakeValueForUnboundKey gets called for + // for the nested object, not the parent object + try + { + Introspector.set( anObject, aKey, aValue ); + } + catch ( NullPrimitiveException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).unableToSetNullForKey( aKey ); + } + else + { + unableToSetNullForKey( anObject, aKey ); + } + } + catch ( MissingPropertyException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).handleTakeValueForUnboundKey( + aValue, aKey ); + } + else + { + handleTakeValueForUnboundKey( anObject, aValue, aKey ); + } + } + + } + + /** + * Returns the value for the private field that + * corresponds to the specified property on + * the specified object. + * + * This implementation currently calls valueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public Object storedValueForKey( + Object anObject, String aKey ) + { + //TODO: this currently just calls valueForKey + return valueForKey( anObject, aKey ); + } + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value on the + * specified object. + * + * This implementation currently calls takeValueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public void takeStoredValueForKey( + Object anObject, Object aValue, String aKey ) + { + //TODO: this currently just calls takeValueForKey + takeValueForKey( anObject, aValue, aKey ); + } + + /** + * Called by valueForKey when the specified key is + * not found on the specified object, if that object + * does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public Object handleQueryWithUnboundKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Key not found for object: " + + aKey + " : " + anObject ); + } + + /** + * Called by takeValueForKey when the specified key + * is not found on the specified object, if that object + * does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void handleTakeValueForUnboundKey( + Object anObject, Object aValue, String aKey ) + { + throw new WotonomyException( + "Key not found for object while setting value: " + + aKey + " : " + anObject + " : " + aValue ); + } + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types, if the specified + * object does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void unableToSetNullForKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Tried to key on object to null: " + + aKey + " : " + anObject ); + } + } + + public class Null { + public Null() { + super(); + } + + public String toString() { + return ""; + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/07 02:43:56 chochos + * added NullValue, which points to a Null instance. + * + * Revision 1.2 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.1 2003/01/17 14:40:49 mpowers + * Adding files to fix build. + * + * Revision 1.2 2001/03/28 16:12:30 mpowers + * Documented interface. + * + * Revision 1.1 2001/03/27 23:25:05 mpowers + * Contributing interface, no docs yet. + * + * + */ + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingAdditions.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingAdditions.java new file mode 100644 index 0000000..785facd --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingAdditions.java @@ -0,0 +1,213 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.List; +import java.util.Map; + +/** +* NSKeyValueCodingAdditions defines an interface for classes +* that need to have more control over the wotonomy's bulk +* property copying and cloning facilities.

+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface NSKeyValueCodingAdditions extends NSKeyValueCoding +{ + /** + * Returns the value for the specified key path, which is + * a series of keys delimited by ".", for example: + * "createTime.year.length". + */ + Object valueForKeyPath( String aKeyPath ); + + /** + * Sets the value for the specified key path, which is + * a series of keys delimited by ".", for example: + * "createTime.year.length". + * The value is set for the last object referenced by + * the key path. + */ + void takeValueForKeyPath( Object aValue, String aKeyPath ); + + /** + * Returns a Map of the specified keys to their values, + * each of which might be obtained by calling valueForKey. + */ + NSDictionary valuesForKeys( List aKeyList ); + + /** + * Takes the keys from the specified map as properties + * and applies the corresponding values, each of which + * might be set by calling takeValueForKey. + */ + void takeValuesFromDictionary( Map aMap ); + + + /** + * Static utility methods that + * call the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public class Utility + { + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static void takeValuesFromDictionary( + Object object, Map dictionary) + { + if (object instanceof NSKeyValueCodingAdditions) { + ((NSKeyValueCodingAdditions)object).takeValuesFromDictionary(dictionary); + } else { + DefaultImplementation.takeValuesFromDictionary(object, dictionary); + } + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static void takeValueForKeyPath( + Object object, Object aValue, String aKeyPath) + { + if (object instanceof NSKeyValueCodingAdditions) { + ((NSKeyValueCodingAdditions)object).takeValueForKeyPath(aValue, aKeyPath); + } else { + DefaultImplementation.takeValueForKeyPath(object, aValue, aKeyPath); + } + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static NSDictionary valuesForKeys( + Object object, List keys) + { + if (object instanceof NSKeyValueCodingAdditions) { + return ((NSKeyValueCodingAdditions)object).valuesForKeys(keys); + } else { + return DefaultImplementation.valuesForKeys(object, keys); + } + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static Object valueForKeyPath( + Object object, String aKeyPath) + { + if (object instanceof NSKeyValueCodingAdditions) { + return ((NSKeyValueCodingAdditions)object).valueForKeyPath(aKeyPath); + } else { + return DefaultImplementation.valueForKeyPath(object, aKeyPath); + } + } + } + + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public class DefaultImplementation + { + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public static void takeValuesFromDictionary( + Object object, Map dictionary) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public static void takeValueForKeyPath( + Object object, Object aValue, String aKeyPath) + { + // currently, NSKeyValueCoding.takeValueForKey accepts paths + NSKeyValueCoding.DefaultImplementation.takeValueForKey( object, aValue, aKeyPath ); + } + + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public static NSDictionary valuesForKeys( + Object object, List keys) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public static Object valueForKeyPath( + Object object, String aKeyPath) + { + // currently, NSKeyValueCoding.valueForKey accepts paths + return NSKeyValueCoding.DefaultImplementation.valueForKey( object, aKeyPath ); + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.1 2003/01/17 14:40:49 mpowers + * Adding files to fix build. + * + * Revision 1.3 2001/12/10 15:25:11 mpowers + * Now properly extending NSKeyValueCoding. + * + * Revision 1.2 2001/04/28 14:12:23 mpowers + * Refactored cloning/copying into KeyValueCodingUtilities. + * + * Revision 1.1 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.2 2001/03/28 16:12:30 mpowers + * Documented interface. + * + * Revision 1.1 2001/03/27 23:25:05 mpowers + * Contributing interface, no docs yet. + * + * + */ + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingSupport.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingSupport.java new file mode 100644 index 0000000..a947896 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSKeyValueCodingSupport.java @@ -0,0 +1,228 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.IntrospectorException; +import net.wotonomy.foundation.internal.MissingPropertyException; +import net.wotonomy.foundation.internal.NullPrimitiveException; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* NSKeyValueCodingSupport defines default behavior for +* classes implementing NSKeyValueSupport.

+* +* On an object that does not implement NSKeyValueCoding, +* wotonomy will call the methods on this class directly. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ +public class NSKeyValueCodingSupport +{ + /** + * Returns the value for the specified property key + * on the specified object.

+ * + * If the property does not exist, this method calls + * handleQueryWithUnboundKey on the object if it + * implements NSKeyValueCoding, otherwise calls + * handleQueryWithUnboundKey on this class.

+ */ + static public Object valueForKey( + Object anObject, String aKey ) + { + //TODO: may need to handle "." nesting here so + // that handleQueryWithUnboundKey gets called for + // for the nested object, not the parent object + + //Correction: need to handle key paths in + // KeyValueCodingAdditionsSupport. + + try + { + return Introspector.get( anObject, aKey ); + } + catch ( IntrospectorException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + return ((NSKeyValueCoding)anObject).handleQueryWithUnboundKey( aKey ); + } + return handleQueryWithUnboundKey( anObject, aKey ); + } + } + + /** + * Sets the property to the specified value on + * the specified object. + * + * If the property does not exist, this method calls + * handleTakeValueForUnboundKey on the object if it + * implements NSKeyValueCoding, otherwise calls + * handleTakeValueForUnboundKey on this class. + * + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey + * on the object if it implements NSKeyValueCoding, + * otherwise calls unableToSetNullForKey on this class. + */ + static public void takeValueForKey( + Object anObject, Object aValue, String aKey ) + { + //TODO: may need to handle "." nesting here so + // that handleTakeValueForUnboundKey gets called for + // for the nested object, not the parent object + + try + { + Introspector.set( anObject, aKey, aValue ); + } + catch ( NullPrimitiveException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).unableToSetNullForKey( aKey ); + } + else + { + unableToSetNullForKey( anObject, aKey ); + } + } + catch ( MissingPropertyException exc ) + { + if ( anObject instanceof NSKeyValueCoding ) + { + ((NSKeyValueCoding)anObject).handleTakeValueForUnboundKey( + aValue, aKey ); + } + else + { + handleTakeValueForUnboundKey( anObject, aValue, aKey ); + } + } + + } + + /** + * Returns the value for the private field that + * corresponds to the specified property on + * the specified object. + * + * This implementation currently calls valueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public Object storedValueForKey( + Object anObject, String aKey ) + { + //TODO: this currently just calls valueForKey + return valueForKey( anObject, aKey ); + } + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value on the + * specified object. + * + * This implementation currently calls takeValueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public void takeStoredValueForKey( + Object anObject, Object aValue, String aKey ) + { + //TODO: this currently just calls takeValueForKey + takeValueForKey( anObject, aValue, aKey ); + } + + /** + * Called by valueForKey when the specified key is + * not found on the specified object, if that object + * does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public Object handleQueryWithUnboundKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Key not found for object: " + + aKey + " : " + anObject ); + } + + /** + * Called by takeValueForKey when the specified key + * is not found on the specified object, if that object + * does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void handleTakeValueForUnboundKey( + Object anObject, Object aValue, String aKey ) + { + throw new WotonomyException( + "Key not found for object while setting value: " + + aKey + " : " + anObject + " : " + aValue ); + } + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types, if the specified + * object does not implement NSKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void unableToSetNullForKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Tried to key on object to null: " + + aKey + " : " + anObject ); + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/17 14:40:50 mpowers + * Adding files to fix build. + * + * Revision 1.4 2001/05/18 21:04:33 mpowers + * Reimplemented EditingContext.initializeObject. + * + * Revision 1.3 2001/04/27 00:28:29 mpowers + * Fixed a return value. + * + * Revision 1.2 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.1 2001/03/28 17:49:33 mpowers + * Implemented NSKeyValueCodingSupport. + * + * + */ + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLock.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLock.java new file mode 100644 index 0000000..e6a5147 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLock.java @@ -0,0 +1,108 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSLock.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import EDU.oswego.cs.dl.util.concurrent.Mutex; + +/** +* A simple mutually-exclusive lock. Currently an API-compliance +* subclass of an external Mutex class, conforming to the API and +* behavior of com.webobjects.foundation.NSLock. This class implements +* the NSLocking protocol (interface), and is implemented using +* Doug Lea's concurrent programming package. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +* +*/ + +public class NSLock extends Mutex implements NSLocking { + + + public NSLock() { + } + + public synchronized void lock() { + try { + acquire(); + } catch (InterruptedException interruptedexception) { + notify(); + } + } + + public synchronized void unlock() { + release(); + } + + public synchronized boolean tryLock() { + return tryLock(0); + } + + public synchronized boolean tryLock(long l) { + try { + return attempt(l); + } catch (InterruptedException interruptedexception) { + notify(); + return false; + } + } + + public boolean tryLock(NSTimestamp nstimestamp) { + return tryLock(nstimestamp.getTime() - System.currentTimeMillis()); + } + + public String toString() { + return getClass().getName() + " <" + (inuse_ ? "Locked" : "Unlocked") + ">"; + } + + public synchronized boolean isLocked() { + return inuse_; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.4 2002/06/25 19:03:02 cgruber + * Internal documentation fixes. + * + * Revision 1.3 2002/06/25 18:52:56 cgruber + * Fix javadocs that resulted from bad cut-and-paste of the + * boilerplate. + * + * Revision 1.2 2002/06/25 17:45:52 cgruber + * Add implementation of NSLock using Doug Lea's concurrent + * programming APIs. + * + * Revision 1.1 2002/06/25 07:52:57 cgruber + * Add quite a few abstract classes, interfaces, and classes. All + * API consistent with WebObjects, but with no implementation, nor + * any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLocking.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLocking.java new file mode 100644 index 0000000..f8c8ff4 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLocking.java @@ -0,0 +1,59 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSLocking.java 892 2006-02-16 12:47:16Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +/** +* Defines a simple locking protocol. Very course-grain locking. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ + +public interface NSLocking { + + public static final long OneSecond = 1000L; + public static final long OneMinute = 60000L; + public static final long OneHour = 0x36ee80L; + public static final long OneDay = 0x5265c00L; + public static final long OneWeek = 0x240c8400L; + public static final long OneYear = 0x758f0dfc0L; + public static final long OneCentury = 0x2debe176700L; + + public abstract void lock(); + + public abstract void unlock(); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.2 2002/06/21 22:11:19 cgruber + * Add a log trail + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLog.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLog.java new file mode 100644 index 0000000..52e4090 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSLog.java @@ -0,0 +1,489 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.util.Date; + +/** +* NSLog is foundation's built-in logging facility: IMPLEMENTED, BUT NOT TESTED. +* By default, all groups are enabled, and debug level is DebugLevelOff. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSLog +{ + public static long DebugGroupApplicationGeneration = 1L << 3; + public static long DebugGroupArchiving = 1L << 6; + public static long DebugGroupAssociations = 1L << 19; + public static long DebugGroupComponentBindings = 1L << 9; + public static long DebugGroupControllers = 1L << 20; + public static long DebugGroupComponents = 1L << 26; + public static long DebugGroupDatabaseAccess = 1L << 16; + public static long DebugGroupDeployment = 1L << 22; + public static long DebugGroupEnterpriseObjects = 1L << 1; + public static long DebugGroupFormatting = 1L << 10; + public static long DebugGroupIO = 1L << 13; + public static long DebugGroupJSPServlets = 1L << 27; + public static long DebugGroupKeyValueCoding = 1L << 8; + public static long DebugGroupModel = 1L << 15; + public static long DebugGroupMultithreading = 1L << 4; + public static long DebugGroupParsing = 1L << 23; + public static long DebugGroupQualifiers = 1L << 11; + public static long DebugGroupReflection = 1L << 24; + public static long DebugGroupRequestHandling = 1L << 25; + public static long DebugGroupResources = 1L << 5; + public static long DebugGroupRules = 1L << 21; + public static long DebugGroupSQLGeneration = 1L << 17; + public static long DebugGroupTiming = 1L << 14; + public static long DebugGroupUserInterface = 1L << 18; + public static long DebugGroupValidation = 1L << 7; + public static long DebugGroupWebObjects = 1L << 2; + public static long DebugGroupWebServices = 1L << 2; + + public static int DebugLevelOff = 0; + public static int DebugLevelCritical = 1; + public static int DebugLevelInformational = 2; + public static int DebugLevelDetailed = 3; + + /** + * The logger to which debug statements should be + * conditionally written. By default, these messages + * appear on the standard error stream. + */ + public static Logger debug; + + /** + * The logger to which error messages should be written, + * which may not always be user-visible. By default, + * these messages appear on the standard error stream. + */ + public static Logger err; + + /** + * The logger to which user-visible messages should be written. + * By default, these messages appear on the standard output stream. + */ + public static Logger out; + + private static long allowedGroups; + private static int allowedLevel; + + static + { + debug = new PrintStreamLogger( System.err ); + err = new PrintStreamLogger( System.err ); + out = new PrintStreamLogger( System.out ); + + //TODO: need to initialize the debug level and groups based + // on the value of the NSDebugLevel and NSDebugGroup properties + allowedGroups = Long.MAX_VALUE; + allowedLevel = 0; + } + + /** + * Adds the specified group masks to those allowed for logging. + */ + public static void allowDebugLoggingForGroups(long aDebugGroups) + { + allowedGroups = allowedGroups | aDebugGroups; + } + + /** + * Returns the current logging debug level. + */ + public static int allowedDebugLevel() + { + return allowedLevel; + } + + /** + * Returns whether logging is allowed for the specified groups masks. + */ + public static boolean debugLoggingAllowedForGroups(long aDebugGroups) + { + return ( allowedGroups == ( allowedGroups | aDebugGroups ) ); + } + + /** + * Returns whether logging is allowed for the specified level. + */ + public static boolean debugLoggingAllowedForLevel(int aDebugLevel) + { + return ( allowedLevel >= aDebugLevel ); + } + + /** + * Returns whether logging allowed for the specified groups masks + * at the specified level. Convenience method. + */ + public static boolean debugLoggingAllowedForLevelAndGroups(int aDebugLevel, + long aDebugGroups) + { + return ( ( allowedLevel >= aDebugLevel ) + && ( allowedGroups == ( allowedGroups | aDebugGroups ) ) ); + } + + /** + * Convenience to obtain a java PrintStream for the specified file path. + * Returns null if the stream could not be created. + */ + public static PrintStream printStreamForPath(String aPath) + { + try + { + return new PrintStream( new FileOutputStream( aPath ) ); + } + catch ( Throwable t ) + { + } + return null; + } + + /** + * Removes the specified group masks from those allowed for logging. + */ + public static void refuseDebugLoggingForGroups(long aDebugGroups) + { + allowedGroups = ( allowedGroups | aDebugGroups ) ^ aDebugGroups; + } + + /** + * Sets the allowed groups to only those specified by the mask. + */ + public static void setAllowedDebugGroups(long aDebugGroups) + { + allowedGroups = aDebugGroups; + } + + /** + * Sets the current debug level. + */ + public static void setAllowedDebugLevel(int aDebugLevel) + { + allowedLevel = aDebugLevel; + } + + /** + * Sets the current debug logger. + * Does nothing if logger is null. + */ + public static void setDebug(NSLog.Logger logger) + { + if ( logger != null ) + { + debug = logger; + } + } + + /** + * Sets the current error logger. + * Does nothing if logger is null. + */ + public static void setErr(NSLog.Logger logger) + { + if ( logger != null ) + { + err = logger; + } + } + + /** + * Sets the current output logger. + * Does nothing if logger is null. + */ + public static void setOut(NSLog.Logger logger) + { + if ( logger != null ) + { + out = logger; + } + } + + /** + * Convenience to write the throwable's stack trace + * to a string. + */ + public static String throwableAsString(Throwable t) + { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream( os ); + t.printStackTrace( ps ); + return os.toString(); + } + + /** + * The abstract superclass of all Logger implementations. + */ + static abstract public class Logger + { + private boolean enabled; + private boolean verbose; + + /** + * Default constructor sets enabled + * and verbose to true. + */ + public Logger() + { + enabled = true; + verbose = true; + } + + /** + * Convenience to append a Boolean. + */ + public void appendln(boolean aValue) + { + appendln( new Boolean( aValue ) ); + } + + /** + * Convenience to append a Byte. + */ + public void appendln(byte aValue) + { + appendln( new Byte( aValue ) ); + } + + /** + * Convenience to write a String + * comprised of the byte array using + * the default encoding. + */ + public void appendln(byte[] aValue) + { + appendln( new String( aValue ) ); + } + + /** + * Convenience to append a Character. + */ + public void appendln(char aValue) + { + appendln( new Character( aValue ) ); + } + + /** + * Convenience to append a String + * comprised of the character array. + */ + public void appendln(char[] aValue) + { + appendln( new String( aValue ) ); + } + + /** + * Convenience to append a Double. + */ + public void appendln(double aValue) + { + appendln( new Double( aValue ) ); + } + + /** + * Convenience to append a Float. + */ + public void appendln(float aValue) + { + appendln( new Float( aValue ) ); + } + + /** + * Convenience to append a Integer. + */ + public void appendln(int aValue) + { + appendln( new Integer( aValue ) ); + } + + /** + * Convenience to append a Long. + */ + public void appendln(long aValue) + { + appendln( new Long( aValue ) ); + } + + /** + * Convenience to append a Short + */ + public void appendln(short aValue) + { + appendln( new Short( aValue ) ); + } + + /** + * Convenience to append a Throwable. + */ + public void appendln(Throwable aValue) + { + appendln( NSLog.throwableAsString( aValue ) ); + } + + /** + * Writes the object to the log. + */ + public abstract void appendln(Object aValue); + + /** + * Appends a line to the log. + */ + public abstract void appendln(); + + /** + * Flushes any buffered output to the log. + */ + public abstract void flush(); + + /** + * Returns whether the logger is enabled, + * the meaning of which is defined by the + * implementing class. + */ + public boolean isEnabled() + { + return enabled; + } + + /** + * Returns whether the logger is verbose, + * the meaning of which is defined by the + * implementing class. + */ + public boolean isVerbose() + { + return verbose; + } + + /** + * Sets whether the logger is enabled, + * the meaning of which is defined by the + * implementing class. + */ + public void setIsEnabled(boolean aBool) + { + enabled = aBool; + } + + /** + * Sets whether the logger is verbose, + * the meaning of which is defined by the + * implementing class. + */ + public void setIsVerbose(boolean aBool) + { + verbose = aBool; + } + } + + /** + * The default implementation of Logger that writes to a Java + * PrintStream. If not enabled, no output is written. + * If verbose, output is in format: "[time] message". + */ + static public class PrintStreamLogger extends Logger + { + private PrintStream thePrintStream; + + /** + * Constructor takes a PrintStream. + */ + public PrintStreamLogger(PrintStream ps) + { + thePrintStream = ps; + } + + /** + * Sends a newline to the print stream. + */ + public void appendln() + { + if ( isEnabled() ) + { + thePrintStream.println(); + } + } + + /** + * Writes the throwable to the print stream. + */ + public void appendln(Throwable aValue) + { + appendln( NSLog.throwableAsString( aValue ) ); + } + + /** + * Writes aValue.toString to the print stream. + */ + public void appendln(Object aValue) + { + if ( isEnabled() ) + { + if ( isVerbose() ) + { + thePrintStream.print( '[' + new Date().toString() + "] <" + + Thread.currentThread().getName() + "> " ); + } + if ( aValue == null ) aValue = "null"; + thePrintStream.println( aValue.toString() ); + } + } + + /** + * Flushes the print stream. + */ + public void flush() + { + thePrintStream.flush(); + } + + /** + * Returns the current print stream. + */ + public PrintStream printStream() + { + return thePrintStream; + } + + /** + * Replaces the current print stream. + */ + public void setPrintStream(PrintStream aStream) + { + thePrintStream = aStream; + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/31 22:33:00 mpowers + * Contributing NSLog. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMultiReaderLock.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMultiReaderLock.java new file mode 100644 index 0000000..abed576 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMultiReaderLock.java @@ -0,0 +1,159 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSMultiReaderLock.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import EDU.oswego.cs.dl.util.concurrent.ReentrantWriterPreferenceReadWriteLock; + +/** +* A Read-Write lock that allows unlimited number of calling threads to +* acquire read locks, but only one thread to acquire a write lock. It +* is also reentrant, allowing each thread to re-acquire it's lock +* recursively. For that reason it is somewhat slower, as there is a +* hash lookup when attempting to acquire and release reader locks. Of +* course a writer lock is quite a bit slower than a reader lock. A +* write lock is mutally exclusive with read locks, though a thread +* that has obtained a read-lock may be promoted to a write lock and +* vice versa when conditions permit. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSMultiReaderLock extends ReentrantWriterPreferenceReadWriteLock implements NSLocking { + + NSMutableDictionary _readerSuspended = new NSMutableDictionary(); + + public NSMultiReaderLock() { + } + + public void lockForReading() { + try { + readerLock_.acquire(); + } catch (InterruptedException interruptedexception) { + // Null behavior, as notify() is already called + // by acquire(); + // We may want to log here. + } + } + + public void unlockForReading() { + readerLock_.release(); + } + + public void lock() { + lockForWriting(); + } + + public void lockForWriting() { + try { + writerLock_.acquire(); + } catch (InterruptedException interruptedexception) { + // Null behavior, as notify() is already called + // by acquire(); + // We may want to log here. + } + } + + public void unlock() { + unlockForWriting(); + } + + public void unlockForWriting() { + writerLock_.release(); + } + + /** @see com.webobjects.foundation.NSMultiReaderLock#suspendReaderLock() */ + public void suspendReaderLocks() { + Thread thisThread = Thread.currentThread(); + Integer suspendedReaders = (Integer)_readerSuspended.get(thisThread); + if (suspendedReaders != null && suspendedReaders.intValue() > 0) return; + // logic is to override startRead / endRead and ensure that the suspension + // isn't improperly stopped. + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @see com.webobjects.foundation.NSMultiReaderLock#retrieveReaderLock() */ + public void retrieveReaderLocks() { + Thread thisThread = Thread.currentThread(); + Integer suspendedReaders = (Integer)_readerSuspended.get(thisThread); + if (suspendedReaders != null && suspendedReaders.intValue() > 0) return; + // logic is to override startRead / endRead and ensure that the suspension + // isn't improperly stopped. + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean tryLockForWriting() { + try { + return writerLock_.attempt(0); + } catch (InterruptedException interruptedexception) { + // notify() is already called by attempt(); + // We may want to log here. + return false; + } + } + + public boolean tryLockForReading() { + try { + return readerLock_.attempt(0); + } catch (InterruptedException interruptedexception) { + // notify() is already called by attempt(); + // We may want to log here. + return false; + } + } + + public String toString() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected String _padString(long l, int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected String _padString(String s, int i, boolean flag) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.2 2002/06/26 00:40:22 cgruber + * Add implementation, using ReentrantWriterPreferenceReadWriteLock + * as a base. + * + * suspendReaderLocks and retreiveReaderLocks is the one + * that's likeliest to be a pain. + * + * Revision 1.1 2002/06/25 07:52:57 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableArray.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableArray.java new file mode 100644 index 0000000..f705000 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableArray.java @@ -0,0 +1,429 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** +* NSMutableArray extends NSArray to allow modification. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSMutableArray extends NSArray +{ + /** + * Returns an NSArray backed by the specified List. + * This is useful to "protect" an internal representation + * that is returned by a method of return type NSArray. + */ + public static NSMutableArray mutableArrayBackedByList( List aList ) + { + return new NSMutableArray( aList, null ); + } + + NSMutableArray( List aList, Object ignored ) // differentiates + { + super( aList, ignored ); + } + + /** + * Default constructor returns an empty array. + */ + public NSMutableArray () + { + super(); +//System.out.println( "NSMutableArray: " + net.wotonomy.ui.swing.util.StackTraceInspector.getMyCaller() ); + } + + /** + * Constructor with a size hint. + */ + public NSMutableArray ( int aSize ) + { + super(); +//System.out.println( "NSMutableArray: " + net.wotonomy.ui.swing.util.StackTraceInspector.getMyCaller() ); + } + + /** + * Produces an array containing only the specified object. + */ + public NSMutableArray (Object anObject) + { + super( anObject ); +//System.out.println( "NSMutableArray: " + net.wotonomy.ui.swing.util.StackTraceInspector.getMyCaller() ); + } + + /** + * Produces an array containing the specified objects. + */ + public NSMutableArray (Object[] anArray) + { + super( anArray ); +//System.out.println( "NSMutableArray: " + net.wotonomy.ui.swing.util.StackTraceInspector.getMyCaller() ); + } + + /** + * Produces an array containing the objects in the specified collection. + */ + public NSMutableArray (Collection aCollection) + { + super( aCollection ); +//System.out.println( "NSMutableArray: " + net.wotonomy.ui.swing.util.StackTraceInspector.getMyCaller() ); + } + + /** + * Removes the last object from the array. + */ + public void removeLastObject () + { + list.remove( count() - 1 ); + } + + /** + * Removes the object at the specified index. + */ + public void removeObjectAtIndex (int index) + { + list.remove( index ); + } + + /** + * Adds all objects in the specified collection. + */ + public void addObjectsFromArray (Collection aCollection) + { + list.addAll( aCollection ); + } + + /** + * Removes all objects from the array. + */ + public void removeAllObjects () + { + list.clear(); + } + + /** + * Removes all objects equivalent to the specified object + * within the range of specified indices. + */ + public void removeObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( list.get( i ) ) ) + { + list.remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all instances of the specified object within the + * range of specified indices, comparing by reference. + */ + public void removeIdenticalObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == list.get( i ) ) + { + list.remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all objects in the specified collection from the array. + */ + public void removeObjectsInArray (Collection aCollection) + { + list.removeAll( aCollection ); + } + + /** + * Removes all objects in the indices within the specified range + * from the array. + */ + public void removeObjectsInRange (NSRange aRange) + { + if ( aRange == null ) return; + + for ( int i = 0; i < aRange.length(); i++ ) + { + list.remove( aRange.location() ); + } + } + + /** + * Replaces objects in the current range with objects from + * the specified range of the specified array. If currentRange + * is larger than otherRange, the extra objects are removed. + * If otherRange is larger than currentRange, the extra objects + * are added. + */ + public void replaceObjectsInRange (NSRange currentRange, + List otherArray, NSRange otherRange) + { + if ( ( currentRange == null ) || ( otherArray == null ) || + ( otherRange == null ) ) return; + + // transform otherRange if out of bounds for array + if ( otherRange.maxRange() > otherArray.size() ) + { + // TODO: Test this logic. + int loc = Math.min( otherRange.location(), otherArray.size() - 1 ); + otherRange = new NSRange( loc, otherArray.size() - loc ); + } + + Object o; + List subList = list.subList( + currentRange.location(), currentRange.maxRange() ); + int otherIndex = otherRange.location(); + // TODO: Test this logic. + for ( int i = 0; i < subList.size(); i++ ) + { + if ( otherIndex < otherRange.maxRange() ) + { // set object + subList.set( i, otherArray.get( otherIndex ) ); + } + else + { // remove extra elements from currentRange + subList.remove( i ); + i--; + } + otherIndex++; + } + // TODO: Test this logic. + for ( int i = otherIndex; i < otherRange.maxRange(); i++ ) + { + list.add( otherArray.get( i ) ); + } + } + + /** + * Clears the current array and then populates it with the + * contents of the specified collection. + */ + public void setArray (Collection aCollection) + { + list.clear(); + list.addAll( aCollection ); + } + + /** + * Sorts this array using the values from the specified selector. + */ + public void sortUsingSelector (NSSelector aSelector) + { + //TODO: implement + throw new UnsupportedOperationException( "Not implemented yet." ); + } + + /** + * Removes all objects equivalent to the specified object. + */ + public void removeObject (Object anObject) + { + list.remove( anObject ); + } + + /** + * Removes all occurences of the specified object, + * comparing by reference. + */ + public void removeIdenticalObject (Object anObject) + { + Iterator it = list.iterator(); + while ( it.hasNext() ) + { + if ( it.next() == anObject ) + { + it.remove(); + } + } + } + + /** + * Inserts the specified object into this array at the + * specified index. + */ + public void insertObjectAtIndex (Object anObject, int anIndex) + { + list.add( anIndex, anObject ); + } + + /** + * Replaces the object at the specified index with the + * specified object. + */ + public void replaceObjectAtIndex (int anIndex, Object anObject) + { + list.set( anIndex, anObject ); + } + + /** + * Adds the specified object to the end of this array. + */ + public void addObject (Object anObject) + { + list.add( anObject ); + } + + public Object clone() + { + return new NSMutableArray( list ); + } + + public NSArray immutableClone() { + return new NSArray(this); + } + + public NSMutableArray mutableClone() { + return new NSMutableArray(this); + } + + // interface List: mutators + + public void add(int index, Object element) + { + list.add( index, element ); + } + + public boolean add(Object o) + { + return list.add(o); + } + + public boolean addAll(Collection coll) + { + return list.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + return list.addAll( index, c ); + } + + public void clear() + { + list.clear(); + } + + public Iterator iterator() + { + return list.iterator(); + } + + public ListIterator listIterator() + { + return list.listIterator(); + } + + public ListIterator listIterator(int index) + { + return list.listIterator(); + } + + public Object remove(int index) + { + return list.remove( index ); + } + + public boolean remove(Object o) + { + return list.remove(o); + } + + public boolean removeAll(Collection coll) + { + return list.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + return list.retainAll(coll); + } + + public Object set(int index, Object element) + { + return list.set( index, element ); + } + + public List subList(int fromIndex, int toIndex) + { + return list.subList( fromIndex, toIndex ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2005/07/13 14:12:44 cgruber + * Add mutableClone() and immutableClone() per. WebObjects 5.3 conformance. + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.5 2003/01/10 19:16:40 mpowers + * Implemented support for page caching. + * + * Revision 1.4 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.3 2002/10/24 18:16:30 mpowers + * Now enforcing NSArray's immutable nature. + * + * Revision 1.2 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.1.1.1 2000/12/21 15:47:31 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableData.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableData.java new file mode 100644 index 0000000..c677ea7 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableData.java @@ -0,0 +1,167 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +/** +* A pure java implementation of NSMutableData, which +* is basically an editable wrapper for a byte array. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ +public class NSMutableData + extends NSData +{ + /** + * Default constructor creates a zero-data object. + */ + public NSMutableData () + { + super(); + } + + /** + * Creates an object containing the contents of the specified URL. + */ + public NSMutableData (java.net.URL aURL) + { + super( aURL ); + } + + /** + * Creates an object containing a copy of the contents of the + * specified NSData object. + */ + public NSMutableData (NSData aData) + { + super( aData ); + } + + /** + * Creates an object containing the specified number of bytes + * initialized to all zeroes. + */ + public NSMutableData (int size) + { + super( new byte[size] ); // inits to zeroes + } + + + /** + * Sets the length of the data to the specified length. + * If shorter, the data is truncated. If longer, the extra + * bytes are initialized to zeroes. + */ + public void setLength (int length) + { + byte[] data = new byte[ length ]; // inits to zeroes + int limit = length; + if (limit > bytes.length) + limit = bytes.length; + for ( int i = 0; i < limit; i++ ) + { + data[i] = this.bytes[ i ]; + } + this.bytes = data; + } + + /** + * Appends the specified data to the end of this data. + */ + public void appendData (NSData aData) + { + int len = aData.length(); + byte[] data = new byte[ bytes.length + len ]; + + int i; + for ( i = 0; i < bytes.length; i++ ) + { + data[i] = bytes[i]; + } + + byte[] src = aData.bytes( 0, len ); + for ( int j = 0; j < len; j++ ) + { + data[i+j] = src[j]; + } + + bytes = data; + } + + public void appendByte(byte b) { + setLength(bytes.length + 1); + bytes[bytes.length-1] = b; + } + + public void appendBytes(byte[] b) { + int origLen = bytes.length; + setLength(origLen + b.length); + for (int i = 0; i < b.length; i++) + bytes[i + origLen] = b[i]; + } + + /** + * Increases the size of the byte array by the specified amount. + */ + public void increaseLengthBy (int increment) + { + setLength( length() + increment ); + } + + /** + * Sets the bytes in the array within the specified range to zero. + */ + public void resetBytesInRange (NSRange aRange) + { + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + bytes[i] = 0; + } + } + + /** + * Copies the data in the specified object to this object, + * completely replacing the previous contents. + */ + public void setData (NSData aData) + { + bytes = aData.bytes( 0, aData.length() ); + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:57:13 chochos + * appendByte(), appendBytes() + * + * Revision 1.1.1.1 2000/12/21 15:47:34 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableDictionary.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableDictionary.java new file mode 100644 index 0000000..0b291a7 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableDictionary.java @@ -0,0 +1,166 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Enumeration; +import java.util.Map; + +/** +* A pure java implementation of NSMutableDictionary that +* implements Map for greater java interoperability. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSMutableDictionary + extends NSDictionary +{ + /** + * Default constructor produces an empty dictionary. + */ + public NSMutableDictionary () + { + super(); + } + + /** + * Default constructor produces an empty dictionary. + */ + public NSMutableDictionary (int initialSize) + { + super(initialSize); + } + + /** + * Produces a dictionary that contains one key referencing one value. + */ + public NSMutableDictionary (Object key, Object value) + { + super( key, value ); + } + + /** + * Produces a dictionary containing the specified keys and values. + * An IllegalArgumentException is thrown if the arrays are not + * of the same length. + */ + public NSMutableDictionary (Object[] keys, Object[] values) + { + super( keys, values ); + } + + /** + * Produces a dictionary that is a copy of the specified map (or dictionary). + */ + public NSMutableDictionary (Map aMap) + { + super( aMap ); + } + + /** + * Removes the key-value pair for the specified key. + */ + public void removeObjectForKey (Object aKey) + { + remove( aKey ); + } + + /** + * Copies all mappings from the specified dictionary to this dictionary, + * replacing any mappings this map had for any keys in the specified map. + */ + public void addEntriesFromDictionary (Map aMap) + { + putAll( aMap ); + } + + /** + * Removes all mappings from this dictionary. + */ + public void removeAllObjects () + { + clear(); + } + + /** + * Removes all keys in the specified array from this dictionary. + */ + public void removeObjectsForKeys (NSArray anArray) + { + Enumeration enumeration = anArray.objectEnumerator(); + while ( enumeration.hasMoreElements() ) + { + removeObjectForKey( enumeration.nextElement() ); + } + } + + /** + * Clears all mappings in this dictionary and then adds all entries + * in the specified dictionary. + */ + public void setDictionary (Map aMap) + { + removeAllObjects(); + addEntriesFromDictionary( aMap ); + } + + /** + * Sets the value for the specified key. If the key currently + * exists to the dictionary, the old value is replaced with the + * specified value. An IllegalArgumentException is thrown if + * either the key or value is null. + */ + public void setObjectForKey (Object aValue, Object aKey) + { + if ( ( aKey == null ) || ( aValue == null ) ) + { + throw new IllegalArgumentException( + "Cannot use null objects with an NSMutableDictionary." ); + } + put( aKey, aValue ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.3 2002/06/30 17:16:26 mpowers + * Added new constructor taking an int: thanks cgruber. + * + * + * Revision 1.2 2001/02/23 23:43:41 mpowers + * Removed ill-advised this. + * + * Revision 1.1.1.1 2000/12/21 15:47:34 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableRange.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableRange.java new file mode 100644 index 0000000..a0bcda1 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSMutableRange.java @@ -0,0 +1,116 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +/** +* A pure java implementation of NSMutableRange. +* An NSMutableRange is a modifiable NSRange. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSMutableRange extends NSRange +{ + /** + * Default constructor produces an empty range. + */ + public NSMutableRange () + { + super(); + } + + /** + * Produces a range that has the same location and length as + * the specified range. + */ + public NSMutableRange (NSRange aRange) + { + super( aRange ); + } + + /** + * Produces a range with the specified location and length. + */ + public NSMutableRange (int location, int length) + { + super( location, length ); + } + + /** + * Sets the location of this range. + */ + public void setLocation (int location) + { + loc = location; + } + + /** + * Sets the length of this range. + */ + public void setLength (int length) + { + len = length; + } + + /** + * Modifies this range to be the union of this + * range and the specified range. + */ + public void unionRange (NSRange aRange) + { + NSRange range = rangeByUnioningRange( aRange ); + setLocation( range.location() ); + setLength( range.length() ); + } + + /** + * Modifies this range to be the intersection of this + * range and the specified range. + */ + public void intersectRange (NSRange aRange) + { + NSRange range = rangeByIntersectingRange( aRange ); + setLocation( range.location() ); + setLength( range.length() ); + } + + /** + * Returns a copy of this range. + */ + public Object clone () + { + return new NSMutableRange( location(), length() ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:36 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotification.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotification.java new file mode 100644 index 0000000..f288d3f --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotification.java @@ -0,0 +1,164 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Map; + +/** +* An NSNotification is a generic message that can be +* dispatched by the NSNotificationCenter. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSNotification +{ + public static boolean showStack = false; + + protected String name; + protected Object object; + protected Map userInfo; + + // for debugging only + private Throwable stackTrace; + + /** + * Default constructor creates a new notification + * with no name, object, or info dictionary. + */ + public NSNotification () + { + this( null, null, null ); + } + + /** + * Constructor specifying name and object. + */ + public NSNotification ( String aName, Object anObject ) + { + this( aName, anObject, null ); + } + + /** + * Constructor specifying name, object, and a Map + * containing application specific information. + */ + public NSNotification ( + String aName, Object anObject, Map aUserInfo ) + { + name = aName; + object = anObject; + if ( showStack ) stackTrace = new RuntimeException(); + userInfo = aUserInfo; + } + + /** + * Returns the name of this notification. + */ + public String name () + { + return name; + } + + /** + * Returns the object of this notification. + */ + public Object object () + { + return object; + } + + /** + * Returns an NSDictionary that is a copy of + * the map containing application specific + * information relating to this notification, + * or null if no such data exists. + */ + public NSDictionary userInfo () + { + if ( userInfo == null ) return null; + return new NSDictionary( userInfo ); + } + + /** + * Returns a Map containing application specific + * information relating to this notification, + * or null if no such data exists. + * Note: this method is not in the spec. + */ + public Map userInfoMap () + { + return userInfo; + } + + /** + * Returns the stack trace when this notification was generated, + * or null if showStack is false, which is the default. + * NOTE: This method is not part of the specification. + */ + public Throwable stackTrace() + { + return stackTrace; + } + + /** + * Returns a human-readable string representation. + */ + public String toString() + { + return "[ " + name() + " : " + object() + " : " + userInfo() + " ]"; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2002/10/24 18:16:04 mpowers + * No longer generating stack trace by default. + * + * Revision 1.6 2002/06/21 22:02:47 mpowers + * Oops: Fixed NPE. + * + * Revision 1.5 2002/06/21 21:50:41 mpowers + * Added a method to get the map directly from the notification. + * Changed the internal representation to a map not a dictionary. + * We had been creating a new dictionary with each creation. + * This also allows people to modify the contents of the userInfo. + * + * Revision 1.4 2001/04/09 21:41:49 mpowers + * Better debugging. + * + * Revision 1.3 2001/02/21 18:31:07 mpowers + * Finished and tested implementation of NSNotificationCenter. + * + * Revision 1.2 2001/02/20 23:57:03 mpowers + * Implemented NSNotificationCenter. + * + * Revision 1.1.1.1 2000/12/21 15:47:36 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationCenter.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationCenter.java new file mode 100644 index 0000000..aaf8261 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationCenter.java @@ -0,0 +1,671 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* NSNotificationCenter broadcasts NSNotifications to +* registered observers. Observers can register for all +* notifications of a specific type, or all notifications +* about a specific object, or both. Observers specify +* the method that will be called when they are notified. +* A global notification center can be accessed with +* defaultCenter(), but other centers can be created and +* used independently of the default center.

+* +* This implementation uses weak references for observers +* and observables. The advantage to this approach is +* that you do not need to explicitly unregister observers +* or observables; they will be unregistered when they are +* garbage-collected. Note that you will need to retain +* a reference to any objects you register or they may +* become unregistered if no other object references them. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSNotificationCenter +{ + /** + * Null marker class simplifies equals() logic + * for CompoundKey class below. + */ + public static final Object NullMarker = new Object(); + + private static NSNotificationCenter defaultCenter = null; + + /** + * A Map of (name,object) pairs to a List + * of (observer,selector) pairs. + */ + private Hashtable observers; // thread-safe + + /** + * Default constructor creates a new notification center. + */ + public NSNotificationCenter() + { + observers = new Hashtable(); + } + + /** + * Returns the system default center, creating one + * if it has not yet been created. + */ + static public NSNotificationCenter defaultCenter() + { + if ( defaultCenter == null ) + { + defaultCenter = new NSNotificationCenter(); + } + return defaultCenter; + } + + /** + * Addes the specified observer to the notification queue for + * notifications with the specified name or the specified + * object or both. + * @param anObserver The observer that wishes to be notified. + * @param aSelector The selector that will be invoked. + * Must have exactly one argument, to which a notification + * will be passed. + * @param notificationName The name of the notifications for + * which the observer will be notified. If null, will notify + * only based on matching anObject. + * @param anObject The object of the notifications for which + * the observer will be notified. If null, will notify + * only based on matching notificationName. + */ + public void addObserver( + Object anObserver, NSSelector aSelector, + String notificationName, Object anObject ) + { + // remove freed objects + processKeyQueue(); + + Object name = notificationName; + if ( name == null ) + { + name = NullMarker; + } + if ( anObject == null ) + { + anObject = NullMarker; + } + Object key = new CompoundKey( name, anObject ); + Object value = new CompoundValue( anObserver, aSelector ); + List list = (List) observers.get( key ); + if ( list == null ) + { + // create new list with value and put it in map + list = new Vector(); // thread-safe + list.add( value ); + observers.put( new CompoundKey( + name, anObject, keyQueue ), list ); + } + else + { + // add only if not already in list + if ( ! list.contains( value ) ) + { + list.add( value ); + } + } + } + + /** + * Posts the specified notification. Notifies all registered + * observers that match either the notification name or + * the notification object, or both. + * @param aNotification The notification that will be passed + * to the observers selector. + */ + public void postNotification( + NSNotification aNotification ) + { + List mergedList = new LinkedList(); + Object key, observerList; + + Object name = aNotification.name(); + Object object = aNotification.object(); + + if ( name != null ) + { + if ( object != null ) + { // both are specified + observerList = observers.get( new CompoundKey( name, object ) ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + observerList = observers.get( new CompoundKey( name, NullMarker ) ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + observerList = observers.get( new CompoundKey( NullMarker, object ) ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + } + else + { // object is null + observerList = observers.get( new CompoundKey( name, NullMarker ) ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + } + } + else + if ( object != null ) + { // name is null + observerList = observers.get( new CompoundKey( NullMarker, object ) ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + } + + key = new CompoundKey( + NullMarker, NullMarker ); + observerList = observers.get( key ); + if ( observerList != null ) + { + mergedList.addAll( (List) observerList ); + } + + CompoundValue value; + Iterator it = mergedList.iterator(); + while ( it.hasNext() ) + { + value = (CompoundValue) it.next(); + if ( value.get() == null ) + { + it.remove(); + } + else + { + try + { + value.selector().invoke( + value.get(), + new Object[] { aNotification } ); + } + catch ( Exception exc ) + { + WotonomyException w = new WotonomyException( + "Error notifying object: " + value.get() + " : " + aNotification, exc ); +// throw w; + w.printStackTrace(); +postNotification( "Error notifying object", this, new NSDictionary( "exception", w ) ); + } + } + } + + } + + /** + * Posts a notification created from the specified name + * and object. Calls postNotification( NSNotification ). + * @param notificationName a String key to distinguish + * this notification. + * @param anObject any object, by convention this is + * the originator of the notification. + */ + public void postNotification( + String notificationName, Object anObject ) + { + postNotification( new NSNotification( + notificationName, anObject ) ); + } + + /** + * Posts a notification created from the specified name, + * object, and info. Calls postNotification( NSNotification ). + * @param notificationName a String key to distinguish + * this notification. + * @param anObject any object, by convention this is + * the originator of the notification. + * @param userInfo a Map containing information specific + * to the originator of the notification and that may + * be of interest to a knowledgable observer. + */ + public void postNotification( + String notificationName, Object anObject, Map userInfo ) + { + postNotification( new NSNotification( + notificationName, anObject, userInfo ) ); + } + + /** + * Unregisters the specified observer from all notification + * queues for which it is registered. + * @param anObserver The observer to be unregistered. + */ + public void removeObserver( + Object anObserver ) + { + // remove freed objects + processKeyQueue(); + + Iterator it = new LinkedList( observers.keySet() ).iterator(); + while ( it.hasNext() ) + { + removeObserver( anObserver, it.next() ); + } + } + + /** + * Unregisters the specified observer from all notifications + * queues associated with the specified name or object or both. + * @param anObserver The observer to be unregistered, if null + * will unregister all observers for the specified notification + * name and object. + * @param notificationName The name of the notification for which + * the observer will be unregistered, if null will unregister + * the specified observer for all notifications with the + * specified object. + * @param anObject The object for the notification for which + * the observer will be unregistered, if null will unregister + * the specified observer for all objects with the specified + * notification. + */ + public void removeObserver( + Object anObserver, String notificationName, Object anObject ) + { + // remove freed objects + processKeyQueue(); + + // get key matches + List keys = matchingKeys( notificationName, anObject ); + + // remove specified observer from each matching key + Iterator it = keys.iterator(); + while ( it.hasNext() ) + { + removeObserver( anObserver, it.next() ); + } + } + + /** + * Returns all keys that match the specified name and object, + * but in this case null parameters are considered wildcards. + * Pass NullMarkers if you want to explicitly match nulls. + */ + private List matchingKeys( String name, Object object ) + { + List result = new LinkedList(); + + boolean willAdd; + CompoundKey key; + Iterator it = observers.keySet().iterator(); + while ( it.hasNext() ) + { + key = (CompoundKey) it.next(); + willAdd = false; + if ( ( name == null ) || ( name == key.name() ) ) + { + if ( ( object == null ) || ( object == key.get() ) ) + { + willAdd = true; + } + } + if ( willAdd ) + { + result.add( key ); + } + } + return result; + } + + /** + * Removes the specified observer from the list referenced + * by the specified key in the observer map. + */ + private void removeObserver( + Object anObserver, Object key ) + { + // if observer null, remove all observers for key + if ( anObserver == null ) + { + observers.remove( key ); + return; + } + + List list = (List) observers.get( key ); + if ( list == null ) return; + + // remove specified observer from list + Object observer; + Iterator it = list.iterator(); + while ( it.hasNext() ) + { + observer = ((CompoundValue)it.next()).get(); + if ( ( observer == null ) || ( anObserver == observer ) ) + { + // remove if match or freed object + it.remove(); + + // do not return; process entire list + } + } + if ( list.size() == 0 ) + { + observers.remove( key ); + } + } + + /* Reference queues for cleared WeakKeys */ + private ReferenceQueue keyQueue = new ReferenceQueue(); + + /** + * Removes any keys whose object has been garbage collected. + * (Garbage collected values are removed as they are encountered.) + */ + private void processKeyQueue() + { + CompoundKey ck; + while ((ck = (CompoundKey)keyQueue.poll()) != null) + { + //System.out.println( "EOObserverCenter.processQueue: removing object" ); + observers.remove(ck); + } + } + + /** + * Key combining a name with an object. + * The object is weakly referenced, and keys + * are deallocated by reference queue. + * equals() compares by reference. + */ + private static class CompoundKey extends WeakReference + { + private Object name; + private int hashCode; + + /** + * Creates compound key. + * Neither name nor object may be null. + * Use NullMarker to represent null + * in either name or object. + */ + public CompoundKey ( + Object aName, Object anObject ) + { + super( anObject ); + name = aName; + hashCode = aName.hashCode() + anObject.hashCode(); + } + + /** + * Creates compound key with queue. + * Neither name nor object may be null. + * Use NullMarker to represent null + * in either name or object. + */ + public CompoundKey ( + Object aName, Object anObject, ReferenceQueue aQueue ) + { + super( anObject, aQueue ); + name = aName; + hashCode = aName.hashCode() + anObject.hashCode(); + } + + public Object name() + { + return name; + } + + public int hashCode() + { + return hashCode; + } + + public boolean equals( Object anObject ) + { + if ( this == anObject ) return true; + // assumes only used with other compound keys + CompoundKey key = (CompoundKey) anObject; + if ( name == key.name || ( name != null && name.equals( key.name ) ) ) + { + Object object = get(); + if ( object != null ) + { + // compares by reference + if ( object == ( key.get() ) ) + { + return true; + } + } + } + return false; + } + + public String toString() + { + return "[CompoundKey:"+name()+":"+get()+"]"; + } + } + + /** + * Value combining an object with a selector. + * The object is weakly referenced, and null + * values are not allowed. + */ + private static class CompoundValue extends WeakReference + { + private NSSelector selector; + private int hashCode; + + public CompoundValue( Object anObject, NSSelector aSelector ) + { + super( anObject ); + hashCode = anObject.hashCode(); + selector = aSelector; + } + + public NSSelector selector() + { + return selector; + } + + public int hashCode() + { + return hashCode; + } + + public boolean equals( Object anObject ) + { + if ( this == anObject ) return true; + // assumes only used with other compound values + CompoundValue value = (CompoundValue) anObject; + if ( selector == value.selector || + ( selector != null && selector.equals( value.selector ) ) ) + { + Object object = get(); + if ( object != null ) + { + if ( object == value.get() ) + { + return true; + } + } + } + return false; + } + + public String toString() + { + return "[CompoundValue:"+get()+":"+selector().name()+"]"; + } + } +/* + public static void main( String[] argv ) + { + Object aSource = "aSource"; + Object bSource = "bSource"; + + Object oneTest = new OneTest(); + Object twoTest = new TwoTest(); + NSSelector notifyMeOnce = + new NSSelector( "notifyMeOnce", + new Class[] { NSNotification.class } ); + NSSelector notifyMeTwice = + new NSSelector( "notifyMeTwice", + new Class[] { NSNotification.class } ); + + NSNotificationCenter.defaultCenter().addObserver( + oneTest, notifyMeOnce, "aMessage", null ); + + NSNotificationCenter.defaultCenter().addObserver( + oneTest, notifyMeOnce, null, aSource ); + + NSNotificationCenter.defaultCenter().addObserver( + twoTest, notifyMeOnce, "aMessage", aSource ); + + NSNotificationCenter.defaultCenter().addObserver( + twoTest, notifyMeTwice, null, null ); + + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", bSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", bSource ); + System.out.println( "---" ); + + NSNotificationCenter.defaultCenter().removeObserver( + oneTest, null, aSource ); + + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", bSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", bSource ); + System.out.println( "---" ); + + NSNotificationCenter.defaultCenter().removeObserver( + null ); + + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "aMessage", bSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", aSource ); + System.out.println(); + NSNotificationCenter.defaultCenter().postNotification( + "bMessage", bSource ); + System.out.println( "---" ); + } + + static private class OneTest + { + public void notifyMeOnce( NSNotification aNotification ) + { + System.out.println( "OneTest.notifyMeOnce: " + aNotification ); + } + } + + static private class TwoTest + { + public void notifyMeOnce( NSNotification aNotification ) + { + System.out.println( "TwoTest.notifyMeOnce: " + aNotification ); + } + public void notifyMeTwice( NSNotification aNotification ) + { + System.out.println( "TwoTest.notifyMeTwice: " + aNotification ); + } + } +*/ +} + + + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.11 2003/06/03 14:51:15 mpowers + * Added commented-out println for debugging. + * + * Revision 1.10 2003/03/27 21:46:00 mpowers + * Better handling for null parameters on subscribe. + * Better handling for null parameters on post. + * + * Revision 1.9 2003/01/28 19:44:38 mpowers + * Now comparing strings by value not reference. + * + * Revision 1.8 2001/06/29 16:14:23 mpowers + * Fixed a javac compiler error that jikes allowed: shoe's on the other foot! + * + * Revision 1.7 2001/06/07 22:09:03 mpowers + * Exceptions during a notification are no longer being thrown + * so we can assure that all notifications get handled. + * Instead, we're printing stack traces... + * + * Revision 1.6 2001/04/09 21:41:50 mpowers + * Better debugging. + * + * Revision 1.5 2001/03/15 21:09:06 mpowers + * Fixed notifications with null objects. + * + * Revision 1.4 2001/02/21 21:18:34 mpowers + * Clarified need to retain references. + * + * Revision 1.3 2001/02/21 18:31:07 mpowers + * Finished and tested implementation of NSNotificationCenter. + * + * Revision 1.1.1.1 2000/12/21 15:47:39 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationQueue.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationQueue.java new file mode 100644 index 0000000..7350a39 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNotificationQueue.java @@ -0,0 +1,345 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** +* NSNotificationQueue coalesces notifications to be +* posted to the NSNotificationCenter and can post them +* asynchronously. While calling postNotification on +* the notification center does not return until all +* receivers have been notified, calling enqueueNotification +* can return immediately. Use this class when you want +* to coalesce notifications or notify asynchronously, or +* both, which is the typical case. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSNotificationQueue +{ + private static NSNotificationQueue defaultQueue = null; + + /** + * Posting style specifying that the notification should + * be posted on the next available event loop. + */ + public static final int PostASAP = 4; + + /** + * Posting style specifying that the notification should + * be posted on the next available idle loop. + */ + public static final int PostWhenIdle = 8; + + /** + * Posting style specifying that the notification should + * be posted immediately. The enqueue method will not + * return until all receivers have been notified. + */ + public static final int PostNow = 16; + + /** + * Used to indicate that this notification should not + * be coalesced with other notifications. + * Ignored if combined with other coalesce flags. + */ + public static final int NotificationNoCoalescing = 0; + + /** + * Used to indicate that this notification should + * be coalesced with other notifications with the + * same name. + * May be combined with NotificationCoalescingOnSender. + */ + public static final int NotificationCoalescingOnName = 1; + + /** + * Used to indicate that this notification should + * be coalesced with other notifications with the + * same object argument (which is typically the sender). + * May be combined with NotificationCoalescingOnName. + */ + public static final int NotificationCoalescingOnSender = 2; + + /** + * The ASAP queue, which should probably use a LinkedList. + */ + private List queue; + + /** + * The idle queue, which should probably use a LinkedList. + */ + private List idleQueue; + + /** + * The notification center we will be using. + */ + private NSNotificationCenter center; + + /** + * Our private ASAP notifier. + */ + private Notifier notifier; + + /** + * Our private idle notifier. + */ + private Notifier idleNotifier; + + /** + * Default constructor creates a new notification queue + * that uses the default notification center. + */ + public NSNotificationQueue() + { + this( NSNotificationCenter.defaultCenter() ); + } + + /** + * Creates a new notification queue that uses the + * specified notification center. + */ + public NSNotificationQueue( + NSNotificationCenter aCenter ) + { + queue = new LinkedList(); + idleQueue = new LinkedList(); + center = aCenter; + notifier = new Notifier( this, queue ); + idleNotifier = new Notifier( this, idleQueue ); + } + + /** + * Returns the system default queue, creating one + * if it has not yet been created. The system default + * queue uses the system default notification center. + */ + static public NSNotificationQueue defaultQueue() + { + if ( defaultQueue == null ) + { + defaultQueue = new NSNotificationQueue(); + } + return defaultQueue; + } + + /** + * Removes notifications from the queue that match + * the specified notification, considering the + * specified coalesce mask. + */ + public void dequeueMatchingNotifications( + NSNotification aNotification, + int aCoalesceMask) + { + if ( aCoalesceMask == NotificationNoCoalescing ) return; + dequeueFromQueue( aNotification, aCoalesceMask, queue ); + dequeueFromQueue( aNotification, aCoalesceMask, idleQueue ); + } + + private void dequeueFromQueue( + NSNotification aNotification, + int aCoalesceMask, + List aQueue ) + { + synchronized ( aQueue ) + { + int flag; + NSNotification notification; + Object name = aNotification.name(); + Object object = aNotification.object(); + Iterator it = aQueue.iterator(); + while ( it.hasNext() ) + { + flag = 0; + notification = (NSNotification) it.next(); + // if NotificationCoalescingOnName + if ( ( aCoalesceMask == 1 ) || ( aCoalesceMask == 3 ) ) + { + if ( name == null ) + { + if ( notification.name() != null ) + { + flag += NotificationCoalescingOnName; + } + } + else + { + // compare by value + if ( name.equals( notification.name() ) ) + { + flag += NotificationCoalescingOnName; + } + } + } + // if NotificationCoalescingOnSender + if ( aCoalesceMask >= 2 ) + { + // compare by reference + if ( object == notification.object() ) + { + flag += NotificationCoalescingOnSender; + } + } + + if ( flag == aCoalesceMask ) + { + it.remove(); + } + } + } + } + + /** + * Adds the notification to the queue to be run at + * the time specified by the posting style. The + * notification will be coalesced with other notifications + * that match the same name and object (sender) argument. + */ + public void enqueueNotification( + NSNotification aNotification, + int aPostingStyle) + { + enqueueNotificationWithCoalesceMaskForModes( + aNotification, aPostingStyle, + NotificationCoalescingOnName + NotificationCoalescingOnSender, + null ); + } + + /** + * Adds the notification to the queue to be run at + * the time specified by the posting style and coelesced + * as the specified mask indicates. + * aModeList is currently ignored and may be null. + */ + public void enqueueNotificationWithCoalesceMaskForModes( + NSNotification aNotification, + int aPostingStyle, + int aCoalesceMask, + List aModeList) + { + dequeueMatchingNotifications( aNotification, aCoalesceMask ); + + if ( aPostingStyle == PostNow ) + { + center.postNotification( aNotification ); + return; + } + + if ( aPostingStyle == PostASAP ) + { + synchronized ( queue ) + { + queue.add( aNotification ); + if ( ! notifier.willRun ) + { + // asap runs at the very first run loop ordering, plus one just in case + NSRunLoop.invokeLaterWithOrder( notifier, 1 ); + notifier.willRun = true; + } + } + return; + } + + if ( aPostingStyle == PostWhenIdle ) + { + synchronized ( idleQueue ) + { + idleQueue.add( aNotification ); + if ( ! idleNotifier.willRun ) + { + // when idle runs at the very last run loop ordering, minus one just in case + NSRunLoop.invokeLaterWithOrder( idleNotifier, Integer.MAX_VALUE - 1 ); + idleNotifier.willRun = true; + } + } + return; + } + + } + + private class Notifier implements Runnable + { + public boolean willRun; + + NSNotificationQueue parent; + List queue; + + public Notifier( + NSNotificationQueue aParent, List aQueue ) + { + willRun = false; + parent = aParent; + queue = aQueue; + } + + public void run() + { + Iterator it = new LinkedList( queue ).iterator(); + synchronized ( queue ) + { + queue.clear(); + } + willRun = false; + + // queue must already be cleared and willRun reset + // because this loop might queue more notifications + while ( it.hasNext() ) + { + parent.center.postNotification( + (NSNotification) it.next() ); + } + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/11 18:18:08 chochos + * improved encoding of strings, removed warnings + * + * Revision 1.5 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2001/11/01 15:49:26 mpowers + * With NSRunLoop, we can now correctly implement PostASAP and PostWhenIdle. + * + * Revision 1.3 2001/06/25 14:47:24 mpowers + * Fixed serious error where some notifications were not being posted at all. + * A simple change to Notifier class fixed the problem. Thanks to glista. + * + * Revision 1.2 2001/02/26 15:53:22 mpowers + * Fine-tuning notification firing. + * Child display groups now update properly after parent save or invalidate. + * + * Revision 1.1 2001/02/24 17:03:22 mpowers + * Implemented the notification queue, and changed editing context to use it. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNull.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNull.java new file mode 100644 index 0000000..db1f216 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNull.java @@ -0,0 +1,104 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.Serializable; + +/** +* NSNull is used to represent null in Collections classes +* because List and Map do not specify whether null values +* are allowed and because NSArray and NSDictionary explicitly +* do not allow null values.

+* +* Use of the static singleton method nullValue() is required +* by this implementation because Java cannot return a singleton +* instance from a constructor. Even then, more than one instance +* may exist in the application due to object serialization. +* Be sure to compare with equals(). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ +public class NSNull implements Serializable +{ + private static final NSNull instance = new NSNull(); + + /** + * Create a new instance of NSNull. + */ + private NSNull () + { + } + + /** + * Returns the static instance of nullValue. + * Note that serialization may mean that more than + * one instance of NSNull exists, so be sure to + * compare with equals(). + */ + public static NSNull nullValue () + { + return instance; + } + + /** + * Returns a human-readable string representation. + */ + public String toString() + { + return "[null]"; + } + + /** + * Hashcode of all instances is zero. + */ + public int hashCode() + { + return 0; + } + + /** + * Implemented to return true for any instance of NSNull. + */ + public boolean equals( Object anObject ) + { + return ( anObject instanceof NSNull ); + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/03/01 20:36:09 mpowers + * Implemented equals, hashcode, and serializable. + * + * Revision 1.1 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNumberFormatter.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNumberFormatter.java new file mode 100644 index 0000000..064ee3c --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSNumberFormatter.java @@ -0,0 +1,57 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSNumberFormatter.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.text.DecimalFormat; + +/** +* A Format that accepts C-style number formatting syntax. +* Not currently implemented, included for compile compatibility. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class NSNumberFormatter extends DecimalFormat +{ + public NSNumberFormatter() + { + super(); + } + + public NSNumberFormatter(String aPattern) + { + super( aPattern ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/17 14:40:50 mpowers + * Adding files to fix build. + * + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java new file mode 100644 index 0000000..7f8bcc9 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSPropertyListSerialization.java @@ -0,0 +1,275 @@ + +package net.wotonomy.foundation; + +public class NSPropertyListSerialization { + + public static final int PLIST_ARRAY = 0; + public static final int PLIST_DICTIONARY = 1; + public static final int PLIST_DATA = 2; + public static final int PLIST_STRING = 3; + + public static final char[] TOKEN_BEGIN = new char[]{ + '(', '{', '<', '"' + }; + public static final char[] TOKEN_END = new char[]{ + ')', '}', '>', '"' + }; + public static final char[] QUOTING_CHARS = new char[]{ + ':', '/', '-', '.', '\\' + }; + + private NSPropertyListSerialization() { + super(); + } + + /** Creates a NSArray object from a string representation. + @s The string representation of a NSArray object. */ + public static NSArray arrayForString(String s) { + s = s.trim(); + if (!(s.charAt(0) == TOKEN_BEGIN[PLIST_ARRAY] && s.charAt(s.length()-1) == TOKEN_END[PLIST_ARRAY])) + return null; + NSMutableArray arr = new NSMutableArray(); + int pos = 1; + int valbegin = -1; + while (pos < s.length()) { + char c = s.charAt(pos); + int tokenCount = 0; + int what = 0; + for (int i = 0 ; i < TOKEN_BEGIN.length; i++) { + if (c == TOKEN_BEGIN[i]) { + tokenCount = 1; + what = i; + break; + } + } + if (tokenCount > 0) { + //mark it + int quote = pos; + //find the closing token + do { + pos++; + try { + c = s.charAt(pos); + } catch (StringIndexOutOfBoundsException ex) { + throw new IllegalArgumentException("Could not parse property list; unclosed token '" + TOKEN_BEGIN[what] + "'"); + } + if (c == '"' && what == PLIST_STRING) { + if (pos > 0 && s.charAt(pos-1) != '\\') { + tokenCount--; + } + } else if (c == TOKEN_BEGIN[what]) + tokenCount++; + else if (c == TOKEN_END[what]) + tokenCount--; + } while (tokenCount > 0); + arr.addObject(propertyListFromString(s.substring(quote, pos+1))); + valbegin = -1; + //advance to the next position + do { + pos++; + c = s.charAt(pos); + } while (Character.isWhitespace(c)); + } + if (c == ',' || c ==')') { + if (valbegin > 0) { + arr.addObject(s.substring(valbegin, pos).trim()); + valbegin = -1; + } + } else if (!Character.isWhitespace(c)) { + if (valbegin < 0) { + valbegin = pos; + } + } + pos++; + } + return arr; + } + + /** Creates a NSDictionary instance from a string representation. + @s The string representation of a NSDictionary. */ + public static NSDictionary dictionaryForString(String s) { + s = s.trim(); + if (!(s.charAt(0) == TOKEN_BEGIN[PLIST_DICTIONARY] && s.charAt(s.length()-1) == TOKEN_END[PLIST_DICTIONARY])) + return null; + NSMutableDictionary d = new NSMutableDictionary(); + int pos = 1; + boolean parsing = true; + Object key = null; + int valbegin = -1; + while (pos < s.length()) { + //look for an opening token + char c = s.charAt(pos); + int tokenCount = 0; + int what = 0; + for (int i = 0 ; i < TOKEN_BEGIN.length; i++) { + if (c == TOKEN_BEGIN[i]) { + tokenCount = 1; + what = i; + break; + } + } + if (tokenCount > 0) { + //mark it + int quote = pos; + //find the closing token + do { + pos++; + try { + c = s.charAt(pos); + } catch (StringIndexOutOfBoundsException ex) { + throw new IllegalArgumentException("Could not parse property list; unclosed token '" + TOKEN_BEGIN[what] + "'"); + } + if (c == '"' && what == PLIST_STRING) { + if (pos > 0 && s.charAt(pos-1) != '\\') { + tokenCount--; + } + } else if (c == TOKEN_BEGIN[what]) + tokenCount++; + else if (c == TOKEN_END[what]) + tokenCount--; + } while (tokenCount > 0); + if (key == null) { + key = propertyListFromString(s.substring(quote, pos+1)); + } else { + d.setObjectForKey(propertyListFromString(s.substring(quote, pos+1)), key); + key = null; + } + valbegin = -1; + //advance to the next position + do { + pos++; + c = s.charAt(pos); + } while (Character.isWhitespace(c)); + } + if (c == ';' || c == '=' || c == '}') { + if (valbegin > 0) { + if (key == null) { + key = s.substring(valbegin, pos).trim(); + } else { + d.setObjectForKey(s.substring(valbegin, pos).trim(), key); + key = null; + } + valbegin = -1; + } + } else if (!Character.isWhitespace(c)) { + if (valbegin < 0) { + valbegin = pos; + } + } + pos++; + } + return d; + } + + public static boolean booleanForString(String s) { + return s.trim().toLowerCase().equals("true"); + } + + /** Creates a NSData instance from a string representation. + @s The string representation of a NSData object. */ + public static NSData dataFromPropertyList(String s) { + String hex = "0123456789ABCDEF"; + s = s.trim(); + if (!(s.charAt(0) == TOKEN_BEGIN[PLIST_DATA] && s.charAt(s.length()-1) == TOKEN_END[PLIST_DATA])) + return null; + int pos = 1; + java.io.ByteArrayOutputStream bout = new java.io.ByteArrayOutputStream(); + while (pos < s.length()-1) { + char c1 = s.charAt(pos); + while (c1 == ' ') { + pos++; + if (pos == s.length()-1) + return new NSData(bout.toByteArray()); + c1 = s.charAt(pos); + } + if (hex.indexOf(c1) < 0) + throw new IllegalArgumentException("The string does not represent a NSData object (" + s + ", pos " + pos + ")"); + pos++; + char c2 = s.charAt(pos); + if (hex.indexOf(c2) < 0) + throw new IllegalArgumentException("The string does not represent a NSData object (" + s + ")"); + int x = (hex.indexOf(c1) << 4) | hex.indexOf(c2); + bout.write(x); + pos++; + } + return new NSData(bout.toByteArray()); + } + + public static int intForString(String s) { + return Integer.parseInt(s); + } + + /** Returns the string representation of a property list. + @plist The property list. It can be a String, NSData, NSArray, NSDictionary. */ + public static String stringForPropertyList(Object plist) { + if (plist == null) + return ""; + if (plist instanceof NSArray || plist instanceof NSDictionary || plist instanceof NSData) + return plist.toString(); + String x = plist.toString(); + boolean quote = false; + for (int i = 0; i < x.length(); i++) { + char c = x.charAt(i); + for (int z = 0; z < TOKEN_BEGIN.length; z++) { + if (c == TOKEN_BEGIN[z] || c == TOKEN_END[z]) + quote = true; + } + if (!quote) { + for (int z = 0; z < QUOTING_CHARS.length; z++) { + if (c == QUOTING_CHARS[z]) + quote = true; + } + } + if (!quote && Character.isWhitespace(c)) { + quote = true; + i = x.length(); + } + } + if (quote) + return "\"" + x + "\""; + return x; + } + + /** Returns an property list created from a string representation. + @s The string with a representation of a property list. + @returns A property list object; either a NSData, NSArray, NSDictionary, or a String. */ + public static Object propertyListFromString(String s) { + s = s.trim(); + int type = -1; + for (int i = 0; i < TOKEN_BEGIN.length; i++) { + if (TOKEN_BEGIN[i] == s.charAt(0)) { + if (TOKEN_END[i] == s.charAt(s.length()-1)) + type = i; + } + } + switch (type) { + case PLIST_DATA: + return dataFromPropertyList(s); + case PLIST_ARRAY: + return arrayForString(s); + case PLIST_DICTIONARY: + return dictionaryForString(s); + case PLIST_STRING: + if (s.equals("\"\"")) + return ""; + return s.substring(1, s.length()-1); + } + return s; + } + + /* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/11 18:18:08 chochos + * improved encoding of strings, removed warnings + * + * Revision 1.4 2003/08/11 17:33:45 chochos + * Implemented detection of escaped quotes (\"). improved quoting strings when converting property lists to strings. + * + * Revision 1.3 2003/08/04 23:50:55 chochos + * propertyListForString() now works. dictionaryForString and arrayForString can parse complex structures with nested arrays and dictionaries. + * + */ +} \ No newline at end of file diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRange.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRange.java new file mode 100644 index 0000000..2de52f5 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRange.java @@ -0,0 +1,351 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +/** +* A pure java implementation of NSRange. +* An NSRange represents a range of numbers +* having a starting location and spanning a +* length. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 920 $ +*/ +public class NSRange implements Cloneable +{ + /** + * An empty range. + */ + public static final NSRange ZeroRange = new NSRange(); + + protected int loc; + protected int len; + + /** + * Default constructor produces an empty range. + */ + public NSRange () + { + this( 0, 0 ); + } + + /** + * Produces a range with the specified location and length. + */ + public NSRange (int location, int length) + { + loc = location; + len = length; + } + + /** + * Produces a range that has the same location and length as + * the specified range. + */ + public NSRange (NSRange aRange) + { + this( aRange.location(), aRange.length() ); + } + + /** + * Returns the location of this range. + */ + public int location () + { + return loc; + } + + /** + * Returns the length of this range. + */ + public int length () + { + return len; + } + + /** + * Returns the maximum extent of the range. This number is + * one more than the last position in the range. + */ + public int maxRange () + { + return location() + length() -1; + } + + /** + * Returns whether this is an empty range, therefore + * whether the length is zero. + */ + public boolean isEmpty () + { + return ( length() == 0 ); + } + + /** + * Returns whether the specified location is contained + * within this range. + */ + public boolean locationInRange (int location) + { + if ( location < location() ) return false; + if ( location >= maxRange() ) return false; + return true; + } + + /** + * Returns whether the specified range is equal to this range. + */ + public boolean isEqualToRange (NSRange aRange) + { + if ( aRange == null ) return false; + return ( ( aRange.location() == location() ) + && ( aRange.length() == length() ) ); + } + + /** + * Returns whether the specified object is equal to this range. + */ + public boolean equals (Object anObject) + { + if ( anObject instanceof NSRange ) + return isEqualToRange( (NSRange) anObject ); + return false; + } + + /** + * Returns a hashCode. + */ + public int hashCode () + { + // TODO: Test this logic. + return ( location() << 2 ) & length(); // bitwise ops never my forte + } + + /** + * Returns a string representation of this range. + */ + public String toString () + { + return "[NSRange: location = " + location() + + "; length = " + length() + "]"; + } + + /** + * Returns the union of this range and the specified range, if any. + * Gaps are filled, so the result is the smallest starting position + * and the largest ending position. + */ + public NSRange rangeByUnioningRange (NSRange aRange) + { + if ( aRange == null ) return this; + + // TODO: Test this logic. + int resultLoc = Math.min( this.location(), aRange.location() ); + int resultLen = Math.max( this.location() + this.length(), + aRange.location() + aRange.length() ) - resultLoc; + return new NSRange( resultLoc, resultLen ); + } + + /** + * Returns the intersection of this range and the specified range, + * if any. If no intersection, returns an empty range. + */ + public NSRange rangeByIntersectingRange (NSRange aRange) + { + // TODO: Test this logic. + if ( ! intersectsRange( aRange ) ) return ZeroRange; + int start = Math.max( this.location(), aRange.location() ); + int end = Math.min( this.location() + this.length(), + aRange.location() + aRange.length() ); + return new NSRange( start, end - start ); + } + + /** + * Returns whether the specified range overlaps + * at any point with this range. + */ + public boolean intersectsRange (NSRange aRange) + { + // TODO: Test this logic. + if ( aRange == null ) return false; + if ( ( this.location() >= aRange.location() ) + && ( this.location() < aRange.location() + aRange.length() ) ) + return true; + if ( ( aRange.location() >= this.location() ) + && ( aRange.location() < this.location() + this.length() ) ) + return true; + return false; + } + + /** + * Returns whether this range is completely + * contained within the specified range. + */ + public boolean isSubrangeOfRange (NSRange aRange) + { + // TODO: Test this logic. + if ( aRange == null ) return false; + if ( ( this.location() >= aRange.location() ) + && ( this.maxRange() <= aRange.maxRange() ) ) + return true; + return false; + } + + /** + * Eliminates any intersections between this range and the specified + * range. This produces two ranges, either of which may be empty. + * These two ranges are returned by modifying the supplied second + * and third parameters. + */ + public void subtractRange (NSRange aRange, + NSMutableRange firstResult, NSMutableRange secondResult) + { + if ( aRange == null ) return; + + // TODO: Test this logic. + // no intersection: return this and aRange without calculation + if ( ! intersectsRange( aRange ) ) + { + if ( firstResult != null ) + { + firstResult.setLocation( this.location() ); + firstResult.setLength( this.length() ); + } + if ( secondResult != null ) + { + secondResult.setLocation( aRange.location() ); + secondResult.setLength( aRange.location() ); + } + return; + } + + // TODO: Test this logic. + // this range is completely contained by other range + if ( isSubrangeOfRange( aRange ) ) + { + if ( firstResult != null ) + { + firstResult.setLocation( aRange.location() ); + firstResult.setLength( this.location() - aRange.location() ); + } + if ( secondResult != null ) + { + secondResult.setLocation( this.maxRange() ); + secondResult.setLength( + aRange.maxRange() - this.maxRange() - 1 ); // test this + } + return; + } + + // TODO: Test this logic. + // other range is completely contained by this range + if ( aRange.isSubrangeOfRange( this ) ) + { + if ( firstResult != null ) + { + firstResult.setLocation( this.location() ); + firstResult.setLength( aRange.location() - this.location() ); + } + if ( secondResult != null ) + { + secondResult.setLocation( aRange.maxRange() ); + secondResult.setLength( + this.maxRange() - aRange.maxRange() - 1 ); // test this + } + return; + } + + // TODO: Test this logic. + // ranges intersect: remove only the intersection + + NSRange firstRange, secondRange; + if ( this.location() <= aRange.location() ) + { + firstRange = this; + secondRange = aRange; + } + else + { + firstRange = aRange; + secondRange = this; + } + + if ( firstResult != null ) + { + firstResult.setLocation( firstRange.location() ); + firstResult.setLength( + secondRange.location() - firstRange.location() ); + } + if ( secondResult != null ) + { + secondResult.setLocation( firstRange.maxRange() ); + secondResult.setLength( + secondRange.maxRange() - aRange.maxRange() - 1 ); // test this + } + return; + + } + + /** + * Returns a copy of this range. + */ + public Object clone () + { + return new NSRange( location(), length() ); + } + + /** + * Parses a range from a string of the form "{x,y}" where + * x is the location and y is the length. If not parsable, + * an IllegalArgumentException is thrown. + */ + public static NSRange fromString (String aString) + { + // TODO: Test this logic. + try + { + java.util.StringTokenizer tokens = + new java.util.StringTokenizer( aString, "{,}" ); + int loc = Integer.parseInt( tokens.nextToken() ); + int len = Integer.parseInt( tokens.nextToken() ); + return new NSRange( loc, len ); + } + catch ( Exception exc ) + { + throw new IllegalArgumentException( exc.toString() ); + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:42 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:38 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRecursiveLock.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRecursiveLock.java new file mode 100644 index 0000000..65f58db --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRecursiveLock.java @@ -0,0 +1,148 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSRecursiveLock.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import EDU.oswego.cs.dl.util.concurrent.ReentrantLock; + +/** +* A lock class that allows a thread to re-acquire it's lock +* recursively. Currently an API-compliance wrapper around Doug Lea's +* ReentrantLock, conforming to the API and behavior of +* com.webobjects.foundation.NSRecursiveLock. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class NSRecursiveLock extends ReentrantLock implements NSLocking { + + public NSRecursiveLock() { + } + /** Acquire the lock, catching the thrown exception to mirror the + * behavior of com.webobjects.foundation.NSRecursiveLock. Note that + * ReentrantLock.acquire() performs a notify() when it's interrupted. + * + * @see edu.oswego.cs.dl.util.concurrent.ReentrantLock#acquire() + */ + public void lock() { + try { + acquire(); + } catch (InterruptedException interruptedexception) { + // Null behavior, as notify() is already called + // by acquire(); + // We may want to log here. + } + } + + /** Pass the buck to tryLock(long), passing zero time as the parameter. + * + * @see #tryLock(long) + */ + public boolean tryLock() { + return tryLock(1); + } + + /** Attempt to acquire the lock, catching the thrown exception to mirror + * the behavior of com.webobjects.foundation.NSRecursiveLock. Note that + * ReentrantLock.attempt(*) performs a notify() when it's interrupted. + * Fail gracefully after the given milliseconds + * + * @param (long) + * @see edu.oswego.cs.dl.util.concurrent.ReentrantLock#acquire() + */ + public boolean tryLock(long milliseconds) { + try { + return attempt(milliseconds); + } catch (InterruptedException interruptedexception) { + // notify() is already called by attempt(); + // We may want to log here. + return false; + } + } + /** + * Attempt to acquire a lock until the timestamp is reached. Add + * 1 to the recursion count if the calling thread already owns the + * lock. Otherwise block until free or until the given timestamp + * is reached. + * + * @see Timestamp + * @see ReentrantLock.attempt(long); + */ + public boolean tryLock(NSTimestamp nstimestamp) { + return tryLock(nstimestamp.getTime() - System.currentTimeMillis()); + } + + /** Unlock the current lock precisely once. + */ + public synchronized void unlock() { + unlock(1); + } + + /** Unlock the current lock count times. + */ + public synchronized void unlock(long count) { + if (owner_ != null && Thread.currentThread() != owner_) + throw new IllegalStateException("Illegal Lock usage: unlocking thread not owner."); + if (owner_ == null || holds_ == 0L) + throw new IllegalStateException("Illegal Lock usage: unlock() called without a lock()."); + release(count); + } + + public synchronized long recursionCount() { + return holds(); + } + + public String toString() { + long holds = holds(); + boolean oneHold = (holds == 1); + boolean noHolds = (holds < 1 || owner_ == null); + return getClass().getName() + " <" + + ((noHolds) ? "Unlocked" : ( "Locked " + holds + " time" + (oneHold ? "" : "s") + " by " + owner_ ) ) + ">"; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.5 2002/06/25 19:02:19 cgruber + * I'm a dumbass. + * + * Revision 1.4 2002/06/25 18:52:56 cgruber + * Fix javadocs that resulted from bad cut-and-paste of the boilerplate. + * + * Revision 1.3 2002/06/25 18:45:27 cgruber + * Add some javadocs. + * + * Revision 1.2 2002/06/25 18:06:48 cgruber + * Add implementation of NSRecursiveLock using Doug Lea's concurrent programming APIs. + * Specifically inherit from ReentrantLock, which magically does the exact job we want! + * + * Revision 1.1 2002/06/25 07:52:56 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRunLoop.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRunLoop.java new file mode 100644 index 0000000..2d122aa --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSRunLoop.java @@ -0,0 +1,522 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.awt.AWTEvent; +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.awt.event.InvocationEvent; +import java.util.EmptyStackException; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +/** +* NSRunLoop is provided specifically for EODelayedObserverQueue +* and EOEditingContext, which assume the existence of a +* prioritized event queue that Java does not provide.

+* +* This extends java.awt.EventQueue and does not conform to the +* NSRunLoop specifications. The only supported methods are +* NSRunLoop.currentRunLoop, performSelectorWithOrder, and +* cancelSelectorWithOrder. Note that in Swing there is only +* one AWT thread and one event queue; newly created threads +* will not get their own run loop as in OpenStep.

+* +* That said, this event queue is servicable as a replacement +* for the default event queue and will provide prioritized +* execution of selectors before and after normal AWT events. +*

+* +* Each run loop dispatches the lowest order event from +* the queue. When queued events have the same ordering, +* they are dispatched as first-in, first-out (FIFO). Because +* all AWT events have the same ordering (AWTEventsRunLoopOrdering), +* they are processed FIFO, just like the default event queue.

+* +* Note that because EventQueue is not well-factored for +* subclassing, pushing a new event queue onto the stack +* on top of this one will only copy the existing AWT events +* to the new queue. For this reason, pushing new event +* queues onto the stack is not supported and will throw +* an exception. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSRunLoop extends EventQueue +{ + /** + * This is the ordering at which the conventional AWT event + * queue will be executed. Selectors with this ordering + * or less will be executed before AWT events, and + * selectors with ordering greater than this ordering will be + * be executed after AWT events. + */ + public static final int AWTEventsRunLoopOrdering = 500000; + + /** + * The singleton instance. + */ + protected static NSRunLoop instance; + + private LinkedList earlyQueue; + private LinkedList lateQueue; + + /** + * Needed because JDK1.4 made our lives more difficult. + */ + private static Toolkit toolkit; + + /** + * Because SunToolkit.flushPendingEvents was changed + * to a static method in 1.4, you can't compile the library + * in such a way that it works with both 1.3 and 1.4. + * So we have to rely on dynamic method invocation, + * which is slower, but we try to make it as fast as + * humanly possible. + */ + private static NSSelector flushPendingEvents; + + /** + * Create a new instance of NSRunLoop. + */ + protected NSRunLoop () + { + earlyQueue = new LinkedList(); + lateQueue = new LinkedList(); + } + + /** + * Returns the singleton instance of NSRunLoop. + * This returns the same instance no matter what + * thread calls it, which is different from OpenStep. + * NSRunLoop is limited to a singleton instance + * because there is no way of obtaining the stack + * of event queues from EventQueue because it is + * private state. + */ + public synchronized static NSRunLoop currentRunLoop () + { + if ( instance == null ) + { + // create and initialize + flushPendingEvents = new NSSelector( "flushPendingEvents" ); + toolkit = Toolkit.getDefaultToolkit(); + instance = new NSRunLoop(); + + toolkit.getSystemEventQueue().push( instance ); + } + + return instance; + } + + /** + * Post a 1.1-style event to the EventQueue. If there is an + * existing event on the queue with the same ID and event source, + * the source Component's coalesceEvents method will be called. + * + * @param theEvent an instance of java.awt.AWTEvent, or a + * subclass of it. + */ + public void postEvent(AWTEvent theEvent) + { + if ( theEvent instanceof OrderedInvocationEvent ) + { + OrderedInvocationEvent event = (OrderedInvocationEvent) theEvent; + if ( event.getOrdering() > AWTEventsRunLoopOrdering ) + { + insertEventIntoQueue( event, lateQueue ); + } + else + { + insertEventIntoQueue( event, earlyQueue ); + } + } + else + { + super.postEvent( theEvent ); + } + } + + private synchronized void insertEventIntoQueue( OrderedInvocationEvent e, LinkedList q ) + { + OrderedInvocationEvent o; + int ordering = e.getOrdering(); + ListIterator iterator = + q.listIterator(); + + // iterate forwards until we find a priority + // greater than our priority, + // then insert ourself before that element. + while ( iterator.hasNext() ) + { + o = (OrderedInvocationEvent) iterator.next(); + if ( o.getOrdering() > ordering ) + { + // back up one + iterator.previous(); + break; + } + } + // add after the current element + iterator.add( e ); + } + + /** + * Useful method, but not in the spec. + * Dispatches the next AWT event in the queue. + * Returns whether a selector or an event was executed: + * if the event queue is empty, returns false. + */ + public boolean dispatchNextEvent() + { + // check for empty queue to avoid blocking + if ( peekEvent() == null ) + { + return false; + } + + // queue not empty: dispatch the next event + try + { + dispatchEvent( getNextEvent() ); + } + catch ( InterruptedException exc ) + { + System.out.println( "NSRunLoop: error while dispatching event: " ); + exc.printStackTrace(); + } + return true; + } + + /** + * Useful method, but not in the spec. + * Dispatches all events in the queue before returning. + */ + public void dispatchAllEvents() + { + while ( dispatchNextEvent() ); + } + + /** + * Remove an event from the EventQueue and return it. + * This override will dispatch all selectors up to 5000, + * and then check if there are AWT events on the queue. + * If the queue is empty, all remaining selectors + * are dispatched. Then, this method calls the + * super class' implementation. + * @return the next AWTEvent + * @exception InterruptedException + * if another thread has interrupted this thread. + */ + public AWTEvent getNextEvent() throws InterruptedException + { + //NOTE: it's currently unclear to me whether we should + // be operating as a run loop or as a priority queue. + // I'm opting for priority queue now, but that means that + // selectors that requeue themselves could hang the application. + // In the future, we could fake a run loop by putting a marker + // event on the AWT queue to mark the boundary between loops. + + AWTEvent result; + + while ( true ) + { + //NOTE: as of java 1.4, we have to flush pending events + // using this cheesy undocumented method on suntoolkit. + // Unsurprisingly, java.awt.EventQueue got worse, not better. + // See notes above about our use of an NSSelector. + try + { + flushPendingEvents.invoke( toolkit ); + } + catch ( Throwable t ) + { + System.out.println( "NSRunLoop.getNextEvent: " + Thread.currentThread() ); + System.err.println( "Unexpected error while flushing pending events: " ); + t.printStackTrace(); + }; + + synchronized( this ) + { + result = popNextEarlyEvent(); + if ( result != null ) + { +//System.out.println( "getNextEvent: early : " + result ); + return result; + } + } + + if ( ( result = peekEvent() ) != null ) + { +//System.out.println( "getNextEvent: AWT : " + result ); + return super.getNextEvent(); + } + + synchronized( this ) + { + result = popNextLateEvent(); + if ( result != null ) + { +//System.out.println( "getNextEvent: late : " + result ); + return result; + } + + // yield +//System.out.println( "getNextEvent: wait" ); + wait(); +//System.out.println( "getNextEvent: notified" ); + } + } + } + + private AWTEvent popNextEarlyEvent() + { + if ( earlyQueue == null ) return null; // shouldn't be necessary, but is + if ( earlyQueue.isEmpty() ) return null; + return (AWTEvent) earlyQueue.removeFirst(); + } + + private AWTEvent popNextLateEvent() + { + if ( lateQueue == null ) return null; // shouldn't be necessary, but is + if ( lateQueue.isEmpty() ) return null; + return (AWTEvent) lateQueue.removeFirst(); + } + + /** + * This implementation calls super and then throws an + * UnsupportedOperationException. Catch that exception + * and ignore it if you know what you are doing. + */ + public synchronized void push(EventQueue newEventQueue) + { + super.push( newEventQueue ); + throw new UnsupportedOperationException( + "NSRunLoop may not function properly with push()" ); + } + + /** + * This implementation calls super and then throws an + * UnsupportedOperationException. Catch that exception + * and ignore it if you know what you are doing. + */ + protected void pop() throws EmptyStackException + { + super.pop(); + throw new UnsupportedOperationException( + "NSRunLoop may not function properly with pop()" ); + } + + /** + * Schedules the specified selector with the specified target and parameter + * to be invoked on the next event loop with the specified ordering. + * The selector must be able to be invoked on the target and the target method + * must accept the parameter. aModeList is currently ignored. + */ + public void performSelectorWithOrder( + NSSelector aSelector, Object aTarget, Object aParameter, int anOrdering, List aModeList ) + { + postEvent( new OrderedInvocationEvent( aSelector, aTarget, aParameter, anOrdering, aModeList ) ); + } + + /** + * Cancels the next scheduled invocation of the specified selector, target, and parameter. + * If no such invocation is scheduled, does nothing. + */ + public synchronized void cancelPerformSelectorWithOrder( + NSSelector aSelector, Object aTarget, Object aParameter ) + { + ListIterator i; + i = earlyQueue.listIterator(); + while ( i.hasNext() ) + { + if ( ((OrderedInvocationEvent)i.next()).compareTo( + aSelector, aTarget, aParameter ) ) + { + i.remove(); + return; + } + } + i = lateQueue.listIterator(); + while ( i.hasNext() ) + { + if ( ((OrderedInvocationEvent)i.next()).compareTo( + aSelector, aTarget, aParameter ) ) + { + i.remove(); + return; + } + } + } + + /** + * Causes runnable to have its run() method on the next + * event loop with the specified priority ordering. + */ + public static void invokeLaterWithOrder(Runnable aRunnable, int anOrdering) { + currentRunLoop().postEvent( + new OrderedInvocationEvent( Toolkit.getDefaultToolkit(), aRunnable, anOrdering ) ); + } + + /** + * An invocation event that can specify a priority for execution. + * The prioritization only works if the current event queue is an + * NSRunLoop; otherwise, performs as a normal invocation event. + */ + private static class OrderedInvocationEvent extends InvocationEvent + { + int ordering; + NSSelector selector = null; + Object target = null; + Object parameter = null; + + /** + * Constructs an InvocationEvent with the specified source which will + * execute the runnable's run() method when dispatched at the specified ordering. + */ + public OrderedInvocationEvent(Object source, + Runnable runnable, int anOrdering) + { + super( source, runnable ); + ordering = anOrdering; + } + + /** + * Constructs an InvocationEvent with the specified source which will + * execute the runnable's run() method when dispatched at the specified ordering. + * If notifier is non-null, notifyAll() will be called on it immediately after run() returns. + */ + public OrderedInvocationEvent(Object source, + Runnable runnable, + Object notifier, + boolean catchExceptions, int anOrdering) + { + super( source, runnable, notifier, catchExceptions ); + ordering = anOrdering; + } + + OrderedInvocationEvent( + final NSSelector aSelector, + final Object aTarget, + final Object aParameter, + int anOrdering, List aModeList) + { + this( Toolkit.getDefaultToolkit(), new Runnable() + { + public void run() + { + try + { + aSelector.invoke( aTarget, aParameter ); + } + catch ( Exception exc ) + { + System.out.println( "NSRunLoop: error invoking selector: " ); + exc.printStackTrace(); + } + } + }, anOrdering ); + + selector = aSelector; + target = aTarget; + parameter = aParameter; + } + + /** + * Called by cancelPerformSelectorWithOrder. + * Compares against the specified arguments. + */ + boolean compareTo( NSSelector aSelector, Object aTarget, Object aParameter ) + { + return ( + compareByValue( selector, aSelector ) && + compareByValue( target, aTarget ) && + compareByValue( parameter, aParameter ) ); + } + + private boolean compareByValue( Object first, Object second ) + { + if ( first == second ) return true; + if ( first == null ) return second.equals( first ); + return first.equals( second ); + + } + + /** + * Returns the ordering for this event in the run loop. + */ + public int getOrdering() + { + return ordering; + } + + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.13 2003/06/06 20:48:19 mpowers + * Fixed race condition when run loop is started from main thread. + * That was causing the dispatch thread to call getNextEvent before the + * static fields had been initialized. + * + * Revision 1.12 2003/06/03 14:52:11 mpowers + * Super constructor was calling getNextEvent before selector was created. + * + * Revision 1.10 2002/05/28 21:59:19 mpowers + * We now can compile against 1.3 and 1.4, as well as run against both too. + * + * Revision 1.9 2002/04/09 18:10:45 mpowers + * Fixes for 1.4. Commented out until we start building on 1.4. + * + * Revision 1.8 2002/02/13 21:20:15 mpowers + * Updated comments. + * + * Revision 1.7 2001/11/01 15:48:49 mpowers + * Additional debug code. + * + * Revision 1.6 2001/10/30 22:14:35 mpowers + * Constructor is now protected, not private. + * + * Revision 1.5 2001/10/29 20:41:49 mpowers + * Improved docs, better support for potential subclassing, invokeLater. + * + * Revision 1.4 2001/10/26 18:46:30 mpowers + * Now running AWT events with the appropriate ordering. + * Added invokeLaterWithOrder for java compatibility. + * + * Revision 1.3 2001/10/26 14:39:46 mpowers + * Completed implementation. + * + * Revision 1.2 2001/10/25 22:20:21 mpowers + * Got to check in an interim version - this will briefly break the build. + * + * Revision 1.1 2001/10/24 19:30:38 mpowers + * Initial check-in: incomplete implementation. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSelector.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSelector.java new file mode 100644 index 0000000..965606b --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSelector.java @@ -0,0 +1,391 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Comparator; + +import net.wotonomy.foundation.internal.PropertyComparator; + +/** +* A pure java implementation of NSSelector. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ +public class NSSelector implements Comparator, Serializable +{ + protected NSMutableDictionary methodMap; // map of classes to methods + protected String methodName; + protected Class[] parameterTypes; + + /** + * A marker to indicate object not found. + */ + protected static final String NOT_FOUND = "NOT_FOUND"; + + /** + * Saves creating a new class array for parameterless method invocation. + */ + protected static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + /** + * Saves creating a new object array for parameterless method invocation. + */ + protected static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + /** + * Constructor specifying a method name and an array of parameter types. + */ + public NSSelector (String aMethodName, Class[] aParameterTypeArray) + { + methodName = aMethodName; + parameterTypes = aParameterTypeArray; + methodMap = new NSMutableDictionary(); + } + + /** + * Constructor specifying a method name with no parameters. + */ + public NSSelector (String aMethodName) + { + this( aMethodName, EMPTY_CLASS_ARRAY ); + } + + /** + * Constructor for custom subclasses that implement specific operators + * and that do not use dynamic method invocation. + */ + protected NSSelector() + { + } + + /** + * Returns the name of the method. + */ + public String name () + { + return methodName; + } + + /** + * Returns the array of parameter types. + */ + public Class[] parameterTypes () + { + return parameterTypes; + } + + /** + * A String description of this selector. + */ + public String toString () + { + StringBuffer result = new StringBuffer(); + result.append( "[" + getClass().getName() + ": name = " + name() + ", parameter types = [" ); + if ( parameterTypes != null ) + { + if ( parameterTypes.length > 0 ) + { + result.append( parameterTypes[0].toString() ); + } + for ( int i = 1; i < parameterTypes.length; i++ ) + { + result.append( ", " ); + result.append( parameterTypes[i].toString() ); + } + } + result.append( "] ]" ); + return result.toString(); + } + + /** + * Returns the appropriate method for the specified class. + */ + public Method methodOnClass (Class aClass) + throws NoSuchMethodException + { + Object result = methodMap.objectForKey( aClass ); + + if ( result == null ) + { + result = getMethodForClass( aClass ); + if ( result == null ) + { + result = NOT_FOUND; + } + methodMap.setObjectForKey( result, aClass ); + } + + if ( result == NOT_FOUND ) + { + throw new NoSuchMethodException(); + } + return (Method) result; + } + + /** + * Returns the appropriate method, or null if not found. + */ + private Method getMethodForClass( Class aClass ) + { + Method[] methods = aClass.getMethods(); + for ( int i = 0; i < methods.length; i++ ) + { + if ( methods[i].getName().equals( name() ) ) + { + Class[] params = methods[i].getParameterTypes(); + if ( params.length == parameterTypes.length ) + { + boolean pass = true; + for ( int j = 0; j < params.length; j++ ) + { + if ( ! params[j].isAssignableFrom( parameterTypes[j] ) ) + { + pass = false; + } + } + if ( pass ) return methods[i]; + } + } + } + return null; + } + + /** + * Convenience to get a method for an object. + */ + public Method methodOnObject (Object anObject) + throws NoSuchMethodException + { + Method m = methodOnClass( anObject.getClass() ); + if ( m == null ) throw new NoSuchMethodException( name() ); + return m; + } + + /** + * Returns whether the class implements the method for this selector. + */ + public boolean implementedByClass (Class aClass) + { + try + { + methodOnClass( aClass ); + return true; + } + catch ( NoSuchMethodException exc ) + { + } + return false; + } + + /** + * Returns whether the object's class implements the method + * for this selector. + */ + public boolean implementedByObject (Object anObject) + { + try + { + methodOnObject( anObject ); + return true; + } + catch ( NoSuchMethodException exc ) + { + } + return false; + } + + /** + * Invokes this selector's method on the specified object + * using the specified parameters. + */ + public Object invoke (Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return methodOnObject( anObject ).invoke( anObject, parameters ); + } + + /** + * Invokes this selector's method on the specified object + * with no parameters. + */ + public Object invoke (Object anObject) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return invoke( anObject, EMPTY_OBJECT_ARRAY ); + } + + /** + * Invokes this selector's method on the specified object + * with the specified parameter. + */ + public Object invoke (Object anObject, Object aParameter) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return invoke( anObject, new Object[] { aParameter } ); + } + + /** + * Invokes this selector's method on the specified object + * using the specified two parameters. + */ + public Object invoke (Object anObject, Object p1, Object p2) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return invoke( anObject, new Object[] { p1, p2 } ); + } + + /** + * Invokes the method with the specified signature on the specified + * object using the specified parameters. + */ + public static Object invoke + (String methodName, Class[] parameterTypes, Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return new NSSelector( methodName, parameterTypes ).invoke( anObject, parameters ); + } + + /** + * Invokes the method with the specified signature on the specified object + * with no parameters. + */ + public static Object invoke + (String methodName, Object anObject) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return NSSelector.invoke( + methodName, EMPTY_CLASS_ARRAY, anObject, EMPTY_OBJECT_ARRAY ); + } + + /** + * Invokes the method with the specified signature on the specified + * object using the specified parameter. + */ + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object aParameter) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return NSSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { aParameter } ); + } + + /** + * Invokes the method with the specified signature on the specified + * object using the specified two parameters. + */ + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object p1, Object p2) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return NSSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { p1, p2 } ); + } + + // interface Comparator + + private Comparator comparator; + + /** + * Constructor specifying a method name and a comparator. + * This is not in the spec. + */ + public NSSelector (String aMethodName, Comparator aComparator) + { + this( aMethodName, EMPTY_CLASS_ARRAY ); + comparator = aComparator; + } + + /** + * Returns the Comparator used for this selector. + * This is not in the spec. + */ + public Comparator comparator() + { + if ( comparator == null ) + { + comparator = new PropertyComparator( methodName ); + } + return comparator; + } + + public int compare(Object o1, Object o2) + { + if ( comparator == null ) + { + comparator = new PropertyComparator( methodName ); + } + return comparator.compare( o1, o2 ); + } + + public boolean equals(Object obj) + { + return ( obj == this ); + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2003/02/12 19:34:35 mpowers + * Added accessor for comparator. + * + * Revision 1.8 2003/02/07 20:23:41 mpowers + * Provided backwards compatibility for comparators. + * + * Revision 1.7 2003/01/22 23:02:25 mpowers + * Fixed a null pointer error in NSSelector.toString. + * + * Revision 1.6 2003/01/18 23:46:58 mpowers + * EOSortOrdering is now correctly using NSSelectors. + * + * Revision 1.4 2001/10/31 15:24:45 mpowers + * Implicit constructor is now protected. + * + * Revision 1.3 2001/02/07 19:25:51 mpowers + * Fixed: method matching uses isAssignableFrom rather than ==. + * + * Revision 1.2 2001/01/08 23:30:16 mpowers + * Fixed major bug - selectors were not supposed to share a method map. + * + * Revision 1.1.1.1 2000/12/21 15:47:45 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:39 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSet.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSet.java new file mode 100644 index 0000000..5318fe8 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSSet.java @@ -0,0 +1,212 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +/** +* A pure java implementation of NSSet that +* implements Set for greater java interoperability. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NSSet extends HashSet +{ + /** + * Default constructor. + */ + public NSSet () + { + super(); + } + + /** + * Constructs a NSSet containing the objects + * in the specified collection. + */ + public NSSet ( Collection aCollection ) + { + super( aCollection ); + } + + /** + * Constructs a NSSet containing only + * the specified object. + */ + public NSSet ( Object anObject ) + { + super(); + add( anObject ); + } + + /** + * Constructs a NSSet containing the objects + * in the specified array. + */ + public NSSet ( Object[] anObjectArray ) + { + super(); + for ( int i = 0; i < anObjectArray.length; i++ ) + { + add( anObjectArray[i] ); + } + } + + /** + * Returns an NSArray containing all objects in the set. + */ + public NSArray allObjects () + { + return new NSArray( this ); + } + + /** + * + */ + public Object anyObject () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether this set contains the + * specified object. + */ + public boolean containsObject ( Object anObject ) + { + return contains( anObject ); + } + + /** + * Returns the number of elements in this set. + */ + public int count () + { + return size(); + } + + /** + * Returns whether this set has one or more + * elements in common with the specified set. + */ + public boolean intersectsSet ( Set aSet ) + { + Iterator it = aSet.iterator(); + while ( it.hasNext() ) + { + if ( this.containsObject( it.next() ) ) + { + return true; + } + } + return false; + + } + + /** + * Returns whether this set contains the + * same object as the specified set. + */ + public boolean isEqualToSet ( Set aSet ) + { + return equals( aSet ); + } + + /** + * Returns whether this set is a subset + * of the specified set. + */ + public boolean isSubsetOfSet ( Set aSet ) + { + return aSet.containsAll( this ); + } + + /** + * + */ + public Object member ( Object anObject ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns an enumerator over the objects + * in this set. + */ + public Enumeration objectEnumerator () + { + return new Vector( this ).elements(); + } + + /** + * Returns a set that is the intersection + * of this set and the specified set. + */ + public NSSet setByIntersectingSet ( Set aSet ) + { + NSSet result = new NSSet( this ); + result.retainAll( aSet ); + return result; + } + + /** + * Returns a set that contains all elements + * in this set that are not in the specified set. + */ + public NSSet setBySubtractingSet ( Set aSet ) + { + NSSet result = new NSSet( this ); + result.removeAll( aSet ); + return result; + } + + /** + * Returns a set that is the union + * of this set and the specified set. + */ + public NSSet setByUnioningSet ( Set aSet ) + { + NSSet result = new NSSet( this ); + result.addAll( aSet ); + return result; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:47:45 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:39 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimeZone.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimeZone.java new file mode 100644 index 0000000..171e756 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimeZone.java @@ -0,0 +1,272 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSTimeZone.java 892 2006-02-16 12:47:16Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.io.ObjectStreamException; +import java.io.Serializable; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +/** +* A channel to the database, representing a communication +* stream within a context of an adaptor. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ + +public class NSTimeZone extends TimeZone + implements Cloneable, Serializable, NSCoding { + protected static class __NSTZPeriodComparator extends NSComparator { + + protected boolean _ascending = false; + + public int compare(Object obj, Object obj1) throws NSComparator.ComparisonException { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public __NSTZPeriodComparator() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public __NSTZPeriodComparator(boolean flag) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + protected static class __NSTZPeriod { + + protected String _abbreviation = null; + protected int _isdst = 0; + protected int _offset = 0; + protected double _startTime = 0; + + protected boolean before(__NSTZPeriod _p_nstzperiod) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected boolean equals(__NSTZPeriod _p_nstzperiod) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected __NSTZPeriod() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + } + + + public static final String SystemTimeZoneDidChangeNotification = "NSSystemTimeZoneDidChangeNotification"; + protected NSData _data = null; + protected transient int _hashCode = 0; + protected transient boolean _initialized = false; + protected transient TimeZone _jdkTimeZone = null; + protected String _name = null; + protected transient int _rawOffset = 0; + protected transient NSMutableArray _timeZonePeriods = null; + protected transient int _timeZonePeriodsCount = 0; + protected transient boolean _useDaylightTime = false; + + public NSTimeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected NSTimeZone(String s, NSData nsdata) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static NSDictionary abbreviationDictionary() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public Class classForCoder() { + return getClass(); + } + + public Object clone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void encodeWithCoder(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized NSTimeZone defaultTimeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static String[] getAvailableIDs() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static TimeZone getDefault() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static NSArray knownTimeZoneNames() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static NSTimeZone localTimeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized void resetSystemTimeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized void setDefault(TimeZone timezone) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized void setDefaultTimeZone(NSTimeZone nstimezone) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void setID(String s) { + throw new IllegalStateException(getClass().getName() + " is immutable."); + } + + public void setRawOffset(int i) { + throw new IllegalStateException(getClass().getName() + " is immutable."); + } + + public static synchronized NSTimeZone systemTimeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized NSTimeZone timeZoneForSecondsFromGMT(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized NSTimeZone timeZoneWithName(String s, boolean flag) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized NSTimeZone timeZoneWithNameAndData(String s, NSData nsdata) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static NSTimeZone _nstimeZoneWithTimeZone(TimeZone timezone) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String abbreviation() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String abbreviationForTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSData data() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean equals(Object obj) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String getDisplayName(boolean flag, int i, Locale locale) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String getID() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int getOffset(int i, int j, int k, int l, int i1, int j1) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int getRawOffset() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public synchronized int hashCode() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean hasSameRules(TimeZone timezone) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean inDaylightTime(Date date) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean isDaylightSavingTime() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean isDaylightSavingTimeForTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean isEqualToTimeZone(NSTimeZone nstimezone) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String name() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int secondsFromGMT() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int secondsFromGMTForTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String toString() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean useDaylightTime() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected Object readResolve() throws ObjectStreamException { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.1 2002/06/25 07:52:57 cgruber + * Add quite a few abstract classes, interfaces, and classes. All API consistent with WebObjects, but with no implementation, nor any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestamp.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestamp.java new file mode 100644 index 0000000..bd246e0 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestamp.java @@ -0,0 +1,285 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSTimestamp.java 892 2006-02-16 12:47:16Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.sql.Timestamp; +import java.util.Date; +import java.util.TimeZone; + +/** +* A channel to the database, representing a communication +* stream within a context of an adaptor. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 892 $ +*/ + +public class NSTimestamp extends Timestamp + implements NSCoding { + + public static class IntRef { + + public int value = 0; + + public String toString() { + return getClass().getName() + " < value = " + value + " >"; + } + + public IntRef() { + } + } + + + public static final NSTimestamp DistantPast = new NSTimestamp(0xffffc77f2e9b6800L); + public static final NSTimestamp DistantFuture = new NSTimestamp(0x7fffffffffffffffL); + public static long currentTimeIntervalSinceReferenceDate() { + return System.currentTimeMillis() / 1000L; + } + + public static NSTimestamp distantFuture() { + return DistantFuture; + } + + public static NSTimestamp distantPast() { + return DistantPast; + } + + public static long millisecondsToTimeInterval(long l) { + return l / 1000L; + } + + public static long timeIntervalToMilliseconds(long l) { + return l * 1000L; + } + + public Class classForCoder() { + return getClass(); + } + + public static Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void encodeWithCoder(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp() { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(long l) { + super(l); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(long l, int i) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(long l, NSTimestamp nstimestamp) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(long l, TimeZone timezone) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(long l, int i, TimeZone timezone) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(int i, int j, int k, int l, int i1, int j1, TimeZone timezone) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(Date date) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp(Timestamp timestamp) { + super(0); + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp timestampByAddingGregorianUnits(int i, int j, int k, int l, int i1, int j1) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp timestampByAddingTimeInterval(long l) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long dayOfCommonEra() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int dayOfMonth() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int dayOfWeek() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int dayOfYear() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int hourOfDay() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int microsecondOfSecond() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int minuteOfHour() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int monthOfYear() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int secondOfMinute() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int yearOfCommonEra() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void gregorianUnitsSinceTimestamp(IntRef intref, IntRef intref1, IntRef intref2, IntRef intref3, IntRef intref4, IntRef intref5, NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long timeIntervalSinceTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long timeIntervalSinceNow() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long timeIntervalSinceReferenceDate() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int compare(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp earlierTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimestamp laterTimestamp(NSTimestamp nstimestamp) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String toString() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSTimeZone timeZone() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long _getTimeInMillis() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void setNanos(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setDate(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setHours(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setMinutes(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setMonth(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setSeconds(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void setTime(long l) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public long getTime() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** @deprecated This method deprecated in parent java.util.Date */ + public void setYear(int i) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } +} +/* + * $Log$ + * Revision 1.1 2006/02/16 12:47:16 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2002/07/14 21:56:16 mpowers + * Contributions from cgruber. + * + * Revision 1.3 2002/06/25 19:06:13 cgruber + * Comment fix. + * + * Revision 1.2 2002/06/25 19:05:27 cgruber + * Add deprecation statements to remove warnings + * about java.util.Date's deprecated APIs. + * + * Revision 1.1 2002/06/25 07:52:56 cgruber + * Add quite a few abstract classes, interfaces, and classes. All + * API consistent with WebObjects, but with no implementation, nor + * any private or package access members from the original. + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestampFormatter.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestampFormatter.java new file mode 100644 index 0000000..ecc67ca --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/NSTimestampFormatter.java @@ -0,0 +1,65 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: NSTimestampFormatter.java 893 2006-02-16 13:22:23Z cgruber $ + +*/ + +package net.wotonomy.foundation; + +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; + +/** +* A Format that accepts C-style date formatting syntax. +* Not currently implemented, included for compile compatibility. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class NSTimestampFormatter extends SimpleDateFormat +{ + public NSTimestampFormatter() + { + super(); + } + + public NSTimestampFormatter(String aPattern) + { + super( aPattern ); + } + + public NSTimestampFormatter(String aPattern, + DateFormatSymbols symbols) + { + super( aPattern, symbols ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:15:00 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/17 14:40:51 mpowers + * Adding files to fix build. + * + * + */ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Duplicator.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Duplicator.java new file mode 100644 index 0000000..ddf347d --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Duplicator.java @@ -0,0 +1,299 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import net.wotonomy.foundation.*; +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.WotonomyException; + +import java.io.*; +import java.util.*; //collections + +/** +* Duplicator makes use of Introspector to duplicate objects, +* either by shallow copy, deep copy, or by copying properties +* from one object to apply to another object. You may find this +* class useful because java.lang.Object.clone() only supports +* shallow copying. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 895 $ +*/ + +public class Duplicator +{ + /** + * Used to represent null values for properties in the + * maps returned by readProperties and cloneProperties + * and in the parameter to writeProperties. + * This actually references the NSNull instance. + */ + public static final Object NULL = NSNull.nullValue(); + private static NSSelector clone = new NSSelector( "clone" ); + + /** + * Returns a list of properties for the specified class + * that are both readable and writable. + */ + static public List editablePropertiesForObject( + Object anObject ) + { + List readProperties = new ArrayList(); + String[] read = Introspector.getReadPropertiesForObject( anObject ); + for ( int i = 0; i < read.length; i++ ) + { + readProperties.add( read[i] ); + } + + List properties = new ArrayList(); + String[] write = Introspector.getWritePropertiesForObject( anObject ); + for ( int i = 0; i < write.length; i++ ) + { + properties.add( write[i] ); + } + + // only use properties on both lists: read/write + properties.retainAll( readProperties ); + + return properties; + } + + /** + * Returns a Map containing only the mutable properties + * for the specified object and their values. + * Any null values for properties will be represented with + * the NULL object. + */ + static public Map readPropertiesForObject( + Object anObject ) + { + NSMutableDictionary result = new NSMutableDictionary(); + + String key; + Object value; + Iterator it = editablePropertiesForObject( anObject ).iterator(); + while ( it.hasNext() ) + { + key = it.next().toString(); + value = Introspector.get( anObject, key ); + if ( value == null ) value = NULL; + result.setObjectForKey( value, key ); + } + return result; + } + + /** + * Returns a Map containing only the mutable properties + * for the specified object and deep clones of their values. + * Nulls are represented by the NULL object. + */ + static public Map clonePropertiesForObject( + Object anObject ) + { + Object key, value; + Map result = readPropertiesForObject( anObject ); + Iterator it = result.keySet().iterator(); + while ( it.hasNext() ) + { + key = it.next(); + value = result.get( key ); + value = deepClone( value ); + result.put( key, value ); + } + return result; + } + + /** + * Applies the map of properties and values to the + * specified object. Null values for properties must + * be represented by the NULL object. + */ + static public void writePropertiesForObject( + Map aMap, Object anObject ) + { + String key; + Object value; + Iterator it = aMap.keySet().iterator(); + while ( it.hasNext() ) + { + key = it.next().toString(); + value = aMap.get( key ); + if ( NULL.equals( value ) ) value = null; + Introspector.set( anObject, key, value ); + } + } + + /** + * Creates a new copy of the specified object. + * This implementation tries to call clone(), + * and failing that, calls newInstance + * and then calls copy() to transfer the values. + * @throws WotonomyException if any operation fails. + */ + static public Object clone( + Object aSource ) + { + Object result = null; + if ( clone.implementedByObject( aSource ) ) + { + try + { + result = clone.invoke( aSource ); + return result; + } + catch ( Exception exc ) + { + // fall back on newInstance() + } + } + + Class c = aSource.getClass(); + try + { + result = c.newInstance(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + return copy( aSource, result ); + } + + /** + * Creates a deep copy of the specified object. + * Every object in this objects graph will be + * duplicated with new instances. + * @throws WotonomyException if any operation fails. + */ + static public Object deepClone( + Object aSource ) + { + // the only known way to deep copy in + // java without native code is serialization + + try + { + ByteArrayOutputStream byteOutput = + new ByteArrayOutputStream(); + ObjectOutputStream objectOutput = + new ObjectOutputStream( byteOutput ); + + objectOutput.writeObject( aSource ); + objectOutput.flush(); + objectOutput.close(); + + ByteArrayInputStream byteInput = + new ByteArrayInputStream( byteOutput.toByteArray() ); + ObjectInputStream objectInput = + new ObjectInputStream( byteInput ); + return objectInput.readObject(); + } + catch ( Exception exc ) + { + throw new WotonomyException( "Error cloning object: " + aSource, exc ); + } + } + + /** + * Copies values from one object to another. + * Returns the destination object. + * @throws WotonomyException if any operation fails. + */ + static public Object copy( + Object aSource, Object aDestination ) + { + try + { + writePropertiesForObject( + readPropertiesForObject( aSource ), aDestination ); + } + catch ( RuntimeException exc ) + { + throw new WotonomyException( exc ); + } + return aDestination; + } + + /** + * Deeply clones the values from one object and applies them + * to another object. + * Returns the destination object. + * @throws WotonomyException if any operation fails. + */ + static public Object deepCopy( + Object aSource, Object aDestination ) + { + try + { + writePropertiesForObject( + clonePropertiesForObject( aSource ), aDestination ); + } + catch ( RuntimeException exc ) + { + throw new WotonomyException( exc ); + } + return aDestination; + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 16:52:12 cgruber + * Add cvsignore crap to find off checking in binary crap. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.11 2001/08/22 19:24:26 mpowers + * Providing a more helpful error message for cloning exceptions. + * + * Revision 1.10 2001/03/29 03:30:36 mpowers + * Refactored duplicator a bit. + * Disabled MissingPropertyExceptions for now. + * + * Revision 1.9 2001/03/28 14:11:23 mpowers + * Removed debugging printlns. + * + * Revision 1.8 2001/03/27 23:25:48 mpowers + * Basically reverting to the previous version. + * + * Revision 1.7 2001/03/06 23:18:13 mpowers + * Clarified some comments. + * + * Revision 1.6 2001/03/01 20:36:35 mpowers + * Better error handling and better handling of nulls. + * + * Revision 1.5 2001/02/27 21:43:40 mpowers + * Removed NullMarker class in favor of NSNull. + * + * Revision 1.4 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * Revision 1.3 2001/02/23 21:07:46 mpowers + * Documented the NULL object. + * + * Revision 1.1 2001/02/16 22:51:29 mpowers + * Now deep-cloning objects passed between editing contexts. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Introspector.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Introspector.java new file mode 100644 index 0000000..2b313d0 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/Introspector.java @@ -0,0 +1,941 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** +* This Introspector is a static utility class written to work +* around limitations in PropertyDescriptor and Introspector.

+* +* Of particular note are the get() and set() methods, which will attempt +* to get and set artibrary values on arbitrary objects to the best of its +* ability, converting values as appropriate. Properties of the form +* "property.nestedproperty.anotherproperty" are supported to get and set +* values on property values directly.

+* +* Note that for naming getter methods, this class supports "get", "is", +* and also the property name itself, which supports NeXT-style properties. +* Introspector supports Maps by treating the keys a property names, +* supports Lists by treating the indexes as property names.

+* +* Numeric and boolean types can be inverted by prepending a "!" before +* the name of the property, like "manager.!active" or "task.!lag". +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class Introspector +{ + // allows "hasProperty" or "property" forms + public static boolean strict = false; + + // print exception stack traces + private static boolean debug = true; + + // path separator + public static final String SEPARATOR = "."; + + // method cache - use hashtables for thread safety + private static Map getterMethods = new Hashtable(); + private static Map setterMethods = new Hashtable(); + + // wildcard value - using this class to represent a "wildcard" generic class. + // we have to do this when matching methods by parameter types and a + // null value is passed in - can't tell what class the null should be. + public static Class WILD = Introspector.class; + + // empty class array - prevents having to create one every time + private static final Class[] EMPTY_CLASS_ARRAY = new Class[0]; + + // use OGNL for property access + private static boolean useOGNL; + + static + { + try + { + useOGNL = ( Class.forName( "ognl.Ognl" ) != null ); + } + catch ( ClassNotFoundException t ) + { + useOGNL = false; + } + } + +/** +* Utility method to get the read method for a property belonging to a class. +* Will search for methods in the form of "getProperty" and failing that +* "isProperty" (to handle booleans). +* @param objectClass the class whose property methods will be retrieved. +* @param aProperty The property whose method will be retrieved. +* @param paramTypes An array of class objects representing the types of parameters. +* @return The appropriate method for the class, or null if not found. +*/ + static public Method getPropertyReadMethod( + Class objectClass, String aProperty, Class[] paramTypes ) + { + Method result = null; + + result = getMethodFromClass( objectClass, aProperty, paramTypes, true ); + + return result; + } + +/** +* Utility method to get the write method for a property belonging to a class. +* Will search for methods in the form of "setProperty". +* @param objectClass the class whose property methods will be retrieved. +* @param aProperty The property whose method will be retrieved. +* @param paramTypes An array of class objects representing the types of parameters. +* @return The appropriate method for the class, or null if not found. +*/ + static public Method getPropertyWriteMethod( + Class objectClass, String aProperty, Class[] paramTypes ) + { + Method result = null; + + result = getMethodFromClass( objectClass, aProperty, paramTypes, false ); + + return result; + } + +/** +* Gets a named method from a class. Using this method is preferred because +* the results are cached and should be faster than calling Class.getMethod(). +* Note that if an object has a "get" getter method and an "is" getter method +* with the same signature defined for a given property. The "get" method +* is called. +* @param objectClass the Class whose property methods will be retrieved. +* @param aMethodName A String containing the name of the desired method. +* @param paramTypes An array of class objects representing the types of parameters. +* @return The appropriate Method from the Class, or null if not found. +*/ + static private Method getMethodFromClass( + Class objectClass, String aProperty, Class[] paramTypes, boolean doGetter) + { // System.out.print( "Introspector.getMethodFromClass: " + aMethodName + " : " ); + + Map classesToMethods = (doGetter ? + getterMethods : + setterMethods); + + Map allMethods = (Map) classesToMethods.get( objectClass ); + if (allMethods == null) + { + // need to build maps for this class + mapPropertiesForClass( objectClass ); + // now the map should exist + allMethods = (Map) classesToMethods.get( objectClass ); + } + + Method[] methods = (Method[]) allMethods.get( aProperty ); + if ( methods == null ) + { + return null; // property doesn't exist + } + + methods_loop: // walks through all methods for name + for ( int i = 0; i < methods.length; i++ ) + { + Class[] types = methods[i].getParameterTypes(); + + // if parameter lengths don't match + if ( types.length != paramTypes.length ) + { + // System.out.println( aMethodName + " : " + types.length + " != " + paramTypes.length ); + continue methods_loop; // continue with outer loop + } + + // match up each parameter + for ( int j = 0; j < types.length; j++ ) + { + // convert primitives so they'll match - ugly + // (would have thought isAssignableFrom() would catch this) + if ( types[j].isPrimitive() ) + { + if ( types[j] == Boolean.TYPE ) + { + types[j] = Boolean.class; + } + else + if ( types[j] == Character.TYPE ) + { + types[j] = Character.class; + } + else + if ( types[j] == Byte.TYPE ) + { + types[j] = Byte.class; + } + else + if ( types[j] == Short.TYPE ) + { + types[j] = Short.class; + } + else + if ( types[j] == Integer.TYPE ) + { + types[j] = Integer.class; + } + else + if ( types[j] == Long.TYPE ) + { + types[j] = Long.class; + } + else + if ( types[j] == Float.TYPE ) + { + types[j] = Float.class; + } + else + if ( types[j] == Double.TYPE ) + { + types[j] = Double.class; + } + } + + // if parameters don't match + if ( ( paramTypes[j] != WILD ) && ( ! types[j].isAssignableFrom( paramTypes[j] ) ) ) + { +// System.out.println( "Introspector.getMethodFromClass: " + +// aProperty + " : " + types[j] + " != " + paramTypes[j] ); + continue methods_loop; // continue with outer loop + } + } + + // all params match + return methods[i]; + } + + // no match + return null; + } + + static private final Method[] getAllMethodsForClass( Class aClass ) + { + Method[] local = aClass.getDeclaredMethods(); // only local + Method[] all = aClass.getMethods(); // all public + Method[] result = new Method[ local.length + all.length ]; + System.arraycopy( local, 0, result, 0, local.length ); + System.arraycopy( all, 0, result, local.length, all.length ); + return result; + } + + /** + * Generates a map of properties to both getter or setter methods for the given class. + * Then assigned those maps into the appropriate getterMethods and setterMethods maps + * keyed by the specified class. Even on error, this method will at least place empty + * property maps into each of the methods maps. + */ + static private void mapPropertiesForClass( Class objectClass ) + { + try + { + Map readProperties = new HashMap(); + getterMethods.put( objectClass, readProperties ); + Map writeProperties = new HashMap(); + setterMethods.put( objectClass, writeProperties ); + + String name, property; + Method[] methods = getAllMethodsForClass( objectClass ); // throws SecurityException + for ( int i = 0; i < methods.length; i++ ) + { + name = methods[i].getName(); + methods[i].setAccessible( true ); // throws SecurityException + if ( name.startsWith( "set" ) ) + { + name = name.substring( 3 ); + if ( ! "".equals( name ) ) // excludes "set()" + { + putMethodIntoPropertyMap( name, methods[i], writeProperties ); + } + } + else + if ( methods[i].getReturnType() != void.class ) + { + String fullname = name; + if ( name.startsWith( "get" ) ) + { + name = name.substring( 3 ); + } + else + if ( name.startsWith( "is" ) ) + { + name = name.substring( 2 ); + } + else + if ( name.startsWith( "has" ) && ( !strict ) ) // what about hashCode()? + { + name = name.substring( 3 ); + } + + if ( ! "".equals( name ) && ( !strict ) ) // excludes "get()", "has()", and "is()" + { + putMethodIntoPropertyMap( name, methods[i], readProperties ); + if ( fullname != name ) + { // allows us to match properties that include the get/set prefix as well + putMethodIntoPropertyMap( fullname, methods[i], readProperties ); + } + } + } + } + } + catch ( SecurityException se ) + { + System.out.println( "Introspector.getMethodFromClass: " + se ); + // this class will show up with empty getter/setter maps + } + } + + /** + * Places a property-method pair into one of the properties maps. + * This in effect maps a property to an array of methods. + */ + private static void putMethodIntoPropertyMap( String aProperty, Method aMethod, Map aMap ) + { + // ensure first character is lower case + StringBuffer buffer = new StringBuffer( aProperty ); + buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0))); + String key = buffer.toString(); + + // build array of methods for property + Method[] result = (Method[]) aMap.get( key ); + if ( result == null ) + { + result = new Method[] { aMethod }; + } + else + { + // create new array that's larger by one and copy + int i; + Method[] enlarged = new Method[ result.length + 1 ]; + for ( i = 0; i < result.length; i ++ ) + { + enlarged[i] = result[i]; + } + // add the new method to end + enlarged[i] = aMethod; + result = enlarged; + } + aMap.put( key, result ); + } + +/** +* Utility method to get a method for a property belonging to a class. +* Use this if you don't feel like making the Class array from the parameters +* you will be using - pass in the parameters themselves. +* @param objectClass the Class whose property methods will be retrieved. +* @param aProperty The property whose method will be retrieved. +* @param params An array of parameters to be used. +* @return The appropriate method for the class, or null if not found. +*/ + static public Method getPropertyReadMethod( + Class objectClass, String aProperty, Object[] params ) + { + // optimization: avoid allocating class array for common case + if ( params.length == 0 ) + { + return getPropertyReadMethod( + objectClass, aProperty, EMPTY_CLASS_ARRAY ); + } + + Class[] paramList = new Class[ params.length ]; + for ( int i = 0; i < params.length; i++ ) + { + if ( params[i] != null ) + { + paramList[i] = params[i].getClass(); + } + else + { + paramList[i] = WILD; + } + } + return getPropertyReadMethod( objectClass, aProperty, paramList ); + } + +/** +* Utility method to get a method for a property belonging to a class. +* Use this if you don't feel like making the Class array from the parameters +* you will be using - pass in the parameters themselves. +* @param objectClass the Class whose property methods will be retrieved. +* @param aProperty The property whose method will be retrieved. +* @param params An array of parameters to be used. +* @return The appropriate method for the class, or null if not found. +*/ + static public Method getPropertyWriteMethod( + Class objectClass, String aProperty, Object[] params ) + { + Class[] paramList = new Class[ params.length ]; + for ( int i = 0; i < params.length; i++ ) + { + if ( params[i] != null ) + { + paramList[i] = params[i].getClass(); + } + else + { + paramList[i] = WILD; + } + } + return getPropertyWriteMethod( objectClass, aProperty, paramList ); + } + + /** + * Gets a list of the readable properties for the given class. + * Note that readable properties may not be writable - see getWriteProperties(). + * @return An array of property names in no particular order + * where each name is a string with the first character in lower case. + */ + public static String[] getReadPropertiesForClass( Class objectClass ) + { + Map properties = (Map) getterMethods.get( objectClass ); + if ( properties == null ) + { + // need to build maps for this class + mapPropertiesForClass( objectClass ); + // now the map should exist + properties = (Map) getterMethods.get( objectClass ); + } + + // put property names into string array + Set keys = properties.keySet(); + Iterator it = keys.iterator(); + int len = keys.size(); + String[] result = new String[ len ]; + for ( int i = 0; i < len; i++ ) + { + result[i] = (String) it.next(); + } + return result; + } + + /** + * Gets a list of the writable properties for the given class. + * Note that writable properties may not be writable - see getReadProperties(). + * @return An array of property names in no particular order + * where each name is a string with the first character in lower case. + */ + public static String[] getWritePropertiesForClass( Class objectClass ) + { + Map properties = (Map) setterMethods.get( objectClass ); + if ( properties == null ) + { + // need to build maps for this class + mapPropertiesForClass( objectClass ); + // now the map should exist + properties = (Map) setterMethods.get( objectClass ); + } + + // put property names into string array + Set keys = properties.keySet(); + Iterator it = keys.iterator(); + int len = keys.size(); + String[] result = new String[ len ]; + for ( int i = 0; i < len; i++ ) + { + result[i] = (String) it.next(); + } + return result; + } + + /** + * Gets a list of the readable properties for the given object, which may + * not be null. This method is more useful than getReadPropertiesForClass + * in that Maps will return their keys as properties and Lists will return + * their element indices as properties. + * Note that readable properties may not be writable - see getWriteProperties(). + * @return An array of property names in no particular order + * where each name is a string with the first character in lower case. + */ + public static String[] getReadPropertiesForObject( Object anObject ) + { + List properties = new ArrayList(); + String[] classProperties = + getReadPropertiesForClass( anObject.getClass() ); + if ( anObject instanceof List ) + { + properties.addAll( getPropertiesForList( (List) anObject ) ); + } + if ( anObject instanceof Map ) + { + properties.addAll( getPropertiesForMap( (Map) anObject ) ); + } + int i; + int len = classProperties.length + properties.size(); + String[] result = new String[ len ]; + for ( i = 0; i < classProperties.length; i++ ) + { + result[i] = classProperties[i]; + } + Iterator it = properties.iterator(); + while ( it.hasNext() ) + { + result[i++] = it.next().toString(); + } + return result; + } + + /** + * Gets a list of the writable properties for the given object, which may + * not be null. This method is more useful than getWritePropertiesForClass + * in that Maps will return their keys as properties and Lists will return + * their element indices as properties. + * Note that writable properties may not be writable - see getReadProperties(). + * @return An array of property names in no particular order + * where each name is a string with the first character in lower case. + */ + public static String[] getWritePropertiesForObject( Object anObject ) + { + List properties = new ArrayList(); + String[] classProperties = + getWritePropertiesForClass( anObject.getClass() ); + if ( anObject instanceof List ) + { + properties.addAll( getPropertiesForList( (List) anObject ) ); + } + if ( anObject instanceof Map ) + { + properties.addAll( getPropertiesForMap( (Map) anObject ) ); + } + + int i; + int len = classProperties.length + properties.size(); + String[] result = new String[ len ]; + for ( i = 0; i < classProperties.length; i++ ) + { + result[i] = classProperties[i]; + } + Iterator it = properties.iterator(); + while ( it.hasNext() ) + { + result[i++] = it.next().toString(); + } + return result; + } + + private static List getPropertiesForList( List aList ) + { + List result = new ArrayList(); + int len = aList.size(); + for ( int i = 0; i < len; i++ ) + { + result.add( new Integer( i ).toString() ); + } + return result; + } + + private static List getPropertiesForMap( Map aMap ) + { + List result = new ArrayList(); + Iterator it = ((Map)aMap).keySet().iterator(); + while ( it.hasNext() ) + { + result.add( it.next().toString() ); + } + return result; + } + + private static Object[] EMPTY_ARRAY = new Object[0]; + + /** + * Convenience to get a value for a property from an object. + * An empty property string is considered the identity property + * and simply returns the object. + * @throws MissingPropertyException if the property cannot be + * found on the object. + */ + public static Object getValueForObject( Object anObject, String aProperty ) + { + if ( ( aProperty == null ) || ( "".equals( aProperty ) ) ) + { + return anObject; + } + + if ( useOGNL && aProperty.startsWith( "ognl:" ) ) + { + try + { + return ognl.Ognl.getValue( aProperty, anObject ); + } + catch ( Throwable t ) + { + if ( debug ) + { + System.err.println( + "Introspector.getValueForObject: " + + anObject + "' ( " + anObject.getClass() + " )" + + ", ognl:" + aProperty ); + System.err.println( t ); + } + return null; + } + } + + boolean invert = false; + if ( aProperty.startsWith( "!" ) ) + { + aProperty = aProperty.substring(1); + invert = true; + } + + Object result = null; + try + { + Method m = Introspector.getPropertyReadMethod( + anObject.getClass(), aProperty, EMPTY_ARRAY ); + if ( m != null ) + { + result = m.invoke( anObject, EMPTY_ARRAY ); + } + else // no method, try for field + { + try + { + Field field = anObject.getClass().getDeclaredField( aProperty ); + if ( field != null ) + { + field.setAccessible( true ); // throws SecurityException + result = field.get( anObject ); + } + } + catch ( Throwable t ) + { + // ignore for now + } + } + + if ( result == null ) + { + if ( anObject instanceof Map ) + { + result = ((Map)anObject).get( aProperty ); + } + else + if ( anObject instanceof List ) + { + result = ((List)anObject).get( Integer.parseInt( aProperty ) ); + } + } + + if ( invert ) + { + Object inverted = ValueConverter.invert( result ); + if ( inverted != null ) result = inverted; + } + //System.out.println( "getValueForObject: " + anObject + " : " + aProperty + " : " + result ); + return result; + } + catch ( Throwable exc ) + { + if ( exc instanceof InvocationTargetException ) + { + exc = ((InvocationTargetException)exc).getTargetException(); + } + if ( exc instanceof RuntimeException ) + { + throw (RuntimeException)exc; + } + if ( debug ) + { + System.out.println( + "Introspector.getValueForObject: " + + anObject + "' ( " + anObject.getClass() + " )" + + ", " + aProperty + ": " ); + } + throw new WotonomyException( exc ); + } +//! throw new MissingPropertyException(); + } + + /** + * Convenience to set a value for a property from an object. + * Returns the return value from executing the specified method, + * or null if the method returns type void. + * @throws MissingPropertyException if the property cannot be + * found on the object. + * @throws NullPrimitiveException if the property is of primitive + * type and the value is null. + */ + public static Object setValueForObject( + Object anObject, String aProperty, Object aValue ) + { + if ( useOGNL && aProperty.startsWith( "ognl:" ) ) + { + try + { + ognl.Ognl.setValue( aProperty, anObject, aValue ); + } + catch ( Throwable t ) + { + if ( debug ) + { + System.err.println( + "Introspector.setValueForObject: " + + anObject + "' ( " + anObject.getClass() + " )" + + ", ognl:" + aProperty + " : " + aValue ); + System.err.println( t ); + } + } + return null; + } + + try + { + if ( aProperty.startsWith( "!" ) ) + { + aProperty = aProperty.substring(1); + Object inverted = ValueConverter.invert( aValue ); + if ( inverted != null ) aValue = inverted; + } + + Method m = null; + if ( aValue != null ) + { + m = Introspector.getPropertyWriteMethod( + anObject.getClass(), aProperty, new Class[] { aValue.getClass() } ); + } + if ( m == null ) + { + m = Introspector.getPropertyWriteMethod( + anObject.getClass(), aProperty, new Class[] { WILD } ); + if ( ( m != null ) && ( aValue != null ) ) + { + // check for null primitive + if ( ( aValue == null ) && + ( m.getParameterTypes()[0].isPrimitive() ) ) + { + throw new NullPrimitiveException(); + } + + // convert if possible + Object o = ValueConverter.convertObjectToClass( + aValue, m.getParameterTypes()[0] ); + if ( o != null ) + { + aValue = o; + } + } + } + if ( m != null ) + { + return m.invoke( anObject, new Object[] { aValue } ); + } + else // no method, try for field + { + try + { + Field field = anObject.getClass().getDeclaredField( aProperty ); + if ( field != null ) + { + field.setAccessible( true ); // throws SecurityException + field.set( anObject, aValue ); + return null; + } + } + catch ( Throwable t ) + { + // ignore for now + } + } + + if ( anObject instanceof Map ) + { + return ((Map)anObject).put( aProperty, aValue ); + } + if ( anObject instanceof List ) + { + List list = (List) anObject; + int i = Integer.parseInt( aProperty ); + if ( list.size() < i+1 ) + { + // expand list as necessary + for ( int j = list.size(); j <= i; j++ ) + { + list.add( new Object() ); // placeholder + } + } + return list.set( i, aValue ); + } + } + catch ( Throwable exc ) + { + if ( exc instanceof IllegalArgumentException ) + { + System.out.println( + "Introspector.setValueForObject: " + + anObject + " , " + aProperty + " , '" + + aValue + "' ):" ); + System.out.println( exc ); + } + else + if ( exc instanceof InvocationTargetException ) + { + exc = ((InvocationTargetException)exc).getTargetException(); + } + if ( exc instanceof RuntimeException ) + { + throw (RuntimeException)exc; + } + if ( debug ) + { + System.out.println( + "Introspector.setValueForObject: " + + anObject + " , " + aProperty + " , '" + + aValue + "' ):" ); + } + throw new WotonomyException( exc ); + } + return null; +//! throw new MissingPropertyException(); + } + + /** + * Gets a value from an object or any of its child objects. + * This will parse the property string for "."'s and get + * values for each successive object's property in the path. + * An empty property string is considered the identity property + * and simply returns the object. + */ + public static Object get( Object anObject, String aProperty ) + { + int i = aProperty.indexOf( SEPARATOR ); + if ( i == -1 ) return getValueForObject( anObject, aProperty ); + + String pathElement = aProperty.substring( 0, i ); + String remainder = aProperty.substring( i+1 ); + + Object result = getValueForObject( anObject, pathElement ); + if ( result == null ) return null; + return get( result, remainder ); + } + + /** + * Sets a value in an object or any of its child objects. + * This will parse the property string for "."'s and set + * values for each successive object's property in the path.

+ * + * If a property is not found, this method will try to + * implicitly create hash maps (if possible) to fill out the path. + * This is useful when dealing with trees of nested maps. + */ + public static Object set( Object anObject, String aProperty, Object aValue ) + { + int i = aProperty.indexOf( SEPARATOR ); + if ( i == -1 ) return setValueForObject( anObject, aProperty, aValue ); + + String pathElement = aProperty.substring( 0, i ); + String remainder = aProperty.substring( i+1 ); + + Object result = getValueForObject( anObject, pathElement ); + if ( result == null ) + { + result = new HashMap(2); + setValueForObject( anObject, pathElement, result ); + } + return set( result, remainder, aValue ); + } + + /** + * If set to true, exceptions printed to System.out.println. + * Defaults to true. + */ + public void setDebug( boolean isDebug ) + { + debug = isDebug; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:11:47 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.19 2004/02/05 02:20:34 mpowers + * Added experimental ognl support (if ognl is present). + * + * Revision 1.18 2003/03/26 16:44:35 mpowers + * Now correctly reflecting on all methods, not just locally declared ones. + * + * Revision 1.17 2003/02/21 21:10:51 mpowers + * Now reaching package, protected, and private methods and fields. + * + * Revision 1.16 2003/01/28 22:11:59 mpowers + * Now more lenient in resolving properties starting with "is" "get" or "has". + * + * Revision 1.15 2003/01/27 15:10:54 mpowers + * Better handling for illegal argument exceptions. + * + * Revision 1.14 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.13 2002/10/11 15:35:12 mpowers + * Removed printlns. + * + * Revision 1.11 2001/05/02 17:58:41 mpowers + * Removed debugging code, added comments. + * + * Revision 1.10 2001/04/08 21:00:54 mpowers + * Changes to support new objectsForFetchSpecification scheme. + * + * Revision 1.9 2001/03/29 03:30:36 mpowers + * Refactored duplicator a bit. + * Disabled MissingPropertyExceptions for now. + * + * Revision 1.8 2001/03/28 17:52:45 mpowers + * Corrected the throws in the docs. + * + * Revision 1.7 2001/03/28 17:49:13 mpowers + * Better exception handling in Introspector. + * + * Revision 1.6 2001/03/13 21:40:20 mpowers + * Improved handling of runtime exceptions. + * + * Revision 1.5 2001/03/09 22:06:35 mpowers + * Now extracting the wrapped exception from InvocationTargetExceptions. + * + * Revision 1.4 2001/03/01 20:36:35 mpowers + * Better error handling and better handling of nulls. + * + * Revision 1.3 2001/01/17 16:20:57 mpowers + * Introspector now handles the identity property. + * + * Revision 1.2 2001/01/09 20:08:17 mpowers + * Slight optimization. + * + * Revision 1.1.1.1 2000/12/21 15:52:04 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/IntrospectorException.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/IntrospectorException.java new file mode 100644 index 0000000..b1ad824 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/IntrospectorException.java @@ -0,0 +1,32 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +/** +* A WotonomyException that is thrown by Introspector. +* This class serves as a base class for other exceptions. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class IntrospectorException extends WotonomyException +{ +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/MissingPropertyException.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/MissingPropertyException.java new file mode 100644 index 0000000..c1e30d3 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/MissingPropertyException.java @@ -0,0 +1,32 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +/** +* A IntrospectorException that is thrown by Introspector when +* a property does not exist for an object. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class MissingPropertyException extends IntrospectorException +{ +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NetworkClassLoader.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NetworkClassLoader.java new file mode 100644 index 0000000..43c14a5 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NetworkClassLoader.java @@ -0,0 +1,368 @@ +/* + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package net.wotonomy.foundation.internal; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; + +/** + * The correct name for this class should be URLClassLoader. + * But there is already a class by that name in JDK1.2. + * + * I have had quite a few problems with URLClassLoader in + * past, so I ended up writing this ClassLoader. I found that + * the Java 2's URLClassLoader, does not close the Jar file once + * opened. It is a pretty good optimization step, but if you + * modify the class in the jar file, it does not pick it up. Some + * operating systems may not let you modify the jar file while it is + * still open. IMHO, it does make sense to close the jar file + * after you are done reading the class data. But this approach may not + * get you the performance of the URLClassLoader, but it works in all + * cases and also runs on JDK1.1. I have enhanced this class loader + * to read all the zip/jar entries once & cache the data, so that + * there is no overhead of opening/closing jar file to pick up + * each entry. + * + * + * @author Harish Prabandham + */ +public class NetworkClassLoader extends ClassLoader { + private ClassLoader parent = null; // parent classloader + private Hashtable classCache = new Hashtable(); + private Hashtable urlset = new Hashtable(); + + /** + * Creates a new instance of the class loader. + * @param delegate/parent class loader. + */ + public NetworkClassLoader(ClassLoader parent) { + setParent(parent); + } + + /** + * Sets the parent/delegate class loader. + * @param delegate/parent class loader. + */ + protected final void setParent(ClassLoader parent) { + this.parent = parent; + } + + /** + * Adds the given URL to this class loader. If the URL + * ends with "/", then it is assumed to be a directory + * otherwise, it is assumed to be a zip/jar file. If the + * same URL is added again, the URL is re-opened and this + * zip/jar file is used for serving any future class requests. + * @param URL where to look for the classes. + */ + public synchronized void addURL(URL url) { + // System.out.println("Adding url: " + url); + if(!urlset.containsKey(url)) { + try { + urlset.put(url, new URLResourceReader(url)); + }catch(IOException ioe){ + // Probably a bad url... + } + } else { + // remove the old one & add a new one... + try{ + URLResourceReader newu = new URLResourceReader(url); + URLResourceReader oldu = (URLResourceReader) urlset.get(url); + oldu.close(); + urlset.remove(url); + urlset.put(url, newu); + } catch (IOException ioe) { + } + } + } + + /** + * @return An enumeration of URLs where this class loader + * looks for classes. + */ + public Enumeration getURLs() { + return urlset.keys(); + } + + /** + * Call this to bypass the implementation of loadClass. + */ + public Class findClass(String name) { + byte[] b = loadClassData(name); + if ( b == null ) return null; + return defineClass(name, b, 0, b.length); + } + + protected byte[] loadResource(URL url, String resourceName) + throws IOException { + URLResourceReader urr = (URLResourceReader) urlset.get(url); + // System.out.println("Loading from " + urr + " " + resourceName); + if(urr != null) { + return urr.getResource(resourceName); + } + + return null; + } + + protected byte[] loadResource(String resource) { + byte[] barray = null; + for(Enumeration e = urlset.keys(); e.hasMoreElements();) { + URL url = (URL) e.nextElement(); + + try { + barray = loadResource(url, resource); + } catch(Exception ex) { + } finally { + if(barray != null) + break; + } + } + + return barray; + } + + protected byte[] loadClassData(String classname) { + String resourceName = classname.replace('.', '/') + ".class"; + return loadResource(resourceName); + } + + /** + * Overridden to search for a resource and return + * a "jar"-style URL or normal "file" URL as necessary. + */ + protected URL findResource(String name) + { //System.out.println( "findResource: " + name ); + URL url; + byte[] barray = null; + + for ( Enumeration e = urlset.keys(); e.hasMoreElements(); ) + { + url = (URL) e.nextElement(); + try + { + barray = loadResource(url, name); // loads fully: wasteful + } + catch(Exception ex) + { + // do nothing + } + if( barray != null ) + { + try + { + String ref = url.toString(); + if ( ref.endsWith( ".jar" ) ) + { + //System.out.println( "jar:" + ref + "!/" + name ); + return new URL( "jar:" + ref + "!/" + name ); + } + else + { + //System.out.println( new URL( url, name ).toString() ); + return new URL( url, name ); + } + } + catch ( Throwable t ) + { + t.printStackTrace(); + } + } + } + + return null; + } + + /** + * @return The resource as the input stream if such a resource + * exists, otherwise returns null. + */ + public InputStream getResourceAsStream(String name) { + //System.out.println( "getResourceAsStream: " + name ); + InputStream istream = null; + + // Algorithm: + // + // 1. first check the system path for the resource + // 2. next check the delegate/parent class loader for the resource + // 3. then attempt to get the resource from the url set. + // + + // Lets check the system path for the resource. + istream = getSystemResourceAsStream(name); + if(istream != null) + return istream; + + // Lets check the parent/delegate class loader for the resource. + if(parent != null) { + istream = parent.getResourceAsStream(name); + if(istream != null) + return istream; + } + + // Lets load it ourselves. + byte[] data = loadResource(name); + if(data != null) { + istream = new ByteArrayInputStream(data); + } + + return istream; + } + + /** + * java.lang.ClassLoader's defineClass method is final, so the + * its subclasses cannot override this method. But, this class + * calls this method in the loadClass() instead. + * @param The name of the class without ".class" extension. + * @param The class data bytes. + * @return The class object. + */ + protected Class defineClass(String classname, byte[] classdata) { + return defineClass(classname, classdata, 0, classdata.length); + } + + public synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + Class c = null; + + // Algorithm: (Please do not change the order; unless you + // have a good reason to do so). + // + // 1. first check the system class loader. + // 2. next check the delegate/parent class loader. + // 3. next check the class cache + // 4. then attempt to load classes from the URL set. + // + + // Lets see if the class is in system class loader. + try { + c = findSystemClass(name); + }catch(ClassNotFoundException cnfe) { + }finally { + if(c != null) + return c; + } + + // Lets see if the class is in parent class loader. + try { + if(parent != null) + c = parent.loadClass(name); + }catch(ClassNotFoundException cnfe) { + }finally { + if(c != null) + return c; + } + + // Lets see if the class is in the cache.. + c = (Class) classCache.get(name); + + if(c != null) + return c; + + + // Lets see if we find the class all by ourselves. + byte[] data = loadClassData(name); + + if(data != null) { + // we did !! + c = defineClass(name, data); + classCache.put(name, c); + if(resolve) + resolveClass(c); + } else { + // We are out of luck at this point... + throw new ClassNotFoundException(name); + } + + return c; + } + + /** + * This method resets this ClassLoader's state. It completely + * removes all the URLs and classes in this class loader cache. + */ + public final void clear() { + urlset.clear(); + classCache.clear(); + } + + /** + * This method resets this ClassLoader's state and resets the + * references for garbage collection. + */ + protected void finalize() throws Throwable { + // Cleanup real well. Otherwise, this can be + // a major source of memory leaks... + + // remove all the urls & class entries. + clear(); + + parent = null; + urlset = null; + classCache = null; + } +} + + + + + + + + + + + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NullPrimitiveException.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NullPrimitiveException.java new file mode 100644 index 0000000..e367211 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/NullPrimitiveException.java @@ -0,0 +1,32 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +/** +* A IntrospectorException that is thrown by Introspector when +* trying to set a primitive type to null. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class NullPrimitiveException extends IntrospectorException +{ +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyComparator.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyComparator.java new file mode 100644 index 0000000..abdc82f --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyComparator.java @@ -0,0 +1,100 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import java.io.Serializable; +import java.util.Comparator; + + +/** +* A Comparator that will sort elements based on the +* property specified in the constructor. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class PropertyComparator implements Comparator, Serializable +{ + private String property; + +/** +* Standard constructor to configure the comparator. +* @param aProperty A property whose value is used to sort elements. +*/ + public PropertyComparator( String aProperty ) + { + property = aProperty; + } + + // interface Comparator + + public int compare(Object o1, Object o2) + { + Object v1 = Introspector.get( o1, property ); + Object v2 = Introspector.get( o2, property ); + if ( v1 instanceof Comparable ) + { + return ((Comparable)v1).compareTo( v2 ); + } + else + if ( v2 instanceof Comparable ) + { + return ((Comparable)v2).compareTo( v1 ); + } + else + { + if ( v1 == null ) + { + if ( v2 == null ) + { + return 0; // both nulls are equal + } + return -1; // null is less than any object + } + else + if ( v2 == null ) + { + return 1; // any object is greater than null + } + } + // last resort: compare string conversions + return v1.toString().compareTo( v2.toString() ); + } + + public boolean equals( Object obj ) + { + return ( obj instanceof PropertyComparator ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:11:47 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:52:07 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyListParser.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyListParser.java new file mode 100644 index 0000000..03231c7 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/PropertyListParser.java @@ -0,0 +1,546 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import java.util.*; //collections +import java.io.*; + +/** + * PropertyListParser can parse a property list (plist) file or string, and + * return the top-level object represented by the plist.

+ * + * A property list is a heirarchical data structure containing only Maps, + * Lists, and Strings -- nothing else. In other words, a property list is + * either a Map, List, or String instance, with the restrictions that the + * collections may only contain Map, List, or String instances.

+ * + * This class can read a particularly-formatted string or file, and create + * the property list structure described. It provides a convenient means + * for having a structured data file, letting programs simply deal with the + * structure rather than having to do a lot of string parsing work as well. + * The concept is similar to Properties files, except that the values can + * be nested Maps or Lists instead of only Strings.

+ * + * A Map is specified in a file by key/value pairs surrounded by brace + * characters. An equal sign (=) must be between the key and value, and + * there must be a semicolon (;) following the value. + * + *

+ *     {
+ *         key1 = value1;
+ *         key2 = value2;
+ *         etc...
+ *     }
+ * 
+ * + * A List is specified by a comma-separated list of values surrounded by parentheses, like: + *
+ *     ( value1, value2, value3, etc... )
+ * 
+ * + * A String can either be quoted in the manner of a constant string in + * Java, or unquoted. If unquoted, the string can only contain + * alphanumerics, underscores (_), periods (.), dollar signs ($), colons + * (:), or forward slashes (/). If any other character appears in the + * string, it must be quoted (i.e., surrounded by " characters). + * Quoted strings may also contain \n, \t, \f, \v, \b, and \a escapes, + * octal escapes of the form \000, and unicode escapes of the form of \U + * followed by four hexadecimal characters. Any other character escaped + * by a backslash will be treated as that character, and the escaping + * backslash character will be omitted. Thus, to represent an actual + * backslash, it must appear as \\ in the quoted string.

+ * + * All whitespace between elements is ignored, and both //-style and + * /*-style comments are allowed to appear anywhere between elements.

+ * + * If there are any syntax errors encountered while parsing, + * RuntimeExceptions are thrown with the line number and column of the + * problem.

+ * + * Currenty, HashMaps and ArrayLists are the actual Map and List classes + * used when creating the property list.

+ * + * Examples:

+
+   // This plist file represents a Map, since it starts with a '{'.
+   {
+       Map1 = { subkey1 = "foo"; };
+       Map2 =
+       {
+           "key1"  = "This is a quoted string.";
+           "key 2" = "bar\nbaz";    // the value has a newline in it
+           key3    = ("a", b, c, "quux quux");   // a List of four Strings
+       };  // We need a semicolon here, since it's following the value of the "Map2" key
+
+       List1 = (foobar,foobaz,"foo,baz", (aa, ab, ac)); // a List of 3 Strings and a List
+
+       // And now a List of two Maps
+       List2 = (
+           {
+               key1 = value1;
+               key2 = "value 2";
+               key3 = (a,b,c,d);
+               key4 = ();
+           },  // We need the comma here
+           {
+               key1 = {};  // an empty Map
+               key2 = "another String value";
+           }
+       );
+   }
+ 
+
+ * For those wondering, this is essentially a re-implementation of + * NeXT/Apple's property lists, except that data values are not supported. + * + * @author clindberg@blacksmith.com + * @version $Revision: 899 $ + */ + +public class PropertyListParser +{ + private char buffer[]; + private int currIndex; + private int lineNumber; + private int currLineStartIndex; + + /** Reads an object (String, List, or Map) from plistString and returns it. + * RuntimeExceptions are raised if there are parse problems. + */ + public static Object propertyListFromString(String plistString) + { + PropertyListParser parser = new PropertyListParser(plistString); + return parser.readTopLevelObject(); + } + + /** + * Reads all remaining characters from the Reader, and returns the + * result of propertyListFromString(). RuntimeExceptions are raised if + * there are parse problems + */ + public static Object propertyListFromReader(Reader reader) throws IOException + { + char charBuffer[] = new char[2048]; + StringBuffer stringBuffer = new StringBuffer(); + int numRead = 0; + + while (numRead >= 0) + { + numRead = reader.read(charBuffer); + if (numRead > 0) stringBuffer.append(charBuffer, 0, numRead); + } + + return propertyListFromString(stringBuffer.toString()); + } + + /** + * Reads the contents of the specified file, and parses the contents. + * If any error occurs, prints out a message using System.out.println() + * and returns null. + */ + public static Object propertyListFromFile(String filename) + { + try { + FileInputStream stream = new FileInputStream(filename); + return propertyListFromReader(new InputStreamReader(stream)); + } catch (Exception exception) { + String errorMessage = exception.getMessage(); + System.out.println("Error parsing property list from "+filename+": "+errorMessage); + } + + return null; + } + + /** + * Creates a new PropertyListParser to parse the contents of the + * specified String. + */ + public PropertyListParser(String plistString) + { + this(plistString.toCharArray()); + } + + /** + * Creates a new PropertyListParser to parse the specified char array. + */ + public PropertyListParser(char[] charArray) + { + buffer = charArray; + lineNumber = 1; + currLineStartIndex = 1; + currIndex = 0; + } + + public Object readTopLevelObject() + { + Object plist = readObject(); + + skipCommentWhitespace(); + if (!isAtEnd()) + { + throwParseException("Extra characters in plist string after parsing object. A plist should only contain one top-level object."); + } + + return plist; + } + + private void throwParseException(String errorMessage) + { + int column = currIndex - currLineStartIndex + 1; + throw new RuntimeException(errorMessage + " (Line " + lineNumber + ", column " + column + ")"); + } + + private void updateLineNumberWithIndex(int lineStartIndex) + { + lineNumber++; + currLineStartIndex = lineStartIndex; + } + + private boolean isAtEnd() + { + return currIndex >= buffer.length; + } + + private void skipDoubleslashComment() + { + while (!isAtEnd() && buffer[currIndex] != '\n') { + currIndex++; + } + } + + private void skipStandardCComment() + { + currIndex++; //skip over the starting '/' + + while (!isAtEnd()) + { + if (buffer[currIndex] == '\n') + updateLineNumberWithIndex(currIndex+1); + + currIndex++; + + if (buffer[currIndex-2] == '*' && buffer[currIndex-1] == '/') + { + return; + } + } + + throwParseException("Input exhausted while parsing comment"); + } + + private void skipWhitespace() + { + while (!isAtEnd() && isWhitespace(buffer[currIndex])) + { + if (buffer[currIndex] == '\n') + updateLineNumberWithIndex(currIndex+1); + currIndex++; + } + } + + private void skipCommentWhitespace() + { + boolean done = false; + + while (!done) + { + done = true; + + skipWhitespace(); + if ((buffer.length - currIndex) > 1 && buffer[currIndex] == '/') + { + if (buffer[currIndex+1] == '/') { + done = false; //iterate again + skipDoubleslashComment(); + } + else if (buffer[currIndex+1] == '*') { + done = false; //iterate again + skipStandardCComment(); + } + } + } + } + + private Object readObject() + { + skipCommentWhitespace(); + if (isAtEnd()) return null; + + // Data (i.e. byte[]) not supported + if (buffer[currIndex] == '"') + return readQuotedString(); + if (buffer[currIndex] == '(') + return readList(); + if (buffer[currIndex] == '{') + return readMap(); + + return readUnquotedString(); + } + + private static final byte valueForHexDigit(char c) + { + if(c >= '0' && c <= '9') return (byte)(c - '0'); + if(c >= 'a' && c <= 'f') return (byte)((c - 'a') + 10); + if(c >= 'A' && c <= 'F') return (byte)((c - 'A') + 10); + + return 0; + } + + private static final boolean isOctalDigit(char c) + { + return c >= '0' && c <= '7'; + } + + private static final boolean isHexDigit(char c) + { + return (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'); + } + + private static String unquotedStringChars = "._$:/"; // chars allowed in unquoted strings + private static String whitespaceChars = " \t\n\r\f"; + + private static final boolean isWhitespace(char c) + { + return whitespaceChars.indexOf(c) >= 0; + } + + private static final boolean isValidUnquotedStringChar(char c) + { + return ((c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + unquotedStringChars.indexOf(c) >= 0); + } + + private String readUnquotedString() + { + int startIndex = currIndex; + + while (!isAtEnd() && isValidUnquotedStringChar(buffer[currIndex])) + currIndex++; + + if (startIndex == currIndex) + throwParseException("No allowable characters found to parse unquoted string"); + + return new String(buffer, startIndex, currIndex - startIndex); + } + + private String readQuotedString() + { + currIndex++; //skip over '"' + + StringBuffer stringBuffer = new StringBuffer(); + int startIndex = currIndex; + + while (!isAtEnd() && buffer[currIndex] != '"') + { + if (buffer[currIndex] != '\\') + { + if (buffer[currIndex] == '\n') + updateLineNumberWithIndex(currIndex+1); + + /* + * Just increment the index -- all these characters will be + * appended in chunks, either before an escape sequence or + * at the end. + */ + currIndex++; + } + else // it's an escape + { + /* Append anything scanned past before the '\\' */ + if (startIndex < currIndex) + stringBuffer.append(buffer, startIndex, currIndex - startIndex); + currIndex++; // skip over '\\' + + if (isAtEnd()) + throwParseException("Input exhausted while parsing escape sequence"); + + switch (buffer[currIndex]) + { + case 't': stringBuffer.append('\t'); currIndex++; break; // tab + case 'n': stringBuffer.append('\n'); currIndex++; break; // newline + case 'r': stringBuffer.append('\r'); currIndex++; break; // carriage return + case 'f': stringBuffer.append('\f'); currIndex++; break; // form feed + case 'b': stringBuffer.append('\b'); currIndex++; break; // backspace + case 'a': stringBuffer.append('\007'); currIndex++; break; // bell + case 'v': stringBuffer.append('\013'); currIndex++; break; // vertical tab + case 'U': + case 'u': + { + /* A Unicode escape. Always followed by 4 hex digits. */ + currIndex++; // skip past the 'U' + if ((currIndex+4) > buffer.length) + throwParseException("Not enough chars to parse \\U sequence"); + + if(!isHexDigit(buffer[currIndex]) || !isHexDigit(buffer[currIndex+1]) || + !isHexDigit(buffer[currIndex+2]) || !isHexDigit(buffer[currIndex+3])) + { + throwParseException("Four hex digits not found for \\U sequence"); + } + + byte byte3 = valueForHexDigit(buffer[currIndex]); + byte byte2 = valueForHexDigit(buffer[currIndex+1]); + byte byte1 = valueForHexDigit(buffer[currIndex+2]); + byte byte0 = valueForHexDigit(buffer[currIndex+3]); + char theChar = (char)((byte3 << 12) + (byte2 << 8) + (byte1 << 4) + byte0); + stringBuffer.append(theChar); + currIndex += 4; + break; + } + case '0': case '1': case '2': case '3': + case '4': case '5': case '6': case '7': + { + /* An octal escape. Expect 1, 2, or 3 octal digits. */ + int digits = 0; + int value = 0; + + do { + value *= 8; + value += (int)(buffer[currIndex] - '0'); + currIndex++; + digits++; + } while (digits <= 3 && !isAtEnd() && isOctalDigit(buffer[currIndex])); + + if (value > 255) + throwParseException("Value too large in octal escape sequence (> 0377)"); + + // This assumes value is in ISO Latin 1 encoding + stringBuffer.append((char)value); + break; + } + /* I guess plists can't have the \x{HEX}{HEX} escapes */ + default: + { + // Unknown escape sequence, just add the character. + // GCC warns if this isn't a '"', '\'', or '\\'... + stringBuffer.append(buffer[currIndex]); + if (buffer[currIndex] == '\n') + updateLineNumberWithIndex(currIndex+1); + currIndex++; + break; + } + } // end case + + /* Reset startIndex, so a verbatim copy will now start from this index */ + startIndex = currIndex; + + } //end '\\' escape + } + + if (isAtEnd()) + throwParseException("Input exhausted while parsing quoted string"); + if (startIndex < currIndex) + stringBuffer.append(buffer, startIndex, currIndex - startIndex); + currIndex++; //skip past '"' + + return stringBuffer.toString(); + } + + private List readList() + { + List newList = new ArrayList(); + + currIndex++; //skip over '(' + skipCommentWhitespace(); + while (!isAtEnd() && buffer[currIndex] != ')') + { + /* A comma is required between list elements */ + if (newList.size() > 0) + { + if (buffer[currIndex] != ',') + throwParseException("List parsing failed: expecting ','"); + currIndex++; + skipCommentWhitespace(); + if (isAtEnd()) + throwParseException("Input exhausted while parsing list"); + } + + if (buffer[currIndex] != ')') + { + Object plistObject = readObject(); + if (plistObject == null) + throwParseException("List parsing failed: could not read contained object."); + newList.add(plistObject); + skipCommentWhitespace(); + } + } + + if (isAtEnd()) + throwParseException("Input exhausted while parsing list"); + currIndex++; //skip past ')' + + return newList; + } + + private Map readMap() + { + HashMap newMap = new HashMap(); + + currIndex++; // skip over open brace + skipCommentWhitespace(); + + while (!isAtEnd() && buffer[currIndex] != '}') + { + Object key; + Object value; + + key = readObject(); + if (key == null || !(key instanceof String)) + throwParseException("Map parsing failed: could not parse key or key is not a String"); + + skipCommentWhitespace(); + if (isAtEnd() || buffer[currIndex] != '=') + throwParseException("Map parsing failed: expecting '='"); + currIndex++; //skip over '=' + skipCommentWhitespace(); + if (isAtEnd()) + throwParseException("Input exhausted while parsing map"); + + value = readObject(); + if (value == null) + throwParseException("Map parsing failed: could not parse value object"); + + skipCommentWhitespace(); + if (isAtEnd() || buffer[currIndex] != ';') + throwParseException("Map parsing failed: expecting ';'"); + currIndex++; //skip over ';' + skipCommentWhitespace(); + + newMap.put(key, value); + } + + if (isAtEnd()) + throwParseException("Input exhausted while parsing map"); + currIndex++; //skip past '}' + + return newMap; + } + + + public static void main(String[] args) + { + String filename = args[0]; + Object plist = PropertyListParser.propertyListFromFile(filename); + System.out.println(plist); + } +} + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/QueueMap.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/QueueMap.java new file mode 100644 index 0000000..6d35b7b --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/QueueMap.java @@ -0,0 +1,538 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import java.util.*; //collections + +/** +* A queue based implementation of the Map interface. This class provides for +* all the opertions of a map, but keeps the entries in the same sequence as +* originally added. The first entry placed in the map will be the first +* entry retreived during iteration: first-in, first-out (FIFO).

+* +* Keys cannot be duplicated. If an entry is made using a key that already +* exists, the value for that key will be replaced with that new value. There +* are no such restrictions on the values. The values may be null.

+* +* Some convenience methods are provided for the queue type operations. The +* first key can be retreived as well as the last key. A key can be used +* to retreive its corresponding value from the map. A value can also be used +* to retreive its key from the map, however, since there may be multiple values +* of the same equality, the first key found will be returned.

+* +* @author rglista@blacksmith.com +* @author mpowers@blacksmith.com +* @date $Date: 2006-02-18 17:41:36 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 899 $ +*/ +public class QueueMap implements Map +{ + List values; + List keys; + Map keyToValue; + +/** +* Creates an empty QueueMap. +*/ + public QueueMap() + { + values = new LinkedList(); + keys = new LinkedList(); + keyToValue = new HashMap(); + } + +/** +* Creates a QueueMap and places the entries from the passed in map into this map. +* The order of the entries is based on however the iterator iteratated through +* the map entries. +* @param t A map object. +*/ + public QueueMap( Map t ) + { + values = new ArrayList(); + keys = new ArrayList(); + keyToValue = new HashMap(); + + putAll( t ); + } + +/** +* Removes all the entries from this map. +*/ + public void clear() + { + values.clear(); + keys.clear(); + keyToValue.clear(); + } + +/** +* Tests to see if the key is contained in the map. If the key is present in +* the map, then TRUE is returned, otherwise FALSE is returned. The equals() +* operation is used to determine equality. +* @return True if the map contains the given key, false otherwise. +*/ + public boolean containsKey( Object key ) + { + return keyToValue.containsKey( key ); + } + +/** +* Tests to see if the value is contained in the map. If the value is present in +* the map, then TRUE is returned, otherwise FALSE is returned. The equals() +* operation is used to determine equality. The value can be null and will +* return TRUE if null is a value in the map. If TRUE is returned, then there +* may be more than one values in the map. +* @return True if the map contains the given value, false otherwise. +*/ + public boolean containsValue( Object value ) + { + return keyToValue.containsValue( value ); + } + +/** +* Returns a set view of the mappings contained in this map. Each element +* in the returned set is a Map.Entry. The returned set is NOT backed +* by the map, so changes to the map are NOT reflected in the set, and vice-versa. +* The returned set is independent of this Map and its underlying structure. +* +* @return a set view of the mappings contained in this map. +*/ + public Set entrySet() + { + Set result = new HashSet(keys.size(), 1F); + + for ( int i = 0; i < keys.size(); i++ ) + { + result.add( new KeyValuePair( keys.get(i), values.get(i) ) ); + } + + return result; + } + +/** +* Compares the specified object with this map for equality. Returns +* true if the given object is also a map and the two Maps +* represent the same mappings. More formally, two maps t1 and +* t2 represent the same mappings if +* t1.entrySet().equals(t2.entrySet()). This ensures that the +* equals method works properly across different implementations +* of the Map interface. +* +* @param o object to be compared for equality with this map. +* @return true if the specified object is equal to this map. +*/ + public boolean equals( Object o ) + { + return keyToValue.equals( o ); + } + +/** +* Returns the corresponding value for the given key. The returned value may be +* null as that is a legal value in this map. However, if the key is not +* contained in this map then null is also returned. Use the containsKey() +* method to distinguish between the two cases. +* @param key A key into the map. +* @return The value corresponding to the key (can be null), or null if the key +* is not contained in the map. +*/ + public Object get( Object key ) + { + return keyToValue.get( key ); + } + +/** +* Returns the hash code value for this map. The hash code of a map +* is defined to be the sum of the hashCodes of each entry in the map's +* entrySet view. This ensures that t1.equals(t2) implies +* that t1.hashCode()==t2.hashCode() for any two maps +* t1 and t2, as required by the general +* contract of Object.hashCode. +* +* @return the hash code value for this map. +*/ + public int hashCode() + { + return keyToValue.hashCode(); + } + +/** +* Returns true is this map contains no key-value mappings. +* @return True is this map contains no entries, false otherwise. +*/ + public boolean isEmpty() + { + return keyToValue.isEmpty(); + } + +/** +* Returns the keys used in the map. There is no order implied in the returned +* set and may be different than the the order in which they were inserted. +* @return A Set containing all the keys used in the map. +*/ + public Set keySet() + { + Set result = new HashSet(keys.size(), 1F); + + for ( int i = 0; i < keys.size(); i++ ) + { + result.add( keys.get(i) ); + } + + return result; + } + +/** +* Places the key/value entry into the map at the end position. If the key +* already exists in the map, then its value is replaced by the new given +* value. The mapping does not change order in this case. The specified +* key cannot be null. +* @param key The key to place into the map, cannot be null. +* @param value The value to associate with the key, may be null. +* @return Null if the key is new, the replaced value if the key already +* existed. (Note: If the key was null, then null is returned.) +*/ + public Object put( Object key, Object value ) + { + if ( key == null ) return null; + + if ( keys.contains( key ) ) + { + values.remove( keys.indexOf( key ) ); + values.add( keys.indexOf( key ), value ); + } + else + { + values.add( value ); + keys.add( key ); + } + + return keyToValue.put( key, value ); + } + +/** +* Places all the entries from this given map into this map. If the keys +* already exist, then there values are replaced. +* @param t A map of key/value entries. +*/ + public void putAll( Map t ) + { + if ( t == null ) + { + // Nothing to do! + return; + } + + // Place the entries from the passed in map into this map. + for ( Iterator it = t.keySet().iterator(); it.hasNext(); ) + { + Object aKey = it.next(); + put( aKey, t.get( aKey ) ); + } + } + +/** +* Remove the mapping of the key and associated value from this map. +* Note: null is a valid value in the map. +* @param key A key to remove from the map, cannot be null. +* @return The value of the removed mapping, null if the mapping did not exist. +* Null could also be returned if the associated value of the key was null. +*/ + public Object remove( Object key ) + { + if ( key == null ) return null; + + Object value = null; + + if ( containsKey( key ) ) + { + value = keyToValue.remove( key ); + int i = values.indexOf( value ); + if ( i != -1 ) + { + keys.remove( i ); + values.remove( i ); + } + } + + return value; + } + +/** +* Returns the number of key/value pairs in this map. +* @return The number of pairs. +*/ + public int size() + { + return values.size(); + } + +/** +* Returns an ordered list of keys from the map. The order is the same +* as the added order of the mapped items.
+* NOTE: The returned list is the underlying keys list used by this class. +* If modification are to be made to this list, it should be cloned first. +* @return An ordered list of keys. +*/ + public List keys() + { + return keys; + } + +/** +* Returns an ordered list of values from the map. The order is the same +* as the added order of the mapped items. +* NOTE: The returned list is the underlying keys list used by this class. +* If modification are to be made to this list, it should be cloned first. +* @return An ordered list of values. +*/ + public Collection values() + { + return values; + } + +/** +* Returns the corresponding value for the given key. The returned value may be +* null as that is a legal value in this map. However, if the key is not +* contained in this map then null is also returned. Use the containsKey() +* method to distinguish between the two cases. +* @param key A key into the map. +* @return The value corresponding to the key (can be null), or null if the key +* is not contained in the map. +*/ + public Object getValueForKey( Object key ) + { + return keyToValue.get( key ); + } + +/** +* Returns the associated key for this value. Since there may be more than one +* of the same value in the map, this returns the first key associated with this +* value. Null is returned if the value is not in the map. +* @param value A value that is contained in this map. +* @return A first key that corresponds to this value. +*/ + public Object getKeyForValue( Object value ) + { + int i = values.indexOf( value ); + if ( i != -1 ) + { + return keys.get( i ); + } + return null; + } + +/** +* Returns the first key in the map. If the map is empty, then null is +* returned. +* @return The first key in the map, null if there are no mappings. +*/ + public Object getFirstKey() + { + if ( keys.size() < 1 ) + { + return null; + } + return keys.get(0); + } + +/** +* Returns the last key in the map. If the map is empty, then null is +* returned. +* @return The last key in the map, null if there are no mappings. +*/ + public Object getLastKey() + { + if ( keys.size() < 1 ) + { + return null; + } + return keys.get( keys.size() -1 ); + } + +/** +* This class contains a key/value pair. The key must be a valid object, +* it cannot be null. The value can be a valid object or null. +*/ + static public class KeyValuePair implements Map.Entry + { + Object key; + Object value; + + /** + * Default constructor. The constructor takes the key and value as parameters. + * Since the key cannot be null, it must be specified during initialization. + * The value can be null. + * @param key The key object of this pair. The key cannot be null. + * @param value The value object of this pair. The value can be null. + */ + public KeyValuePair( Object aKey, Object aValue ) + { + key = aKey; + value = aValue; + } + + /** + * Returns the key object of this pair. + * @return The key object. + */ + public Object getKey() + { + return key; + } + + /** + * Returns the the value object of this pair. May be null. + * @return The value object. + */ + public Object getValue() + { + return value; + } + + /** + * Sets the value object of this pair. May be an object or null. + * @param aValue The value object to place into this pair. + */ + public Object setValue( Object aValue ) + { + Object result = value; + value = aValue; + return result; + } + + public boolean equals( Object o ) + { + if ( o instanceof KeyValuePair ) + { + KeyValuePair p = (KeyValuePair) o; + if ( ( key.equals( p.getKey() ) ) && ( value.equals( p.getValue() ) ) ) + { + return true; + } + } + return false; + } + } + + /** + * Returns a string reprsentation of this class. The contents of the + * map are placed in the string in its proper order. + */ + public String toString() + { + int max = size() - 1; + StringBuffer buf = new StringBuffer(); + + buf.append("{"); + for (int j = 0; j <= max; j++) + { + buf.append(keys.get(j) + "=" + values.get(j)); + if (j < max) + { + buf.append(", "); + } + } + buf.append("}"); + + return buf.toString(); + } + + // unit test + public static void main( String[] argv ) + { + QueueMap qMap; + + qMap = new QueueMap(); + for (int i = 0; i < 5; i++) + { + qMap.put(Integer.toString(i), Integer.toString(i)); + } + System.out.println("\nMap = " + qMap); + System.out.println("Keys = " + qMap.keys()); + System.out.println("Values = " + qMap.values()); + + qMap = new QueueMap(); + for (int i = 0; i < 5; i++) + { + qMap.put(Integer.toString(i), "A"); + } + System.out.println("\nMap = " + qMap); + System.out.println("Keys = " + qMap.keys()); + System.out.println("Values = " + qMap.values()); + + qMap = new QueueMap(); + for (int i = 0; i < 5; i++) + { + qMap.put(Integer.toString(i), null); + } + System.out.println("\nMap = " + qMap); + System.out.println("Keys = " + qMap.keys()); + System.out.println("Values = " + qMap.values()); + + Map aMap = new HashMap(); + for (int i = 0; i < 5; i++) + { + aMap.put(Integer.toString(i), Integer.toString(i)); + } + qMap = new QueueMap( aMap ); + System.out.println("\nHashMap = " + aMap); + System.out.println("Map = " + qMap); + System.out.println("Keys = " + qMap.keys()); + System.out.println("Values = " + qMap.values()); + + qMap = new QueueMap(); + qMap.put( "Test1", "String1" ); + qMap.put( "Test2", "String2" ); + qMap.put( "Test3", "String3" ); + qMap.put( "Test4", "String4" ); + qMap.put( "Test5", "String5" ); + System.out.println("\nStandard Test, Map = " + qMap); + qMap.put( "Test6", "String6" ); + qMap.put( "Test7", "String7" ); + System.out.println("Put New Test, Map = " + qMap); + qMap.put( "Test2", "New String2" ); + qMap.put( "Test6", "New String6" ); + System.out.println("Put Existing Test, Map = " + qMap); + qMap.put( "Test5", null ); + qMap.put( "Test8", null ); + System.out.println("Put Null Test, Map = " + qMap); + qMap.remove( "Test1" ); + qMap.remove( "Test3" ); + qMap.remove( "Test5" ); + qMap.remove( "Test9" ); + System.out.println("Remove Test, Map = " + qMap); + System.out.println(" Keys = " + qMap.keys()); + System.out.println(" Values = " + qMap.values()); + qMap.clear(); + qMap.put( "Test10", "String10" ); + qMap.put( "Test11", "String11" ); + qMap.put( "Test12", "String12" ); + System.out.println("Clear Test, Map = " + qMap); + + aMap = new HashMap(); + aMap.put( "Test10", "String10" ); + aMap.put( "Test11", "String11" ); + aMap.put( "Test12", "String12" ); + System.out.println("Equality Test, Equal = " + qMap.equals( aMap )); + } + +} + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/URLResourceReader.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/URLResourceReader.java new file mode 100644 index 0000000..9133a8d --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/URLResourceReader.java @@ -0,0 +1,207 @@ +/* + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + */ + +package net.wotonomy.foundation.internal; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * This implementation of URL Resource Reader assumes 2 types + * of base urls. A base url that ends with / is considered a + * resource folder, whereas a resource that does not end with + * / is considered a zip/jar resource folder. + * + * If the resource folder happens is a zip/jar archive, the + * entries are always cached. + * For non-zip base urls, one could specify whether or not it should + * be cached. + * + * @author Harish Prabandham + */ +public class URLResourceReader { + private Hashtable resourceCache = new Hashtable(); + private boolean iszip = true; + private URL url = null; + private boolean cache = true; + + /** + * Creates a new URLResourceReader object. You can either give + * the URL of the zip/jar file or a base url where to + * look for additional resources. If the url ends with + * "/" then it is assumed to be a Base URL. + * @param The base url to look for the resources. + * @param If the base url is not a zip/jar, then true indicates + * that entries should be cached, false otherwise. + */ + public URLResourceReader(URL baseurl, boolean cache) throws IOException { + this.url = baseurl; + this.cache = cache; + this.iszip = !url.getFile().endsWith("/"); + if(this.iszip) + this.cache = true; + initialize(); + } + + /** + * equivalent to URLResourceReader(baseurl, false) + */ + public URLResourceReader(URL baseurl) throws IOException { + this(baseurl, false); + } + + /** + * Creates a new URLResourceReader object with the given + * input stream. The stream is assumed to be a zip/jar + * stream. + */ + public URLResourceReader(InputStream is) throws IOException { + init(is); + } + + private void initialize() throws IOException { + if(iszip) { + InputStream is = url.openStream(); + init(is); + is.close(); + } + } + + private byte[] readFully(InputStream is) throws IOException { + byte[] buf = new byte[1024]; + int num = 0; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + + while( (num = is.read(buf)) != -1) { + bout.write(buf, 0, num); + } + + return bout.toByteArray(); + } + + private void init(InputStream is) throws IOException { + ZipInputStream zstream = new ZipInputStream(is); + ZipEntry entry; + + while( (entry = zstream.getNextEntry()) != null) { + byte[] entryData = readFully(zstream); + if(cache) + resourceCache.put(entry.getName(), entryData); + zstream.closeEntry(); + } + + zstream.close(); + } + + /** + * Returns an Enumeration of all "known" resource names. + */ + public Enumeration getResourceNames() { + return resourceCache.keys(); + } + + /** + * Returns an array of bytes read for this resource if the + * resource exists. This method blocks until the resource + * has been fully read. If the resource does not exist, + * this method returns null. + */ + public byte[] getResource(String resource) { + // lookup the data in the cache... + byte[] data = (byte[]) resourceCache.get(resource); + if(data != null) { + return data; + } + + // if the data was to come from a zip file that we + // already read fully & cached , then it is probably + // not there. + if(iszip) { + return null; + } + + // Now the only choice left is to make a url connection. + try { + URL realURL = new URL(url.getProtocol(), url.getHost(), + url.getFile() + resource); + data = readFully(realURL.openStream()); + // add it to cache if needed... + if(cache) + resourceCache.put(resource, data); + return data; + } catch(Exception e) { + return null; + } + } + + public void close() { + resourceCache.clear(); + resourceCache = null; + } + + public String toString() { + return url.toString(); + } +} + + + + + + + + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ValueConverter.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ValueConverter.java new file mode 100644 index 0000000..d6bc797 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/ValueConverter.java @@ -0,0 +1,718 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +/** +* A utility class to convert objects to a desired class. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class ValueConverter +{ + /** + * Returns the specified object as converted to an instance of the + * specified class, or null if the conversion could not be performed. + */ + static public Object convertObjectToClass( Object anObject, Class aClass ) + { + if ( aClass == String.class ) + { + return getString( anObject ); + } + if ( aClass == Short.class ) + { + return getShort( anObject ); + } + if ( aClass == short.class ) + { + return getShort( anObject ); + } + if ( aClass == Integer.class ) + { + return getInteger( anObject ); + } + if ( aClass == int.class ) + { + return getInteger( anObject ); + } + if ( aClass == Long.class ) + { + return getLong( anObject ); + } + if ( aClass == long.class ) + { + return getLong( anObject ); + } + if ( aClass == Float.class ) + { + return getFloat( anObject ); + } + if ( aClass == float.class ) + { + return getFloat( anObject ); + } + if ( aClass == Double.class ) + { + return getDouble( anObject ); + } + if ( aClass == double.class ) + { + return getDouble( anObject ); + } + if ( aClass == java.util.Date.class ) + { + return getDate( anObject ); + } + if ( aClass == Boolean.class ) + { + return getBoolean( anObject ); + } + if ( aClass == boolean.class ) + { + return getBoolean( anObject ); + } + if ( aClass == Character.class ) + { + return getCharacter( anObject ); + } + if ( aClass == char.class ) + { + return getCharacter( anObject ); + } + if ( aClass == Byte.class ) + { + return getByte( anObject ); + } + if ( aClass == byte.class ) + { + return getByte( anObject ); + } + if ( Collection.class.isAssignableFrom( aClass ) ) + { + return getCollection( anObject, aClass ); + } + if ( aClass.isArray() ) + { + return getArray( anObject, aClass ); + } + + return convert( anObject, aClass ); + } + + /** + * Called by convertObjectToClass() when we need to + * convert to an unrecognized type. + * This implementation scans the constructors of the + * specified class for the best fit to the object. + * and returns a new instance with that constructor. + * Subclasses can override to directly support specific + * types. + */ + static protected Object convert( Object anObject, Class aClass ) + { + Constructor[] ctors = aClass.getConstructors(); + + Class[] types; + for ( int i = 0; i < ctors.length; i++ ) + { + types = ctors[i].getParameterTypes(); + if ( types.length == 1 ) + { + if ( types[0].equals( anObject.getClass() ) ) + { + try + { + return ctors[i].newInstance( new Object[] { anObject } ); + } + catch ( Exception exc ) + { + // fall through + } + } + } + } + + for ( int i = 0; i < ctors.length; i++ ) + { + types = ctors[i].getParameterTypes(); + if ( types.length == 1 ) + { + if ( anObject.getClass().isAssignableFrom( types[0] ) ) + { + try + { + return ctors[i].newInstance( new Object[] { anObject } ); + } + catch ( Exception exc ) + { + // fall through + } + } + } + } + + return null; + } + + /** + * Tries to convert all objects to either Numbers or objects + * that will produce a parsable toString result. + */ + static protected Object preprocess( Object anObject ) + { + if ( anObject instanceof Boolean ) + { + if ( ((Boolean)anObject).booleanValue() ) + { + return new Double( 1.0 ); + } + return new Double( 0.0 ); + } + + if ( anObject instanceof Character ) + { + return anObject.toString(); + } + + return anObject; + } + + static public short getShortValue( Object anObject ) + { + Short result = getShort( anObject ); + return ( result == null ) ? (short) 0 : result.shortValue(); + } + static public Short getShort( Object anObject ) + { + if ( anObject == null ) return new Short( (short) 0 ); + if ( "".equals( anObject ) ) return new Short( (short) 0 ); + if ( anObject instanceof Short ) return (Short) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Short( ((Number)anObject).shortValue() ); + } + + try + { + return Short.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public int getIntValue( Object anObject ) + { + Integer result = getInteger( anObject ); + return ( result == null ) ? 0 : result.intValue(); + } + static public Integer getInteger( Object anObject ) + { + if ( anObject == null ) return new Integer( 0 ); + if ( "".equals( anObject ) ) return new Integer( 0 ); + if ( anObject instanceof Integer ) return (Integer) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Integer( ((Number)anObject).intValue() ); + } + + try + { + return Integer.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public long getLongValue ( Object anObject ) + { + Long result = getLong( anObject ); + return ( result == null ) ? (long) 0 : result.longValue(); + } + static public Long getLong( Object anObject ) + { + if ( anObject == null ) return new Long( 0 ); + if ( "".equals( anObject ) ) return new Long( 0 ); + if ( anObject instanceof Long ) return (Long) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Long( ((Number)anObject).longValue() ); + } + + try + { + return Long.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public double getDoubleValue ( Object anObject ) + { + Double result = getDouble( anObject ); + return ( result == null ) ? 0.0f : result.doubleValue(); + } + static public Double getDouble( Object anObject ) + { + if ( anObject == null ) return new Double( 0.0 ); + if ( "".equals( anObject ) ) return new Double( 0 ); + if ( anObject instanceof Double ) return (Double) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Double( ((Number)anObject).doubleValue() ); + } + + try + { + return Double.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public float getFloatValue( Object anObject ) + { + Float result = getFloat( anObject ); + return ( result == null ) ? 0.0f : result.floatValue(); + } + static public Float getFloat( Object anObject ) + { + if ( anObject == null ) return new Float( 0.0 ); + if ( "".equals( anObject ) ) return new Float( 0.0 ); + if ( anObject instanceof Float ) return (Float) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Float( ((Number)anObject).floatValue() ); + } + + try + { + return Float.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public char getCharValue( Object anObject ) + { + Character result = getCharacter( anObject ); + return ( result == null ) ? (char) 0 : result.charValue(); + } + static public Character getCharacter( Object anObject ) + { + if ( anObject == null ) return new Character( (char) 0 ); + if ( anObject instanceof Character ) return (Character) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Character( (char) ((Number)anObject).byteValue() ); + } + + try + { + return new Character( anObject.toString().charAt( 0 ) ); + } + catch ( Exception exc ) + { + return null; + } + } + + static public byte getByteValue( Object anObject ) + { + Byte result = getByte ( anObject ); + return ( result == null ) ? (byte) 0 : result.byteValue(); + } + static public Byte getByte( Object anObject ) + { + if ( anObject == null ) return new Byte( Byte.MIN_VALUE ); + if ( "".equals( anObject ) ) return new Byte( Byte.MIN_VALUE ); + if ( anObject instanceof Byte ) return (Byte) anObject; + + anObject = preprocess( anObject ); + + if ( anObject instanceof Number ) + { + return new Byte( ((Number)anObject).byteValue() ); + } + + try + { + return Byte.decode( anObject.toString() ); + } + catch ( Exception exc ) + { + // fall through + } + + try + { + return Byte.valueOf( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + /** + * Calls getBoolean and converts result to primitive. + */ + static public boolean getBooleanValue( Object anObject ) + { + Boolean result = getBoolean( anObject ); + return ( result == null ) ? false : result.booleanValue(); + } + + /** + * Numbers equal to zero are true; Strings equal to "yes" are true; + * Strings are then passed to the Boolean constructor. + * Other values return null. + */ + static public Boolean getBoolean( Object anObject ) + { + if ( anObject instanceof Boolean ) + { + return (Boolean) anObject; + } + if ( anObject instanceof Number ) + { + return new Boolean( ((Number)anObject).doubleValue() == 0.0 ); + } + + if ( anObject instanceof String ) + { + if ( anObject.toString().toLowerCase().equals( "yes" ) ) + { + return Boolean.TRUE; + } + return new Boolean( (String) anObject ); + } + + return null; + } + + /** + * Get an appropriate String representation for the + * object. Nulls are converted to "null". Date are + * formatted according to the current date format. + * All else uses toString. + */ + static public String getString( Object anObject ) + { + if ( anObject == null ) return "null"; + if ( anObject instanceof java.util.Date ) + { + return dateFormat.format( (java.util.Date) anObject ); + } + return anObject.toString(); + } + + /** + * Converts the object into the specified collection class. + * If unable to convert in any other way, resorts to creating + * a new collection of the specified type containing the + * specified object. + */ + static public Collection getCollection( Object anObject, Class aCollectionClass ) + { + if ( anObject == null ) return null; + if ( aCollectionClass.isAssignableFrom( anObject.getClass() ) ) + { + return (Collection) anObject; + } + + Collection converted = null; + + // convert to collection class + if ( anObject instanceof Collection ) + { + converted = (Collection) anObject; + } + else + // try to convert an array + if ( anObject.getClass().isArray() ) + { + try + { + int length = Array.getLength( anObject ); + converted = new LinkedList(); + for ( int i = 0; i < length; i++ ) + { + converted.add( Array.get( anObject, i ) ); + } + } + catch ( Exception exc ) + { + // try another approach + } + } + else + // convert map values to collection and pass through + if ( anObject instanceof Map ) + { + converted = ((Map)anObject).values(); + } + + // fall back on list containing the object + if ( converted == null ) + { + converted = new LinkedList(); + converted.add( anObject ); + } + + Collection result = null; + + if ( converted != null ) + { + try + { + // collections required to have the copy constructor. + Constructor ctor = aCollectionClass.getConstructor( + new Class[] { Collection.class } ); + result = (Collection) ctor.newInstance( new Object[] { converted } ); + } + catch ( Exception exc ) + { + try + { + result = new LinkedList(); + result.addAll( converted ); + } + catch ( Exception exc2 ) + { + // all attempts failed + result = null; + } + } + } + + return result; + } + + /** + * Convert the object to the specified array type. + */ + static public Object getArray( Object anObject, Class anArrayClass ) + { + if ( anObject == null ) return null; + + // try to convert an array + if ( anObject.getClass().isArray() ) + { + try + { + int length = Array.getLength( anObject ); + Object result = Array.newInstance( + anArrayClass.getComponentType(), length ); + for ( int i = 0; i < length; i++ ) + { + Array.set( result, i, Array.get( anObject, i ) ); + } + return result; + } + catch ( Exception exc ) + { + // try another approach + } + } + // convert map values to collection and pass through + if ( anObject instanceof Map ) + { + anObject = ((Map)anObject).values(); + } + // try to convert a collection + if ( anObject instanceof Collection ) + { + try + { + int length = ((Collection)anObject).size(); + Object result = Array.newInstance( + anArrayClass.getComponentType(), length ); + Iterator it = ((Collection)anObject).iterator(); + for ( int i = 0; i < length; i++ ) + { + Array.set( result, i, it.next() ); + } + return result; + } + catch ( Exception exc ) + { + // try another approach + } + } + // if appropriate type, put the object in a single element array + if ( anObject.getClass().equals( anArrayClass.getComponentType() ) ) + { + try + { + Object result = Array.newInstance( + anArrayClass.getComponentType(), 1 ); + Array.set( result, 0, anObject ); + return result; + } + catch ( Exception exc ) + { + // try another approach + } + } + return null; + } + + /** + * Get an appropriate Date from the given object. + */ + static public java.util.Date getDate( Object anObject ) + { + if ( anObject == null ) return new java.util.Date( 0 ); + if ( anObject instanceof java.util.Date ) + return (java.util.Date) anObject; + + if ( anObject instanceof Number ) + { + return new java.util.Date( getLongValue( anObject ) ); + } + + try + { + return dateFormat.parse( anObject.toString() ); + } + catch ( Exception exc ) + { + return null; + } + } + + static private java.text.DateFormat dateFormat = + new java.text.SimpleDateFormat(); + static public java.text.DateFormat getDateFormat() + { + return dateFormat; + } + static public void setDateFormat( java.text.DateFormat aDateFormat ) + { + if ( aDateFormat != null ) + { + dateFormat = aDateFormat; + } + } + + /** + * Returns the "inverted" value of the specified object. + * Numbers except for chars and bytes are converted to + * their negative representation. Chars and bytes are + * treated as booleans. String are converted to booleans. + * Booleans are converted to their opposite. + * All other types return null. + */ + public static Object invert( Object anObject ) + { + if ( anObject == null ) return null; + Class aClass = anObject.getClass(); + + if ( ( ( anObject instanceof Number ) + &&! ( anObject instanceof Byte ) + &&! ( anObject instanceof Character ) ) + || ( aClass == short.class ) + || ( aClass == int.class ) + || ( aClass == long.class ) + || ( aClass == float.class ) + || ( aClass == double.class ) ) + { + return convertObjectToClass( + new Double( getDoubleValue( anObject ) * -1 ), aClass ); + } + + Boolean converted = getBoolean( anObject ); + if ( converted != null ) + { + if ( converted.booleanValue() ) + { + return convertObjectToClass( + Boolean.FALSE, anObject.getClass() ); + } + else + { + return convertObjectToClass( + Boolean.TRUE, anObject.getClass() ); + } + } + + return null; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 13:11:47 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2002/10/11 15:33:53 mpowers + * Now supporting "!" to invert the value of a string property. + * + * Revision 1.3 2001/07/02 16:29:08 mpowers + * XMLRPC decoder was relying on ValueConverter to convert LinkedLists into + * the appropriate type. This is now implemented in ValueConverter. + * + * Revision 1.2 2001/03/01 20:36:35 mpowers + * Better error handling and better handling of nulls. + * + * Revision 1.1.1.1 2000/12/21 15:52:33 mpowers + * Contributing wotonomy. + * + * Revision 1.8 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/WotonomyException.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/WotonomyException.java new file mode 100644 index 0000000..e2210d0 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/internal/WotonomyException.java @@ -0,0 +1,134 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.internal; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** +* This is a simple RuntimeException that can encapsulate +* another throwable. Behaves as a normal RuntimeException +* except that it prints a stack trace of the wrapped +* throwable if one is specified. +* +* Intended to be used anytime you'd +* report an exception with System.out.println: that is, +* for debugging and non-user-visible exceptions. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ + +public class WotonomyException extends RuntimeException +{ + protected String message; + protected Throwable wrappedThrowable; + + /** + * Default constructor. + */ + public WotonomyException() + { + super(); + message = null; + wrappedThrowable = null; + } + + /** + * Standard constructor with message. + */ + public WotonomyException( String aMessage ) + { + super( aMessage ); + message = aMessage; + wrappedThrowable = null; + } + + /** + * Specifies a throwable to wrap. + */ + public WotonomyException( Throwable aThrowable ) + { + super(); + message = null; + wrappedThrowable = aThrowable; + } + + /** + * Specifies a message and a throwable to wrap. + */ + public WotonomyException( String aMessage, Throwable aThrowable ) + { + super( aMessage ); + message = aMessage; + wrappedThrowable = aThrowable; + } + + /** + * Returns the wrapped throwable. + */ + public Throwable getWrappedThrowable() + { + return wrappedThrowable; + } + + public void printStackTrace(PrintWriter s) + { + if ( message != null ) + { + s.println( "Exception: " + message ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace( s ); + return; + } + super.printStackTrace( s ); + } + + public void printStackTrace(PrintStream s) + { + if ( message != null ) + { + s.println( "Exception: " + message ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace( s ); + return; + } + super.printStackTrace( s ); + } + + public void printStackTrace() + { + if ( message != null ) + { + System.err.println( "Exception: " + message ); + } + if ( wrappedThrowable != null ) + { + wrappedThrowable.printStackTrace(); + return; + } + super.printStackTrace(); + } + +} diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/package.html b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/package.html new file mode 100644 index 0000000..c52ca95 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/package.html @@ -0,0 +1,18 @@ + +

+A set of collections classes that are needed +by the ui and web packages. These correspond +to the foundation classes in the OpenStep +specification. +

+

+These classes for the most part extend their +java counterparts, adding methods to be API +compatible with the OpenStep specification. +In this way, they retain compatibility with +the Java Collections APIs. +

+

+This package has no external dependencies. +

+ diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLDecoder.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLDecoder.java new file mode 100644 index 0000000..64ac4b2 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLDecoder.java @@ -0,0 +1,68 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.xml; + +import java.io.InputStream; +import java.net.URL; + +/** +* Defines an interface for classes that can de-serialize +* java objects from an XML representation. +*/ +public interface XMLDecoder +{ + /** + * Decodes an object from the specified input stream. + * @param anInputStream The input stream from which to read. + * The stream will be read fully. + * @param aDescription A description to accompany error messages + * for the stream, typically a file name. + * @param aURL A URL against which relative references within the + * XML will be resolved. + * @return The object that was constructed from the XML content, + * or null if no object could be constructed. + */ + public Object decode( + InputStream anInputStream, String aDescription, URL aURL ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/18 22:21:10 cgruber + * Add in simple xml interfaces from net.wotonomy.xml project. + * Add cobertura.ser to .cvsignore. God I wish sourceforge would move on this subversion thing... + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:33 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLEncoder.java b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLEncoder.java new file mode 100644 index 0000000..e543bca --- /dev/null +++ b/projects/net.wotonomy.foundation/src/main/java/net/wotonomy/foundation/xml/XMLEncoder.java @@ -0,0 +1,61 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.foundation.xml; + +import java.io.OutputStream; + +/** +* Defines an interface for classes that can serialize +* an arbitrary java object into XML format. +*/ +public interface XMLEncoder +{ + /** + * Encodes an object to the specified output stream as XML. + * @param anObject The object to be serialized to XML format. + * @param anOutputStream The output stream to which the object + * will be written. The stream will not be flushed nor closed. + */ + public void encode( Object anObject, OutputStream anOutputStream ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/18 22:21:10 cgruber + * Add in simple xml interfaces from net.wotonomy.xml project. + * Add cobertura.ser to .cvsignore. God I wish sourceforge would move on this subversion thing... + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:33 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.foundation/src/test/java/AllTests.java b/projects/net.wotonomy.foundation/src/test/java/AllTests.java new file mode 100644 index 0000000..1b36871 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/test/java/AllTests.java @@ -0,0 +1,17 @@ +import junit.framework.Test; +import junit.framework.TestSuite; + +public class AllTests { + + public static void main(String[] args) { + } + + public static Test suite() { + TestSuite suite = new TestSuite("Test for default package"); + //$JUnit-BEGIN$ + suite.addTestSuite(net.wotonomy.foundation.AllTests.class); + //$JUnit-END$ + return suite; + } + +} diff --git a/projects/net.wotonomy.foundation/src/test/java/TestBundle.java b/projects/net.wotonomy.foundation/src/test/java/TestBundle.java new file mode 100644 index 0000000..7fb5020 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/test/java/TestBundle.java @@ -0,0 +1,21 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +public class TestBundle { + public String toString() { return "This is a test"; } + + public static void main(String[] argv) { + Properties p = System.getProperties(); + List keyList = new ArrayList(p.keySet()); + Collections.sort(keyList); + Iterator keys = keyList.iterator(); + while (keys.hasNext()) { + String key = (String)keys.next(); + System.out.println(key + "=" + p.getProperty(key)); + } + + } +} diff --git a/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/AllTests.java b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/AllTests.java new file mode 100644 index 0000000..920b0a7 --- /dev/null +++ b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/AllTests.java @@ -0,0 +1,17 @@ +package net.wotonomy.foundation; + +import junit.framework.Test; +import junit.framework.TestSuite; + +public class AllTests { + + public static Test suite() { + TestSuite suite = new TestSuite("Test for net.wotonomy.foundation"); + //$JUnit-BEGIN$ + suite.addTestSuite(NSArrayTest.class); + suite.addTestSuite(NSBundleTest.class); + //$JUnit-END$ + return suite; + } + +} diff --git a/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSArrayTest.java b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSArrayTest.java new file mode 100644 index 0000000..8873f1a --- /dev/null +++ b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSArrayTest.java @@ -0,0 +1,638 @@ +package net.wotonomy.foundation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import junit.framework.TestCase; + +public class NSArrayTest extends TestCase { + + + Object o1, o2, o3, o4, o5; + + public void setUp() throws Exception { + o1 = "o1"; + o2 = new Integer(2); + o3 = null; + o4 = "o4"; + super.setUp(); + } + + public void tearDown() throws Exception { + o1 = null; + o2 = null; + o3 = null; + o4 = null; + super.tearDown(); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.hashCode()' + */ + public void testHashCode() { + NSArray array1 = new NSArray(o1); + NSArray array2 = new NSArray(o1); //same content, same hashcode. + NSArray array3 = new NSArray("Different"); + assertNotSame(array1,array2); + assertEquals(array1.hashCode(),array2.hashCode()); + assertFalse("Should have different hashcodes",array1.hashCode() == array3.hashCode()); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.arrayBackedByList(List)' + */ + public void testArrayBackedByList() { + List list = new ArrayList(); + NSArray array = NSArray.arrayBackedByList(list); + assertNotNull(array); + assertSame(list,array.list); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(List, Object)' + */ + public void testEmptyList() { + assertNotNull(NSArray.EmptyArray); + assertEquals(0,NSArray.EmptyArray.count()); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(List, Object)' + */ + public void testNSArrayListObject() { + List list = new ArrayList(); + NSArray array = new NSArray(list, null); + assertNotNull(array); + assertSame(list,array.list); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(int)' + */ + public void testNSArrayInt() { + NSArray array = new NSArray(5); + assertNotNull(array); + array = new NSArray(0); + assertNotNull(array); + try { + array = new NSArray(-1); + fail("Failed to catch IllegalArgumentException."); + } catch (IllegalArgumentException e) {} + assertNotNull(array); + array = new NSArray(1000); + assertNotNull(array); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray()' + */ + public void testNSArray() { + NSArray array = new NSArray(); + assertNotNull(array); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(Object)' + */ + public void testNSArrayObject() { + NSArray array = new NSArray(o1); + assertNotNull(array); + assertEquals(1,array.count()); + assertEquals(o1,array.get(0)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(Object)' + */ + public void testNSArrayObjectWithNull() { + NSArray array = new NSArray((Object)null); + assertNotNull(array); + assertEquals(1,array.count()); + assertEquals(null,array.get(0)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(Object[])' + */ + public void testNSArrayObjectArray() { + Object[] objects = { o1, o2, o3 }; + NSArray array = new NSArray(objects); + assertEquals(3,array.count()); + for (int i = 0; i < objects.length ; i++) { + assertEquals(objects[i],array.get(i)); + } + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.NSArray(Collection)' + */ + public void testNSArrayCollection() { + ArrayList list = new ArrayList(); + list.add(o1); + list.add(o2); + list.add(o3); + NSArray array = new NSArray(list); + assertNotNull(array); + assertNotSame(list,array.list); + assertEquals(3,array.count()); + // the following only works because we used ArrayList. + // A collection that doesn't guarrantee order may not + // pass this test. + for (int i = 0; i < list.size() ; i++) { + assertEquals(list.get(i),array.get(i)); + } + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.arrayByAddingObject(Object)' + */ + public void testArrayByAddingObject() { + NSArray array = new NSArray(o1); + NSArray array2 = array.arrayByAddingObject(o2); + assertEquals(2,array2.count()); + assertEquals(o1,array2.get(0)); + assertEquals(o2,array2.get(1)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.arrayByAddingObjectsFromArray(Collection)' + */ + public void testArrayByAddingObjectsFromArray() { + NSArray array = new NSArray(o1); + List list = new ArrayList(); + list.add(o2); + list.add(o3); + NSArray array2 = array.arrayByAddingObjectsFromArray(list); + assertEquals(3,array2.count()); + assertEquals(o1,array2.get(0)); + assertEquals(o2,array2.get(1)); + assertEquals(o3,array2.get(2)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.componentsJoinedByString(String)' + */ + public void testComponentsJoinedByString() { + Object[] objects = { o1, o2, o3 }; + NSArray array = new NSArray(objects); + assertEquals("o1, 2, null", array.componentsJoinedByString(", ")); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.containsObject(Object)' + */ + public void testContainsObject() { + NSArray array = new NSArray(o1); + assertTrue(array.contains(o1)); + assertFalse(array.contains(o2)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.firstObjectCommonWithArray(Collection)' + */ + public void testFirstObjectCommonWithArray() { + ArrayList list = new ArrayList(); + list.add(o2); + list.add(o3); + list.add(o4); + NSArray array = new NSArray(new Object[]{o1, o3, o4}); + assertEquals(o3,array.firstObjectCommonWithArray(list)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.isEqualToArray(List)' + */ + public void testEqualsAndIsEqualToArray() { + NSArray array1 = new NSArray(o1); + NSArray array2 = new NSArray(o1); //same content, same hashcode. + NSArray array3 = new NSArray(o2); + assertNotSame(array1,array2); + assertTrue("Should be equal",array1.equals(array2)); + assertTrue("Should be equal",array1.isEqualToArray(array2)); + assertFalse("Should be unequal",array1.equals(array3)); + assertFalse("Should be unequal",array1.isEqualToArray(array3)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.lastObject()' + */ + public void testLastObject() { + NSArray array = new NSArray(new Object[]{o1, o2}); + assertEquals(o2,array.lastObject()); + // test for empty array + assertEquals(null,NSArray.EmptyArray.lastObject()); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.subarrayWithRange(NSRange)' + * TODO: Add ranges that exceed the size of the array. + */ + public void testSubarrayWithRange() { + NSArray array = new NSArray(new Object[]{o1, o2, o3, o4}); + NSArray subarray = array.subarrayWithRange(new NSRange(1,2)); + assertEquals(2,subarray.count()); + assertEquals(o2,subarray.get(0)); + assertEquals(o3,subarray.get(1)); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.objectEnumerator()' + */ + public void testObjectEnumerator() { + NSArray array = new NSArray(new Object[]{o1, o2, o3, o4}); + Enumeration e = array.objectEnumerator(); + assertTrue(e.hasMoreElements()); + assertEquals(o1,e.nextElement()); + assertEquals(o2,e.nextElement()); + assertEquals(o3,e.nextElement()); + assertEquals(o4,e.nextElement()); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.reverseObjectEnumerator()' + */ + public void testReverseObjectEnumerator() { + NSArray array = new NSArray(new Object[]{o1, o2, o3, o4}); + Enumeration e = array.reverseObjectEnumerator(); + assertTrue(e.hasMoreElements()); + assertEquals(o4,e.nextElement()); + assertEquals(o3,e.nextElement()); + assertEquals(o2,e.nextElement()); + assertEquals(o1,e.nextElement()); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.getObjects(Object[])' + */ + public void testGetObjectsObjectArray() { + Object[] oa = new Object[4]; + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + array.getObjects(oa); + assertEquals(o1,oa[0]); + assertEquals(o2,oa[1]); + assertEquals(o3,oa[2]); + assertEquals(o4,oa[3]); + assertEquals(4,oa.length); + } + + public void testGetObjectsObjectArrayWithSmallArray() { + Object[] oa = new Object[2]; + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + array.getObjects(oa); + assertEquals(o1,oa[0]); + assertEquals(o2,oa[1]); + assertEquals(2,oa.length); + } + + public void testGetObjectsObjectArrayWithLargeArray() { + Object[] oa = new Object[5]; + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + array.getObjects(oa); + assertEquals(o1,oa[0]); + assertEquals(o2,oa[1]); + assertEquals(o3,oa[2]); + assertEquals(o4,oa[3]); + assertEquals(null,oa[4]); + assertEquals(5,oa.length); + } + /* + * Test method for 'net.wotonomy.foundation.NSArray.getObjects(Object[], NSRange)' + * TODO: Try more ranges. + */ + public void testGetObjectsObjectArrayNSRange() { + Object[] oa = new Object[2]; + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + array.getObjects(oa, new NSRange(1,2)); + assertEquals(o2,oa[0]); + assertEquals(o3,oa[1]); + assertEquals(2,oa.length); + } + + public void testGetObjectsObjectArrayNSRangeWithLargeRange() { + Object[] oa = new Object[4]; + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + array.getObjects(oa, new NSRange(1,90)); + assertEquals(o2,oa[0]); + assertEquals(o3,oa[1]); + assertEquals(o4,oa[2]); + assertEquals(null,oa[3]); + assertEquals(4,oa.length); + } + + + /* + * Test method for 'net.wotonomy.foundation.NSArray.indexOfObject(Object)' + */ + public void testIndexOfObjectObject() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + assertEquals(0,array.indexOfObject(o1)); + assertEquals(1,array.indexOfObject(o2)); + assertEquals(2,array.indexOfObject(o3)); + assertEquals(3,array.indexOfObject(o4)); + assertEquals(-1,array.indexOfObject("No Such Object")); + } + + /* + * Test method for 'net.wotonomy.foundation.NSArray.indexOfObject(Object, NSRange)' + */ + public void testIndexOfObjectObjectNSRange() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + NSRange range = new NSRange(1,2); + assertEquals(-1,array.indexOfObject(o1,range)); + assertEquals(1,array.indexOfObject(o2,range)); + + // should be -1 because o3 == null; + assertEquals(-1,array.indexOfObject(o3,range)); + assertEquals(-1,array.indexOfObject(o4,range)); + assertEquals(-1,array.indexOfObject("No Such Object")); + } + + + public void testIndexOfIdenticalObjectObject() { + Integer i1 = new Integer(3-1); + Integer i2 = new Integer(4/2); + NSArray array = new NSArray(new Object[] { o1,i2,o3,o4 }); + assertEquals(1,array.indexOfObject(i1)); + assertFalse(1 == array.indexOfIdenticalObject(i1)); + assertEquals(1, array.indexOfIdenticalObject(i2)); + } + + public void testIndexOfIdenticalObjectObjectNSRange() { + Integer i1 = new Integer(3-1); + Integer i2 = new Integer(4/2); + NSRange range = new NSRange(1,2); + NSArray array = new NSArray(new Object[] { o1,i2,o3,o4 }); + assertEquals(1,array.indexOfObject(i1,range)); + assertFalse(1 == array.indexOfIdenticalObject(i1,range)); + assertEquals(1, array.indexOfIdenticalObject(i2,range)); + NSRange range2 = new NSRange(3,2); + assertEquals(-1, array.indexOfObject(i1,range2)); + assertEquals(-1, array.indexOfIdenticalObject(i1,range2)); + assertEquals(-1, array.indexOfIdenticalObject(i2,range2)); + } + + public void testObjectAtIndex() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + assertEquals(o1,array.objectAtIndex(0)); + assertEquals(o2,array.objectAtIndex(1)); + assertEquals(o3,array.objectAtIndex(2)); + assertEquals(o4,array.objectAtIndex(3)); + try { + assertEquals(null,array.objectAtIndex(4)); + fail("Should have thrown index out of bounds."); + } catch (IndexOutOfBoundsException e) {} + } + + public void testComponentsSeparatedByString() { + String arrayString = "word0 word1 word2 word3"; + NSArray array = NSArray.componentsSeparatedByString(arrayString," "); + assertEquals("word0",array.objectAtIndex(0)); + assertEquals("word1",array.objectAtIndex(1)); + assertEquals("word2",array.objectAtIndex(2)); + assertEquals("word3",array.objectAtIndex(3)); + } + +/* + public void testClone() { + fail("Not yet implemented"); + + } + + public void testImmutableClone() { + fail("Not yet implemented"); + + } + + public void testMutableClone() { + fail("Not yet implemented"); + + } +*/ + + public void testToString() { + NSArray array = new NSArray(new Object[] { "o1",new Integer(1),"o3" }); + assertEquals("(o1, 1, o3)",array.toString()); + } + + + + public void testContains() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + assertTrue("Should contain object",array.contains(o1)); + assertTrue("Should contain object",array.contains(o2)); + assertFalse("Should not contain object",array.contains(o3)); + assertTrue("Should contain object",array.contains(o4)); + } + public void testContainsAll() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + ArrayList list = new ArrayList(); + list.add(o1); + list.add(o2); + assertTrue("Should have all elements of provided list.",array.containsAll(list)); + ArrayList list2 = new ArrayList(); + list2.add(o2); + list2.add(o3); + assertFalse("Should not have all elements of provided list.",array.containsAll(list2)); + } + + public void testGet() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + assertEquals(o1,array.get(0)); + assertEquals(o2,array.get(1)); + assertEquals(o4,array.get(2)); + try { + array.get(3); + } catch (IndexOutOfBoundsException e) {} + } + + public void testIndexOf() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + assertEquals(0,array.indexOf(o1)); + assertEquals(1,array.indexOf(o2)); + assertEquals(-1,array.indexOf(o3)); + assertEquals(2,array.indexOf(o4)); + } + + public void testIsEmpty() { + assertFalse(new NSArray(new Object[] { o1,o2,o4 } ).isEmpty()); + assertTrue(new NSArray(new Object[] {} ).isEmpty()); + } + + public void testLastIndexOf() { + NSArray array = new NSArray(new Object[] { o1,o4,o2,o4 }); + assertEquals(0,array.lastIndexOf(o1)); + assertEquals(2,array.lastIndexOf(o2)); + assertEquals(-1,array.lastIndexOf(o3)); + assertEquals(3,array.lastIndexOf(o4)); + } + + public void testSize() { + assertEquals(3,new NSArray(new Object[] { o1,o2,o4 }).size()); + assertEquals(1,new NSArray(new Object[] { o1 }).size()); + assertEquals(0,new NSArray(new Object[] { }).size()); + } + + public void testToArray() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + Object[] oarray = array.toArray(); + assertEquals(oarray[0],o1); + assertEquals(oarray[1],o2); + assertEquals(oarray[2],o4); + } + + public void testToArrayObjectArray() { + NSArray array = new NSArray(new Object[] { o1,o2,o4 }); + Object[] oa0 = new Object[3]; + Object[] oa1 = array.toArray(oa0); + assertSame(oa0,oa1); + assertEquals(oa1[0],o1); + assertEquals(oa1[1],o2); + assertEquals(oa1[2],o4); + } + + public void testAddIntObject() { + NSArray array = new NSArray(new Object[] { o1,o3,o4 }); + array.add(1,o2); + assertEquals(0,array.indexOfObject(o1)); + assertEquals(1,array.indexOfObject(o2)); + assertEquals(2,array.indexOfObject(o3)); + assertEquals(3,array.indexOfObject(o4)); + } + + public void testAddObject() { + NSArray array = new NSArray(new Object[] { o1,o2,o3 }); + array.add(o4); + assertEquals(0,array.indexOfObject(o1)); + assertEquals(1,array.indexOfObject(o2)); + assertEquals(2,array.indexOfObject(o3)); + assertEquals(3,array.indexOfObject(o4)); + } + + public void testAddAllCollection() { + NSArray array = new NSArray(new Object[] { o1,o2 }); + ArrayList list = new ArrayList(); + list.add(o3); + list.add(o4); + array.addAll(list); + assertEquals(0,array.indexOfObject(o1)); + assertEquals(1,array.indexOfObject(o2)); + assertEquals(2,array.indexOfObject(o3)); + assertEquals(3,array.indexOfObject(o4)); + } + + + public void testAddAllIntCollection() { + NSArray array = new NSArray(new Object[] { o1,o4 }); + ArrayList list = new ArrayList(); + list.add(o2); + list.add(o3); + array.addAll(1,list); + assertEquals(0,array.indexOfObject(o1)); + assertEquals(1,array.indexOfObject(o2)); + assertEquals(2,array.indexOfObject(o3)); + assertEquals(3,array.indexOfObject(o4)); + } + + + public void testClear() { + NSArray array = new NSArray(new Object[] { o1,o2 }); + array.clear(); + assertEquals(0,array.size()); + } + + public void testIterator() { + NSArray array = new NSArray(new Object[] { o1,o4 }); + Iterator i = array.iterator(); + assertEquals(o1,i.next()); + assertEquals(o4,i.next()); + assertFalse(i.hasNext()); + } + + public void testListIterator() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + ListIterator i = array.listIterator(); + assertEquals(o1,i.next()); + assertEquals(o2,i.next()); + assertEquals(o3,i.next()); + assertEquals(o4,i.next()); + assertEquals(o4,i.previous()); + assertEquals(o3,i.previous()); + assertEquals(o3,i.next()); + assertEquals(o4,i.next()); + assertFalse(i.hasNext()); + } + + public void testListIteratorInt() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + ListIterator i = array.listIterator(1); + assertEquals(o2,i.next()); + assertEquals(o3,i.next()); + assertEquals(o4,i.next()); + assertEquals(o4,i.previous()); + assertEquals(o3,i.previous()); + assertEquals(o3,i.next()); + assertEquals(o4,i.next()); + assertFalse(i.hasNext()); + } + + public void testRemoveInt() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + assertEquals(o2,array.remove(1)); + assertEquals(o3,array.remove(1)); + assertEquals(o1,array.objectAtIndex(0)); + assertEquals(o4,array.objectAtIndex(1)); + } + + public void testRemoveObject() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + assertTrue(array.remove(o2)); + assertTrue(array.remove(o3)); + assertFalse(array.remove("blah")); + assertEquals(o1,array.objectAtIndex(0)); + assertEquals(o4,array.objectAtIndex(1)); + } + + public void testRemoveAll() { + NSArray array = new NSArray(new Object[] { o1,o2,o3,o4 }); + ArrayList list = new ArrayList(); + list.add(o1); + list.add(o2); + array.removeAll(list); + assertEquals(2,array.size()); + assertEquals(o3,array.objectAtIndex(0)); + assertEquals(o4,array.objectAtIndex(1)); + } + + /* + public void testRetainAll() { + fail("Not yet implemented"); + + } + + public void testSet() { + fail("Not yet implemented"); + + } + + public void testSubList() { + fail("Not yet implemented"); + + } + + public void testProtectedAdd() { + fail("Not yet implemented"); + + } + + public void testProtectedAddAll() { + fail("Not yet implemented"); + + } +*/ + + +} diff --git a/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSBundleTest.java b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSBundleTest.java new file mode 100644 index 0000000..d441bec --- /dev/null +++ b/projects/net.wotonomy.foundation/src/test/java/net/wotonomy/foundation/NSBundleTest.java @@ -0,0 +1,179 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2005 Israfil Consulting Services Corporation +Copyright (C) 2005 Christian Edward Gruber + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.foundation; + +import java.io.File; +import java.net.URL; +import java.util.Properties; + +import junit.framework.Assert; +import junit.framework.TestCase; + +public class NSBundleTest extends TestCase { + + public NSBundleTest(String arg0) { + super(arg0); + } + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + /* + public void testNSBundle() { + //TODO Implement NSBundle(). + fail("Test not implemented."); + } + + public void testBundleForClass() { + //TODO Implement bundleForClass(). + fail("Test not implemented."); + } + */ + + public void testBundleWithURL() throws Exception{ + /* + URL url = new File(System.getProperty("user.dir")+"/target/test-classes/TestBundle.framework").toURI().toURL(); + System.out.println(url.toString()); + NSBundle bundle = NSBundle.bundleWithURL(url); + Assert.assertNotNull(bundle); + Assert.assertEquals("TestBundle",bundle.name()); + Assert.assertEquals(true,bundle.isFramework()); + Properties p = bundle.properties(); + Assert.assertNotNull(p); + Assert.assertEquals("TestValue",p.getProperty("TestKey")); + */ + } + /* + public void testBundleForName() { + //TODO Implement bundleForName(). + fail("Test not implemented."); + } + + public void testFrameworkBundles() { + //TODO Implement frameworkBundles(). + fail("Test not implemented."); + } + + public void testSetMainBundle() { + //TODO Implement setMainBundle(). + fail("Test not implemented."); + } + + public void testMainBundle() { + //TODO Implement mainBundle(). + fail("Test not implemented."); + } + + public void testDefaultLocalePrefix() { + //TODO Implement defaultLocalePrefix(). + fail("Test not implemented."); + } + + public void testFindOrCreateBundleWithPath() { + //TODO Implement findOrCreateBundleWithPath(). + fail("Test not implemented."); + } + + public void testBundleClassPackageNames() { + //TODO Implement bundleClassPackageNames(). + fail("Test not implemented."); + } + + public void testBundlePath() { + //TODO Implement bundlePath(). + fail("Test not implemented."); + } + + public void testBytesForResourcePath() { + //TODO Implement bytesForResourcePath(). + fail("Test not implemented."); + } + + public void testBundleClassNames() { + //TODO Implement bundleClassNames(). + fail("Test not implemented."); + } + + public void testInfoDictionary() { + //TODO Implement infoDictionary(). + fail("Test not implemented."); + } + + public void testInputStreamForResourcePath() { + //TODO Implement inputStreamForResourcePath(). + fail("Test not implemented."); + } + + public void testIsFramework() { + //TODO Implement isFramework(). + fail("Test not implemented."); + } + + public void testLoad() { + //TODO Implement load(). + fail("Test not implemented."); + } + + public void testName() { + //TODO Implement name(). + fail("Test not implemented."); + } + + public void testPrincipalClass() { + //TODO Implement principalClass(). + fail("Test not implemented."); + } + + public void testProperties() { + //TODO Implement properties(). + fail("Test not implemented."); + } + + public void testResourcePathForLocalizedResourceNamed() { + //TODO Implement resourcePathForLocalizedResourceNamed(). + fail("Test not implemented."); + } + + public void testResourcePathsForDirectories() { + //TODO Implement resourcePathsForDirectories(). + fail("Test not implemented."); + } + + public void testResourcePathsForLocalizedResources() { + //TODO Implement resourcePathsForLocalizedResources(). + fail("Test not implemented."); + } + + public void testResourcePathsForResources() { + //TODO Implement resourcePathsForResources(). + fail("Test not implemented."); + } + + public void testToString() { + //TODO Implement toString(). + fail("Test not implemented."); + } + + */ + +} diff --git a/projects/net.wotonomy.foundation/src/test/resources/README.txt b/projects/net.wotonomy.foundation/src/test/resources/README.txt new file mode 100644 index 0000000..e69de29 diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/.classpath b/projects/net.wotonomy.persistence.adapter.jdbc/.classpath new file mode 100644 index 0000000..dce51f2 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/.cvsignore b/projects/net.wotonomy.persistence.adapter.jdbc/.cvsignore new file mode 100644 index 0000000..7df6d55 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/.cvsignore @@ -0,0 +1,2 @@ +target +cobertura.ser \ No newline at end of file diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/.project b/projects/net.wotonomy.persistence.adapter.jdbc/.project new file mode 100644 index 0000000..36515ce --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.persistence.adapter.jdbc + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/pom.xml b/projects/net.wotonomy.persistence.adapter.jdbc/pom.xml new file mode 100644 index 0000000..4ec838a --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-persistence-adapter-jdbc + Wotonomy - Persistence - JDBC Adapter + jar + + + net.wotonomy + wotonomy-persistence + 1.0-alpha5-SNAPSHOT + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.persistence.adapter.jdbc + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.persistence.adapter.jdbc + + diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptor.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptor.java new file mode 100644 index 0000000..ece159b --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptor.java @@ -0,0 +1,123 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.jdbcadaptor; + +import net.wotonomy.access.EOAdaptor; +import net.wotonomy.access.EOAdaptorContext; +import net.wotonomy.access.EOModel; +import net.wotonomy.access.EOSQLExpressionFactory; +import net.wotonomy.foundation.NSDictionary; + + +/** +* An adaptor that connects to a databaser server via JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCAdaptor extends EOAdaptor { + + protected EOSQLExpressionFactory _expressionFactory; + protected String _driverName; + protected NSDictionary _jdbcInfo; + + /** + * Creates a new instance. + * @param name The name of the adaptor (should always be JDBC) + */ + public JDBCAdaptor(String name) { + super(name); + } + + public void setConnectionDictionary(NSDictionary dict) { + super.setConnectionDictionary(dict); + if (dict.objectForKey("driver") != null) + _driverName = (String)dict.objectForKey("driver"); + else + throw new JDBCAdaptorException("Connection dictionary must have a 'driver' key.", null); + if (dict.objectForKey("jdbc2Info") != null) + _jdbcInfo = (NSDictionary)dict.objectForKey("jdbc2Info"); + else + throw new JDBCAdaptorException("Connection dictionary must have a 'jdbc2Info' key.", null); + } + + /* Checks to see if the connection dictionary is valid. + * Throws an exception if it's not. + * @see net.wotonomy.access.EOAdaptor#assertConnectionDictionaryIsValid() + */ + public void assertConnectionDictionaryIsValid() { + JDBCContext context = new JDBCContext(this); + context.connect(); + context.disconnect(); + } + + /* Creates a JDBCContext. + * @see net.wotonomy.access.EOAdaptor#createAdaptorContext() + */ + public EOAdaptorContext createAdaptorContext() { + JDBCContext context = new JDBCContext(this); + _contexts.addObject(context); + return context; + } + + /* Returns the JDBCExpression class. + * @see net.wotonomy.access.EOAdaptor#defaultExpressionClass() + */ + public Class defaultExpressionClass() { + return JDBCExpression.class; + } + + /* Returns a JDBCExpressionFactory. + * @see net.wotonomy.access.EOAdaptor#expressionFactory() + */ + public EOSQLExpressionFactory expressionFactory() { + if (_expressionFactory == null) + _expressionFactory = new JDBCExpressionFactory(this); + return _expressionFactory; + } + + /* Determines if a qualifier type is valid. + * @see net.wotonomy.access.EOAdaptor#isValidQualifierType(java.lang.String, net.wotonomy.access.EOModel) + */ + public boolean isValidQualifierType(String typeName, EOModel model) { + // TODO Auto-generated method stub + return false; + } + + public String driverName() { + return _driverName; + } + + public NSDictionary jdbcInfo() { + return _jdbcInfo; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 22:59:22 cgruber + * make it compile with maven dependencies and add a cvsignore. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 20:09:37 chochos + * a concrete implementation of EOAdaptor for use with JDBC + * + */ diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptorException.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptorException.java new file mode 100644 index 0000000..5a999d0 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCAdaptorException.java @@ -0,0 +1,68 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */package net.wotonomy.jdbcadaptor; + +import java.sql.SQLException; + +import net.wotonomy.access.EOGeneralAdaptorException; + +/** +* Concrete implementation of EOSQLExpression for use with JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCAdaptorException extends EOGeneralAdaptorException { + + protected SQLException _sqlException; + + /** + * @param msg The message for the exception + * @sqlex The original SQLException + */ + public JDBCAdaptorException(String msg, SQLException sqlex) { + super(msg); + _sqlException = sqlex; + } + + /** + * @param name + * @param userInfo + */ + public JDBCAdaptorException(SQLException sqlex) { + super(sqlex.getMessage()); + _sqlException = sqlex; + } + + public SQLException sqlException() { + return _sqlException; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 22:59:22 cgruber + * make it compile with maven dependencies and add a cvsignore. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/13 20:11:30 chochos + * forgot to add the wotonomy disclaimer... + * + */ diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCChannel.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCChannel.java new file mode 100644 index 0000000..c62c463 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCChannel.java @@ -0,0 +1,449 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.jdbcadaptor; + +import java.math.BigDecimal; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; + +import net.wotonomy.access.EOAdaptorChannel; +import net.wotonomy.access.EOAttribute; +import net.wotonomy.access.EOEntity; +import net.wotonomy.access.EOGeneralAdaptorException; +import net.wotonomy.access.EOSQLExpression; +import net.wotonomy.access.EOStoredProcedure; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSTimestamp; + +/** +* Concrete implementation of EOAdaptorChannel for use with JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCChannel extends EOAdaptorChannel { + + protected boolean _fetchInProgress; + protected ResultSet _resultSet; + protected Statement _statement; + protected NSArray _attsToFetch; + protected NSArray _resultAttributes; + protected boolean _transactionWasOpen; + protected NSDictionary _spReturnValues; + protected int _resultCount; + + /** + * Creates a new JDBCChannel. + * @param context The JDBCContext this channel belongs to. + */ + public JDBCChannel(JDBCContext context) { + super(context); + } + + protected JDBCContext _context() { + return (JDBCContext)adaptorContext(); + } + + /* Sets the attributes to be fetched from the database. + * @see net.wotonomy.access.EOAdaptorChannel#setAttributesToFetch(net.wotonomy.foundation.NSArray) + */ + public void setAttributesToFetch(NSArray atts) { + _attsToFetch = atts; + } + + /* Returns an array with the attributes that will be fetched. + * @see net.wotonomy.access.EOAdaptorChannel#attributesToFetch() + */ + public NSArray attributesToFetch() { + return _attsToFetch; + } + + /* Cancels the fetch, rolling back the transaction. + * @see net.wotonomy.access.EOAdaptorChannel#cancelFetch() + */ + public void cancelFetch() { + if (_statement == null || _resultSet == null) + return; + try { + _resultSet.close(); + _statement.cancel(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot cancel fetch in database.", ex); + } + } + + /* Closes the jdbc channel. + * @see net.wotonomy.access.EOAdaptorChannel#closeChannel() + */ + public void closeChannel() { + if (_statement == null) + return; + try { + _statement.close(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to close the channel.", ex); + } + } + + /* If the fetch was done with an array of EOAttributes, returns + * that same array; otherwise it creates an array of EOAttributes + * based on the column names that will be fetched. + * @see net.wotonomy.access.EOAdaptorChannel#describeResults() + */ + public NSArray describeResults() { + if (_resultSet == null || !_fetchInProgress) + throw new EOGeneralAdaptorException("Cannot describe results without a result set."); + if (_resultAttributes == null) { + try { + ResultSetMetaData _rsmeta = _resultSet.getMetaData(); + EOAttribute[] attarr = new EOAttribute[_rsmeta.getColumnCount()]; + for (int i = 1; i <= attarr.length; i++) { + EOAttribute a = new EOAttribute(); + a.setName("Attribute " + (i)); + a.setColumnName(_rsmeta.getColumnName(i)); + a.setClassName(_rsmeta.getColumnClassName(i)); + a.setExternalType(_rsmeta.getColumnTypeName(i)); + a.setPrecision(_rsmeta.getPrecision(i)); + a.setScale(_rsmeta.getScale(i)); + a.setAllowsNull(_rsmeta.isNullable(i) == ResultSetMetaData.columnNullable); + a.setWidth(_rsmeta.getColumnDisplaySize(i)); + a.setReadOnly(_rsmeta.isReadOnly(i)); + attarr[i-1] = a; + } + _resultAttributes = new NSArray(attarr); + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to get the result set metadata.", ex); + } + } + return _resultAttributes; + } + + /* Deletes from the database the rows described by the qualifier, + * in the specified entity. + * @see net.wotonomy.access.EOAdaptorChannel#deleteRowsDescribedByQualifier(net.wotonomy.control.EOQualifier, net.wotonomy.access.EOEntity) + */ + public int deleteRowsDescribedByQualifier(EOQualifier q, EOEntity entity) { + EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); + exp.prepareDeleteExpressionForQualifier(q); + evaluateExpression(exp); + return _resultCount; + } + + /* Creates a java.sql.Statement object and executes it. + * If there is an open transaction, the statement is executed inside it; + * otherwise a transaction is started, the statement executed, and + * the transaction is committed. + * @see net.wotonomy.access.EOAdaptorChannel#evaluateExpression(net.wotonomy.access.EOSQLExpression) + */ + public void evaluateExpression(EOSQLExpression sql) { + if (!isOpen()) + throw new EOGeneralAdaptorException("Attempt to evaluate expression without opening the channel first."); + try { + _statement = _context().connection().createStatement(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot create java.sql.Statement", ex); + } + _resultSet = null; + boolean isQuery = false; + String text = sql.statement(); + try { + //run an executeUpdate with these prefixes + if (text.startsWith("INSERT") || text.startsWith("DELETE") || text.startsWith("UPDATE")) { + conditionalBeginTransaction(); + _resultCount = _statement.executeUpdate(text); + conditionalCommitTransaction(); + return; + } else if (text.startsWith("SELECT")) { + //run an executeQuery with SELECT + if (_resultCount > 0) + _statement.setMaxRows(_resultCount); + _resultSet = _statement.executeQuery(text); + _fetchInProgress = true; + return; + } else { //just plain execute + conditionalBeginTransaction(); + isQuery = _statement.execute(text); + } + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to execute expression '" + text + "'", ex); + } + try { + if (isQuery) { + if (_resultCount > 0) + _statement.setMaxRows(_resultCount); + _resultSet = _statement.getResultSet(); + } else { + _resultCount = _statement.getUpdateCount(); + conditionalCommitTransaction(); + } + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to get the result set.", ex); + } + } + + /* Executes a stored procedure with the specified parameters. + * Any results that the procedure returns should be obtained + * by calling returnValuesForLastStoredProcedureInvocation. + * @see net.wotonomy.access.EOAdaptorChannel#executeStoredProcedure(net.wotonomy.access.EOStoredProcedure, net.wotonomy.foundation.NSDictionary) + */ + public void executeStoredProcedure( + EOStoredProcedure proc, NSDictionary values) { + if (!isOpen()) + throw new EOGeneralAdaptorException("Attempt to execute a stored procedure on a closed channel."); + conditionalBeginTransaction(); + try { + //Assemble the procedure call + StringBuffer buf = new StringBuffer("{ call "); + buf.append(proc.externalName()); + NSArray args = proc.arguments(); + if (args != null && args.count() > 0) { + buf.append("["); + for (int i = 0; i < args.count(); i++) { + EOAttribute a = (EOAttribute)args.objectAtIndex(i); + if (a.parameterDirection() != EOAttribute.OutParameter) { + buf.append('?'); + buf.append(", "); + } + } + buf.delete(buf.length()-2, buf.length()); + buf.append("]"); + } + buf.append(" }"); + //get the callable statement + CallableStatement sp = _context().connection().prepareCall(buf.toString()); + if (args != null && args.count() > 0) { + int pos = 1; + //set the in and inOut parameters + for (int i = 0; i < args.count(); i++) { + EOAttribute a = (EOAttribute)args.objectAtIndex(i); + if (a.parameterDirection() != EOAttribute.OutParameter) { + Object val = values.objectForKey(a.name()); + if (val == NSKeyValueCoding.NullValue) + sp.setNull(pos, 0); //TODO: check sql type + if (val instanceof String) + sp.setString(pos, (String)val); + else if (val instanceof BigDecimal) + sp.setBigDecimal(pos, (BigDecimal)val); + else if (val instanceof NSTimestamp) + sp.setTimestamp(pos, (NSTimestamp)val); + else if (val instanceof NSData) + sp.setBytes(pos, ((NSData)val).bytes()); + else if (val instanceof Integer) + sp.setInt(pos, ((Integer)val).intValue()); + else if (val instanceof Long) + sp.setLong(pos, ((Long)val).longValue()); + else + sp.setObject(pos, val); + pos++; + } + } + } + //run the procedure + sp.execute(); + //get the return values + if (args != null && args.count() > 0) { + int pos = 1; + NSMutableDictionary retvals = new NSMutableDictionary(); + for (int i = 0; i < args.count(); i++) { + EOAttribute a = (EOAttribute)args.objectAtIndex(i); + if (a.parameterDirection() != EOAttribute.InParameter) { + Object val = sp.getObject(pos); + if (val == null) + retvals.setObjectForKey(NSKeyValueCoding.NullValue, a.name()); + else if (val instanceof Blob) { + try { + retvals.setObjectForKey(new NSData(((Blob)val).getBinaryStream(), 1024), a.name()); + } catch (java.io.IOException ex) { + //what should I do here? + retvals.setObjectForKey(NSData.EmptyData, a.name()); + } + } else + retvals.setObjectForKey(val, a.name()); + pos++; + } + } + _spReturnValues = retvals; + } + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to execute stored procedure.", ex); + } + conditionalCommitTransaction(); + } + + /* Fetches one row from the database + * @see net.wotonomy.access.EOAdaptorChannel#fetchRow() + */ + public NSMutableDictionary fetchRow() { + if (_resultSet == null) { + return null; + } + if (attributesToFetch() == null) + throw new EOGeneralAdaptorException("Attempt to fetchRow without setting attributes to fetch first."); + try { + //If the current result set ends, there may be another one + if (!_resultSet.next()) { + _resultSet.close(); + _resultAttributes = null; + _fetchInProgress = _statement.getMoreResults(); + if (_fetchInProgress) + _resultSet = _statement.getResultSet(); + return null; + } + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to fetch row.", ex); + } + + //Assemble the dictionary + NSMutableDictionary dict = new NSMutableDictionary(attributesToFetch().count()); + try { + for (int i = 0; i < attributesToFetch().count(); i++) { + EOAttribute a = (EOAttribute)attributesToFetch().objectAtIndex(i); + Object o = _resultSet.getObject(i+1); + if (o == null) + o = NSKeyValueCoding.NullValue; + dict.setObjectForKey(o, a.name()); + } + } catch (SQLException ex) { + throw new JDBCAdaptorException("While trying to create row.", ex); + } + return dict; + } + + /* Inserts a row into a table in the database. + * @see net.wotonomy.access.EOAdaptorChannel#insertRow(net.wotonomy.foundation.NSDictionary, net.wotonomy.access.EOEntity) + */ + public void insertRow(NSDictionary row, EOEntity entity) { + EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); + exp.prepareInsertExpressionWithRow(row); + evaluateExpression(exp); + } + + /* Indicates if a fetch is in progress; that is, if a SELECT statement + * was executed and there are still rows to be fetched. + * @see net.wotonomy.access.EOAdaptorChannel#isFetchInProgress() + */ + public boolean isFetchInProgress() { + return _fetchInProgress; + } + + /* Indicates if the channel is open. + * @see net.wotonomy.access.EOAdaptorChannel#isOpen() + */ + public boolean isOpen() { + boolean open = (_context().connection() != null); + try { + open = open || !_context().connection().isClosed(); + } catch (SQLException ex) { + open = false; + } + return open; + } + + /* Opens the channel. If the adaptor context has not yet made + * a connection to the database, this forces the context to + * connect. + * @see net.wotonomy.access.EOAdaptorChannel#openChannel() + */ + public void openChannel() { + try { + if (_context().connection() == null || _context().connection().isClosed()) + _context().connect(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot open connection to database.", ex); + } + } + + /* Returns the values obtained from the last stored procedure executed. + * @see net.wotonomy.access.EOAdaptorChannel#returnValuesForLastStoredProcedureInvocation() + */ + public NSDictionary returnValuesForLastStoredProcedureInvocation() { + return _spReturnValues; + } + + /* Creates a SELECT expression and executes it. If the attribute array is null, + * then the result's metadata is used to dynamically create an array + * of attributes. + * @see net.wotonomy.access.EOAdaptorChannel#selectAttributes(net.wotonomy.foundation.NSArray, net.wotonomy.control.EOFetchSpecification, boolean, net.wotonomy.access.EOEntity) + */ + public void selectAttributes( + NSArray atts, EOFetchSpecification fspec, + boolean lock, EOEntity entity) { + _resultAttributes = atts; + EOSQLExpression expr = adaptorContext().adaptor().expressionFactory().createExpression(entity); + _fetchInProgress = true; + expr.prepareSelectExpressionWithAttributes(atts, lock, fspec); + //for now we store the fetch limit here + if (fspec != null) + _resultCount = fspec.fetchLimit(); + evaluateExpression(expr); + } + + /* Creates and executes an UPDATE statement. + * @see net.wotonomy.access.EOAdaptorChannel#updateValuesInRowsDescribedByQualifier(net.wotonomy.foundation.NSDictionary, net.wotonomy.control.EOQualifier, net.wotonomy.access.EOEntity) + */ + public int updateValuesInRowsDescribedByQualifier( + NSDictionary row, EOQualifier q, EOEntity entity) { + EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); + exp.prepareUpdateExpressionWithRow(row, q); + evaluateExpression(exp); + return _resultCount; + } + + protected void conditionalBeginTransaction() { + _transactionWasOpen = adaptorContext().hasOpenTransaction(); + if (!_transactionWasOpen) + adaptorContext().beginTransaction(); + } + + protected void conditionalCommitTransaction() { + if (!_transactionWasOpen) + adaptorContext().commitTransaction(); + _transactionWasOpen = false; + } + +} +/* +* $Log$ +* Revision 1.2 2006/02/18 22:59:22 cgruber +* make it compile with maven dependencies and add a cvsignore. +* +* Revision 1.1 2006/02/16 13:22:23 cgruber +* Check in all sources in eclipse-friendly maven-enabled packages. +* +* Revision 1.3 2003/08/14 02:15:11 chochos +* added lots of comments +* +* Revision 1.2 2003/08/13 20:45:20 chochos +* small fixes in evaluateExpression, which has been successfully tested with a SELECT statement. +* +* Revision 1.1 2003/08/13 20:12:48 chochos +* a subclass of EOAdaptorChannel to be used with JDBC. +* +*/ + diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCContext.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCContext.java new file mode 100644 index 0000000..445ffb5 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCContext.java @@ -0,0 +1,146 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.jdbcadaptor; + +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; + +import net.wotonomy.access.EOAdaptor; +import net.wotonomy.access.EOAdaptorChannel; +import net.wotonomy.access.EOAdaptorContext; + +/** +* Concrete implementation of EOAdaptorContext for use with JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCContext extends EOAdaptorContext { + + protected Connection _jdbcConnection; + + /** + * Creates a new instance. + * @param adaptor The adaptor this context belongs to. + */ + public JDBCContext(EOAdaptor adaptor) { + super(adaptor); + } + + public void connect() throws JDBCAdaptorException { + try { + if (_jdbcConnection != null && !_jdbcConnection.isClosed()) + throw new JDBCAdaptorException("Attempt to connect when already connected.", null); + Class.forName(((JDBCAdaptor)adaptor()).driverName()); + String url = (String)adaptor().connectionDictionary().objectForKey("URL"); + Driver driver = DriverManager.getDriver(url); + java.util.Properties props = new java.util.Properties(); + props.setProperty("user", (String)adaptor().connectionDictionary().objectForKey("username")); + props.setProperty("password", (String)adaptor().connectionDictionary().objectForKey("password")); + _jdbcConnection = driver.connect(url, props); + _jdbcConnection.setAutoCommit(false); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot connect to database", ex); + } catch (ClassNotFoundException ex) { + throw new JDBCAdaptorException("Cannot find JDBC driver " + ex.getMessage(), null); + } + } + + public void disconnect() throws JDBCAdaptorException { + if (_jdbcConnection != null) { + try { + _jdbcConnection.close(); + _jdbcConnection = null; + transactionDidRollback(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Trying to close connection.", ex); + } + } + } + + public Connection connection() { + return _jdbcConnection; + } + + /* Begins a transaction. Actually it does nothing because it's not + * necessary. + * @see net.wotonomy.access.EOAdaptorContext#beginTransaction() + */ + public void beginTransaction() { + if (hasOpenTransaction()) + throw new JDBCAdaptorException("Cannot nest transactions.", null); + transactionDidBegin(); + } + + /* Commits a transaction. + * @see net.wotonomy.access.EOAdaptorContext#commitTransaction() + */ + public void commitTransaction() { + try { + _jdbcConnection.commit(); + transactionDidCommit(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot commit.", ex); + } + } + + /* Rolls back a transacion. + * @see net.wotonomy.access.EOAdaptorContext#rollbackTransaction() + */ + public void rollbackTransaction() { + try { + _jdbcConnection.rollback(); + transactionDidRollback(); + } catch (SQLException ex) { + throw new JDBCAdaptorException("Cannot commit.", ex); + } + } + + /* Creates a JDBCChannel instance. + * @see net.wotonomy.access.EOAdaptorContext#createAdaptorChannel() + */ + public EOAdaptorChannel createAdaptorChannel() { + JDBCChannel channel = new JDBCChannel(this); + _channels.addObject(channel); + return channel; + } + + /* I don't know what to do here. Throw something, maybe? + * @see net.wotonomy.access.EOAdaptorContext#handleDroppedConnection() + */ + public void handleDroppedConnection() { + // TODO Auto-generated method stub + + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 22:59:22 cgruber + * make it compile with maven dependencies and add a cvsignore. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 20:13:33 chochos + * concrete implementation of EOAdaptorContext for use with JDBC. This is the class that holds the physical connection to the database. + * + */ diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpression.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpression.java new file mode 100644 index 0000000..2258a65 --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpression.java @@ -0,0 +1,73 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.jdbcadaptor; + +import net.wotonomy.access.EOAttribute; +import net.wotonomy.access.EOEntity; +import net.wotonomy.access.EOSQLExpression; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + + +/** +* Concrete implementation of EOSQLExpression for use with JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCExpression extends EOSQLExpression { + + protected NSDictionary _jdbcInfo; + + /** + * @param entity + */ + public JDBCExpression(EOEntity entity) { + super(entity); + } + + public void setJdbcInfo(NSDictionary info) { + _jdbcInfo = info; + } + public NSDictionary jdbcInfo() { + return _jdbcInfo; + } + + /* (non-Javadoc) + * @see net.wotonomy.access.EOSQLExpression#bindVariableDictionaryForAttribute(net.wotonomy.access.EOAttribute, java.lang.Object) + */ + public NSMutableDictionary bindVariableDictionaryForAttribute( + EOAttribute attr, Object variable) { + // TODO Auto-generated method stub + return null; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 22:59:22 cgruber + * make it compile with maven dependencies and add a cvsignore. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 20:14:38 chochos + * subclass of EOSQLExpression. Still needs a lot of work for bindings, mostly. + * + */ diff --git a/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpressionFactory.java b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpressionFactory.java new file mode 100644 index 0000000..15c167f --- /dev/null +++ b/projects/net.wotonomy.persistence.adapter.jdbc/src/main/java/net/wotonomy/jdbcadaptor/JDBCExpressionFactory.java @@ -0,0 +1,65 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.jdbcadaptor; + +import net.wotonomy.access.EOAdaptor; +import net.wotonomy.access.EOEntity; +import net.wotonomy.access.EOSQLExpression; +import net.wotonomy.access.EOSQLExpressionFactory; +import net.wotonomy.foundation.NSDictionary; + + +/** +* Concrete implementation of EOSQLExpressionFactory for use with JDBC. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 903 $ +*/ +public class JDBCExpressionFactory extends EOSQLExpressionFactory { + + protected NSDictionary _jdbcInfo; + + /** + * Creates a new instance. + * @param adaptor The adaptor for this factory. + */ + public JDBCExpressionFactory(EOAdaptor adaptor) { + super(adaptor); + _jdbcInfo = ((JDBCAdaptor)adaptor()).jdbcInfo(); + } + + public EOSQLExpression createExpression(EOEntity entity) { + JDBCExpression expr = new JDBCExpression(entity); + expr.setJdbcInfo(_jdbcInfo); + return expr; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 22:59:22 cgruber + * make it compile with maven dependencies and add a cvsignore. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 20:15:05 chochos + * the subclass of EOSQLExpressionFactory to be used with JDBC. + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/.classpath b/projects/net.wotonomy.persistence/.classpath new file mode 100644 index 0000000..38003e1 --- /dev/null +++ b/projects/net.wotonomy.persistence/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/projects/net.wotonomy.persistence/.cvsignore b/projects/net.wotonomy.persistence/.cvsignore new file mode 100644 index 0000000..c14b7c4 --- /dev/null +++ b/projects/net.wotonomy.persistence/.cvsignore @@ -0,0 +1,3 @@ +target +target/* +cobertura.ser diff --git a/projects/net.wotonomy.persistence/.project b/projects/net.wotonomy.persistence/.project new file mode 100644 index 0000000..d5d8f41 --- /dev/null +++ b/projects/net.wotonomy.persistence/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.persistence + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.core.prefs b/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..9cfcb04 --- /dev/null +++ b/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Thu Feb 16 21:52:22 EST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.ui.prefs b/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..4728b95 --- /dev/null +++ b/projects/net.wotonomy.persistence/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Thu Feb 16 21:52:22 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/projects/net.wotonomy.persistence/pom.xml b/projects/net.wotonomy.persistence/pom.xml new file mode 100644 index 0000000..7990db5 --- /dev/null +++ b/projects/net.wotonomy.persistence/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-persistence + Wotonomy - Persistence + jar + + + ${project.groupId} + wotonomy-foundation + ${project.version} + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.persistence + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.persistence + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessArrayFaultHandler.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessArrayFaultHandler.java new file mode 100644 index 0000000..1aafa13 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessArrayFaultHandler.java @@ -0,0 +1,71 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyGlobalID; + +/** +* A fault handler for to-many relationships. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/public class EOAccessArrayFaultHandler extends EOAccessGenericFaultHandler { + + protected EOKeyGlobalID _sourceID; + protected String _relation; + + public EOAccessArrayFaultHandler(EOKeyGlobalID sourceID, String relationName, EODatabaseContext dbc, EOEditingContext ec) { + super(); + _sourceID = sourceID; + _relation = relationName; + setContext(dbc, ec); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOFaultHandler#completeInitializationOfObject(java.lang.Object) + */ + public void completeInitializationOfObject(Object obj) { + // TODO Auto-generated method stub + + } + + public String relationshipName() { + return _relation; + } + + public EOKeyGlobalID sourceGlobalID() { + return _sourceID; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/19 19:53:20 chochos + * EOAccess fault handlers (still incomplete) + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessFaultHandler.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessFaultHandler.java new file mode 100644 index 0000000..d4cabe9 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessFaultHandler.java @@ -0,0 +1,66 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyGlobalID; + +/** +* A fault handler for single objects. Usually the destinations of a +* to-one relationship. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/public class EOAccessFaultHandler extends EOAccessGenericFaultHandler { + + protected EOKeyGlobalID _gid; + + public EOAccessFaultHandler(EOKeyGlobalID gid, EODatabaseContext dbc, EOEditingContext ec) { + super(); + _gid = gid; + setContext(dbc, ec); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOFaultHandler#completeInitializationOfObject(java.lang.Object) + */ + public void completeInitializationOfObject(Object obj) { + // TODO Auto-generated method stub + + } + + public EOKeyGlobalID globalID() { + return _gid; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/19 19:53:20 chochos + * EOAccess fault handlers (still incomplete) + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessGenericFaultHandler.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessGenericFaultHandler.java new file mode 100644 index 0000000..6876151 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessGenericFaultHandler.java @@ -0,0 +1,75 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFaultHandler; + +/** +* A generic fault handler for EOAccess. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EOAccessGenericFaultHandler extends EOFaultHandler { + + protected EODatabaseContext _dbContext; + protected EOEditingContext _ec; + + public EOAccessGenericFaultHandler() { + super(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOFaultHandler#faultWillFire(java.lang.Object) + */ + public void faultWillFire(Object obj) { + // TODO Auto-generated method stub + + } + + protected void setContext(EODatabaseContext dbc, EOEditingContext ec) { + _dbContext = dbc; + _ec = ec; + } + + public EODatabaseContext databaseContext() { + return _dbContext; + } + + public EOEditingContext editingContext() { + return _ec; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/19 19:53:20 chochos + * EOAccess fault handlers (still incomplete) + * + */ + \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessLock.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessLock.java new file mode 100644 index 0000000..28199c5 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAccessLock.java @@ -0,0 +1,60 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSRecursiveLock; + +/** + * This class offers a very simple interface to a global locking + * mechanism to be used by the whole access layer. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 894 $ + */ +public class EOAccessLock { + + private static NSRecursiveLock _lock = new NSRecursiveLock(); + + private EOAccessLock() { + super(); + } + + public static void lock() { + _lock.lock(); + } + + public static void unlock() { + _lock.unlock(); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/29 20:43:25 chochos + * a global access layer lock + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptor.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptor.java new file mode 100644 index 0000000..28295d1 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptor.java @@ -0,0 +1,269 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSTimestamp; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public abstract class EOAdaptor { + + protected String _name; + protected NSDictionary _connectionDictionary = NSDictionary.EmptyDictionary; + protected NSMutableArray _contexts = new NSMutableArray(); + protected Class _expressionClass; + private static NSMutableDictionary _expressionClassesByName = new NSMutableDictionary(); + + public EOAdaptor(String name) { + super(); + _name = name; + } + + /** + * Creates an adaptor with model's adaptorName and sets its connection + * dictionary to the model's connection dictionary. + * @param model The model to take adaptorName and connectionDictionary from. + * @return The adaptor specified in model. + */ + public static EOAdaptor adaptorWithModel(EOModel model) { + if (model == null) + throw new IllegalArgumentException("Model must not be null."); + if (model.adaptorName() == null || model.adaptorName().length() == 0) + throw new IllegalArgumentException("Cannot create an adaptor with an empty name."); + EOAdaptor adaptor = adaptorWithName(model.adaptorName()); + if (adaptor == null) + throw new IllegalArgumentException("Cannot create adaptor with name " + model.adaptorName()); + adaptor.setConnectionDictionary(model.connectionDictionary()); + return adaptor; + } + + /** + * Instantiates an adaptor of a concrete EOAdaptor subclass, based on name. + * If name is a fully qualified class name, then it returns an instance of that class + * by invoking the constructor with a string argument. Otherwise, it + * tries to find a class called (name)Adaptor in a package called + * net.wotonomy.(lowercase name)adaptor; if it can't find one, an exception is raised. + * @param name The name of the adaptor, or a fully qualified class name. + * @return + */ + public static EOAdaptor adaptorWithName(String name) { + Class adaptorClass = null; + String cname = null; + if (name.endsWith("Adaptor") && name.indexOf('.') > 0) { + cname = name; + int lastdot = name.lastIndexOf('.'); + //take off the package and the 'Adaptor' suffix + name = cname.substring(lastdot, cname.length() - 7); + } else { + //construct the fully qualified class name + cname = "net.wotonomy." + name.toLowerCase() + "adaptor." + name + "Adaptor"; + } + try { + adaptorClass = Class.forName(cname); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Cannot find class named " + name); + } + EOAdaptor adaptor = null; + java.lang.reflect.Constructor callme = null; + try { + callme = adaptorClass.getConstructor(new Class[]{ String.class }); + adaptor = (EOAdaptor)callme.newInstance((Object[])new String[]{ name }); + } catch (ClassCastException ex) { + throw new IllegalArgumentException("Class " + adaptorClass.getName() + " must inherit from net.wotonomy.access.EOAdaptor"); + }catch (Exception ex) { + throw new IllegalArgumentException("Cannot find or invoke constructor with name argument in class " + adaptorClass.getName()); + } + return adaptor; + } + + public static void setExpressionClassName(String expClassName, String adaptorClassName) { + _expressionClassesByName.setObjectForKey(expClassName, adaptorClassName); + } + public static String expressionClassName(String adaptorClassName) { + return (String)_expressionClassesByName.objectForKey(adaptorClassName); + } + + public void assignExternalInfoForAttribute(EOAttribute attribute) { + if (!attribute.isDerived()) { + attribute.setColumnName(attribute.name().toUpperCase()); + } + assignExternalTypeForAttribute(attribute); + } + + public void assignExternalTypeForAttribute(EOAttribute attribute) { + } + + public void assignExternalInfoForEntity(EOEntity entity) { + entity.setExternalName(entity.name().toUpperCase()); + } + + public void assignExternalInfoForEntireModel(EOModel model) { + NSArray ents = model.entities(); + for (int i = 0; i < ents.count(); i++) { + EOEntity e = (EOEntity)ents.objectAtIndex(i); + //TODO: check that entity is not a prototypes entity + NSArray atts = e.attributes(); + for (int j = 0; j < atts.count(); j++) { + EOAttribute a = (EOAttribute)atts.objectAtIndex(i); + assignExternalInfoForAttribute(a); + } + assignExternalInfoForEntity(e); + } + } + + public boolean canServiceModel(EOModel model) { + NSDictionary mcd = model.connectionDictionary(); + if (mcd == null && _connectionDictionary == null) + return true; + if (mcd == null || _connectionDictionary == null) + return false; + return mcd.equals(_connectionDictionary); + } + + public void setConnectionDictionary(NSDictionary connection) { + _connectionDictionary = connection; + } + public NSDictionary connectionDictionary() { + return _connectionDictionary; + } + + public NSArray contexts() { + return new NSArray(_contexts); + } + + public abstract void assertConnectionDictionaryIsValid(); + + public abstract EOAdaptorContext createAdaptorContext(); + + public abstract Class defaultExpressionClass(); + + public abstract EOSQLExpressionFactory expressionFactory(); + + public abstract boolean isValidQualifierType(String typeName, EOModel model); + + public Class expressionClass() { + if (_expressionClass != null) + return _expressionClass; + String cname = expressionClassName(name()); + if (cname != null) { + try { + _expressionClass = Class.forName(cname); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException("Cannot find expression class named " + cname); + } + } + return defaultExpressionClass(); + } + + public NSArray externalTypesWithModel(EOModel model) { + return NSArray.EmptyArray; + } + + public NSData fetchedValueForDataValue(NSData value, EOAttribute attr) { + return value; + } + + public NSTimestamp fetchedValueForDateValue(NSTimestamp value, EOAttribute attr) { + return value; + } + + public Number fetchedValueForNumberValue(Number value, EOAttribute attr) { + return value; + } + + public String fetchedValueForStringValue(String value, EOAttribute attr) { + return value; + } + + public Object fetchedValueForValue(Object value, EOAttribute attr) { + if (value == NSKeyValueCoding.NullValue) + return value; + if (value instanceof String) + return fetchedValueForStringValue((String)value, attr); + if (value instanceof NSData) + return fetchedValueForDataValue((NSData)value, attr); + if (value instanceof Number) + return fetchedValueForNumberValue((Number)value, attr); + if (value instanceof NSTimestamp) + return fetchedValueForDateValue((NSTimestamp)value, attr); + return value; + } + + public void handleDroppedConnection() { + for (int i = 0; i < _contexts.count(); i++) { + EOAdaptorContext c = (EOAdaptorContext)_contexts.objectAtIndex(i); + c.transactionDidRollback(); + c.handleDroppedConnection(); + } + _contexts.removeAllObjects(); + } + + public boolean hasOpenChannels() { + for (int i = 0; i < _contexts.count(); i++) { + EOAdaptorContext c = (EOAdaptorContext)_contexts.objectAtIndex(i); + if (c.hasOpenChannels()) + return true; + } + return false; + } + + public String internalTypeForExternalType(String extType, EOModel model) { + return null; + } + + public boolean isDroppedConnectionException(Exception ex) { + return false; + } + + public String name() { + return _name; + } + + public NSArray prototypeAttributes() { + return NSArray.EmptyArray; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2005/12/08 06:52:32 cgruber + * Move tests, improve build.xml, and make certain casts explicit so that Java 1.5 doesn't complain about varargs. + * + * Revision 1.1 2003/08/13 00:37:45 chochos + * an almost complete implementation of the abstract adaptor-layer classes + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorChannel.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorChannel.java new file mode 100644 index 0000000..09b8a6d --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorChannel.java @@ -0,0 +1,244 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EOAdaptorChannel { + + protected EOAdaptorContext _context; + + public EOAdaptorChannel(EOAdaptorContext context) { + super(); + _context = context; + } + + public EOAdaptorContext adaptorContext() { + return _context; + } + + public void addStoredProceduresNamed(NSArray names, EOModel model) { + } + + public abstract NSArray attributesToFetch(); + + public abstract void cancelFetch(); + + public abstract void closeChannel(); + + public abstract NSArray describeResults(); + + public abstract int deleteRowsDescribedByQualifier(EOQualifier q, EOEntity entity); + + public abstract void evaluateExpression(EOSQLExpression sql); + + public abstract void executeStoredProcedure(EOStoredProcedure proc, NSDictionary values); + + public abstract NSMutableDictionary fetchRow(); + + public abstract void insertRow(NSDictionary row, EOEntity entity); + + public abstract boolean isFetchInProgress(); + + public abstract boolean isOpen(); + + public abstract void openChannel(); + + public abstract NSDictionary returnValuesForLastStoredProcedureInvocation(); + + public abstract void selectAttributes(NSArray atts, EOFetchSpecification fspec, boolean lock, EOEntity entity); + + public abstract void setAttributesToFetch(NSArray atts); + + public abstract int updateValuesInRowsDescribedByQualifier(NSDictionary row, EOQualifier q, EOEntity entity); + + public void deleteRowDescribedByQualifier(EOQualifier q, EOEntity entity) { + adaptorContext().beginTransaction(); + int count = deleteRowsDescribedByQualifier(q, entity); + if (count != 1) { + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("Qualifier deleted " + count + " rows instead of exactly one."); + } + adaptorContext().commitTransaction(); + } + + public EOModel describeModelWithTableNames(NSArray names) { + return null; + } + + public NSArray describeStoredProcedureNames() { + return NSArray.EmptyArray; + } + + public NSArray describeTableNames() { + return NSArray.EmptyArray; + } + + public NSMutableDictionary dictionaryWithObjectsForAttributes(Object[] values, NSArray attributes) { + Object[] keys = new Object[attributes.count()]; + for (int i = 0; i < attributes.count(); i++) + keys[i] = ((EOAttribute)attributes.objectAtIndex(i)).name(); + return new NSMutableDictionary(values, keys); + } + + public void lockRowComparingAttributes(NSArray atts, EOEntity entity, EOQualifier q, NSDictionary snapshot) { + EOFetchSpecification fspec = new EOFetchSpecification(entity.name(), q, null); + adaptorContext().beginTransaction(); + selectAttributes(atts, fspec, true, entity); + if (isFetchInProgress()) { + NSDictionary row = fetchRow(); + if (row == null) { + cancelFetch(); + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("Cannot obtain row to lock. Probably modified from the outside."); + } + if (isFetchInProgress()) { + if (fetchRow() != null) { + cancelFetch(); + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("Qualifier returns more than one row."); + } + } + java.util.Enumeration enumeration = snapshot.keyEnumerator(); + while (enumeration.hasMoreElements()) { + Object key = enumeration.nextElement(); + Object svalue = snapshot.objectForKey(key); + Object rvalue = row.objectForKey(key); + if (rvalue == null) { + cancelFetch(); + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("Value for key " + key + " not found in locked row."); + } + if (!rvalue.equals(svalue)) { + cancelFetch(); + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("Value for key " + key + " differes from snapshot."); + } + } + adaptorContext().commitTransaction(); + return; + } + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("A fetch was never generated."); + } + + public void performAdaptorOperation(EOAdaptorOperation operation) { + int opcode = operation.adaptorOperator(); + switch (opcode) { + case EODatabaseOperation.AdaptorLockOperator: + if (operation.entity() == null) + throw new EOGeneralAdaptorException("A lock operation must have an entity assigned to it.", + new NSDictionary(operation, "operation")); + if (operation.qualifier() == null) + throw new EOGeneralAdaptorException("A lock operation must have a qualifier assigned to it.", + new NSDictionary(operation, "operation")); + if (operation.qualifier() == null) + throw new EOGeneralAdaptorException("A lock operation must have changedValues assigned to it.", + new NSDictionary(operation, "operation")); + lockRowComparingAttributes(operation.attributes(), operation.entity(), operation.qualifier(), operation.changedValues()); + break; + case EODatabaseOperation.AdaptorInsertOperator: + if (operation.entity() == null) + throw new EOGeneralAdaptorException("An insert operation must have an entity assigned to it.", + new NSDictionary(operation, "operation")); + if (operation.changedValues() == null) + throw new EOGeneralAdaptorException("An insert operation must have changedValues assigned to it.", + new NSDictionary(operation, "operation")); + insertRow(operation.changedValues(), operation.entity()); + break; + case EODatabaseOperation.AdaptorUpdateOperator: + if (operation.entity() == null) + throw new EOGeneralAdaptorException("An update operation must have an entity assigned to it.", + new NSDictionary(operation, "operation")); + if (operation.changedValues() == null) + throw new EOGeneralAdaptorException("An update operation must have changedValues assigned to it.", + new NSDictionary(operation, "operation")); + updateValuesInRowsDescribedByQualifier(operation.changedValues(), operation.qualifier(), operation.entity()); + break; + case EODatabaseOperation.AdaptorDeleteOperator: + if (operation.entity() == null) + throw new EOGeneralAdaptorException("A delete operation must have an entity assigned to it.", + new NSDictionary(operation, "operation")); + deleteRowsDescribedByQualifier(operation.qualifier(), operation.entity()); + break; + case EODatabaseOperation.AdaptorStoredProcedureOperator: + if (operation.storedProcedure() == null) + throw new EOGeneralAdaptorException("A stored procedure operation must have a stored procedure assigned to it.", + new NSDictionary(operation, "operation")); + executeStoredProcedure(operation.storedProcedure(), operation.changedValues()); + break; + default: + throw new EOGeneralAdaptorException("I don't know how to perform an operation with code " + opcode, + new NSDictionary(operation, "operation")); + } + } + + public void performAdaptorOperations(NSArray ops) { + for (int i = 0; i < ops.count(); i++) { + EOAdaptorOperation adop = (EOAdaptorOperation)ops.objectAtIndex(i); + performAdaptorOperation(adop); + } + } + + public NSArray primaryKeysForNewRowsWithEntity(int count, EOEntity entity) { + NSDictionary[] keys = new NSDictionary[count]; + for (int i = 0; i < count; i++) + keys[i] = NSDictionary.EmptyDictionary; + return new NSArray(keys); + } + + public void updateValuesInRowDescribedByQualifier(NSDictionary row, EOQualifier q, EOEntity entity) { + adaptorContext().beginTransaction(); + int count = updateValuesInRowsDescribedByQualifier(row, q, entity); + if (count != 1) { + adaptorContext().rollbackTransaction(); + throw new EOGeneralAdaptorException("The qualifier should describe exactly one row (updated " + count + " rows)"); + } + adaptorContext().commitTransaction(); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.1 2003/08/13 00:37:45 chochos + * an almost complete implementation of the abstract adaptor-layer classes + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorContext.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorContext.java new file mode 100644 index 0000000..aff0ddd --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorContext.java @@ -0,0 +1,118 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSNotificationCenter; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public abstract class EOAdaptorContext { + + public static final String AdaptorContextBeginTransactionNotification = "AdaptorContextBeginTransaction"; + public static final String AdaptorContextCommitTransactionNotification = "AdaptorContextCommitTransaction"; + public static final String AdaptorContextRollbackTransactionNotification = "AdaptorContextRollbackTransaction"; + + protected EOAdaptor _adaptor; + protected NSMutableArray _channels = new NSMutableArray(); + protected boolean _hasOpenTransaction; + + public EOAdaptorContext(EOAdaptor adaptor) { + super(); + _adaptor = adaptor; + } + + public EOAdaptor adaptor() { + return _adaptor; + } + + public abstract void beginTransaction(); + + public abstract void commitTransaction(); + + public abstract void rollbackTransaction(); + + public abstract EOAdaptorChannel createAdaptorChannel(); + + public abstract void handleDroppedConnection(); + + public NSArray channels() { + return new NSArray(_channels); + } + + public boolean hasBusyChannels() { + for (int i = 0; i < _channels.count(); i++) { + EOAdaptorChannel chan = (EOAdaptorChannel)_channels.objectAtIndex(i); + if (chan.isFetchInProgress()) + return true; + } + return false; + } + + public boolean hasOpenChannels() { + for (int i = 0; i < _channels.count(); i++) { + EOAdaptorChannel chan = (EOAdaptorChannel)_channels.objectAtIndex(i); + if (chan.isOpen()) + return true; + } + return false; + } + + public boolean hasOpenTransaction() { + return _hasOpenTransaction; + } + + public void transactionDidBegin() { + _hasOpenTransaction = true; + NSNotificationCenter.defaultCenter().postNotification( + AdaptorContextBeginTransactionNotification, this); + } + + public void transactionDidCommit() { + _hasOpenTransaction = false; + NSNotificationCenter.defaultCenter().postNotification( + AdaptorContextCommitTransactionNotification, this); + } + + public void transactionDidRollback() { + _hasOpenTransaction = false; + NSNotificationCenter.defaultCenter().postNotification( + AdaptorContextRollbackTransactionNotification, this); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 00:37:45 chochos + * an almost complete implementation of the abstract adaptor-layer classes + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorOperation.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorOperation.java new file mode 100644 index 0000000..818985d --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAdaptorOperation.java @@ -0,0 +1,120 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* Represents a single primitive operation in a database server. +* Can be insert, update, delete, lock a row, or execute a +* stored procedure. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOAdaptorOperation { + + protected EOEntity _entity; + protected NSArray _attributes; + protected NSDictionary _changedValues; + protected Throwable _exception; + protected EOQualifier _qualifier; + protected EOStoredProcedure _proc; + protected int _adaptorOp; + + public EOAdaptorOperation(EOEntity entity) { + super(); + _entity = entity; + } + + public void setAdaptorOperator(int adOp) { + _adaptorOp = adOp; + } + public int adaptorOperator() { + return _adaptorOp; + } + + public void setAttributes(NSArray atts) { + _attributes = atts; + } + public NSArray attributes() { + return _attributes; + } + + public void setChangedValues(NSDictionary values) { + _changedValues = values; + } + public NSDictionary changedValues() { + return _changedValues; + } + + public int compareAdaptorOperation(EOAdaptorOperation op) { + if (op.entity() != null && entity() != null) { + if (!op.entity().name().equals(entity().name())) + op.entity().name().compareTo(entity().name()); + } + if (adaptorOperator() < op.adaptorOperator()) + return -1; + if (adaptorOperator() > op.adaptorOperator()) + return 1; + return 0; + } + + public EOEntity entity() { + return _entity; + } + + public void setException(Throwable t) { + _exception = t; + } + public Throwable exception() { + return _exception; + } + + public void setQualifier(EOQualifier q) { + _qualifier = q; + } + public EOQualifier qualifier() { + return _qualifier; + } + + public void setStoredProcedure(EOStoredProcedure sp) { + _proc = sp; + } + public EOStoredProcedure storedProcedure() { + return _proc; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 00:37:45 chochos + * an almost complete implementation of the abstract adaptor-layer classes + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAttribute.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAttribute.java new file mode 100644 index 0000000..8b651ec --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOAttribute.java @@ -0,0 +1,381 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; + +/** +* Represents an attribute inside an entity. Contains mapping data for +* the attribute's external name, external and internal datatypes, etc. +* It can also represent a flattened or derived attribute, or a prototype; +* and they are also used to represent parameters in a stored procedure. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOAttribute extends EOProperty implements EOPropertyListEncoding { + + //These are used for stored procedure parameters. + public static final int Void = 0; + public static final int InParameter = 1; + public static final int OutParameter = 2; + public static final int InOutParameter = 3; + + protected EOEntity _entity; + protected String _name; + protected String _columnName; + protected String _definition; + protected String _className; + protected String _externalType; + protected Class _valueClass; + protected String _valueClassName; + protected String _valueType; + protected String _valueFactoryMethodName; + protected String _readFormat; + protected String _writeFormat; + protected String _prototypeName; + protected EOAttribute _prototype; + protected NSSelector _valueFactoryMethod; + protected boolean _allowsNull; + protected boolean _readOnly; + protected boolean _isFlattened; + protected boolean _knowsIfFlattened; + protected int _precision; + protected int _scale; + protected int _width; + protected int _parameterDirection; + protected NSDictionary _internalInfo; + protected NSDictionary _userInfo; + + protected boolean _has_allowsNull; + + public EOAttribute() { + super(); + _allowsNull = true; + } + + public EOAttribute(NSDictionary dict, Object obj) { + super(); + if (obj instanceof EOEntity) + _entity = (EOEntity)obj; + setName((String)dict.objectForKey("name")); + if (dict.objectForKey("columnName") != null) + setColumnName((String)dict.objectForKey("columnName")); + if (dict.objectForKey("definition") != null) + setDefinition((String)dict.objectForKey("definition")); + _prototypeName = (String)dict.objectForKey("prototypeName"); + setExternalType((String)dict.objectForKey("externalType")); + setClassName((String)dict.objectForKey("valueClassName")); + setValueType((String)dict.objectForKey("valueType")); + _writeFormat = (String)dict.objectForKey("writeFormat"); + _readFormat = (String)dict.objectForKey("readFormat"); + if (dict.objectForKey("precision") != null) + setPrecision(Integer.parseInt((String)dict.objectForKey("precision"))); + if (dict.objectForKey("scale") != null) + setScale(Integer.parseInt((String)dict.objectForKey("scale"))); + if (dict.objectForKey("width") != null) + setWidth(Integer.parseInt((String)dict.objectForKey("width"))); + if (dict.objectForKey("parameterDirection") != null) + setParameterDirection(Integer.parseInt((String)dict.objectForKey("parameterDirection"))); + setAllowsNull("Y".equals(dict.objectForKey("allowsNull"))); + } + + void setEntity(EOEntity value) { + _entity = value; + } + + public void setName(String name) { + _name = name; + } + public String name() { + return _name; + } + + public void setColumnName(String name) { + _columnName = name; + } + public String columnName() { + if (_columnName != null) + return _columnName; + if (prototype() != null) + if (_prototype.columnName() != null) + return _prototype.columnName(); + return null; + } + + public void setClassName(String name) { + _className = name; + } + public String className() { + if (_className != null) + return _className; + if (prototype() != null) + if (_prototype.className() != null) + return _prototype.className(); + return null; + } + + public void setDefinition(String def) { + _definition = def; + _columnName = null; + } + public String definition() { + if (_definition != null) + return _definition; + if (prototype() != null) + if (_prototype.definition() != null) + return _prototype.definition(); + return null; + } + + public void setExternalType(String type) { + _externalType = type; + } + public String externalType() { + if (_externalType != null) + return _externalType; + if (prototype() != null) + if (_prototype.externalType() != null) + return _prototype.externalType(); + return null; + } + + public void setAllowsNull(boolean flag) { + _allowsNull = flag; + _has_allowsNull = true; + } + public boolean allowsNull() { + if (_has_allowsNull) + return _allowsNull; + if (prototype() != null) + return _prototype.allowsNull(); + return _allowsNull; + } + + public void setReadOnly(boolean flag) { + _readOnly = flag; + } + public boolean readOnly() { + return _readOnly; + } + + public void setPrototype(EOAttribute proto) { + _prototype = proto; + if (proto != null) + _prototypeName = proto.name(); + else + _prototypeName = null; + } + public EOAttribute prototype() { + if (_prototypeName != null && _prototype == null) { + try { + EOModel m = _entity.model(); + EOModelGroup g = m.modelGroup(); + EOEntity protos = g.entityNamed("EO" + m.adaptorName() + "Prototypes"); + if (protos == null) + protos = g.entityNamed("EOPrototypes"); + _prototype = protos.attributeNamed(_prototypeName); + } catch (NullPointerException e) { + } + } + return _prototype; + } + + public void setPrecision(int value) { + _precision = value; + } + public int precision() { + if (_precision > 0) + return _precision; + if (prototype() != null) + return _prototype.precision(); + return _precision; + } + + public void setScale(int value) { + _scale = value; + } + public int scale() { + if (_scale > 0) + return _scale; + if (prototype() != null) + return _prototype.scale(); + return _scale; + } + + public void setWidth(int value) { + _width = value; + } + public int width() { + if (_width > 0) + return _width; + if (prototype() != null) + return _prototype.width(); + return _width; + } + + /** @deprecated Use setClassName instead. */ + public void setValueClassName(String name) { + setClassName(name); + } + /** @deprecated Use className() instead. */ + public String valueClassName() { + return className(); + } + + public void setValueType(String type) { + _valueType = type; + } + public String valueType() { + if (_valueType != null) + return _valueType; + if (prototype() != null) + return _prototype.valueType(); + return null; + } + + public void setReadFormat(String value) { + _readFormat = value; + } + public String readFormat() { + return _readFormat; + } + + public void setWriteFormat(String value) { + _writeFormat = value; + } + public String writeFormat() { + return _writeFormat; + } + + public boolean isDerived() { + return (definition() != null); + } + + /** Determines whether the receiver is a flattened attribute. + * A flattened attribute has as its definition a relationship + * path that can be resolved to an attribute. + * @return true if the receiver is flattened. + */ + public boolean isFlattened() { + if (_knowsIfFlattened) + return _isFlattened; + _knowsIfFlattened = true; + if (definition() == null) + return false; + _isFlattened = (entity()._attributeForPath(definition()) != null); + return _isFlattened; + } + + public EOEntity entity() { + return _entity; + } + + public void setParameterDirection(int dir) { + _parameterDirection = dir; + } + public int parameterDirection() { + return _parameterDirection; + } + + public String relationshipPath() { + if (isFlattened()) + return definition(); + return null; + } + + public void setUserInfo(NSDictionary value) { + _userInfo = value; + } + public NSDictionary userInfo() { + return _userInfo; + } + + public void awakeWithPropertyList(NSDictionary plist) { + } + + public void encodeIntoPropertyList(NSMutableDictionary dict) { + dict.setObjectForKey(name(), "name"); + if (_prototypeName != null) + dict.setObjectForKey(_prototypeName, "prototypeName"); + if (_columnName != null) + dict.setObjectForKey(_columnName, "columnName"); + if (_definition != null) + dict.setObjectForKey(_definition, "definition"); + if (_className != null) + dict.setObjectForKey(_className, "valueClassName"); + if (_valueType != null) + dict.setObjectForKey(_valueType, "valueType"); + if (_precision > 0) + dict.setObjectForKey(new Integer(_precision), "precision"); + if (_scale > 0) + dict.setObjectForKey(new Integer(_scale), "scale"); + if (_width > 0) + dict.setObjectForKey(new Integer(_width), "width"); + if (_externalType != null) + dict.setObjectForKey(_externalType, "externalType"); + if (_readFormat != null) + dict.setObjectForKey(_readFormat, "readFormat"); + if (_writeFormat != null) + dict.setObjectForKey(_writeFormat, "writeFormat"); + if (_allowsNull) + dict.setObjectForKey("Y", "allowsNull"); + if (_entity == null) + dict.setObjectForKey(new Integer(parameterDirection()), "parameterDirection"); + if (userInfo() != null && userInfo().count() > 0) + dict.setObjectForKey(userInfo(), "userInfo"); + if (_internalInfo != null && _internalInfo.count() > 0) + dict.setObjectForKey(_internalInfo, "internalInfo"); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2003/08/14 02:13:56 chochos + * implement and use _attributeForPath() + * + * Revision 1.6 2003/08/12 01:45:04 chochos + * added some code to handle prototypes + * + * Revision 1.5 2003/08/11 19:38:27 chochos + * Can now read from a file and re-write to another file. + * + * Revision 1.4 2003/08/09 01:35:35 chochos + * implement EOPropertyListEncoding + * + * Revision 1.3 2003/08/08 06:52:09 chochos + * isFlattened() works + * + * Revision 1.2 2003/08/08 02:15:03 chochos + * added parameterDirection (for use with stored procedures) + * + * Revision 1.1 2003/08/07 02:39:45 chochos + * EOAttribute. Can be initialized from a property list. + * +*/ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabase.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabase.java new file mode 100644 index 0000000..a9177c5 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabase.java @@ -0,0 +1,267 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import java.util.Enumeration; + +import net.wotonomy.control.EOEnterpriseObject; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSTimestamp; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EODatabase { + + protected EOAdaptor _adaptor; + protected NSMutableArray _models = new NSMutableArray(); + protected NSMutableArray _contexts = new NSMutableArray(); + protected NSMutableDictionary _resultCache = new NSMutableDictionary(); + protected NSMutableDictionary _snapshots = new NSMutableDictionary(); + protected NSTimestamp _timestamp; + protected static boolean _releaseUnrefSnapshots = true; + + public EODatabase(EOAdaptor adaptor) { + super(); + if (adaptor == null) + throw new IllegalArgumentException("Adaptor cannot be null."); + _adaptor = adaptor; + } + + public EODatabase(EOModel model) { + super(); + _adaptor = EOAdaptor.adaptorWithModel(model); + addModel(model); + } + + public EOAdaptor adaptor() { + return _adaptor; + } + + public void addModel(EOModel model) { + if (!addModelIfCompatible(model)) + throw new IllegalArgumentException("Model is not compatible with this database."); + } + + public void removeMode(EOModel model) { + _models.removeObject(model); + } + + public boolean addModelIfCompatible(EOModel model) { + if (_models.containsObject(model)) + return false; + if (model.adaptorName().equals(adaptor().name())) { + if (adaptor().canServiceModel(model)) { + _models.addObject(model); + return true; + } + } + return false; + } + + public void decrementSnapshotCountForGlobalID(EOGlobalID gid) { + if (_releaseUnrefSnapshots) { + } + } + + public void incrementSnapshotCountForGlobalID(EOGlobalID gid) { + if (_releaseUnrefSnapshots) { + } + } + + public static void disableSnapshotRefCounting() { + _releaseUnrefSnapshots = false; + } + + public EOEntity entityForObject(EOEnterpriseObject eo) { + String cname = eo.getClass().getName(); + for (int i = 0; i < _models.count(); i++) { + EOModel m = (EOModel)_models.objectAtIndex(i); + NSArray ents = m.entities(); + for (int j = 0; j < ents.count(); j++) { + EOEntity e = (EOEntity)ents.objectAtIndex(i); + if (e.className().equals(cname)) + return e; + } + } + return null; + } + + public EOEntity entityNamed(String name) { + for (int i = 0; i < _models.count(); i++) { + EOModel m = (EOModel)_models.objectAtIndex(i); + NSArray ents = m.entities(); + for (int j = 0; j < ents.count(); j++) { + EOEntity e = (EOEntity)ents.objectAtIndex(i); + if (e.name().equals(name)) + return e; + } + } + return null; + } + + public void forgetAllSnapshots() { + _snapshots.removeAllObjects(); + } + + public void forgetSnapshotForGlobalID(EOGlobalID gid) { + _snapshots.removeObjectForKey(gid); + } + + public void forgetSnapshotsForGlobalIDs(NSArray gids) { + for (int i = 0; i < gids.count(); i++) + forgetSnapshotForGlobalID((EOGlobalID)gids.objectAtIndex(i)); + } + + public void handleDroppedConnection() { + adaptor().handleDroppedConnection(); + for (int i = 0; i < _contexts.count(); i++) { + EODatabaseContext c = (EODatabaseContext)_contexts.objectAtIndex(i); + c.handleDroppedConnection(); + } + } + + public void invalidateResultCache() { + _resultCache.removeAllObjects(); + } + + public void invalidateResultCacheForEntityNamed(String name) { + _resultCache.removeObjectForKey(name); + } + + public NSArray models() { + return new NSArray(_models); + } + + public void recordSnapshotForGlobalID(NSDictionary snap, EOGlobalID gid) { + _snapshots.setObjectForKey(snap, gid); + } + + public void recordSnapshotForSourceGlobalID(NSArray gids, EOGlobalID gid, String name) { + NSMutableDictionary d = (NSMutableDictionary)_snapshots.objectForKey(gid); + if (d == null) { + d = new NSMutableDictionary(); + _snapshots.setObjectForKey(d, gid); + } + d.setObjectForKey(gids, name); + } + + public void recordSnapshots(NSDictionary snaps) { + _snapshots.addEntriesFromDictionary(snaps); + } + + public void recordToManySnapshots(NSDictionary snaps) { + Enumeration enumeration = snaps.keyEnumerator(); + while (enumeration.hasMoreElements()) { + EOGlobalID gid = (EOGlobalID)enumeration.nextElement(); + NSDictionary rels = (NSDictionary)snaps.objectForKey(gid); + Enumeration relEnum = rels.keyEnumerator(); + while (relEnum.hasMoreElements()) { + String relName = (String)relEnum.nextElement(); + NSArray gids = (NSArray)rels.objectForKey(relName); + recordSnapshotForSourceGlobalID(gids, gid, relName); + } + } + } + + public void registerContext(EODatabaseContext context) { + if (!_contexts.contains(context)) { + if (context.database() != this) + throw new IllegalStateException("Cannot register context assigned to a different database."); + _contexts.addObject(context); + } + } + + public void unregisterContext(EODatabaseContext context) { + _contexts.removeObject(context); + } + + public NSArray registeredContexts() { + return new NSArray(_contexts); + } + + public NSArray resultCacheForEntityNamed(String name) { + return (NSArray)_resultCache.objectForKey(name); + } + + public void setResultCache(NSArray cache, String entityName) { + _resultCache.setObjectForKey(cache, entityName); + } + + public void setTimestampToNow() { + _timestamp = new NSTimestamp(); + } + + public NSDictionary snapshotForGlobalID(EOGlobalID gid) { + return (NSDictionary)_snapshots.objectForKey(gid); + } + + public NSDictionary snapshotForGlobalID(EOGlobalID gid, long l) { + return null; + } + + public NSArray snapshotForSourceGlobalID(EOGlobalID gid, String name) { + NSDictionary d = (NSDictionary)_snapshots.objectForKey(gid); + if (d == null) + return null; + return (NSArray)d.objectForKey(name); + } + + public NSDictionary snapshotForSourceGlobalID(EOGlobalID gid, String s, long l) { + return null; + } + + public NSDictionary snapshots() { + return _snapshots; + } + + public long timestampForGlobalID(EOGlobalID gid) { + return NSTimestamp.DistantPast.timeIntervalSinceReferenceDate(); + } + + public long timestampForSourceGlobalID(EOGlobalID gid, String s) { + return 0; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.1 2003/08/19 01:54:43 chochos + * The EODatabase layer still needs a lot of work, but it's on its way... + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseChannel.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseChannel.java new file mode 100644 index 0000000..10424e9 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseChannel.java @@ -0,0 +1,138 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOKeyValueCodingAdditions; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EODatabaseChannel { + + protected EODatabaseContext _context; + protected EOAdaptorChannel _channel; + protected EOEntity _currEntity; + protected EOEditingContext _currEC; + protected NSArray _attributes; + protected boolean _locking; + protected boolean _refreshing; + + public EODatabaseChannel(EODatabaseContext context) { + super(); + _context = context; + _channel = _context.adaptorContext().createAdaptorChannel(); + } + + public EOAdaptorChannel adaptorChannel() { + return _channel; + } + + public EODatabaseContext databaseContext() { + return _context; + } + + public void cancelFetch() { + _channel.cancelFetch(); + } + + public Object fetchObject() { + NSDictionary r = _channel.fetchRow(); + EOGlobalID gid = _currEntity.globalIDForRow(r); + Object eo = _currEC.objectForGlobalID(gid); + if (eo == null) { + eo = EOClassDescription.classDescriptionForEntityName(_currEntity.name()).createInstanceWithEditingContext(_currEC, gid); + if (eo instanceof EOKeyValueCodingAdditions) + ((EOKeyValueCodingAdditions)eo).takeValuesFromDictionary(r); + else + EOKeyValueCodingAdditions.DefaultImplementation.takeValuesFromDictionary(eo, r); + } else { + if (isRefreshingObjects()) { + //TODO: refresh object (how?) + } + } + return eo; + } + + public boolean isFetchInProgress() { + return _channel.isFetchInProgress(); + } + + public void setIsLocking(boolean flag) { + _locking = flag; + } + + public boolean isLocking() { + return _locking; + } + + public void setIsRefreshingObjects(boolean flag) { + _refreshing = flag; + } + + public boolean isRefreshingObjects() { + return _refreshing; + } + + public void selectObjectsWithFetchSpecification(EOFetchSpecification fspec, EOEditingContext ec) { + setIsLocking(fspec.locksObjects()); + setIsRefreshingObjects(fspec.refreshesRefetchedObjects()); + setCurrentEditingContext(ec); + setCurrentEntity(databaseContext().database().entityNamed(fspec.entityName())); + NSMutableArray atts = new NSMutableArray(); + atts.addObjectsFromArray(_currEntity.attributes()); + adaptorChannel().selectAttributes(atts, fspec, isLocking(), _currEntity); + adaptorChannel().setAttributesToFetch(atts); + _attributes = atts; + } + + public void setCurrentEditingContext(EOEditingContext ec) { + _currEC = ec; + } + + public void setCurrentEntity(EOEntity entity) { + _currEntity = entity; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/29 21:15:46 chochos + * use EOEntity's globalIDForRow method. + * + * Revision 1.1 2003/08/19 01:54:43 chochos + * The EODatabase layer still needs a lot of work, but it's on its way... + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseContext.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseContext.java new file mode 100644 index 0000000..af696fe --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseContext.java @@ -0,0 +1,568 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import net.wotonomy.control.EOAndQualifier; +import net.wotonomy.control.EOCooperatingObjectStore; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOEnterpriseObject; +import net.wotonomy.control.EOFaultHandler; +import net.wotonomy.control.EOFaulting; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOKeyGlobalID; +import net.wotonomy.control.EOKeyValueCoding; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.control.EOKeyValueQualifier; +import net.wotonomy.control.EOObjectStoreCoordinator; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSLocking; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EODatabaseContext + extends EOCooperatingObjectStore implements NSLocking { + + private static Class _contextClass; + protected EODatabase _database; + protected EOAdaptorContext _context; + protected NSMutableArray _channels = new NSMutableArray(); + protected NSMutableArray _lockedObjects = new NSMutableArray(); + + protected NSMutableDictionary _simpleSnaps; + protected NSMutableDictionary _manySnaps; + + protected EOObjectStoreCoordinator _coordinator; + protected EOEditingContext _currEC; + protected int _updateStrategy; + + public EODatabaseContext(EODatabase database) { + super(); + _database = database; + _context = _database.adaptor().createAdaptorContext(); + } + + public EOAdaptorContext adaptorContext() { + return _context; + } + + public EODatabase database() { + return _database; + } + + public EODatabaseChannel availableChannel() { + for (int i = 0; i < _channels.count(); i++) { + EODatabaseChannel c = (EODatabaseChannel)_channels.objectAtIndex(i); + if (!c.isFetchInProgress()) + return c; + } + EODatabaseChannel c = new EODatabaseChannel(this); + registerChannel(c); + return c; + } + + public void batchFetchRelationship(EORelationship rel, NSArray arr, EOEditingContext ec) { + } + + public static void setContextClassToRegister(Class contextClass) { + _contextClass = contextClass; + } + public static Class contextClassToRegister() { + if (_contextClass == null) + _contextClass = EODatabaseContext.class; + return _contextClass; + } + + public EOObjectStoreCoordinator coordinator() { + return _coordinator; + } + + public void editingContextDidForgetObjectWithGlobalID(EOEditingContext ec, EOGlobalID gid) { + database().decrementSnapshotCountForGlobalID(gid); + } + + public void handleDroppedConnection() { + //TODO: unregister channels + adaptorContext().handleDroppedConnection(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#ownsGlobalID(net.wotonomy.control.EOGlobalID) + */ + public boolean ownsGlobalID(EOGlobalID gid) { + if (!(gid instanceof EOKeyGlobalID)) + return false; + return (database().entityNamed(((EOKeyGlobalID)gid).entityName()) != null); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#ownsObject(net.wotonomy.control.EOEnterpriseObject) + */ + public boolean ownsObject(EOEnterpriseObject eo) { + if (eo.entityName() == null) + return false; + return (database().entityNamed(eo.entityName()) != null); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#handlesFetchSpecification(net.wotonomy.control.EOFetchSpecification) + */ + public boolean handlesFetchSpecification(EOFetchSpecification fspec) { + String ename = fspec.entityName(); + return (database().entityNamed(ename) != null); + } + + public boolean hasBusyChannels() { + return adaptorContext().hasBusyChannels(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#prepareForSaveWithCoordinator(net.wotonomy.control.EOObjectStoreCoordinator, net.wotonomy.control.EOEditingContext) + */ + public void prepareForSaveWithCoordinator(EOObjectStoreCoordinator coord, EOEditingContext ec) { + // TODO Auto-generated method stub + _coordinator = coord; + _currEC = ec; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#recordChangesInEditingContext() + */ + public void recordChangesInEditingContext() { + // TODO insert, delete, update + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#recordUpdateForObject(net.wotonomy.control.EOEnterpriseObject, net.wotonomy.foundation.NSDictionary) + */ + public void recordUpdateForObject(EOEnterpriseObject eo, NSDictionary changes) { + // TODO Auto-generated method stub + } + + public void recordSnapshotForGlobalID(NSDictionary snap, EOGlobalID gid) { + if (_simpleSnaps == null) + throw new IllegalArgumentException("Attempt to record a snapshot without a transaction in progress"); + _simpleSnaps.setObjectForKey(snap, gid); + } + + public void recordSnapshotForSourceGlobalID(NSArray gids, EOGlobalID gid, String relationName) { + if (_manySnaps == null) + throw new IllegalArgumentException("Attempt to record a snapshot without a transaction in progress"); + NSMutableDictionary d = (NSMutableDictionary)_manySnaps.objectForKey(gid); + if (d == null) { + d = new NSMutableDictionary(); + _manySnaps.setObjectForKey(d, gid); + } + d.setObjectForKey(gids, relationName); + } + + public void recordSnapshots(NSDictionary snaps) { + if (_simpleSnaps == null) + throw new IllegalArgumentException("Attempt to record snapshots without a transaction in progress."); + _simpleSnaps.addEntriesFromDictionary(snaps); + /* Make sure we don't need to do this instead + Enumeration enumeration = snaps.keyEnumerator(); + while (enumeration.hasMoreElements()) { + EOGlobalID g = (EOGlobalID)enumeration.nextElement(); + NSDictionary d = (NSDictionary)snaps.objectForKey(g); + recordSnapshotForGlobalID(d, g); + }*/ + } + + public void recordToManySnapshots(NSDictionary snaps) { + if (_manySnaps == null) + throw new IllegalArgumentException("Attempt to record snapshots without a transaction in progress."); + Enumeration enumeration = snaps.keyEnumerator(); + while (enumeration.hasMoreElements()) { + Object key = enumeration.nextElement(); + NSDictionary d = (NSDictionary)snaps.objectForKey(key); + NSMutableDictionary d2 = (NSMutableDictionary)_manySnaps.objectForKey(key); + if (d2 == null) { + d2 = new NSMutableDictionary(); + _manySnaps.setObjectForKey(d2, key); + } + //this could also be done with many calls to recordSnapshotForSourceGID + d2.addEntriesFromDictionary(d); + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#performChanges() + */ + public void performChanges() { + // TODO Auto-generated method stub + + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#commitChanges() + */ + public void commitChanges() { + adaptorContext().commitTransaction(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#rollbackChanges() + */ + public void rollbackChanges() { + adaptorContext().rollbackTransaction(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOCooperatingObjectStore#valuesForKeys(net.wotonomy.foundation.NSArray, net.wotonomy.control.EOEnterpriseObject) + */ + public NSDictionary valuesForKeys(NSArray keys, EOEnterpriseObject eo) { + // TODO check snapshots; eo could be a fault + return eo.valuesForKeys(keys); + } + + /* (non-Javadoc) + * @see net.wotonomy.foundation.NSLocking#lock() + */ + public void lock() { + EOAccessLock.lock(); + } + + /* (non-Javadoc) + * @see net.wotonomy.foundation.NSLocking#unlock() + */ + public void unlock() { + EOAccessLock.unlock(); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#arrayFaultWithSourceGlobalID(net.wotonomy.control.EOGlobalID, java.lang.String, net.wotonomy.control.EOEditingContext) + */ + public NSArray arrayFaultWithSourceGlobalID( + EOGlobalID gid, String relName, EOEditingContext ec) { + if (!(gid instanceof EOKeyGlobalID)) + throw new IllegalArgumentException("an EOKeyGlobalID is needed."); + EOAccessArrayFaultHandler handler = new EOAccessArrayFaultHandler((EOKeyGlobalID)gid, relName, this, ec); + return new NSArray(handler); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#faultForGlobalID(net.wotonomy.control.EOGlobalID, net.wotonomy.control.EOEditingContext) + */ + public /*EOEnterpriseObject*/Object faultForGlobalID(EOGlobalID gid, EOEditingContext ec) { + if (!(gid instanceof EOKeyGlobalID)) + throw new IllegalArgumentException("Cannot fault an object that doesn't have a key global ID."); + EOAccessFaultHandler handler = new EOAccessFaultHandler((EOKeyGlobalID)gid, this, ec); + EOEntity e = database().entityNamed(((EOKeyGlobalID)gid).entityName()); + Object o = e.classDescriptionForInstances().createInstanceWithEditingContext(ec, gid); + EOFaultHandler.makeObjectIntoFault(o, handler); + return o; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#faultForRawRow(java.util.Map, java.lang.String, net.wotonomy.control.EOEditingContext) + */ + public /*EOEnterpriseObject*/ Object faultForRawRow(Map row, String entityName, EOEditingContext ec) { + EOEntity e = database().entityNamed(entityName); + EOGlobalID gid = e.globalIDForRow(row); + return faultForGlobalID(gid, ec); + } + + public void forgetSnapshotForGlobalID(EOGlobalID gid) { + if (_simpleSnaps == null) + throw new IllegalArgumentException("Attempt to forget snapshot with no transaction in progress."); + _simpleSnaps.removeObjectForKey(gid); + _manySnaps.removeObjectForKey(gid); + } + + public void forgetSnapshotsForGlobalIDs(List gids) { + for (int i = 0; i < gids.size(); i++) { + EOGlobalID g = (EOGlobalID)gids.get(i); + forgetSnapshotForGlobalID(g); + database().forgetSnapshotForGlobalID(g); + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#initializeObject(java.lang.Object, net.wotonomy.control.EOGlobalID, net.wotonomy.control.EOEditingContext) + */ + public void initializeObject(/*EOEnterpriseObject*/Object eo, EOGlobalID gid, EOEditingContext ec) { + if (gid.isTemporary()) + return; + NSDictionary snap = snapshotForGlobalID(gid); + Object obj = ec.objectForGlobalID(gid); + EOEntity e = database().entityNamed(((EOKeyGlobalID)gid).entityName()); + NSArray props = e.classProperties(); + for (int i = 0; i < props.count(); i++) { + EOProperty p = (EOProperty)props.objectAtIndex(i); + Object val = snap.objectForKey(p.name()); + if (p instanceof EOAttribute) { + if ( eo instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)eo).takeValueForKey(val, p.name()); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( eo, val, p.name() ); + } + } else if (p instanceof EORelationship) { + if (((EORelationship)p).isToMany()) { + val = arrayFaultWithSourceGlobalID(gid, p.name(), ec); + } else { + EOEntity dest = ((EORelationship)p).destinationEntity(); + EOKeyGlobalID kgid = (EOKeyGlobalID)dest.globalIDForRow(snap); + val = ec.objectForGlobalID(kgid); + if (val == null) + val = new EOAccessFaultHandler(kgid, this, ec); + } + if (val == EOKeyValueCoding.NullValue) + val = null; + if ( eo instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)eo).takeValueForKey(val, p.name()); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( eo, val, p.name() ); + } + } + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#invalidateAllObjects() + */ + public void invalidateAllObjects() { + invalidateObjectsWithGlobalIDs(database().snapshots().allKeys()); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#invalidateObjectsWithGlobalIDs(java.util.List) + */ + public void invalidateObjectsWithGlobalIDs(List aList) { + forgetSnapshotsForGlobalIDs(aList); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#isObjectLockedWithGlobalID(net.wotonomy.control.EOGlobalID, net.wotonomy.control.EOEditingContext) + */ + public boolean isObjectLockedWithGlobalID(EOGlobalID gid, EOEditingContext ec) { + return isObjectLockedWithGlobalID(gid); + } + + public boolean isObjectLockedWithGlobalID(EOGlobalID gid) { + return _lockedObjects.containsObject(gid); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#lockObjectWithGlobalID(net.wotonomy.control.EOGlobalID, net.wotonomy.control.EOEditingContext) + */ + public void lockObjectWithGlobalID(EOGlobalID gid, EOEditingContext ec) { + NSDictionary snap = snapshotForGlobalID(gid); + if (snap == null) + return; + if (!(gid instanceof EOKeyGlobalID)) + return; + EOEntity e = database().entityNamed(((EOKeyGlobalID)gid).entityName()); + EOQualifier q = e.qualifierForPrimaryKey(snap); + EOFetchSpecification fspec = new EOFetchSpecification(e.name(), q, null); + fspec.setLocksObjects(true); + NSArray arr = ec.objectsWithFetchSpecification(fspec); + if (arr.count() != 1) + throw new IllegalStateException("Cannot lock object with Global ID " + gid); + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#objectsForSourceGlobalID(net.wotonomy.control.EOGlobalID, java.lang.String, net.wotonomy.control.EOEditingContext) + */ + public NSArray objectsForSourceGlobalID( + EOGlobalID gid, String relationName, EOEditingContext ec) { + EOEnterpriseObject eo = (EOEnterpriseObject)ec.objectForGlobalID(gid); + if (eo == null) + throw new IllegalStateException("Cannot find object for global ID " + gid + " in specified editing context."); + //Get the source object + EOEnterpriseObject source = (EOEnterpriseObject) faultForGlobalID(gid, ec); + if (source == null) + throw new IllegalStateException("There is no snapshot for source global ID " + gid); + + //Check if there is already a value here + NSArray value = (NSArray)source.valueForKey(relationName); + EOAccessArrayFaultHandler handler = null; + if (value != null) { + if (EOFaultHandler.isFault(value)) { + handler = new EOAccessArrayFaultHandler((EOKeyGlobalID)gid, relationName, this, ec); + //TODO: fire the fault an return the value + } else + return value; + } + + //Get the relationship + EOEntity entity = database().entityNamed(eo.entityName()); + EORelationship rel = entity.relationshipNamed(relationName); + if (rel == null) + throw new IllegalStateException("Cannot find relationship named " + relationName + " in entity " + entity.name()); + + //create a fetch specification for this + EOQualifier q = null; + NSArray joins = rel.joins(); + NSMutableArray subq = new NSMutableArray(joins.count()); + for (int i = 0; i < joins.count(); i++) { + EOJoin j = (EOJoin)joins.objectAtIndex(i); + String key = j.destinationAttribute().name(); + Object val = eo.valueForKey(j.sourceAttribute().name()); + subq.addObject(new EOKeyValueQualifier(key, EOQualifier.QualifierOperatorEqual, val)); + } + if (subq.count() == 1) { + q = (EOQualifier)subq.objectAtIndex(0); + } else { + q = new EOAndQualifier(subq); + } + EOFetchSpecification fspec = new EOFetchSpecification(rel.destinationEntity().name(), q, null); + NSArray res = ec.objectsWithFetchSpecification(fspec, ec); + NSMutableArray gids = new NSMutableArray(res.count()); + for (int i = 0; i < res.count(); i++) + gids.addObject(ec.globalIDForObject(res.objectAtIndex(i))); + recordSnapshotForSourceGlobalID(gids, gid, relationName); + return res; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#objectsWithFetchSpecification(net.wotonomy.control.EOFetchSpecification, net.wotonomy.control.EOEditingContext) + */ + public NSArray objectsWithFetchSpecification(EOFetchSpecification fspec, EOEditingContext ec) { + EODatabaseChannel channel = availableChannel(); + channel.selectObjectsWithFetchSpecification(fspec, ec); + NSMutableArray arr = new NSMutableArray(); + while (channel.isFetchInProgress()) { + Object o = channel.fetchObject(); + if (o != null) { + arr.addObject(o); + } + } + return arr; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#refaultObject(java.lang.Object, net.wotonomy.control.EOGlobalID, net.wotonomy.control.EOEditingContext) + */ + public void refaultObject(Object obj, EOGlobalID gid, EOEditingContext ec) { + if (!(gid instanceof EOKeyGlobalID)) + throw new IllegalArgumentException("GlobalID must be an EOKeyGlobalID"); + if (obj instanceof EOFaulting) + //check if it's already a fault + if (!EOFaultHandler.isFault(obj)) { + ((EOFaulting)obj).turnIntoFault(EOFaultHandler.handlerForFault(obj)); + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOObjectStore#saveChangesInEditingContext(net.wotonomy.control.EOEditingContext) + */ + public void saveChangesInEditingContext(EOEditingContext ec) { + prepareForSaveWithCoordinator(null, ec); + recordChangesInEditingContext(); + performChanges(); + commitChanges(); + } + + public void registerChannel(EODatabaseChannel channel) { + if (channel.databaseContext() != this) + throw new IllegalArgumentException("Cannot register a channel on a context other than its own."); + if (_channels.containsObject(channel)) + throw new IllegalArgumentException("Attempt to register a channel more than once."); + _channels.addObject(channel); + } + + public void unregisterChannel(EODatabaseChannel channel) { + if (channel.databaseContext() != this) + throw new IllegalArgumentException("Attempt to unregister a channel from a context other than its own."); + _channels.removeObject(channel); + } + + public NSArray registeredChannels() { + return new NSArray(_channels); + } + + public NSDictionary snapshotForGlobalID(EOGlobalID gid) { + NSDictionary d = null; + if (_simpleSnaps != null) { + d = (NSDictionary)_simpleSnaps.objectForKey(gid); + } + if (d == null) + d = database().snapshotForGlobalID(gid); + return d; + } + + public NSArray snapshotForSourceGlobalID(EOGlobalID gid, String name) { + NSArray a = null; + if (_manySnaps != null) { + NSDictionary d = (NSDictionary)_manySnaps.objectForKey(gid); + a = (NSArray)d.objectForKey(name); + } + if (a == null) + a = database().snapshotForSourceGlobalID(gid, name); + return a; + } + + public void setUpdateStrategy(int strategy) { + _updateStrategy = strategy; + } + + public int updateStrategy() { + return _updateStrategy; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.4 2003/12/18 15:37:38 mpowers + * Changes to retain ability to work with objects that don't necessarily + * implement EOEnterpriseObject. I would still like to preserve this case + * for general usage, however the access package is free to assume that + * those objects will be EOs and cast appropriately. + * + * Revision 1.3 2003/08/29 21:14:44 chochos + * implement a couple more methods. + * + * Revision 1.2 2003/08/20 01:16:22 chochos + * more methods have code now, but there's no way to test this yet. The core is still pending. + * + * Revision 1.1 2003/08/19 01:54:43 chochos + * The EODatabase layer still needs a lot of work, but it's on its way... + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseOperation.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseOperation.java new file mode 100644 index 0000000..596180e --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EODatabaseOperation.java @@ -0,0 +1,48 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EODatabaseOperation { + + public static final int AdaptorLockOperator = 0; + public static final int AdaptorInsertOperator = 1; + public static final int AdaptorUpdateOperator = 2; + public static final int AdaptorDeleteOperator = 3; + public static final int AdaptorStoredProcedureOperator = 4; + + public static final int DatabaseNothingOperator = 0; + public static final int DatabaseInsertOperator = 1; + public static final int DatabaseUpdateOperator = 2; + public static final int DatabaseDeleteOperator = 3; + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 00:38:47 chochos + * for the moment, this only contains some constants needed by EOAdaptorChannel and EOAdaptorOperation. + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntity.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntity.java new file mode 100644 index 0000000..4adc4a1 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntity.java @@ -0,0 +1,637 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Map; + +import net.wotonomy.control.EOAndQualifier; +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGenericRecord; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOIntegralKeyGlobalID; +import net.wotonomy.control.EOKeyGlobalID; +import net.wotonomy.control.EOKeyValueArchiver; +import net.wotonomy.control.EOKeyValueQualifier; +import net.wotonomy.control.EOKeyValueUnarchiver; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.EOVectorKeyGlobalID; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSPropertyListSerialization; + +/** +* An EOEntity is a mapping between a Java class and a database table or view. +* It indicates which attributes should be fetched from the table/view, what +* attributes are part of the primary key, what class the entity should map to. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOEntity implements EOPropertyListEncoding { + + protected NSMutableDictionary _attributes = new NSMutableDictionary(); + protected NSMutableDictionary _relations = new NSMutableDictionary(); + private NSMutableArray _classPropertyNames = new NSMutableArray(); + private NSMutableArray _classProperties = new NSMutableArray(); + private NSMutableArray _classPropertyAttributes = new NSMutableArray(); + private NSMutableArray _classPropertyManyRelationships = new NSMutableArray(); + private NSMutableArray _classPropertyOneRelationships = new NSMutableArray(); + protected NSArray _pkAttributes = NSArray.EmptyArray; + protected NSArray _pkAttributeNames = NSArray.EmptyArray; + protected NSMutableDictionary _fetchSpecs = new NSMutableDictionary(); + protected NSMutableArray _lockingAttributes = new NSMutableArray(); + + protected String _className; + protected String _name; + protected String _externalName; + protected boolean _isAbstract; + protected boolean _isReadOnly; + protected EOModel _model; + protected NSDictionary _userInfo; + protected NSDictionary _internalInfo; + private boolean _loadedFetchSpecs; + + public EOEntity() { + super(); + } + + public EOEntity(NSDictionary dict, Object obj) { + super(); + _model = (EOModel)obj; + setName((String)dict.objectForKey("name")); + setExternalName((String)dict.objectForKey("externalName")); + setClassName((String)dict.objectForKey("className")); + if (dict.objectForKey("internalInfo") != null) + _internalInfo = (NSDictionary)dict.objectForKey("internalInfo"); + if (dict.objectForKey("userInfo") != null) + _userInfo = (NSDictionary)dict.objectForKey("userInfo"); + + //Read the attributes + NSArray atr = (NSArray)dict.objectForKey("attributes"); + for (int i = 0; i < atr.count(); i++) { + NSDictionary d = (NSDictionary)atr.objectAtIndex(i); + EOAttribute atrib = new EOAttribute(d, this); + addAttribute(atrib); + } + + //Set the primary key + atr = (NSArray)dict.objectForKey("primaryKeyAttributes"); + NSMutableArray pka = new NSMutableArray(); + for (int i = 0; i < atr.count(); i++) { + EOAttribute a = attributeNamed((String)atr.objectAtIndex(i)); + pka.addObject(a); + } + _pkAttributes = new NSArray(pka); + _pkAttributeNames = atr; + + //attributes used for locking + _lockingAttributes.removeAllObjects(); + atr = (NSArray)dict.objectForKey("attributesUsedForLocking"); + for (int i = 0; i < atr.count(); i++) { + String x = (String)atr.objectAtIndex(i); + EOAttribute a = attributeNamed(x); + _lockingAttributes.addObject(a); + } + + //class properties + atr = (NSArray)dict.objectForKey("classProperties"); + if (atr != null) { + for (int i = 0; i < atr.count(); i++) + if (!_classPropertyNames.containsObject((atr.objectAtIndex(i)))) + _classPropertyNames.addObject(atr.objectAtIndex(i)); + } + + //Read the relationships + atr = (NSArray)dict.objectForKey("relationships"); + if (atr != null) { + for (int i = 0; i < atr.count(); i++) { + NSDictionary d = (NSDictionary)atr.objectAtIndex(i); + EORelationship rel = new EORelationship(d, this); + addRelationship(rel); + } + } + } + + public void addAttribute(EOAttribute atr) { + if (atr.name() == null) + throw new IllegalArgumentException("Cannot add an unnamed attribute to an entity."); + if (_attributes.objectForKey(atr.name()) != null) + throw new IllegalArgumentException("Entity " + name() + " already has an attribute named " + atr.name()); + _attributes.setObjectForKey(atr, atr.name()); + atr.setEntity(this); + _lockingAttributes.addObject(atr); + _classProperties.addObject(atr); + _classPropertyNames.addObject(atr.name()); + _classPropertyAttributes.addObject(atr); + } + + public void removeAttribute(EOAttribute atr) { + _attributes.removeObjectForKey(atr.name()); + atr.setEntity(null); + _classProperties.removeObject(atr); + _classPropertyNames.removeObject(atr.name()); + _classPropertyAttributes.removeObject(atr); + } + + public void addFetchSpecification(EOFetchSpecification fspec, String name) { + loadFetchSpecifications(); + if (_fetchSpecs.objectForKey(name) != null) + throw new IllegalArgumentException("Entity " + name() + " already has a fetch specification named " + name); + _fetchSpecs.setObjectForKey(fspec, name); + } + + public void removeFetchSpecificationNamed(String name) { + _fetchSpecs.removeObjectForKey(name); + } + + public EOFetchSpecification fetchSpecificationNamed(String name) { + loadFetchSpecifications(); + return (EOFetchSpecification)_fetchSpecs.objectForKey(name); + } + + public NSArray fetchSpecificationNames() { + loadFetchSpecifications(); + return _fetchSpecs.allKeys(); + } + + /** Loads fetch specifications from the .fspec file, + * if one exists. + */ + private void loadFetchSpecifications() { + if (_loadedFetchSpecs) + return; + _loadedFetchSpecs = true; + if (model().path() == null) + return; + File f = new File(model().path()); + //Read the fetch specification file, if it exists + f = new File(f, name() + ".fspec"); + if (!f.exists()) + return; + NSDictionary fdict = null; + String x = null; + try { + FileInputStream fin = new FileInputStream(f); + byte[] b = new byte[fin.available()]; + fin.read(b); + fin.close(); + x = new String(b); + } catch (IOException ex) { + throw new IllegalArgumentException("Cannot read file " + f); + } + fdict = NSPropertyListSerialization.dictionaryForString(x); + if (fdict == null) + throw new IllegalArgumentException("Cannot read dictionary from " + f); + NSArray keys = fdict.allKeys(); + + //Unarchive the fetch specification + EOKeyValueUnarchiver unarch = new EOKeyValueUnarchiver(fdict); + for (int i = 0; i < keys.count(); i++) { + String k = (String)keys.objectAtIndex(i); + EOFetchSpecification fs = (EOFetchSpecification)unarch.decodeObjectForKey(k); + if (fs != null) + _fetchSpecs.setObjectForKey(fs, k); + } + } + + public NSArray attributes() { + return _attributes.allValues(); + } + + public EOAttribute attributeNamed(String name) { + return (EOAttribute)_attributes.objectForKey(name); + } + + public NSArray flattenedAttributes() { + return null; + } + + public void setClassName(String name) { + _className = name; + } + public String className() { + return _className; + } + + public void setName(String name) { + _name = name; + } + public String name() { + return _name; + } + + public void setExternalName(String name) { + _externalName = name; + } + public String externalName() { + return _externalName; + } + + public void addRelationship(EORelationship rel) { + if (rel.name() == null) + throw new IllegalArgumentException("Cannot add an unnamed relationship to an entity."); + if (_relations.objectForKey(rel.name()) != null) + throw new IllegalArgumentException("Entity " + name() + " already has a relationship named " + rel.name()); + if (_attributes.objectForKey(rel.name()) != null) + throw new IllegalArgumentException("Entity " + name() + " has an attribute named " + rel.name()); + _relations.setObjectForKey(rel, rel.name()); + _classProperties.addObject(rel); + _classPropertyNames.addObject(rel.name()); + if (rel.isToMany()) + _classPropertyManyRelationships.addObject(rel); + else + _classPropertyOneRelationships.addObject(rel); + } + + public void removeRelationship(EORelationship rel) { + _relations.removeObjectForKey(rel.name()); + _classProperties.removeObject(rel); + _classPropertyNames.removeObject(rel.name()); + _classPropertyManyRelationships.removeObject(rel); + _classPropertyOneRelationships.removeObject(rel); + } + + /** Returns the relationships from this entity to other entities. + * @return An array of the relationships of this entity. + */ + public NSArray relationships() { + return _relations.allValues(); + } + + public EORelationship relationshipNamed(String name) { + return (EORelationship)_relations.objectForKey(name); + } + public EOModel model() { + return _model; + } + + public void setPrimaryKeyAtributes(NSArray pk) { + _pkAttributes = pk; + } + public NSArray primaryKeyAttributes() { + return _pkAttributes; + } + + public NSArray primaryKeyAttributeNames() { + if (_pkAttributeNames.count() != _pkAttributes.count()) { + NSMutableArray arr = new NSMutableArray(); + for (int i = 0; i < _pkAttributes.count(); i++) { + EOAttribute a = (EOAttribute)_pkAttributes.objectAtIndex(i); + arr.addObject(a.name()); + } + _pkAttributeNames = new NSArray(arr); + } + return _pkAttributeNames; + } + + public boolean hasSimplePrimaryKey() { + return _pkAttributes.count() == 1; + } + + public boolean isValidPrimaryKeyAttribute(EOAttribute attr) { + return !attr.allowsNull(); + } + + public void setAttributesUsedForLocking(NSArray value) { + _lockingAttributes.removeAllObjects(); + _lockingAttributes.addObjectsFromArray(value); + } + public NSArray attributesUsedForLocking() { + return new NSArray(_lockingAttributes); + } + + public void setClassProperties(NSArray value) { + _classProperties.removeAllObjects(); + _classProperties.addObjectsFromArray(value); + _classPropertyNames.removeAllObjects(); + _classPropertyAttributes.removeAllObjects(); + _classPropertyOneRelationships.removeAllObjects(); + _classPropertyManyRelationships.removeAllObjects(); + for (int i = 0; i < value.count(); i++) { + EOProperty o = (EOProperty)value.objectAtIndex(i); + _classPropertyNames.addObject(o.name()); + if (o instanceof EOAttribute) { + _classPropertyAttributes.addObject(o); + } else if (o instanceof EORelationship) { + if (((EORelationship)o).isToMany()) + _classPropertyManyRelationships.addObject(o); + else + _classPropertyOneRelationships.addObject(o); + } + } + } + public NSArray classProperties() { + if (_classProperties == null) { + if (_classPropertyNames == null) + return NSArray.EmptyArray; + else { + NSMutableArray props = new NSMutableArray(); + NSMutableArray atribs = new NSMutableArray(); + NSMutableArray ones = new NSMutableArray(); + NSMutableArray manies = new NSMutableArray(); + for (int i = 0; i < _classPropertyNames.count(); i++) { + String name = (String)_classPropertyNames.objectAtIndex(i); + EOAttribute a = attributeNamed(name); + EORelationship r = relationshipNamed(name); + if (a != null) { + props.addObject(a); + atribs.addObject(a); + } else if (r != null) { + props.addObject(r); + if (r.isToMany()) + manies.addObject(r); + else + ones.addObject(r); + } else + throw new IllegalArgumentException("Cannot find attribute or relationship named " + name); + } + _classProperties = props; + _classPropertyAttributes = atribs; + _classPropertyOneRelationships = ones; + _classPropertyManyRelationships = manies; + } + } + return _classProperties; + } + + public NSArray classPropertyNames() { + return _classPropertyNames; + } + + public NSArray classPropertyAttributeNames() { + if (_classPropertyAttributes == null) + return NSArray.EmptyArray; + NSMutableArray arr = new NSMutableArray(_classPropertyAttributes.count()); + for (int i = 0 ; i < _classPropertyAttributes.count(); i++) { + EOAttribute a = (EOAttribute)_classPropertyAttributes.objectAtIndex(i); + arr.addObject(a.name()); + } + return arr; + } + + public NSArray classPropertyToManyRelationshipNames() { + if (_classPropertyManyRelationships == null) + return NSArray.EmptyArray; + NSMutableArray arr = new NSMutableArray(_classPropertyManyRelationships.count()); + for (int i = 0 ; i < _classPropertyManyRelationships.count(); i++) { + EOAttribute a = (EOAttribute)_classPropertyManyRelationships.objectAtIndex(i); + arr.addObject(a.name()); + } + return arr; + } + + public NSArray classPropertyToOneRelationshipNames() { + if (_classPropertyOneRelationships == null) + return NSArray.EmptyArray; + NSMutableArray arr = new NSMutableArray(_classPropertyOneRelationships.count()); + for (int i = 0 ; i < _classPropertyOneRelationships.count(); i++) { + EOAttribute a = (EOAttribute)_classPropertyOneRelationships.objectAtIndex(i); + arr.addObject(a.name()); + } + return arr; + } + + public void setIsAbstractEntity(boolean flag) { + _isAbstract = flag; + } + public boolean isAbstractEntity() { + return _isAbstract; + } + + public void setReadOnly(boolean flag) { + _isReadOnly = flag; + } + public boolean isReadOnly() { + return _isReadOnly; + } + + public void setStoredProcedure(EOStoredProcedure proc, String operation) { + } + public EOStoredProcedure storedProcedureForOperation(String operation) { + return null; + } + + public NSArray subEntities() { + return null; + } + + public NSArray attributesToFetch() { + return attributes(); + } + + public NSArray externalModelsReferenced() { + return null; + } + + public EOClassDescription classDescriptionForInstances() { + EOClassDescription cd = EOClassDescription.classDescriptionForEntityName(name()); + if (cd == null) { + cd = new EOEntityClassDescription(this); + Class cl = null; + try { + cl = Class.forName(className()); + } catch (ClassNotFoundException ex) { + cl = EOGenericRecord.class; + } + EOClassDescription.registerClassDescription(cd, cl); + } + return cd; + } + + /** + * Creates a global ID for a row. The row must have values + * for all the primary key attributes. + * @param row A raw row for this entity. + * @return A key global ID to identify this row. + */ + public EOGlobalID globalIDForRow(Map row) { + NSArray pknames = primaryKeyAttributeNames(); + EOKeyGlobalID gid = null; + if (pknames.count() == 1 && row.get(pknames.objectAtIndex(0)) instanceof Number) { + Number n = (Number)row.get(pknames.objectAtIndex(0)); + gid = new EOIntegralKeyGlobalID(name(), n); + } else { + Object[] vals = new Object[pknames.count()]; + for (int i = 0; i < pknames.count(); i++) { + Object v = row.get(pknames.objectAtIndex(i)); + vals[i] = v; + } + gid = new EOVectorKeyGlobalID(name(), vals); + } + return gid; + } + + /** + * Returns a dictionary with the primary key values contained in + * the global id. + * @param gid A Key global ID. + * @return A dictionary with the primary key values for gid. + */ + public NSDictionary primaryKeyForGlobalID(EOGlobalID gid) { + if (!(gid instanceof EOKeyGlobalID)) + return null; + Object[] vals = ((EOKeyGlobalID)gid).keyValues(); + NSArray pknames = primaryKeyAttributeNames(); + return new NSDictionary(vals, pknames.toArray()); + } + + public EOQualifier qualifierForPrimaryKey(Map pkey) { + NSArray pknames = primaryKeyAttributeNames(); + EOQualifier q = null; + NSMutableArray subq = new NSMutableArray(pknames.count()); + for (int i = 0; i < pknames.count(); i++) { + String key = (String)pknames.objectAtIndex(i); + Object v = pkey.get(key); + if (v == null || v == NSKeyValueCoding.NullValue) + throw new IllegalArgumentException("Primary key with null values."); + subq.addObject(new EOKeyValueQualifier(key, EOQualifier.QualifierOperatorEqual, v)); + } + if (subq.count() == 1) + q = (EOQualifier)subq.objectAtIndex(0); + else + q = new EOAndQualifier(subq); + return q; + } + + public void setUserInfo(NSDictionary value) { + _userInfo = value; + } + public NSDictionary userInfo() { + return _userInfo; + } + + public void awakeWithPropertyList(NSDictionary plist) { + } + + public void encodeIntoPropertyList(NSMutableDictionary dict) { + dict.setObjectForKey(name(), "name"); + dict.setObjectForKey(externalName(), "externalName"); + dict.setObjectForKey(className(), "className"); + + //Encode attributes + NSMutableArray arr = new NSMutableArray(_attributes.allValues()); + for (int i = 0; i < _attributes.count(); i++) { + EOAttribute a = (EOAttribute)arr.objectAtIndex(i); + NSMutableDictionary d = new NSMutableDictionary(); + a.encodeIntoPropertyList(d); + arr.replaceObjectAtIndex(i, d); + } + dict.setObjectForKey(arr, "attributes"); + //Encode relationships + if (_relations.count() > 0) { + arr = new NSMutableArray(_relations.allValues()); + for (int i = 0; i < _relations.count(); i++) { + EORelationship r = (EORelationship)arr.objectAtIndex(i); + NSMutableDictionary d = new NSMutableDictionary(); + r.encodeIntoPropertyList(d); + arr.replaceObjectAtIndex(i, d); + } + dict.setObjectForKey(arr, "relationships"); + } + + //Fetch specifications + NSMutableDictionary d = new NSMutableDictionary(); + loadFetchSpecifications(); + java.util.Enumeration enumeration = _fetchSpecs.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String k = (String)enumeration.nextElement(); + EOFetchSpecification f = (EOFetchSpecification)_fetchSpecs.objectForKey(k); + EOKeyValueArchiver arch = new EOKeyValueArchiver(); + f.encodeWithKeyValueArchiver(arch); + d.setObjectForKey(arch.dictionary(), k); + } + dict.setObjectForKey(d, "fetchSpecificationDictionary"); + + //Other information + dict.setObjectForKey(_classPropertyNames, "classProperties"); + dict.setObjectForKey(_pkAttributeNames, "primaryKeyAttributes"); + arr = new NSMutableArray(_lockingAttributes); + for (int i = 0; i < arr.count(); i++) + arr.replaceObjectAtIndex(i, ((EOAttribute)arr.objectAtIndex(i)).name()); + dict.setObjectForKey(arr, "attributesUsedForLocking"); + if (_userInfo != null && _userInfo.count() > 0 ) + dict.setObjectForKey(_userInfo, "userInfo"); + if (_internalInfo != null && _internalInfo.count() > 0 ) + dict.setObjectForKey(_internalInfo, "internalInfo"); + } + + public EOAttribute _attributeForPath(String path) { + NSArray comps = NSArray.componentsSeparatedByString(path, "."); + if (comps.count() < 2) + return null; + EORelationship r = null; + EOEntity e = this; + for (int i = 0; i < comps.count()-1; i++) { + String name = (String)comps.objectAtIndex(i); + r = e.relationshipNamed(name); + if (r == null) + return null; + e = r.destinationEntity(); + } + return e.attributeNamed((String)comps.lastObject()); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.8 2003/08/19 19:47:58 chochos + * added some methods to handle EOGlobalIDs and primary keys. + * + * Revision 1.7 2003/08/14 02:13:56 chochos + * implement and use _attributeForPath() + * + * Revision 1.6 2003/08/11 19:38:27 chochos + * Can now read from a file and re-write to another file. + * + * Revision 1.5 2003/08/11 18:19:33 chochos + * encoding into property list seems to work fine now. + * + * Revision 1.4 2003/08/09 01:39:04 chochos + * better handling of class properties; use EOKeyValueArchiving to encode/decode fetch specifications; implement EOPropertyListEncoding + * + * Revision 1.3 2003/08/08 05:52:21 chochos + * gets the class description for the entity. + * + * Revision 1.2 2003/08/08 02:14:20 chochos + * now it can read stored procedures. + * + * Revision 1.1 2003/08/07 02:38:33 chochos + * implementation of EOEntity. What works for now is reading an entity from file. + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntityClassDescription.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntityClassDescription.java new file mode 100644 index 0000000..a235c99 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOEntityClassDescription.java @@ -0,0 +1,185 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.access; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGenericRecord; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; + +/** +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOEntityClassDescription extends EOClassDescription { + + protected EOEntity _entity; + + public EOEntityClassDescription() { + super(); + } + + public EOEntityClassDescription(EOEntity entity) { + this(); + _entity = entity; + } + + public NSArray allAttributeKeys() { + NSArray arr = entity().attributes(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EOAttribute atrib = (EOAttribute)arr.objectAtIndex(i); + a.addObject(atrib); + } + return a; + } + + public NSArray allPropertyKeys() { + return entity().classPropertyNames(); + } + + public NSArray allToManyRelationshipKeys() { + NSArray arr = entity().relationships(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EORelationship r = (EORelationship)arr.objectAtIndex(i); + if (r.isToMany()) + a.addObject(r); + } + return a; + } + + public NSArray allToOneRelationshipKeys() { + NSArray arr = entity().relationships(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EORelationship r = (EORelationship)arr.objectAtIndex(i); + if (!r.isToMany()) + a.addObject(r); + } + return a; + } + + /** Returns all attributes that correspond to columns + * in a database table. + */ + public NSArray attributeKeys() { + NSArray arr = entity().attributes(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EOAttribute atrib = (EOAttribute)arr.objectAtIndex(i); + if (!atrib.isDerived()) + a.addObject(atrib); + } + return a; + } + + public EOClassDescription classDescriptionForDestinationKey(String key) { + EORelationship r = entity().relationshipNamed(key); + if (r == null) + return null; + return r.destinationEntity().classDescriptionForInstances(); + } + + public NSArray clientAttributeKeys() { + return null; + } + + public NSArray clientToManyRelationshipKeys() { + return null; + } + + public NSArray clientToOneRelationshipKeys() { + return null; + } + + public EOEntity entity() { + return _entity; + } + + public String entityName() { + return _entity.name(); + } + + public EOFetchSpecification fetchSpecificationNamed(String name) { + return entity().fetchSpecificationNamed(name); + } + + public NSArray toManyRelationshipKeys() { + NSArray arr = entity().relationships(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EORelationship r = (EORelationship)arr.objectAtIndex(i); + if (r.isToMany() && !r.isFlattened()) + a.addObject(r); + } + return a; + } + + public NSArray toOneRelationshipKeys() { + NSArray arr = entity().relationships(); + NSMutableArray a = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + EORelationship r = (EORelationship)arr.objectAtIndex(i); + if (!r.isToMany() && !r.isFlattened()) + a.addObject(r); + } + return a; + } + + public Object createInstanceWithEditingContext(EOEditingContext ec, EOGlobalID gid) { + if (theClass == null) { + try { + theClass = Class.forName(entity().className()); + } catch (ClassNotFoundException ex) { + if (entity().className().equals("net.wotonomy.control.EOGenericRecord")) + throw new IllegalArgumentException("Cannot find class " + entity().className()); + theClass = EOGenericRecord.class; + } + } + return super.createInstanceWithEditingContext(ec, gid); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/19 01:55:54 chochos + * the behavior is now more consistent with its Apple counterpart. + * + * Revision 1.3 2003/08/09 01:40:31 chochos + * use EOClassDescription's methods to get a destination entity's class description. + * + * Revision 1.2 2003/08/08 05:51:59 chochos + * createInstanceWithEditingContext now works. It sets theClass with the class from the entity className() before calling super. + * + * Revision 1.1 2003/08/08 00:36:19 chochos + * concrete implementation of EOClassDescription that uses an EOEntity + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOGeneralAdaptorException.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOGeneralAdaptorException.java new file mode 100644 index 0000000..6c48a3f --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOGeneralAdaptorException.java @@ -0,0 +1,62 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSDictionary; + +/** +* Generic exception thrown by wotonomy.access. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOGeneralAdaptorException extends RuntimeException implements java.io.Serializable { + + private NSDictionary _userInfo; + + public EOGeneralAdaptorException(String msg) { + super(msg); + } + + public EOGeneralAdaptorException(String name, NSDictionary userInfo) { + super(name); + _userInfo = userInfo; + } + + public EOGeneralAdaptorException(String selectorName, String className, String msg) { + super(msg); + _userInfo = new NSDictionary( + new Object[]{ selectorName, className }, + new Object[]{ "selectorName", "className" }); + } + + public NSDictionary userInfo() { + return _userInfo; + } + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 00:41:50 chochos + * general adaptor exception + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOJoin.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOJoin.java new file mode 100644 index 0000000..693a7d0 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOJoin.java @@ -0,0 +1,56 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +/** +* An EOJoin represents a connection between two attributes in +* different entities. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOJoin { + + protected EOAttribute _source; + protected EOAttribute _destination; + + public EOJoin(EOAttribute src, EOAttribute dst) { + super(); + _source = src; + _destination = dst; + } + + public EOAttribute sourceAttribute() { + return _source; + } + + public EOAttribute destinationAttribute() { + return _destination; + } + + /* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/07 02:41:04 chochos + * these don't do much for now. + * + */ +} diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModel.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModel.java new file mode 100644 index 0000000..46fc8d0 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModel.java @@ -0,0 +1,396 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Enumeration; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSPropertyListSerialization; + +/** +* An EOModel is a set of entities and stored procedures, along with a connection +* dictionary to connect to a database. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOModel { + + protected static final String IDX_NAME = "index.eomodeld"; + + //This array contains dictionaries with "className" and "name" + protected NSMutableArray _entities = new NSMutableArray(); + protected NSMutableDictionary _entitiesByName = new NSMutableDictionary(); + protected NSMutableDictionary _entitiesByClass = new NSMutableDictionary(); + protected NSDictionary _connectionDictionary = NSDictionary.EmptyDictionary; + protected String _adaptorName = "JDBC"; + protected NSMutableDictionary _prototypesByName = new NSMutableDictionary(); + protected NSMutableDictionary _storedProcedures = new NSMutableDictionary(); + protected NSMutableArray _storedProcedureNames = new NSMutableArray(); + protected NSDictionary _userInfo = NSDictionary.EmptyDictionary; + protected String _name; + protected String _path; + protected NSDictionary _internalInfo = NSDictionary.EmptyDictionary; + protected EOModelGroup _group; + + public EOModel() { + super(); + } + + public EOModel(NSDictionary dict, Object o) { + this(); + } + + public EOModel(String path) { + this(); + File f = new File(path); + _path = f.getAbsolutePath(); + if (!(f.exists() && f.isDirectory())) + throw new IllegalArgumentException("The path is invalid (" + f + ")"); + _name = f.getName(); + f = new File(f, "index.eomodeld"); + if (!(f.exists() && f.isFile())) + throw new IllegalArgumentException("Cannot find " + f); + String x = null; + try { + FileInputStream in = new FileInputStream(f); + byte[] b = new byte[in.available()]; + in.read(b); + in.close(); + x = new String(b); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot read index.eomodeld"); + } + NSDictionary d = NSPropertyListSerialization.dictionaryForString(x); + String version = (String)d.objectForKey("EOModelVersion"); + if (version == null || !version.startsWith("2.")) + throw new IllegalArgumentException("Invalid eomodel version: " + version); + setAdaptorName((String)d.objectForKey("adaptorName")); + setConnectionDictionary((NSDictionary)d.objectForKey("connectionDictionary")); + _entities = ((NSArray)d.objectForKey("entities")).mutableClone(); + if (d.objectForKey("storedProcedures") != null) + _storedProcedureNames.addObjectsFromArray((NSArray)d.objectForKey("storedProcedures")); + if (d.objectForKey("internalInfo") != null) + _internalInfo = (NSDictionary)d.objectForKey("internalInfo"); + if (d.objectForKey("userInfo") != null) + _userInfo = (NSDictionary)d.objectForKey("userInfo"); + entityNamed("EOPrototypes"); + } + + public void setConnectionDictionary(NSDictionary dict) { + _connectionDictionary = dict; + } + public NSDictionary connectionDictionary() { + return _connectionDictionary; + } + + public void addEntity(EOEntity ent) { + _entitiesByName.setObjectForKey(ent, ent.name()); + _entitiesByClass.setObjectForKey(ent, ent.className()); + } + + public void removeEntity(EOEntity ent) { + _entitiesByName.removeObjectForKey(ent.name()); + _entitiesByClass.removeObjectForKey(ent.className()); + } + + public void addStoredProcedure(EOStoredProcedure proc) { + if (!_storedProcedureNames.containsObject(proc)) + _storedProcedureNames.addObject(proc); + _storedProcedures.setObjectForKey(proc, proc.name()); + } + + public void removeStoredProcedure(EOStoredProcedure proc) { + _storedProcedureNames.removeObject(proc.name()); + _storedProcedures.removeObjectForKey(proc.name()); + } + + public EOStoredProcedure storedProcedureNamed(String name) { + EOStoredProcedure proc = (EOStoredProcedure)_storedProcedures.objectForKey(name); + //if we can't find it, check if we should load it + if (proc == null && _path != null && _storedProcedureNames.containsObject(name)) { + //try to read it from file + File f = new File(new File(_path), name + ".storedProcedure"); + if (!f.exists()) + return null; + String sdict = null; + try { + FileInputStream fin = new FileInputStream(f); + byte[] b = new byte[fin.available()]; + fin.read(b); + fin.close(); + sdict = new String(b); + } catch (IOException ex) { + return null; + } + NSDictionary plist = NSPropertyListSerialization.dictionaryForString(sdict); + if (plist == null) + throw new IllegalArgumentException("File " + f + " does not contain a valid stored procedure property list."); + proc = new EOStoredProcedure(plist, this); + //add it to our collection + _storedProcedures.setObjectForKey(proc, proc.name()); + } + return proc; + } + + public NSArray storedProcedures() { + if (_storedProcedures.count() < _storedProcedureNames.count()) { + for (int i=0; i<_storedProcedureNames.count(); i++) + storedProcedureNamed((String)_storedProcedureNames.objectAtIndex(i)); + } + return _storedProcedures.allValues(); + } + + public NSArray storedProcedureNames() { + return _storedProcedures.allKeys(); + } + + public void setAdaptorName(String value) { + _adaptorName = value; + } + public String adaptorName() { + return _adaptorName; + } + + public NSArray entities() { + if (_entitiesByName.count() >= _entities.count()) + return _entitiesByName.allValues(); + NSMutableArray es = new NSMutableArray(); + for (int i = 0; i < _entities.count(); i++) { + NSDictionary d = (NSDictionary)_entities.objectAtIndex(i); + es.addObject(entityNamed((String)d.objectForKey("name"))); + } + return es; + } + + public NSArray entityNames() { + if (_entitiesByName.count() >= _entities.count()) + return _entitiesByName.allKeys(); + NSMutableArray names = new NSMutableArray(); + for (int i = 0; i < _entities.count(); i++) { + NSDictionary d = (NSDictionary)_entities.objectAtIndex(i); + names.addObject(d.objectForKey("name")); + } + return names; + } + + public EOEntity entityNamed(String name) { + EOEntity e = (EOEntity)_entitiesByName.objectForKey(name); + if (e == null && path() != null) { + boolean exists = false; + for (int i = 0; i < _entities.count(); i++) { + NSDictionary d = (NSDictionary)_entities.objectAtIndex(i); + if (d.objectForKey("name").equals(name)) + exists = true; + } + if (!exists) + return null; + File f = new File(new File(path()), name + ".plist"); + if (!(f.exists() && f.isFile())) + throw new IllegalArgumentException("Cannot find " + name + ".plist"); + String s = null; + try { + FileInputStream fin = new FileInputStream(f); + byte[] b = new byte[fin.available()]; + fin.read(b); + fin.close(); + s = new String(b); + } catch (IOException ex) { + throw new IllegalArgumentException("Cannot read " + f); + } + NSDictionary d = NSPropertyListSerialization.dictionaryForString(s); + if (d == null) + throw new IllegalArgumentException("Cannot parse dictionary for " + f); + e = new EOEntity(d, this); + _entitiesByName.setObjectForKey(e, e.name()); + } + return e; + } + + public String name() { + return _name; + } + + public String path() { + return _path; + } + + public void setModelGroup(EOModelGroup group) { + _group = group; + } + public EOModelGroup modelGroup() { + return _group; + } + + public void setUserInfo(NSDictionary dict) { + _userInfo = dict; + } + + public NSDictionary userInfo() { + return _userInfo; + } + + public void write() { + } + + public void writeToFile(String path) { + if (!path.endsWith(".eomodeld")) + path += ".eomodeld"; + File f = new File(path); + if (f.exists()) { + if (f.isDirectory()) { + File[] kids = f.listFiles(); + for (int i = 0; i < kids.length; i++) { + String fname = kids[i].getName(); + if (kids[i].isFile() && (fname.endsWith(".plist") || fname.endsWith(".eomodeld") || fname.endsWith(".fspec") || fname.endsWith(".storedProcedure"))) + kids[i].delete(); + } + } else + f.delete(); + } else + f.mkdirs(); + File kid = new File(f, "index.eomodeld"); + + //encode the index file + NSMutableDictionary d = new NSMutableDictionary(); + d.setObjectForKey(_adaptorName, "adaptorName"); + d.setObjectForKey("2.1", "EOModelVersion"); + d.setObjectForKey(_connectionDictionary, "connectionDictionary"); + if (_internalInfo != null && _internalInfo.count() > 0) + d.setObjectForKey(_internalInfo, "internalInfo"); + if (_userInfo != null && _userInfo.count() > 0) + d.setObjectForKey(_userInfo, "userInfo"); + if (_storedProcedureNames.count() > 0) + d.setObjectForKey(_storedProcedureNames, "storedProcedures"); + + //encode the entity list + entities(); + storedProcedures(); + NSMutableArray arr = new NSMutableArray(_entitiesByName.count()); + Enumeration enumeration = _entitiesByName.keyEnumerator(); + NSMutableDictionary md = new NSMutableDictionary(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + EOEntity ent = (EOEntity)_entitiesByName.objectForKey(key); + md.removeAllObjects(); + ent.encodeIntoPropertyList(md); + File plist; + NSDictionary fetchSpecs = (NSDictionary)md.objectForKey("fetchSpecificationDictionary"); + if (fetchSpecs != null) { + if (fetchSpecs.count() > 0) { + plist = new File(f, key + ".fspec"); + String ps = NSPropertyListSerialization.stringForPropertyList(md.objectForKey("fetchSpecificationDictionary")); + try { + PrintStream stream = new PrintStream(new FileOutputStream(plist)); + stream.println(ps); + stream.close(); + } catch (IOException ex) { + } + } + md.removeObjectForKey("fetchSpecificationDictionary"); + } + plist = new File(f, key + ".plist"); + String ps = NSPropertyListSerialization.stringForPropertyList(md); + try { + PrintStream stream = new PrintStream(new FileOutputStream(plist)); + stream.println(ps); + stream.close(); + } catch (IOException ex) { + } + //add to the entity list for the index + arr.addObject(new NSDictionary( + new Object[]{ ent.name(), ent.className() }, + new Object[]{ "name", "className" })); + } + d.setObjectForKey(arr, "entities"); + + //write the index file + String s = NSPropertyListSerialization.stringForPropertyList(d); + try { + PrintStream stream = new PrintStream(new FileOutputStream(kid)); + stream.println(s); + stream.close(); + } catch (IOException ex) { + throw new RuntimeException("Cannot write index.eomodeld"); + } + + //write the stored procedures + for (int i = 0; i < _storedProcedureNames.count(); i++) { + EOStoredProcedure proc = (EOStoredProcedure)_storedProcedures.objectForKey(_storedProcedureNames.objectAtIndex(i)); + kid = new File(f, proc.name() + ".storedProcedure"); + d.removeAllObjects(); + proc.encodeIntoPropertyList(d); + s = NSPropertyListSerialization.stringForPropertyList(d); + try { + PrintStream stream = new PrintStream(new FileOutputStream(kid)); + stream.println(s); + stream.close(); + } catch (IOException ex) { + throw new RuntimeException("Cannot write " + f); + } + } + _path = f.getAbsolutePath(); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.7 2003/08/13 20:46:12 chochos + * entityNamed() only throws if the searched entity is in the entities array and not in a file. + * + * Revision 1.6 2003/08/12 01:45:04 chochos + * added some code to handle prototypes + * + * Revision 1.5 2003/08/11 19:38:27 chochos + * Can now read from a file and re-write to another file. + * + * Revision 1.4 2003/08/11 18:20:08 chochos + * saving to a file seems to work now. + * + * Revision 1.3 2003/08/08 02:16:55 chochos + * can now read stored procedures from file. + * + * Revision 1.2 2003/08/08 00:37:00 chochos + * add stored procedure functionality + * + * Revision 1.1 2003/08/07 02:42:28 chochos + * EOModel can read an .eomodeld file. EOModelGroup doesn't do much for now. + * +*/ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModelGroup.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModelGroup.java new file mode 100644 index 0000000..dbab09d --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOModelGroup.java @@ -0,0 +1,198 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOObjectStoreCoordinator; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A group of models that connect to the same database. Entities in +* these models can have relationships that point to other entities +* in any model of the same group. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOModelGroup { + + private static EOModelGroup _defaultGroup; + private static EOModelGroup _globalGroup; + protected NSMutableDictionary _models; + + public EOModelGroup() { + super(); + } + + public void addModel(EOModel model) { + if (model.name() == null) + throw new IllegalArgumentException("Cannot add an unnamed model to a group."); + if (_models.objectForKey(model.name()) != null) + throw new IllegalArgumentException("Cannot add model " + model.name() + + " to group because it already contains a model with the same name."); + NSArray ents = model.entityNames(); + for (int i = 0; i < ents.count(); i++) { + String ename = (String)ents.objectAtIndex(i); + if (entityNamed(ename) != null) + throw new IllegalArgumentException("Cannot add model " + model.name() + + " to group because it contains entity named " + ename); + } + _models.setObjectForKey(model, model.name()); + } + + public void removeModel(EOModel model) { + _models.removeObjectForKey(model.name()); + } + + public void addModelWithPath(String path) { + EOModel model = new EOModel(path); + addModel(model); + } + + public void addModelsFromDirectory(String dir) { + } + + public static void setDefaultGroup(EOModelGroup group) { + _defaultGroup = group; + } + public static EOModelGroup defaultGroup() { + if (_defaultGroup == null) { + _defaultGroup = globalModelGroup(); + } + return _defaultGroup; + } + + public static EOModelGroup globalModelGroup() { + if (_globalGroup == null) { + _globalGroup = new EOModelGroup(); + //TODO: read all frameworks and get models from them + } + return _globalGroup; + } + + public EOEntity entityForObject(net.wotonomy.control.EOEnterpriseObject eo) { + return null; + } + + public EOEntity entityNamed(String name) { + java.util.Enumeration enumeration = _models.objectEnumerator(); + while (enumeration.hasMoreElements()) { + EOModel m = (EOModel)enumeration.nextElement(); + if (m.entityNamed(name) != null) + return m.entityNamed(name); + } + return null; + } + + public EOModel modelNamed(String name) { + return (EOModel)_models.objectForKey(name); + } + + public NSArray modelNames() { + return _models.allKeys(); + } + + public NSArray models() { + return _models.allValues(); + } + + public EOModel modelWithPath(String path) { + java.util.Enumeration enumeration = _models.objectEnumerator(); + while (enumeration.hasMoreElements()) { + EOModel m = (EOModel)enumeration.nextElement(); + if (m.path() != null && m.path().equals(path)) + return m; + } + return null; + } + + public EOStoredProcedure storedProcedureNamed(String name) { + java.util.Enumeration enumeration = _models.objectEnumerator(); + while (enumeration.hasMoreElements()) { + EOModel m = (EOModel)enumeration.nextElement(); + if (m.storedProcedureNamed(name) != null) + return m.storedProcedureNamed(name); + } + return null; + } + + public EOFetchSpecification fetchSpecificationNamed(String fetchSpecName, String entityName) { + EOEntity e = entityNamed(entityName); + if (e == null) + return null; + return e.fetchSpecificationNamed(fetchSpecName); + } + + public void loadAllModelObjects() { + java.util.Enumeration enumeration = _models.objectEnumerator(); + while (enumeration.hasMoreElements()) { + EOModel m = (EOModel)enumeration.nextElement(); + //this causes all entities to be loaded + NSArray ents = m.entities(); + for (int i=0; i 1) { + buf.insert(0, '('); + buf.append(')'); + } + return buf.toString(); + } + + public EOQualifier schemaBasedQualifierWithRootEntity(EOQualifier q, EOEntity e) { + return q; + } + + public EOQualifier qualifierMigratedFromEntityRelationshipPath(EOQualifier q, EOEntity e, String path) { + return q; + } + + } + + public class OrQualifierSupport extends Support { + + public OrQualifierSupport() { + super(); + } + + public String sqlStringForSQLExpression(EOQualifier qualifier, EOSQLExpression exp) { + EOOrQualifier q = (EOOrQualifier)qualifier; + NSArray qus = q.qualifiers(); + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < qus.count(); i++) { + EOQualifier sub = (EOQualifier)qus.objectAtIndex(i); + EOQualifierSQLGeneration.Support sup = EOQualifierSQLGeneration.Support.supportForClass(sub.getClass()); + if (sup == null) + throw new IllegalStateException("Cannot find support class for " + sub.getClass().getName()); + buf.append(sup.sqlStringForSQLExpression(sub, exp)); + if (i < qus.count()-1) + buf.append(" OR "); + } + if (qus.count() > 1) { + buf.insert(0, '('); + buf.append(')'); + } + return buf.toString(); + } + + public EOQualifier schemaBasedQualifierWithRootEntity(EOQualifier q, EOEntity e) { + return q; + } + + public EOQualifier qualifierMigratedFromEntityRelationshipPath(EOQualifier q, EOEntity e, String path) { + return q; + } + + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/14 02:13:10 chochos + * KeyValueQualifierSupport generates proper SQL + * + * Revision 1.2 2003/08/14 01:05:51 chochos + * added abstract Support inner class, with incomplete implementations for the main qualifiers (not, and, or, key-value, key-comparison) + * + * Revision 1.1 2003/08/12 01:45:49 chochos + * interface to be implemented by qualifiers (or support classes) to indicate they can generate SQL + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EORelationship.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EORelationship.java new file mode 100644 index 0000000..a5a207f --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EORelationship.java @@ -0,0 +1,319 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +/** +* Represents a relationship from one entity to another. Relationships are unidirectional. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EORelationship extends EOProperty implements EOPropertyListEncoding { + + public static final int InnerJoin = 0; + public static final int FullOuterJoin = 1; + public static final int LeftOuterJoin = 2; + public static final int RightOuterJoin = 3; + + protected String _name; + protected int _batchCount; + protected int _deleteRule; + protected int _joinSemantic; + protected EOEntity _destination; + protected EOEntity _entity; + protected NSMutableArray _joins = new NSMutableArray(); + protected boolean _isMandatory; + protected boolean _isToMany; + protected boolean _isFlattened; + protected boolean _knowsIfFlattened; + protected boolean _ownsDestination; + protected boolean _propagatesPrimaryKey; + protected boolean _useBatchFaulting; + protected NSDictionary _userInfo = NSDictionary.EmptyDictionary; + protected NSDictionary _internalInfo = NSDictionary.EmptyDictionary; + protected NSDictionary plist; + protected String _definition; + + public EORelationship() { + super(); + } + + public EORelationship(NSDictionary dict, Object obj) { + super(); + _entity = (EOEntity)obj; + setName((String)dict.objectForKey("name")); + setToMany("Y".equals(dict.objectForKey("isToMany"))); + setPropagatesPrimaryKey("Y".equals(dict.objectForKey("propagatesPrimaryKey"))); + setIsMandatory("Y".equals(dict.objectForKey("isMandatory"))); + setOwnsDestination("Y".equals(dict.objectForKey("ownsDestination"))); + setDefinition((String)dict.objectForKey("definition")); + String delrule = (String)dict.objectForKey("deleteRule"); + if (delrule != null) { + if (delrule.equals("EODeleteRuleCascade")) + setDeleteRule(0); + else if (delrule.equals("EODeleteRuleDeny")) + setDeleteRule(0); + else if (delrule.equals("EODeleteRuleNoAction")) + setDeleteRule(0); + else if (delrule.equals("EODeleteRuleNullify")) + setDeleteRule(0); + } + delrule = (String)dict.objectForKey("joinSemantic"); + if (delrule != null) { + if (delrule.equals("EOInnerJoin")) + setJoinSemantic(InnerJoin); + else if (delrule.equals("EOFullOuterJoin")) + setJoinSemantic(FullOuterJoin); + else if (delrule.equals("EOLeftOuterJoin")) + setJoinSemantic(LeftOuterJoin); + else if (delrule.equals("EORightOuterJoin")) + setJoinSemantic(RightOuterJoin); + } + delrule = (String)dict.objectForKey("batchCount"); + if (delrule != null) + setNumberOfToManyFaultsToBatchFetch(Integer.parseInt(delrule)); + NSDictionary d = (NSDictionary)dict.objectForKey("userInfo"); + if (d != null) + _userInfo = d; + d = (NSDictionary)dict.objectForKey("internalInfo"); + if (d != null) + _internalInfo = d; + plist = dict; + } + + public void setName(String name) { + _name = name; + } + public String name() { + return _name; + } + + public void addJoin(EOJoin join) { + _joins.addObject(join); + } + + public void removeJoin(EOJoin join) { + _joins.removeObject(join); + } + + public EOEntity entity() { + return _entity; + } + + public EOEntity destinationEntity() { + isFlattened(); + if (_destination == null && plist != null) { + EOModel model = _entity.model(); + EOModelGroup group = model.modelGroup(); + String destEntity = (String)plist.objectForKey("destination"); + if (group != null) + _destination = group.entityNamed(destEntity); + else + _destination = model.entityNamed(destEntity); + } + return _destination; + } + + public void setOwnsDestination(boolean flag) { + _ownsDestination = flag; + } + public boolean ownsDestination() { + return _ownsDestination; + } + + public void setToMany(boolean flag) { + _isToMany = flag; + } + public boolean isToMany() { + return _isToMany; + } + + public void setIsMandatory(boolean flag) { + _isMandatory = flag; + } + public boolean isMandatory() { + return _isMandatory; + } + + public void setPropagatesPrimaryKey(boolean flag) { + _propagatesPrimaryKey = flag; + } + public boolean propagatesPrimaryKey() { + return _propagatesPrimaryKey; + } + + public void setDeleteRule(int value) { + _deleteRule = value; + } + public int deleteRule() { + return _deleteRule; + } + + public void setJoinSemantic(int value) { + _joinSemantic = value; + } + public int joinSemantic() { + return _joinSemantic; + } + + public void setNumberOfToManyFaultsToBatchFetch(int count) { + _batchCount = count; + } + public int numberOfToManyFaultsToBatchFetch() { + return _batchCount; + } + + public NSArray joins() { + if (_joins.count() == 0 && plist != null) { + NSArray joins = (NSArray)plist.objectForKey("joins"); + for (int i = 0; i < joins.count(); i++) { + NSDictionary d = (NSDictionary)joins.objectAtIndex(i); + String srcName = (String)d.objectForKey("sourceAttribute"); + String dstName = (String)d.objectForKey("destinationAttribute"); + EOAttribute a1 = _entity.attributeNamed(srcName); + EOAttribute a2 = destinationEntity().attributeNamed(dstName); + EOJoin j = new EOJoin(a1, a2); + addJoin(j); + } + } + return new NSArray(_joins); + } + + public void setDefinition(String def) { + _definition = def; + } + public String definition() { + return _definition; + } + + public boolean isFlattened() { + if (_knowsIfFlattened) + return _isFlattened; + _knowsIfFlattened = true; + if (definition() == null) + return false; + NSArray comps = NSArray.componentsSeparatedByString(definition(), "."); + if (comps.count() < 2) + return false; + EORelationship r = null; + EOEntity e = entity(); + for (int i = 0; i < comps.count(); i++) { + String name = (String)comps.objectAtIndex(i); + r = e.relationshipNamed(name); + if (r == null) + return false; + e = r.destinationEntity(); + } + _destination = e; + _isFlattened = true; + return _isFlattened; + } + + public boolean isMultiHop() { + return false; + } + + public String relationshipPath() { + if (isFlattened()) + return _definition; + return null; + } + + public void setUserInfo(NSDictionary value) { + _userInfo = value; + } + public NSDictionary userInfo() { + return _userInfo; + } + + public void awakeWithPropertyList(NSDictionary plist) { + } + + public void encodeIntoPropertyList(NSMutableDictionary dict) { + dict.setObjectForKey(name(), "name"); + if (destinationEntity() != null && definition() == null) + dict.setObjectForKey(_destination.name(), "destination"); + if (_internalInfo != null && _internalInfo.count() > 0) + dict.setObjectForKey(_internalInfo, "internalInfo"); + if (_userInfo != null && _userInfo.count() > 0) + dict.setObjectForKey(_userInfo, "userInfo"); + if (isToMany()) + dict.setObjectForKey("Y", "isToMany"); + switch (_joinSemantic) { + case InnerJoin: + dict.setObjectForKey("EOInnerJoin", "joinSemantic"); + break; + case FullOuterJoin: + dict.setObjectForKey("EOFullOuterJoin", "joinSemantic"); + break; + case LeftOuterJoin: + dict.setObjectForKey("EOLefOuterJoin", "joinSemantic"); + break; + case RightOuterJoin: + dict.setObjectForKey("EORightOuterJoin", "joinSemantic"); + break; + } + if (_batchCount > 0) + dict.setObjectForKey(new Integer(_batchCount), "batchCount"); + if (definition() != null) + dict.setObjectForKey(definition(), "definition"); + else { + NSMutableArray jarr = new NSMutableArray(joins().count()); + for (int i = 0; i < _joins.count(); i++) { + EOJoin j = (EOJoin)_joins.objectAtIndex(i); + NSDictionary d = new NSDictionary( + new Object[]{ j.sourceAttribute().name(), j.destinationAttribute().name() }, + new Object[]{ "sourceAttribute", "destinationAttribute" }); + jarr.addObject(d); + } + dict.setObjectForKey(jarr, "joins"); + } + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:13 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/11 19:38:27 chochos + * Can now read from a file and re-write to another file. + * + * Revision 1.4 2003/08/09 01:35:35 chochos + * implement EOPropertyListEncoding + * + * Revision 1.3 2003/08/08 06:51:53 chochos + * isFlattened() works + * + * Revision 1.2 2003/08/08 02:17:43 chochos + * main accessors are in place. + * + * Revision 1.1 2003/08/07 02:41:30 chochos + * a relationship that for the moment can be created from a property list. + * +*/ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpression.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpression.java new file mode 100644 index 0000000..4cb15e5 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpression.java @@ -0,0 +1,697 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.access; + +import java.util.Enumeration; + +import net.wotonomy.control.EOAndQualifier; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOKeyComparisonQualifier; +import net.wotonomy.control.EOKeyValueQualifier; +import net.wotonomy.control.EOOrQualifier; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.NSTimestamp; +import net.wotonomy.foundation.NSTimestampFormatter; + +/** +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EOSQLExpression { + + public static final String BindVariableAttributeKey = "BindVariableAttribute"; + public static final String BindVariableColumnKey = "BindVariableColumn"; + public static final String BindVariableNameKey = "BindVariableName"; + public static final String BindVariablePlaceHolderKey = "BindVariablePlaceholder"; + public static final String BindVariableValueKey = "BindVariableValue"; + private static int UseBindings; + private static final int _DefaultFormatSQLStringLength = 64; + private static final int _DefaultListStringLength = 256; + private static final int _DefaultOrderByStringLength = 128; + private static final int _DefaultPathLength = 128; + private static final int _DefaultTableListLength = 128; + protected static final char[] _hexChars = new char[]{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static final int _ValueLengthLimit = 40; + protected NSMutableDictionary _aliasesByRelationshipPath; + protected NSMutableDictionary _aliasesByEntityName; + protected NSMutableArray _bindings; + protected NSMutableArray _contextStack; + protected static NSTimestampFormatter _defaultDateFormatter; + protected EOEntity _entity; + protected StringBuffer _joinClauseString; + protected StringBuffer _listString; + protected StringBuffer _orderByString; + protected String _statement; + protected String _upperFunctionName; + protected boolean _useAliases = true; + protected static boolean _quoteExternalNames; + protected StringBuffer _valueListString; + protected String _whereClauseString; + + private EOSQLExpression() { + super(); + } + + public EOSQLExpression(EOEntity entity) { + super(); + _entity = entity; + } + + public String _aliasForRelatedAttributeRelationshipPath(EOAttribute a, String path) { + return null; + } + + public String _aliasForRelationshipPath(String path) { + return (String)_aliasesByRelationshipPath.objectForKey(path); + } + + protected NSTimestampFormatter _defaultDateFormatter() { + return _defaultDateFormatter; + } + + protected StringBuffer _listString() { + if (_listString == null) + _listString = new StringBuffer(); + return _listString; + } + + protected StringBuffer _orderByString() { + if (_orderByString == null) + _orderByString = new StringBuffer(); + return _orderByString; + } + + public EOEntity _rootEntityForExpression() { + return _entity; + } + + public void _setEntity(EOEntity value) { + _entity = value; + } + + public String _sqlStringForJoinSemanticMatchSemantic(int semantic,int match) { + return null; + } + + protected String _stringForDate(NSTimestamp timestamp) { + return null; + } + + protected StringBuffer _valueList() { + if (_valueListString == null) + _valueListString = new StringBuffer(); + return _valueListString; + } + + public void addBindVariableDictionary( NSDictionary dict ) { + } + + /** Adds the SQL to create the attribute to the attribute list. + * The appended text is of the form attr_name attr_type allow_null + * @param attr The attribute to create the SQL for. + */ + public void addCreateClauseForAttribute(EOAttribute attr) { + StringBuffer buf = new StringBuffer(attr.columnName()); + buf.append(' '); + buf.append(columnTypeStringForAttribute(attr)); + buf.append(allowsNullClauseForConstraint(attr.allowsNull())); + appendItemToListString(buf.toString(), _listString()); + } + + public void addInsertListAttribute(EOAttribute attr, Object o) { + } + + public void addJoinClause(String left, String right, int semantic) { + String s = assembleJoinClause(left, right, semantic); + if (_joinClauseString == null) + _joinClauseString = new StringBuffer(); + if (_joinClauseString.length() > 0) + _joinClauseString.append(" AND "); + _joinClauseString.append(s); + } + + public void addOrderByAttributeOrdering(EOSortOrdering order) { + String sql = sqlStringForAttributeNamed(order.key()); + if (order.selector().equals(EOSortOrdering.CompareCaseInsensitiveAscending) || order.selector().equals(EOSortOrdering.CompareCaseInsensitiveDescending)) + sql = "UPPER(" + sql + ")"; + if (order.selector().equals(EOSortOrdering.CompareCaseInsensitiveAscending) || order.selector().equals(EOSortOrdering.CompareAscending)) + sql += " ASC"; + else + sql += " DESC"; + appendItemToListString(sql, _orderByString()); + } + + public void addSelectListAttribute(EOAttribute attr) { + appendItemToListString(formatSQLString(sqlStringForAttribute(attr), attr.readFormat()), _listString()); + } + + public void addUpdateListAttribute(EOAttribute attr, Object o) { + StringBuffer buf = new StringBuffer(attr.columnName()); + buf.append('='); + buf.append(formatSQLString(formatValueForAttribute(o, attr), attr.writeFormat())); + appendItemToListString(buf.toString(), _listString()); + } + + public NSMutableDictionary aliasesByRelationshipPath() { + if (_aliasesByRelationshipPath == null) { + _aliasesByRelationshipPath = new NSMutableDictionary(); + if (!_useAliases) + return _aliasesByRelationshipPath; + _aliasesByRelationshipPath.setObjectForKey("t0", ""); + } + return _aliasesByRelationshipPath; + } + + public String allowsNullClauseForConstraint(boolean flag) { + return flag ? "" : " NOT NULL"; + } + + public void appendItemToListString(String item, StringBuffer list) { + if (list.length() > 0) + list.append(", "); + list.append(item); + } + + public String assembleDeleteStatementWithQualifier(EOQualifier q, String tableList, String whereClause) { + String s = "DELETE FROM " + tableList; + if (whereClause != null && whereClause.length() > 0) + s += " WHERE " + whereClause; + return s; + } + + public String assembleInsertStatementWithRow(NSDictionary row, + String tableList, String columnList, String valueList) { + String sql = "INSERT INTO " + tableList; + if (columnList != null) + sql += " (" + columnList + ")"; + sql += " VALUES " + valueList; + return sql; + } + + public String assembleJoinClause(String leftName, String rightName, int semantic) { + String op = "="; + if (semantic == EORelationship.LeftOuterJoin) + op = "*="; + else if (semantic == EORelationship.RightOuterJoin) + op = "=*"; + return leftName + op + rightName; + } + + public String assembleSelectStatementWithAttributes(NSArray attributes, + boolean lock, EOQualifier q, NSArray fetchOrder, String selectString, String columnList, + String tableList, String whereClause, String joinClause, String orderByClause, String lockClause) { + String sql = selectString + " " + columnList + " FROM " + tableList; + if (lockClause != null) + sql += " " + lockClause; + if (whereClause != null || joinClause != null) + sql += " WHERE "; + if (whereClause != null) + sql += whereClause; + if (whereClause != null && joinClause != null) + sql += " AND "; + if (joinClause != null) + sql += joinClause; + if (orderByClause != null) + sql += " ORDER BY " + orderByClause; + return sql; + } + + public String assembleUpdateStatementWithRow(NSDictionary row, EOQualifier q, String tableList, String updateList, String whereClause) { + String s = "UPDATE " + tableList + " SET " + updateList; + if (whereClause != null && whereClause.length() > 0) + s += " WHERE " + whereClause; + return s; + } + + public NSArray bindVariableDictionaries() { + return null; + } + + public abstract NSMutableDictionary bindVariableDictionaryForAttribute(EOAttribute attr, Object o); + + public String columnTypeStringForAttribute(EOAttribute attr) { + String x = attr.externalType(); + if (attr.precision() > 0) { + x += " (" + attr.precision() + "," + attr.scale() + ")"; + } else if (attr.width() > 0) { + x += " (" + attr.width() + ")"; + } + return x; + } + + public EOEntity entity() { + return _entity; + } + + public String externalNameQuoteCharacter() { + return "\""; + } + + public String formatSQLString(String value, String format) { + if (format == null) + return value; + return value; + } + + /** + * Returns the received string wrapped in single quotes, + * with any quotes or escape chars found inside it + * properly escaped. + * @param s The string to format. + */ + public String formatStringValue(String s) { + StringBuffer buf = new StringBuffer(s); + for (int i = buf.length()-1; i >= 0; i--) { + if (buf.charAt(i) == sqlEscapeChar()) { + buf.insert(i, sqlEscapeChar()); + i++; + } + if (buf.charAt(i) == '\'') { + buf.insert(i, sqlEscapeChar()); + i++; + } + } + buf.append('\''); + buf.insert(0, '\''); + return buf.toString(); + } + + public String formatValueForAttribute(Object value, EOAttribute attr) { + if (value == null || value == NSKeyValueCoding.NullValue) + return "NULL"; + if (value instanceof String) + return formatStringValue((String)value); + if (value instanceof Number) + return sqlStringForNumber((Number)value); + //TODO: format timestamps + return value.toString(); + } + + public String joinClauseString() { + if (_joinClauseString == null) + return null; + return _joinClauseString.toString(); + } + + public void joinExpression() { + _joinClauseString = null; + if (_aliasesByEntityName.count() > 1) { + } + } + + public String listString() { + return _listString().toString(); + } + + public String lockClause() { + return ""; + } + + public boolean mustUseBindVariableForAttribute(EOAttribute attr) { + return false; + } + + public String orderByString() { + if (_orderByString == null) + return null; + return _orderByString.toString(); + } + + public void prepareConstraintStatementForRelationship(EORelationship rel, NSArray arr1, NSArray arr2) { + } + + public void prepareDeleteExpressionForQualifier(EOQualifier q) { + String where = null; + setStatement(assembleDeleteStatementWithQualifier(q, _entity.externalName(), where)); + } + + public void prepareInsertExpressionWithRow(NSDictionary row) { + StringBuffer cols = new StringBuffer("("); + StringBuffer values = new StringBuffer("("); + Enumeration enumeration = row.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + EOAttribute a = _entity.attributeNamed(key); + cols.append(a.columnName()); + values.append(formatValueForAttribute(row.objectForKey(key), a)); + if (enumeration.hasMoreElements()) { + cols.append(", "); + values.append(", "); + } + } + cols.append(")"); + cols.append(")"); + setStatement(assembleInsertStatementWithRow(row, _entity.externalName(), cols.toString(), values.toString())); + } + + public void prepareSelectExpressionWithAttributes(NSArray atts, boolean lock, EOFetchSpecification fspec) { + _aliasesByRelationshipPath = new NSMutableDictionary(); + _aliasesByEntityName = new NSMutableDictionary(); + EOQualifier q = null; + NSArray order = null; + if (fspec != null) { + q = fspec.qualifier(); + order = fspec.sortOrderings(); + } + //Assemble the column list (this yields the alias list) + for (int i = 0; i < atts.count(); i++) + addSelectListAttribute((EOAttribute)atts.objectAtIndex(i)); + //assemble the where string + if (q != null) { + if (q instanceof EOQualifierSQLGeneration) + setWhereClauseString(sqlStringForQualifier((EOQualifierSQLGeneration)q)); + else { + EOQualifierSQLGeneration.Support sup = EOQualifierSQLGeneration.Support.supportForClass(q.getClass()); + setWhereClauseString(sup.sqlStringForSQLExpression(q, this)); + } + } + //assemble the join string + joinExpression(); + //assemble the order by string + if (order != null && order.count() > 0) { + for (int i = 0; i < order.count(); i++) { + EOSortOrdering so = (EOSortOrdering)order.objectAtIndex(i); + addOrderByAttributeOrdering(so); + } + } + //create the statement + setStatement(assembleSelectStatementWithAttributes(atts, lock, q, order, "SELECT", listString(), + tableListWithRootEntity(_entity), whereClauseString(), joinClauseString(), orderByString(), lockClause())); + } + + /** Build an UPDATE statement with the given information. */ + public void prepareUpdateExpressionWithRow(NSDictionary row, EOQualifier q) { + StringBuffer buf = new StringBuffer(); + Enumeration enumeration = row.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + EOAttribute a = _entity.attributeNamed(key); + if (a == null) + throw new EOGeneralAdaptorException("Cannot find attribute named " + key + " in entity " + _entity.name()); + buf.append(a.columnName()); + buf.append('='); + buf.append(formatValueForAttribute(row.objectForKey(key), a)); + if (enumeration.hasMoreElements()) + buf.append(", "); + } + if (q != null) { + setWhereClauseString(sqlStringForQualifier(null)); + } + setStatement(assembleUpdateStatementWithRow(row, q, _entity.externalName(), buf.toString(), whereClauseString())); + } + + public void setStatement(String statement) { + _statement = statement; + } + + public String statement() { + return _statement; + } + + public void setUseAliases(boolean flag) { + _useAliases = flag; + } + + public boolean useAliases() { + return _useAliases; + } + + public void setUseBindVariables(boolean flag) { + } + + public boolean useBindVariables() { + return System.getProperty("EOAdaptorUseBindVariables", "false").equals("true"); + } + + /** @deprecated Check externalNameQuoteCharacter instead. */ + public static void setUseQuotedExternalNames(boolean flag) { + _quoteExternalNames = flag; + } + /** @deprecated Use the instance method externalNameQuoteCharacter instead. */ + public static boolean useQuotedExternalNames() { + return _quoteExternalNames; + } + + public void setWhereClauseString(String clause) { + _whereClauseString = clause; + } + + public String whereClauseString() { + return _whereClauseString; + } + + public boolean shouldUseBindVariableForAttribute(EOAttribute attr) { + return false; + } + + public char sqlEscapeChar() { + return '\\'; + } + + public String sqlPatternFromShellPattern(String pattern) { + return sqlPatternFromShellPatternWithEscapeCharacter(pattern, sqlEscapeChar()); + } + + public String sqlPatternFromShellPatternWithEscapeCharacter(String pattern, char escape) { + StringBuffer buf = new StringBuffer(pattern); + int idx = 0; + //escape all '%' + do { + idx = buf.indexOf("%"); + if (idx == 0) + buf.insert(escape, 0); + else if (idx > 0 && buf.charAt(idx-1) != escape) + buf.insert(escape, idx); + } while (idx >= 0); + //escape all '_' + do { + idx = buf.indexOf("_"); + if (idx == 0) + buf.insert(escape, 0); + else if (idx > 0 && buf.charAt(idx-1) != escape) + buf.insert(escape, idx); + } while (idx >= 0); + //substitute all '*' + do { + idx = buf.indexOf("*"); + if (idx >= 0) + buf.replace(idx, idx+1, "%"); + } while (idx >= 0); + //substitute all '?' + do { + idx = buf.indexOf("?"); + if (idx >= 0) + buf.replace(idx, idx+1, "_"); + } while (idx >= 0); + return buf.toString(); + } + + public String sqlStringForAttribute(EOAttribute attr) { + if (_aliasesByEntityName == null) + _aliasesByEntityName = new NSMutableDictionary(); + String alias = (String)_aliasesByEntityName.objectForKey(attr.entity().name()); + if (alias == null) { + alias = "t" + (_aliasesByEntityName.count() + 1); + _aliasesByEntityName.setObjectForKey(alias, attr.entity().name()); + } + if (useAliases()) + return alias + "." + attr.columnName(); + else + return attr.entity().externalName() + "." + attr.columnName(); + } + + public String sqlStringForAttributeNamed(String name) { + if (name.indexOf('.') > 0) { + return sqlStringForAttribute(_entity._attributeForPath(name)); + } + return sqlStringForAttribute(_entity.attributeNamed(name)); + + } + + /** + * Returns a string representing the path from the first + * relationship in the array to the last one. + * @param path An array of EORelationship objects. + * @return A string consisting of the names of the relationships + * separated by dots. + */ + public String sqlStringForAttributePath(NSArray path) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < path.count(); i++) { + EORelationship rel = (EORelationship)path.objectAtIndex(i); + if (i > 0) + buf.append('.'); + buf.append(rel.name()); + } + return buf.toString(); + } + + public String sqlStringForCaseInsensitiveLike(String key, String value) { + return "LOWER(" + key + ") LIKE LOWER(" + sqlPatternFromShellPattern(value) + ")"; + } + + public String sqlStringForConjoinedQualifiers(NSArray qualifiers) { + EOAndQualifier q = new EOAndQualifier(qualifiers); + return EOQualifierSQLGeneration.Support.supportForClass(EOAndQualifier.class).sqlStringForSQLExpression(q, this); + } + + public String sqlStringForData(NSData data) { + byte[] b = data.bytes(); + char[] c = new char[b.length * 2]; + int pos = 0; + for (int i = 0; i < b.length; i++) { + int x = (b[i] & 0xf0) >> 4; + c[pos++] = _hexChars[x]; + x = (b[i] & 0x0f); + c[pos++] = _hexChars[x]; + } + return new String(c); + } + + public String sqlStringForDisjoinedQualifiers(NSArray qualifiers) { + EOOrQualifier q = new EOOrQualifier(qualifiers); + return EOQualifierSQLGeneration.Support.supportForClass(EOOrQualifier.class).sqlStringForSQLExpression(q, this); + } + + public String sqlStringForKeyComparisonQualifier(EOKeyComparisonQualifier q) { + return EOQualifierSQLGeneration.Support.supportForClass(EOKeyComparisonQualifier.class).sqlStringForSQLExpression(q, this); + } + + public String sqlStringForKeyValueQualifier(EOKeyValueQualifier q) { + return EOQualifierSQLGeneration.Support.supportForClass(EOKeyValueQualifier.class).sqlStringForSQLExpression(q, this); + } + + public String sqlStringForNegatedQualifier(EOQualifier q) { + EOQualifierSQLGeneration.Support sup = EOQualifierSQLGeneration.Support.supportForClass(q.getClass()); + String sql = sup.sqlStringForSQLExpression(q, this); + return "NOT (" + sql + ")"; + } + + public static String sqlStringForNumber(Number number) { + return number.toString(); + } + + public String sqlStringForQualifier(EOQualifierSQLGeneration sql) { + return sql.sqlStringForSQLExpression(this); + } + + public String sqlStringForSchemaObjectName(String name) { + return name; + } + + public String sqlStringForSelector(NSSelector sel, Object value) { + if (sel == EOQualifier.QualifierOperatorEqual) { + if (value == NSKeyValueCoding.NullValue) + return " is "; + return "="; + } else if (sel == EOQualifier.QualifierOperatorNotEqual) { + if (value == NSKeyValueCoding.NullValue) + return " is not "; + return "<>"; + } else if (sel == EOQualifier.QualifierOperatorLessThan) { + return "<"; + } else if (sel == EOQualifier.QualifierOperatorGreaterThan) { + return ">"; + } else if (sel == EOQualifier.QualifierOperatorLessThanOrEqualTo) { + return "<="; + } else if (sel == EOQualifier.QualifierOperatorGreaterThanOrEqualTo) { + return ">="; + } else if (sel == EOQualifier.QualifierOperatorLike || sel == EOQualifier.QualifierOperatorCaseInsensitiveLike) { + return " like "; + } + return sel.name(); + } + + public static String sqlStringForString(String s) { + return s; + } + + public String sqlStringForValue(Object value, String keyPath) { + EOAttribute a = _entity._attributeForPath(keyPath); + return formatValueForAttribute(value, a); + } + + public String tableListWithRootEntity(EOEntity root) { + StringBuffer buf = new StringBuffer(root.externalName()); + if (useAliases()) { + buf.append(" "); + buf.append(_aliasesByEntityName.objectForKey(root.name())); + } + if (_aliasesByEntityName.count() > 0) { + Enumeration enumeration = _aliasesByEntityName.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (!key.equals(root.name())) { + buf.append(", "); + buf.append(key); + if (useAliases()) + buf.append(_aliasesByEntityName.objectForKey(key)); + } + } + } + return buf.toString(); + } + + public String toString() { + return "<" + getClass().getName() + "> " + statement(); + } + + public String valueList() { + return _valueList().toString(); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.4 2003/08/29 21:14:18 chochos + * fix the algorithm in formatStringValue. + * + * Revision 1.3 2003/08/14 02:12:32 chochos + * implemented use of (simple) qualifiers + * + * Revision 1.2 2003/08/13 22:59:39 chochos + * Can now generate simple one-entity select statements. + * + * Revision 1.1 2003/08/13 01:04:32 chochos + * the SQL generation classes. EOSQLExpression still needs a lot of work, but the factory is pretty much done. + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpressionFactory.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpressionFactory.java new file mode 100644 index 0000000..6bdbffe --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOSQLExpressionFactory.java @@ -0,0 +1,131 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.access; + +import java.lang.reflect.Constructor; + +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOSQLExpressionFactory { + + protected EOAdaptor _adaptor; + protected Class _expressionClass; + private Constructor _instantiator; + + public EOSQLExpressionFactory(EOAdaptor adaptor) { + super(); + _adaptor = adaptor; + _expressionClass = _adaptor.expressionClass(); + if (_expressionClass == null) + throw new IllegalStateException("EOAdaptor " + _adaptor.name() + " returned null for expressionClass()"); + } + + public EOAdaptor adaptor() { + return _adaptor; + } + + /** + * Creates an instance of the adaptor's expression class, + * with entity assigned to it. + * @param entity The entity with which to initialize the expression. + * @return An EOSQLExpression. + */ + public EOSQLExpression createExpression(EOEntity entity) { + EOSQLExpression expr = null; + if (_instantiator == null) { + try { + _instantiator = _expressionClass.getConstructor(new Class[]{ EOEntity.class }); + } catch (Exception ex) { + throw new IllegalArgumentException("The expression class " + _expressionClass.getName() + " has no constructor with an entity parameter."); + } + } + try { + expr = (EOSQLExpression)_instantiator.newInstance(new Object[]{ entity }); + } catch (Exception ex) { + throw new IllegalArgumentException("Cannot create new expression of class " + _expressionClass.getName()); + } + return expr; + } + + public EOSQLExpression expressionForEntity(EOEntity entity) { + return createExpression(entity); + } + + public EOSQLExpression deleteStatementWithQualifier(EOQualifier qualifier, EOEntity entity) { + EOSQLExpression expr = createExpression(entity); + expr.prepareDeleteExpressionForQualifier(qualifier); + return expr; + } + + public EOSQLExpression insertStatementForRow(NSDictionary row, EOEntity entity) { + EOSQLExpression expr = createExpression(entity); + expr.prepareInsertExpressionWithRow(row); + return expr; + } + + public EOSQLExpression selectStatementForAttributes(NSArray atts, boolean lock, EOFetchSpecification fspec, EOEntity entity) { + EOSQLExpression expr = createExpression(entity); + expr.prepareSelectExpressionWithAttributes(atts, lock, fspec); + return expr; + } + + public EOSQLExpression updateStatementForRow(NSDictionary row, EOQualifier qualifier, EOEntity entity) { + EOSQLExpression expr = createExpression(entity); + expr.prepareUpdateExpressionWithRow(row, qualifier); + return expr; + } + + public EOSQLExpression expressionForString(String sql) { + EOSQLExpression expr = null; + try { + expr = (EOSQLExpression)_expressionClass.newInstance(); + } catch (Exception e) { + return null; + } + expr.setStatement(sql); + return expr; + } + + public Class expressionClass() { + return _expressionClass; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/13 01:04:32 chochos + * the SQL generation classes. EOSQLExpression still needs a lot of work, but the factory is pretty much done. + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOStoredProcedure.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOStoredProcedure.java new file mode 100644 index 0000000..f38ec2a --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/access/EOStoredProcedure.java @@ -0,0 +1,141 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.access; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +/** +* Represents a stored procedure in a database. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOStoredProcedure implements EOPropertyListEncoding { + + protected String _name; + protected String _externalName; + protected EOModel _model; + protected NSArray _arguments = NSArray.EmptyArray; + protected NSDictionary _userInfo = NSDictionary.EmptyDictionary; + protected NSDictionary _internalInfo = NSDictionary.EmptyDictionary; + + /** Creates a stored procedure from a property list. */ + public EOStoredProcedure(NSDictionary dict, Object obj) { + super(); + if (obj instanceof EOModel) + _model = (EOModel)obj; + NSArray a = (NSArray)dict.objectForKey("arguments"); + if (a != null) { + NSMutableArray args = new NSMutableArray(a.count()); + for (int i = 0; i < a.count(); i++) { + NSDictionary ad = (NSDictionary)a.objectAtIndex(i); + EOAttribute arg = new EOAttribute(ad, this); + args.addObject(arg); + } + _arguments = args; + } + setName((String)dict.objectForKey("name")); + setExternalName((String)dict.objectForKey("externalName")); + if (dict.objectForKey("userInfo") != null) + setUserInfo((NSDictionary)dict.objectForKey("userInfo")); + if (dict.objectForKey("internalInfo") != null) + _internalInfo = (NSDictionary)dict.objectForKey("internalInfo"); + } + + public EOStoredProcedure(String withName) { + super(); + setName(withName); + } + + public void setName(String value) { + _name = value; + } + public String name() { + return _name; + } + + public void setExternalName(String value) { + _externalName = value; + } + public String externalName() { + return _externalName; + } + + public void setArguments(NSArray value) { + _arguments = value; + } + public NSArray arguments() { + return _arguments; + } + + public EOModel model() { + return _model; + } + + public void setUserInfo(NSDictionary info) { + _userInfo = info; + } + public NSDictionary userInfo() { + return _userInfo; + } + + public void awakeWithPropertyList(NSDictionary plist) { + } + + public void encodeIntoPropertyList(NSMutableDictionary plist) { + plist.setObjectForKey(name(), "name"); + plist.setObjectForKey(externalName(), "externalName"); + NSMutableArray arr = new NSMutableArray(_arguments.count()); + NSMutableDictionary d = null; + for (int i = 0; i < _arguments.count(); i++) { + EOAttribute a = (EOAttribute)_arguments.objectAtIndex(i); + d = new NSMutableDictionary(); + a.encodeIntoPropertyList(d); + arr.addObject(d); + } + plist.setObjectForKey(arr, "arguments"); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/11 19:38:27 chochos + * Can now read from a file and re-write to another file. + * + * Revision 1.3 2003/08/09 01:36:32 chochos + * implement EOPropertyListEncoding + * + * Revision 1.2 2003/08/08 02:14:43 chochos + * can create a stored procedure from a property list. main accessors are in place. + * + * Revision 1.1 2003/08/07 02:41:04 chochos + * these don't do much for now. + * +*/ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java new file mode 100644 index 0000000..ddaacf5 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/AbstractObjectStore.java @@ -0,0 +1,762 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSNotificationQueue; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An abstract implementation of object store that +* implements common functionality. Subclasses must +* implement data object creation, initialization, and +* refault logic, as well as logic to commit an editing +* context. +*/ +public abstract class AbstractObjectStore extends EOObjectStore +{ + private NSMutableArray insertedIDsBuffer; + private NSMutableArray updatedIDsBuffer; + private NSMutableArray deletedIDsBuffer; + private NSMutableArray invalidatedIDsBuffer; + + private Map snapshots; + private List exceptionList; + + /** + * Constructs a new instance of this object store. + */ + public AbstractObjectStore() + { + snapshots = new HashMap(); + exceptionList = null; + + insertedIDsBuffer = new NSMutableArray(); + updatedIDsBuffer = new NSMutableArray(); + deletedIDsBuffer = new NSMutableArray(); + invalidatedIDsBuffer = new NSMutableArray(); + + // register for notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + NSNotificationCenter.defaultCenter().addObserver( + this, + handleNotification, + EOClassDescription.ClassDescriptionNeededForEntityNameNotification, + null ); + } + + /** + * This implementation returns an appropriately configured array fault. + */ + public NSArray arrayFaultWithSourceGlobalID( EOGlobalID aGlobalID, + String aRelationship, EOEditingContext aContext ) + { // System.out.println( "arrayFaultWithSourceGlobalID: " + aGlobalID + " : " + aRelationship ); + return new ArrayFault( + aGlobalID, aRelationship, aContext ); + } + + /** + * This implementation returns the actual object for the specified id. + */ + public /*EOEnterpriseObject*/Object faultForGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { // System.out.println( "faultForGlobalID: " + aGlobalID ); + return /*(EOEnterpriseObject)*/createInstanceWithEditingContext( aGlobalID, aContext ); + } + + /** + * Returns a fault representing an object of the specified entity type with + * values from the specified dictionary. The fault should belong to the + * specified editing context. + * NOTE: Faults are not supported yet. + */ + public /*EOEnterpriseObject*/Object faultForRawRow( Map aDictionary, String anEntityName, + EOEditingContext aContext ) + { + //TODO: raw rows are not yet supported + throw new WotonomyException( "Faults are not yet supported." ); + } + + /** + * Given a newly instantiated object, this method initializes its + * properties to values appropriate for the specified id. The object + * should belong to the specified editing context. This method is called + * to populate faults. + */ + public void initializeObject( Object anObject, EOGlobalID aGlobalID, + EOEditingContext aContext ) + { //System.out.println( "initializeObject: " + aGlobalID ); + try + { + String entity = entityForGlobalIDOrObject( aGlobalID, null ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + Collection attributes = classDesc.attributeKeys(); + Map data = readFromCache( aGlobalID, attributes ); + String key; + Iterator iterator = attributes.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next().toString(); + + // write the snapshot's reference into the object + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).takeStoredValueForKey( data.get( key ), key ); + } + else + { + EOKeyValueCodingSupport.takeStoredValueForKey( anObject, data.get( key ), key ); + } + + //NOTE: our objects are expected to make a copy + // of their data before it is modified, so it's okay + // to return them our copy of the data: + // we trust that they will not modify it. + } + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + /** + * Reads the local data snapshot for the specified id. + * If no snapshot exists, a new snapshot is created. + * If the specified keys are not in the snapshot, + * new data is fetched into the snapshot. + * If null is specified, all known keys are returned. + * Will not return null. + * Result will have values for those keys and only + * those keys requested. Missing keys indicate an + * error occurred. + */ + protected Map readFromCache( EOGlobalID aGlobalID, Collection keys ) + { + Map snapshot = (Map) snapshots.get( aGlobalID ); + + // if no snapshot for this id, create an empty one + if ( snapshot == null ) + { + snapshot = new HashMap(); + snapshots.put( aGlobalID, snapshot ); + } + + // if we don't have all the necessary keys + if ( ( keys == null ) || ( ! snapshot.keySet().containsAll( keys ) ) ) + { + // we need to make a server call + try + { + Map data = readObject( aGlobalID, keys ); + + // compare timestamps + Comparable localTimestamp = (Comparable) timestampForData( snapshot ); + // if our local snapshot has an timestamp (new snapshots don't have timestamp) + if ( localTimestamp != null ) + { + Comparable incomingTimestamp = (Comparable) timestampForData( data ); + if ( incomingTimestamp == null ) + { + // not allowed to happen + new RuntimeException( "Server returned data without an timestamp" ).printStackTrace(); + // however, we can just assume it's a newer timestamp and continue + } + + // if timestamps don't match + if ( ( incomingTimestamp == null ) || ( ! incomingTimestamp.equals( localTimestamp ) ) ) + { + // dump our existing snapshot's data + snapshot.clear(); + // queue for a notification on this oid as updated + //TODO: implement this + } + } + + // copy new data into our local snapshot + snapshot.putAll( data ); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + // return just the requested keys from our updated snapshot + Map result = new HashMap(); + if ( keys == null ) + { + result.putAll( snapshot ); + } + else + { + Object key; + Iterator iterator = keys.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next(); + result.put( key, snapshot.get( key ) ); + } + } + return snapshot; + } + + /** + * Returns a comparable object (typically a Date or Long) for + * the given data map or snapshot. This is used to determine + * whether a local snapshot should be dumped in favor of fetched + * data from the server. + * Returns null if no timestamp can be determined, in which + * case the fetched data will assumed to be more recent than + * any local snapshot. + */ + abstract protected Comparable timestampForData( Map aDataMap ); + + /** + * Extracts the global id for the fetched data or snapshot. + * Some entities have multi-attribute keys that would be + * assembled into a single instance of EOGlobalID. + */ + abstract protected EOGlobalID globalIDForData( Map aDataMap ); + + /** + * Returns the entity that corresponds to the specified global id + * and/or object. Either may be null, but both will not be null. + * //FIXME: This is less than elegant. + */ + abstract protected String entityForGlobalIDOrObject( + EOGlobalID aGlobalID, Object anObject ); + + /** + * Returns the keys that have changed on the specified object. + * If null, all keys are presumed changed, including relationships. + */ + abstract protected Collection changedKeysForObject( Object anObject ); + + /** + * Returns the data for the row corresponding to the specified id + * containing at least the specified keys. Implementations are allowed + * to return more data than requested, and callers are advised to take + * advantage of the returned data. + */ + abstract protected Map readObject( EOGlobalID aGlobalID, Collection keys ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Map insertObject( EOGlobalID aGlobalID, Map aDataMap ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Object updateObject( EOGlobalID aGlobalID, Map aDataMap ); + + /** + * Returns the data for the row corresponding to the specified id. + * //TODO: Need a better return value? How to return invalidated list? + */ + abstract protected Object deleteObject( EOGlobalID aGlobalID ); + + /** + * Creates a new instance of an object that corresponds to the + * specified global id and is registered in the specified context. + * This implementation extracts the entity type from getEntityForGlobaID + * and construct a new instance from the class description that + * corresponds to the entity type. Override to change this behavior. + */ + protected Object createInstanceWithEditingContext( + EOGlobalID aGlobalID, EOEditingContext aContext ) + { + String entity = entityForGlobalIDOrObject( aGlobalID, null ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + Object result = classDesc.createInstanceWithEditingContext( aContext, aGlobalID ); + if ( result instanceof EOFaulting ) + { + ((EOFaulting)result).turnIntoFault( null ); + } + return result; + } + + /** + * Dumps the snapshot corresponding to the specified id. + */ + protected void invalidateObject( EOGlobalID aGlobalID ) + { + snapshots.remove( aGlobalID ); + } + + /** + * Dumps all snapshots. + */ + protected void invalidateAllCache() + { + snapshots.clear(); + } + + /** + * Remove all values from all objects in memory, turning them into faults, + * and posts a notification that all objects have been invalidated. + */ + public void invalidateAllObjects() + { + invalidateAllCache(); + + // post notification + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( + InvalidatedAllObjectsInStoreNotification, this ), + NSNotificationQueue.PostNow ); + } + + /** + * Removes values with the specified ids from memory, turning them into + * faults, and posts a notification that those objects have been invalidated. + */ + public void invalidateObjectsWithGlobalIDs( List aList ) + { + NSArray empty = new NSArray(); + NSMutableArray invalidated = new NSMutableArray(); + + Object object; + Iterator iterator = aList.iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + invalidateObject( (EOGlobalID) object ); + invalidated.addObject( object ); + } + + NSMutableDictionary info = new NSMutableDictionary(); + info.setObjectForKey( empty, InsertedKey ); + info.setObjectForKey( empty, UpdatedKey ); + info.setObjectForKey( empty, DeletedKey ); + info.setObjectForKey( invalidated, InvalidatedKey ); + + // post notification + NSNotificationQueue.defaultQueue(). + enqueueNotificationWithCoalesceMaskForModes( new NSNotification( + ObjectsChangedInStoreNotification, this, info ), + NSNotificationQueue.PostNow, + NSNotificationQueue.NotificationNoCoalescing, null ); + } + + /** + * Returns false because locking is not currently permitted. + */ + public boolean isObjectLockedWithGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + return false; + } + + /** + * Does nothing because locking is not currently permitted. + */ + public void lockObjectWithGlobalID( EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + // does nothing + } + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship. This method may not return an array fault + * because array faults call this method to fetch on demand. + * All objects must be registered the specified editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public NSArray objectsForSourceGlobalID( EOGlobalID aGlobalID, + String aRelationship, EOEditingContext aContext ) + { // System.out.println( "objectsForSourceGlobalID: " + aGlobalID + " : " + aRelationship + " : " ); + + Map snapshot = readFromCache( aGlobalID, new NSArray( aRelationship ) ); + Object value = snapshot.get( aRelationship ); + if ( value == null ) value = new NSArray(); // empty list + if ( ! ( value instanceof Collection ) ) + { + throw new RuntimeException( "Specified relationship is not a collection: " + + aRelationship + " : " + aGlobalID + " : " + value ); + } + + NSArray result = new NSMutableArray(); + + // get fault for each id + EOGlobalID id; + Object fault; + Iterator iterator = ((Collection)value).iterator(); + while ( iterator.hasNext() ) + { + id = (EOGlobalID) iterator.next(); + + // get registered fault + fault = aContext.faultForGlobalID( id, aContext ); + + // assert fault + if ( fault == null ) + { + // this should never happen + throw new RuntimeException( + "Could not find fault for ID: " + id ); + } + + result.add( fault ); + } + + fireObjectsChangedInStore(); + +//System.out.println( "done" ); + return result; + } + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. Faults are not allowed in the array. + * Each object is registered with the specified editing context. + * If any object is already fetched in the specified context, + * it is not refetched and that object should be used in the array. + */ + public NSArray objectsWithFetchSpecification( + EOFetchSpecification aFetchSpec, EOEditingContext aContext ) + { + NSMutableArray result = new NSMutableArray(); + + //TODO: implement this + + return result; + } + + /** + * Fires ObjectsChangedInStoreNotification + * with contents of buffers and then clears buffers. + * If buffers are empty, does nothing. + */ + private void fireObjectsChangedInStore() + { + // check for changes to broadcast + if ( insertedIDsBuffer.size() + updatedIDsBuffer.size() + + deletedIDsBuffer.size() + invalidatedIDsBuffer.size() == 0 ) + { + return; + } + + // broadcast ObjectsChangedInStoreNotification + // for the benefit of child editing contexts + + NSMutableDictionary storeInfo = new NSMutableDictionary(); + + storeInfo.setObjectForKey( + new NSArray( (Collection) insertedIDsBuffer ), + EOObjectStore.InsertedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) updatedIDsBuffer ), + EOObjectStore.UpdatedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) deletedIDsBuffer ), + EOObjectStore.DeletedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) invalidatedIDsBuffer ), + EOObjectStore.InvalidatedKey ); + + // clear buffers + + insertedIDsBuffer.removeAllObjects(); + updatedIDsBuffer.removeAllObjects(); + deletedIDsBuffer.removeAllObjects(); + invalidatedIDsBuffer.removeAllObjects(); + + // post notification + NSNotificationQueue.defaultQueue(). + enqueueNotificationWithCoalesceMaskForModes( new NSNotification( + ObjectsChangedInStoreNotification, this, storeInfo ), + NSNotificationQueue.PostNow, + NSNotificationQueue.NotificationNoCoalescing, null ); + } + + /** + * Removes all values from the specified object, + * converting it into a fault for the specified id. + * New or deleted objects should not be refaulted. + */ + public void refaultObject( Object anObject, EOGlobalID aGlobalID, + EOEditingContext aContext ) + { +//System.out.println( "refaultObject: " + aGlobalID ); +//new net.wotonomy.ui.swing.util.StackTraceInspector(); + if ( anObject instanceof EOFaulting ) + { + ((EOFaulting)anObject).turnIntoFault( null ); + } + } + + /** + * Writes all changes in the specified editing context + * to the respository. + */ + public void saveChangesInEditingContext ( EOEditingContext aContext ) + { + Object result; // need a container result? + Map updateMap; + Object object; + EOGlobalID id; + Iterator iterator; + + //TODO: the ordering of operations here + // needs to be a lot more sophisticated. + + // process deletes first + iterator = aContext.deletedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + id = aContext.globalIDForObject( object ); + try + { + result = deleteObject( id ); + } + catch ( Exception exc ) + { + System.out.println( "Error deleting object: " + id ); + exc.printStackTrace(); + } + } + + // process inserts next + iterator = aContext.insertedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + processInsert( aContext, object ); + } + + // process updates last + iterator = aContext.updatedObjects().iterator(); + while ( iterator.hasNext() ) + { + object = iterator.next(); + id = aContext.globalIDForObject( object ); + try + { + updateMap = getUpdateMap( aContext, object ); + result = updateObject( id, updateMap ); + } + catch ( Exception exc ) + { + System.out.println( "Error updating object: " + id ); + exc.printStackTrace(); + } + } + + //aContext.invalidateAllObjects(); + } + + protected Object processInsert( EOEditingContext aContext, Object object ) + { + Map result = null; + EOGlobalID id = aContext.globalIDForObject( object ); + try + { + Map updateMap; + updateMap = getUpdateMap( aContext, object ); + result = insertObject( id, updateMap ); + id = globalIDForData( result ); // read new permanent id + + // broadcast that the global id has changed. + NSMutableDictionary userInfo = new NSMutableDictionary(); + userInfo.setObjectForKey( id, aContext.globalIDForObject( object ) ); + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( EOGlobalID.GlobalIDChangedNotification, + null , userInfo ), NSNotificationQueue.PostNow ); + + } + catch ( Exception exc ) + { + System.out.println( "Error inserting object: " + id ); + exc.printStackTrace(); + } + return result; + } + + /** + * This method returns a map containing just the keys that are modified + * for a given object, converting any to-one or to-many relationships + * to id references. + */ + protected Map getUpdateMap( EOEditingContext aContext, Object anObject ) + { + Map result = new HashMap(); + EOEditingContext context = aContext; + + String entity = entityForGlobalIDOrObject( null, anObject ); + EOClassDescription classDesc = + EOClassDescription.classDescriptionForEntityName( entity ); + if ( classDesc == null ) + { + throw new WotonomyException( "Unknown entity type: " + entity ); + } + + NSArray oneKeys = classDesc.toOneRelationshipKeys(); + NSArray manyKeys = classDesc.toManyRelationshipKeys(); + + String key; + Object value; + EOGlobalID id; + + Collection changedKeys = changedKeysForObject( anObject ); + if ( changedKeys == null ) + { + // assume all keys changed + changedKeys = classDesc.attributeKeys(); + changedKeys.addAll( oneKeys ); + changedKeys.addAll( manyKeys ); + } + Iterator iterator = changedKeys.iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next().toString(); + if ( anObject instanceof EOKeyValueCoding ) + { + value = ((EOKeyValueCoding)anObject).storedValueForKey( key ); + } + else + { + value = EOKeyValueCodingSupport.storedValueForKey( anObject, key ); + } + + // convert to-one relationship to oid + if ( oneKeys.contains( key ) ) + { + id = context.globalIDForObject( value ); + + // if this id hasn't been persisted, save it first + // NOTE: this won't work for self-referential graphs of objects! + if ( id.isTemporary() ) + { + processInsert( aContext, value ); + id = context.globalIDForObject( value ); + } + + value = id; + } + else + // convert to-many relationship list to oid list + if ( manyKeys.contains( key ) ) + { + //NOTE: we can assume that array faults that + // are marked as changed have been fired. + if ( value instanceof Collection ) + { + Object object; + Collection newValue = new LinkedList(); + Iterator jiterator = ((Collection)value).iterator(); + while ( jiterator.hasNext() ) + { + object = jiterator.next(); + id = context.globalIDForObject( object ); + + // if this id hasn't been persisted, save it first + // NOTE: this won't work for self-referential graphs of objects! + if ( id.isTemporary() ) + { + processInsert( aContext, object ); + id = context.globalIDForObject( object ); + } + newValue.add( id ); + } + value = newValue; + } + else + { + // should never happen + new RuntimeException( + "Can't update to-many relationship because it's not a Collection." ) + .printStackTrace(); + } + } + + // place value in map + result.put( key, value ); + } + +System.out.println( result ); + return result; + } + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/12/18 15:37:38 mpowers + * Changes to retain ability to work with objects that don't necessarily + * implement EOEnterpriseObject. I would still like to preserve this case + * for general usage, however the access package is free to assume that + * those objects will be EOs and cast appropriately. + * + * Revision 1.4 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.3 2002/10/24 18:18:12 mpowers + * NSArray's are now considered read-only, so we can return our internal + * representation to reduce unnecessary object allocation. + * + * Revision 1.2 2002/01/19 17:27:49 mpowers + * Implemented most of it. + * + * Revision 1.1 2001/11/25 22:44:02 mpowers + * Contributing draft of AbstractObjectStore. + * + * + */ +} + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ArrayFault.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ArrayFault.java new file mode 100644 index 0000000..59e135b --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ArrayFault.java @@ -0,0 +1,219 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import net.wotonomy.foundation.NSArray; + +/** +* A class that extends NSArray to intercept any accessor calls +* in order to defer loading until the last possible moment.

+* +* Because ArrayFault inherits from NSArray which implements +* List which implements Collection, data objects may declare +* their relationships to be of type NSArray, List, or Collection.

+* +* This class should be returned by implementations of +* EOObjectStore.arrayFaultForSourceGlobalID(). +*/ +public class ArrayFault extends NSArray +{ + private EOEditingContext editingContext; + private EOGlobalID sourceID; + private String relationshipKey; + private boolean fetched; + + public ArrayFault( + EOGlobalID aSourceID, + String aRelationshipKey, + EOEditingContext aContext ) + { + super(); + editingContext = aContext; + sourceID = aSourceID; + relationshipKey = aRelationshipKey; + fetched = false; + } + + public boolean isFetched() + { + return fetched; + } + + protected void fireFault() + { + if ( !fetched ) + { +//new net.wotonomy.ui.swing.util.StackTraceInspector(); +//System.out.println( "ArrayFault.fireFault: before:" + this ); + fetched = true; + super.protectedAddAll( + editingContext.parentObjectStore().objectsForSourceGlobalID( + sourceID, + relationshipKey, + editingContext ) ); +//System.out.println( "ArrayFault.fireFault: after:" + this ); + } + } + + public Object clone() + { + fireFault(); + return super.clone(); + } + + public boolean contains(Object elem) + { + fireFault(); + return super.contains( elem ); + } + + public boolean equals(Object o) + { + fireFault(); + return super.equals( o ); + } + + public Object get(int index) + { + fireFault(); + return super.get( index ); + } + + /** + * Overridden to return the identity hash. + * This somewhat violates the List contract, + * but otherwise calling hash code would + * fire the fault. Bottom line: don't use + * array faults as keys in hash maps. + */ + public int hashCode() + { + return System.identityHashCode( this ); + } + + public int indexOf(Object o) + { + fireFault(); + return super.indexOf( o ); + } + + public boolean isEmpty() + { + fireFault(); + return super.isEmpty(); + } + + public Iterator iterator() + { + fireFault(); + return super.iterator(); + } + + public int lastIndexOf(Object o) + { + fireFault(); + return super.lastIndexOf( o ); + } + + public ListIterator listIterator() + { + fireFault(); + return super.listIterator(); + } + + public ListIterator listIterator(int index) + { + fireFault(); + return super.listIterator( index ); + } + + public int size() + { + fireFault(); + return super.size(); + } + + public List subList(int fromIndex, int toIndex) + { + fireFault(); + return super.subList( fromIndex, toIndex ); + } + + public Object[] toArray() + { + fireFault(); + return super.toArray(); + } + + public Object[] toArray(Object[] a) + { + fireFault(); + return super.toArray( a ); + } + + /** + * Overridden to display information about + * the fault only if not fetched. + * Calls to super if fetched. + */ + public String toString() + { + if ( isFetched() ) + { + return super.toString(); + } + return "[ArrayFault@"+Integer.toHexString( System.identityHashCode( this ) )+":"+sourceID+":"+relationshipKey+"]"; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/12/18 15:37:38 mpowers + * Changes to retain ability to work with objects that don't necessarily + * implement EOEnterpriseObject. I would still like to preserve this case + * for general usage, however the access package is free to assume that + * those objects will be EOs and cast appropriately. + * + * Revision 1.4 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.3 2002/10/24 18:17:37 mpowers + * ArrayFaults are now read-only. + * + * Revision 1.2 2001/05/06 22:22:55 mpowers + * Debugging. + * + * Revision 1.1 2001/05/05 23:05:42 mpowers + * Implemented Array Faults. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ChildDataSource.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ChildDataSource.java new file mode 100644 index 0000000..8123668 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ChildDataSource.java @@ -0,0 +1,187 @@ +package net.wotonomy.control; + +import java.util.Collection; +import java.util.Iterator; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; + +/** +* A data source that automates the process of +* creating a child editing context and copying +* objects from a parent context into it. +* Attach this data source to a display group +* that represents a "detail" or "drill-down" +* view.

+* +* Once created, editingContext() will return the +* child context, and fetch() will return the objects +* that were copied into the child context. +*/ +public class ChildDataSource extends EODataSource +{ + private EODataSource parent; + private EOEditingContext context; + private EOClassDescription classDescription; + private NSMutableArray objects; + + /** + * Creates a child editing context for the + * specified parent's context and copies the + * specified object into the child context. + * The object must exist in the parent context. + * fetch() will return the child's object. + */ + public ChildDataSource( + EODataSource aParentSource, + Object anObject ) + { + this( aParentSource, new NSArray( (Object) anObject ) ); + } + + /** + * Creates a child editing context for the + * specified parent's context and copies the + * specified objects into the child context. + * The objects must exist in the parent context. + * The order of the parent's objects in the + * collection will determine the order in + * which the child objects are returned from + * fetch(). + */ + public ChildDataSource( + EODataSource aParentSource, + Collection anObjectList ) + { + EOEditingContext parentContext = + aParentSource.editingContext(); + + parent = aParentSource; + context = new EOEditingContext( parentContext ); +//!new net.wotonomy.ui.swing.util.ObjectInspector( context ); + objects = new NSMutableArray(); + classDescription = null; + + Object o; + Object copy; + boolean allSameClass = true; + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + + // determine class + if ( allSameClass == true ) + { + Class c = o.getClass(); + if ( classDescription == null ) + { + classDescription = + EOClassDescription.classDescriptionForClass( c ); + } + else + { + if ( c != classDescription.getDescribedClass() ) + { + allSameClass = false; + classDescription = null; + } + } + } + + // copy and add to list + objects.addObject( parentContext.faultForGlobalID( + parentContext.globalIDForObject( o ), context ) ); + } + } + + /** + * Returns the editing context for this data source, + * which was created in the constructor and whose + * parent is the editing context specified in the + * constructor. + */ + public EOEditingContext editingContext() + { + return context; + } + + /** + * This implementation does nothing. + */ + public void insertObject ( Object anObject ) + { + + } + + /** + * This implementation does nothing. + */ + public void deleteObject ( Object anObject ) + { + + } + + /** + * Returns a List containing the objects in this + * data source. This implementation returns all + * TestObjects that have been persisted to the + * datastore in the data directory. + */ + public NSArray fetchObjects () + { + return new NSArray( (Collection) objects ); + } + + /** + * Returns a data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * This implementation forwards the call to + * the parent data source. + * @see #qualifyWithRelationshipKey + */ + public EODataSource + dataSourceQualifiedByKey ( String aKey ) + { + //FIXME: This is fundamentally broken. + // Objects vended from the returned source + // are not registered in our editing context. + // We probably need yet another utility data + // source class that would wrap another source + // and convert vended objects into a different + // context. + + return parent.dataSourceQualifiedByKey( aKey ); + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + * This implementation forwards the call to + * the parent data source. + */ + public void + qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + parent.qualifyWithRelationshipKey( aKey, anObject ); + } + + /** + * Returns the description of the class of the + * objects that is vended by this data source, + * or null if this cannot be determined. + * This implementation returns the class of the + * objects passed to the constructor if they are + * all the same class, otherwise returns null. + */ + public EOClassDescription + classDescriptionForObjects () + { + return classDescription; + } + +} diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOAndQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOAndQualifier.java new file mode 100644 index 0000000..049eb33 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOAndQualifier.java @@ -0,0 +1,167 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOAndQualifier contains other EOQualifiers, +* evaluating as true only if all of the contained +* qualifiers evaluate as true. +* +* @author michael@mpowers.net +* @author yjcheung@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOAndQualifier extends EOQualifier + implements EOKeyValueArchiving, EOQualifierEvaluation +{ + private List qualifiers; + + public EOAndQualifier( + List aQualifierList ) + { + qualifiers = new LinkedList( aQualifierList ); + } + + /** + * Returns a List of qualifiers contained by this qualifier. + */ + public NSArray qualifiers() + { + return new NSArray( qualifiers ); + } + + /** + * Add a new qualifier to the list. + */ + public void addQualifier(EOQualifier qualifier) + { + qualifiers.add(qualifier); + } + + /** + * Evaluates this qualifier for the specified object, + * and returns whether the object is qualified. + * selector() is invoked on the value for key() on the + * specified object, with value() as the parameter. + * + * Note: this has the lazy "and" implementation. Ex. + * Qal1 and Qal2. If the Qal1 is evaluated to be false, then it returns + * false without evaluating Qa12. + */ + public boolean evaluateWithObject( Object anObject ) + { + boolean retVal = true; + Iterator it = qualifiers.iterator(); + while (it.hasNext() && retVal) + { + retVal = ((EOQualifier) it.next()).evaluateWithObject(anObject); + } + return retVal; + } + + /** + * Returns a string representation of this qualifier. + */ + public String toString() + { + StringBuffer myBuf = new StringBuffer("("); + Iterator it = qualifiers.iterator(); + while (it.hasNext()) + { + myBuf = myBuf.append(((EOQualifier) it.next()).toString()).append(" and "); + } + String myStr = myBuf.toString(); + myStr = myStr.substring(0, myStr.lastIndexOf(" and")).concat(")"); + return myStr; + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver arch) { + NSArray a = (NSArray)arch.decodeObjectForKey("qualifiers"); + if (a == null) + return null; + NSMutableArray l = new NSMutableArray(); + for (int i = 0; i < a.count(); i++) { + NSDictionary d = (NSDictionary)a.objectAtIndex(i); + EOKeyValueUnarchiver ua = new EOKeyValueUnarchiver(d); + EOQualifier q = (EOQualifier)EOQualifier.decodeWithKeyValueUnarchiver(ua); + if (q != null) + l.addObject(q); + } + return new EOAndQualifier(l); + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOAndQualifier", "class"); + NSMutableArray arr = new NSMutableArray(qualifiers.size()); + for (int i = 0; i < qualifiers.size(); i++) { + EOQualifier q = (EOQualifier)qualifiers.get(i); + if (q instanceof EOKeyValueArchiving) { + EOKeyValueArchiver ar2 = new EOKeyValueArchiver(); + ((EOKeyValueArchiving)q).encodeWithKeyValueArchiver(ar2); + arr.addObject(ar2.dictionary()); + } else + throw new WotonomyException("Cannot archive instance of " + q.getClass().getName()); + } + arch.encodeObject(arr, "qualifiers"); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/12 01:43:04 chochos + * formally implement EOQualifierEvaluation + * + * Revision 1.5 2003/08/09 01:22:51 chochos + * qualifiers implement EOKeyValueArchiving + * + * Revision 1.4 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.3 2001/10/31 15:25:14 mpowers + * Cleanup of qualifiers. + * + * Revision 1.2 2001/10/30 22:57:28 mpowers + * EOQualifier framework is now working. + * + * Revision 1.1 2001/09/13 15:25:56 mpowers + * Started implementation of the EOQualifier framework. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOClassDescription.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOClassDescription.java new file mode 100644 index 0000000..cd07ebb --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOClassDescription.java @@ -0,0 +1,601 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.HashMap; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOClassDescription provides meta-information about a class +* and is used to customize certain behaviors within wotonomy +* and specifically within editing contexts and object stores. +*

+* +* The default implementation works for most well-formed java beans, +* but you will want to create your own subclass most typically +* to customize the toOne and toMany relationships for your +* class to ensure that an entire graph of objects is not +* persisted in order to perist a single object. +*

+* +* The easiest way to register your subclass is to create it +* in the same package as the class it describes but with +* a "ClassDesc" suffix. For example, "my.package.MyEntity" +* would be described by "my.package.MyEntityClassDesc".

+* +* Note that while the interface is the same, the implementation +* of this class differs substantially from the specification +* in order to be more useful for java classes. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 900 $ +*/ +public class EOClassDescription +{ + /** + * A delete rule specifying that object(s) that reference + * this object should have those references set to null + * when this object is deleted. + */ + public static final int DeleteRuleNullify = 0; + + /** + * A delete rule specifying that object(s) referenced by + * this object should also be deleted when this object + * is deleted. + */ + public static final int DeleteRuleCascade = 1; + + /** + * A delete rule specicying that this object should + * not be allowed to be deleted if it references any + * object(s). + */ + public static final int DeleteRuleDeny = 2; + + /** + * A delete rule specifying that no action be taken + * when this object is deleted. This is the default. + */ + public static final int DeleteRuleNoAction = 3; + + /** + * Notification fired when a class description has been requested + * for a class. Observers should watch for this notification and + * call registerClassDescription so that class descriptions can be + * loaded on-demand. + * The notification's object is the requested class and the + * user info dictionary is null. + */ + public static final String ClassDescriptionNeededForClassNotification = + "ClassDescriptionNeededForClassNotification"; + + /** + * Notification fired when a class description has been requested + * for an entity name. Observers should watch for this notification and + * call registerClassDescription so that class descriptions can be + * loaded on-demand. + * The notification's object is the requested name and the + * user info dictionary is null. + */ + public static final String ClassDescriptionNeededForEntityNameNotification = + "ClassDescriptionNeededForEntityNameNotification"; + + public EOClassDescription() { + super(); + } + + /** + * Returns the class description that corresponds to the specified class. + * If the class description has not already been loaded, a + * ClassDescriptionNeededForClassNotification is posted. + * If the class description is still not found, the class' loader + * is consulted for a class in the same package and named the same as the + * specified class but appended with "ClassDesc", e.g. "EmployeeObjectClassDesc". + * If the class description is still not found, a class description is + * returned that uses java bean introspection to provide reasonable values. + */ + public static EOClassDescription classDescriptionForClass( + Class aClass ) + { + if ( classMap == null ) classMap = new HashMap(); + EOClassDescription result = (EOClassDescription) classMap.get( aClass ); + if ( result == null ) + { + // if not found, post notification + NSNotificationCenter.defaultCenter().postNotification( + ClassDescriptionNeededForClassNotification, aClass, null ); + result = (EOClassDescription) classMap.get( aClass ); + } + if ( result == null ) + { + // if not found, look for similarly named class + String className = aClass.getName() + ClassNameSuffix; + Class classDesc; + try + { + classDesc = aClass.getClassLoader().loadClass( className ); + if ( classDesc != null ) + { + result = (EOClassDescription) classDesc.newInstance(); + registerClassDescription( result, aClass ); + } + } + catch ( Exception exc ) + { + // ignore exceptions and resume + } + } + if ( result == null ) + { + // if not found, default to this class + result = new EOClassDescription( aClass ); + registerClassDescription( result, aClass ); + } + return result; + } + + /** + * Returns the class description that corresponds to the specified + * entity name. If the class description has not already been + * loaded, a ClassDescriptionNeededForEntityNameNotification is posted. + * Returns null if no class description can be found for the entity name. + */ + public static EOClassDescription classDescriptionForEntityName( + String aName ) + { + if ( entityMap == null ) entityMap = new HashMap(); + EOClassDescription result = (EOClassDescription) entityMap.get( aName ); + if ( result == null ) + { + // if not found, post notification + NSNotificationCenter.defaultCenter().postNotification( + ClassDescriptionNeededForEntityNameNotification, aName, null ); + result = (EOClassDescription) entityMap.get( aName ); + } + return result; + } + + /** + * Clears all cached class descriptions so that new requests + * for class descriptions will be re-loaded on-demand. + */ + public static void invalidateClassDescriptionCache() + { + classMap.clear(); + entityMap.clear(); + } + + /** + * Registers the specified class descriptiong for the specified class. + * Nulls are not allowed - to clear the cache call invalidateClassDescriptionCache(). + */ + public static void registerClassDescription( + EOClassDescription description, + Class aClass ) + { + if ( classMap == null ) classMap = new HashMap(); + if ( entityMap == null ) entityMap = new HashMap(); + description.theClass = aClass; + classMap.put( aClass, description ); + entityMap.put( description.entityName(), description ); + } + +/* + public static Object classDelegate() + { + throw new WotonomyException( "Not implemented yet." ); + } + + public static void setClassDelegate( + Object aDelegate) + { + throw new WotonomyException( "Not implemented yet." ); + } +*/ + + /** + * The string appended to the java class name when + * searching the class path for an appropriate description. + */ + private final static String ClassNameSuffix = "ClassDesc"; + + private static Map classMap; + private static Map entityMap; + + protected Class theClass; + private NSMutableArray attributes; + + /** + * Constructor may only be called by subclasses. + */ + protected EOClassDescription( Class aClass ) + { + theClass = aClass; + } + + /** + * Returns a List of all the attributes for this class. + * This implementation reflects on the java class to produce + * a list of attributes, and then removes those keys that + * are returned by toOneRelationshipKeys and toManyRelationhipKeys. + */ + public NSArray attributeKeys() + { + if ( attributes == null ) + { + NSMutableArray readProperties = new NSMutableArray(); + String[] read = Introspector.getReadPropertiesForClass( theClass ); + for ( int i = 0; i < read.length; i++ ) + { + readProperties.addObject( read[i] ); + } + + attributes = new NSMutableArray(); + String[] write = Introspector.getWritePropertiesForClass( theClass ); + for ( int i = 0; i < write.length; i++ ) + { + attributes.addObject( write[i] ); + } + + // only use properties on both lists: read/write + attributes.retainAll( readProperties ); + + // remove relationship keys + attributes.removeAll( toOneRelationshipKeys() ); + attributes.removeAll( toManyRelationshipKeys() ); + } + return attributes; + } + + /** + * This method is called when the specified object has been + * fetched into the specified editing context. Fetch means + * an object was fetched using a fetch specification - it is + * not the same thing as an insertion. + * This implementation does nothing. + */ + public void awakeObjectFromFetch( + Object object, + EOEditingContext anEditingContext ) + { + } + + /** + * This method is called when the specified object has been + * inserted into the specified editing context. Insertion + * means an object was inserted by a display group - it does + * not mean the same thing as a fetch. + * This implementation does nothing. + */ + public void awakeObjectFromInsertion( + Object object, + EOEditingContext anEditingContext ) + { + // does nothing + } + + /** + * Returns the class decription for the object referenced + * by the specified relationship key, or null if the + * class description cannot be determined for that key. + * This implementation returns null. + */ + public EOClassDescription classDescriptionForDestinationKey( + String detailKey ) + { + return null; + } + + /** + * Creates a new instance of the class represented by this + * class description, registering it with the specified + * editing context and global id. The class description + * may not keep references to the newly created object. + * The editing context and/or the id may be null. + * This implementation constructs a new instance of the class + * and registers it with the specified editing context. + * If the global id is specified, the object will be populated + * with the appropriate data, otherwise the object will be + * treated as a newly inserted object. + * If no editing context is specified, the global id is + * ignored and the new instance of the class is returned. + */ + public Object createInstanceWithEditingContext( + EOEditingContext anEditingContext, + EOGlobalID globalID ) + { +//System.out.println( "createInstanceWithEditingContext: " + this + " : " + theClass ); + Object result = null; + try + { + result = theClass.newInstance(); + if ( anEditingContext != null ) + { + if ( globalID != null ) + { + if ( result instanceof EOEnterpriseObject ) + { + ((EOEnterpriseObject)result).awakeFromFetch( anEditingContext ); + } + // register in editing context + anEditingContext.recordObject( result, globalID ); + } + else // no global id specified + { + if ( result instanceof EOEnterpriseObject ) + { + ((EOEnterpriseObject)result).awakeFromInsertion( anEditingContext ); + } + // register as new object in editing context + anEditingContext.insertObject( result ); + } + } + } + catch ( Exception exc ) + { + // error instantiating + throw new WotonomyException( exc ); + } + return result; + } + +/* + public NSFormatter defaultFormatterForKey( + String key ) + { + throw new WotonomyException( "Not implemented yet." ); + } +*/ + + /** + * Returns the delete rule to be used for the specified + * relationship key. + * This implementation returns DeleteRuleNoAction. + */ + public int deleteRuleForRelationshipKey( + String relationshipKey ) + { + return DeleteRuleNoAction; + } + + /** + * Returns a human-readable title for the specified key. + * For example, displayNameForKey( "firstName" ) might + * return "First Name". + * This implementation attempts to construct such a string + * from the key, uppercasing the first character and + * inserting spaces before subsequent uppercase characters. + */ + public String displayNameForKey( + String key ) + { + if ( key == null ) return ""; + if ( key.length() == 0 ) return ""; + + StringBuffer result = new StringBuffer(); + result.append( Character.toUpperCase( key.charAt(0) ) ); + + char c; + int len = key.length(); + for ( int i = 1; i < len; i++ ) + { + c = key.charAt(i); + if ( Character.isUpperCase( c ) ) + { + result.append( ' ' ); + } + result.append( c ); + } + + return result.toString(); + } + + /** + * Returns a human-readable title for the class of objects + * that this class description represents. For example, + * class CustomerObject might return "Customer". + * This implementation returns the class name. + */ + public String entityName() + { + String result = theClass.getName(); + int index = result.lastIndexOf( "." ); + if ( index == -1 ) return result; + return result.substring( index+1 ); + } + + /** + * Returns the fetch specification associated with this + * class description that corresponds to the specified name, + * or null if not found. + * This implementation returns null. + */ + public EOFetchSpecification fetchSpecificationNamed( + String aString ) + { + return null; + } + + /** + * Returns the relationship key by which the object at the + * other end of the specified relationship key refers to + * this object, or null if not found. + * This implementation returns null. + */ + public String inverseForRelationshipKey( + String relationshipKey ) + { + return null; + } + + public boolean ownsDestinationObjectsForRelationshipKey( + String relationshipKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Called when this object has been deleted from the + * specified editing context. The delete rules for this + * object's relationships should be executed. + */ + public void propagateDeleteForObject( + Object object, + EOEditingContext anEditingContext ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a List of the "to many" relationships for + * this class. + * This implementation returns an empty list. + */ + public NSArray toManyRelationshipKeys() + { + return NSArray.EmptyArray; + } + + /** + * Returns a List of the "to one" relationships for + * this class. + * This implementation returns an empty list. + */ + public NSArray toOneRelationshipKeys() + { + return NSArray.EmptyArray; + } + + /** + * Returns a human-readable description of the specified object + * that should not exceed 60 characters. + * This implementation returns anObject.toString(). + */ + public String userPresentableDescriptionForObject( + Object anObject ) + { + return anObject.toString(); + } + + /** + * Verifies that the specified object may be deleted. + * Throws an exception with a user-readable error message + * if the delete operation should not be allowed. + * This implementation does nothing. + */ + public void validateObjectForDelete( + Object object ) + { + // does nothing + } + + /** + * Verifies that the specified object may be saved. + * Throws an exception with a user-readable error message + * if the save operation should not be allowed. + * This implementation does nothing. + */ + public void validateObjectForSave( + Object object ) + { + // does nothing + } + + /** + * Validates the specified value for the specified key on this + * this class. Returns null if the value is acceptable, or + * returns an object that should be used in place of the specified + * object, or throws an exception with a user-readable error message + * if no acceptable value can be determined. + * This implementation returns null. + */ + public Object validateValueForKey( Object value, String key) + { + return null; + } + + /** + * Returns the Java Class that this description describes. + * NOTE: This method is not in the specification. + */ + public Class getDescribedClass() + { + return theClass; + } + +} + +/* + * $Log$ + * Revision 1.3 2006/02/18 22:46:44 cgruber + * Add Surrogate map from .util into control's internal package, and fix imports. + * + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.11 2003/08/08 05:50:32 chochos + * theClass is protected instead of private + * + * Revision 1.10 2003/08/08 00:37:44 chochos + * default constructor is needed by subclasses + * + * Revision 1.9 2001/12/20 18:55:46 mpowers + * Hooks for awakeFromInsertion and awakeFromFetch. + * + * Revision 1.8 2001/12/01 23:51:45 mpowers + * Corrected createWithEditingContext. + * + * Revision 1.7 2001/11/25 22:43:38 mpowers + * Corrected createInstanceWithEditingContext. + * + * Revision 1.6 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.5 2001/04/28 22:17:51 mpowers + * Revised PropertyDataSource to be EOClassDescription-aware. + * + * Revision 1.4 2001/04/28 14:12:23 mpowers + * Refactored cloning/copying into KeyValueCodingUtilities. + * + * Revision 1.3 2001/04/27 23:37:20 mpowers + * Now using EOClassDescription in the EODataSource class, as we should. + * + * Revision 1.2 2001/04/27 00:27:42 mpowers + * Partial implementation. + * + * Revision 1.1 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCooperatingObjectStore.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCooperatingObjectStore.java new file mode 100644 index 0000000..072f867 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCooperatingObjectStore.java @@ -0,0 +1,82 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: EOCooperatingObjectStore.java 894 2006-02-16 16:47:14Z cgruber $ + +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSLocking; +/** +* A representation of a channel of communication to the database. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public abstract class EOCooperatingObjectStore extends EOObjectStore + implements NSLocking { + + public EOCooperatingObjectStore() { + } + + public abstract boolean ownsGlobalID(EOGlobalID eoglobalid); + + public abstract boolean ownsObject(EOEnterpriseObject eoenterpriseobject); + + public abstract boolean handlesFetchSpecification(EOFetchSpecification eofetchspecification); + + public abstract void prepareForSaveWithCoordinator(EOObjectStoreCoordinator eoobjectstorecoordinator, EOEditingContext eoeditingcontext); + + public abstract void recordChangesInEditingContext(); + + public abstract void recordUpdateForObject(EOEnterpriseObject eoenterpriseobject, NSDictionary nsdictionary); + + public abstract void performChanges(); + + public abstract void commitChanges(); + + public abstract void rollbackChanges(); + + public abstract NSDictionary valuesForKeys(NSArray nsarray, EOEnterpriseObject eoenterpriseobject); + + public abstract void lock(); + + public abstract void unlock(); + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/07/14 21:59:06 mpowers + * Contributions from cgruber. + * + * Revision 1.2 2002/06/21 22:14:30 cgruber + * Add a log trail + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCustomObject.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCustomObject.java new file mode 100644 index 0000000..6b262cb --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOCustomObject.java @@ -0,0 +1,673 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSSet; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOCustomObject implements all the necessary interfaces to +* receive first-class treatment from the control framework. +* The implementation delegates as much class meta-behavior as +* possible to EOClassDescription, letting subclasses +* focus exclusively on business logic while still allowing +* them to customize as much class behavior as needed. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOCustomObject + implements EOEnterpriseObject, + EOKeyValueCodingAdditions, + EODeferredFaulting, + EORelationshipManipulation, + EOValidation +{ + private transient static EOClassDescription classDescription; + private transient EOEditingContext editingContext; + + // static configuration + + /** + * Specifies whether the implementation of EOKeyValueCoding + * is permitted to access field directly. This implementation + * returns true; subclasses may override to customize this behavior. + */ + public static boolean canAccessFieldsDirectly() + { + return true; + } + + /** + * Specifies whether the implementation of EOKeyValueCoding + * is permitted to access private accessors. This implementation + * returns true; subclasses may override to customize this behavior. + */ + public static boolean shouldUseStoredAccessors() + { + return true; + } + + /** + * Specifies whether deferred faults should be used. This implementation + * returns false; subclasses may override to customize this behavior. + */ + public static boolean usesDeferredFaultCreation() + { + return false; + } + + // constructors + + /** + * Default constructor initializes private state. + * EditingContext and ClassDescription are set to null. + */ + public EOCustomObject() + { + editingContext = null; + classDescription = null; + } + + /** + * Preferred constructor, specifying an editing context, + * a class description, and a global id, any or all of which + * may be null. Subclasses should invoke this constructor. + */ + public EOCustomObject( + EOEditingContext aContext, + EOClassDescription aClassDescription, + EOGlobalID aGlobalID ) + { + editingContext = aContext; + classDescription = aClassDescription; + } + + // interface EOEnterpriseObject + + /** + * Returns a List of all property keys defined on this object. + * This includes both attributes and relationships. + * This implementation returns the union of attributeKeys, + * toOneRelationshipKeys, and toManyRelationshipKeys. + */ + public NSArray allPropertyKeys() + { + NSSet union = new NSSet(); + union.addAll( attributeKeys() ); + union.addAll( toOneRelationshipKeys() ); + union.addAll( toManyRelationshipKeys() ); + return new NSArray( (Collection) union ); + } + + /** + * Returns a list of all attributes defined on this object. + * Attributes are all properties that are not relationships. + * This implementation retrieves the keys from the class + * description. + */ + public NSArray attributeKeys() + { + return classDescription().attributeKeys(); + } + + //void awakeFromClientUpdate(EOEditingContext aContext) + + /** + * Called when the object has first been fetched into the + * specified editing context. This implementation calls + * awakeObjectFromFetch on the class description. + */ + public void awakeFromFetch(EOEditingContext anEditingContext) + { + classDescription().awakeObjectFromFetch( this, anEditingContext ); + } + + /** + * Called when the object has been inserted into the + * specified editing context. This implementation calls + * awakeObjectFromInsertion on the class description. + */ + public void awakeFromInsertion(EOEditingContext anEditingContext) + { + classDescription().awakeObjectFromInsertion( this, anEditingContext ); + } + + /** + * Returns a Map representing the delta of the current state + * from the state represented in the specified snapshot. + * The result will contain only the keys that have changed + * and their values. Relationship keys will map to an NSArray + * that contains an NSArray of added objects and an NSArray + * of removed objects, in that order. + */ + public NSDictionary changesFromSnapshot(NSDictionary snapshot) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a class description for this object. + * Calls EOClassDescription.classDescriptionForClass. + */ + public EOClassDescription classDescription() + { + if ( classDescription == null ) + { + classDescription = EOClassDescription.classDescriptionForClass( getClass() ); + if ( classDescription == null ) + { + throw new WotonomyException( + "No class description found for class: " + getClass() ); + } + } + return classDescription; + } + + /** + * Returns a class description for the object at the + * other end of the specified relationship key. + * This implementation calls to the classDescription. + */ + public EOClassDescription classDescriptionForDestinationKey(String aKey) + { + return classDescription().classDescriptionForDestinationKey( aKey ); + } + + /** + * Clears all property values for this object. + * This method is called to clean-up an object that + * will no longer be used, and implementations should + * ensure that all references are set to null to + * prevent problems with garbage-collection. + */ + public void clearProperties() + { + //FIXME: clear properties here + } + + /** + * Returns the delete rule constant defined on EOClassDescription + * for the relationship defined by the specified key. + * This implementation calls to the classDescription. + */ + public int deleteRuleForRelationshipKey(String aRelationshipKey) + { + return classDescription().deleteRuleForRelationshipKey( aRelationshipKey ); + } + + /** + * Returns the editing context in which this object is registered. + */ + public EOEditingContext editingContext() + { + return editingContext; + } + + /** + * Returns the name of the entity that this object represents. + */ + public String entityName() + { + return classDescription().entityName(); + } + + /** + * Returns a String containing all property keys and values for + * this object. Relationships should be represented by calling + * eoShallowDescription() on the object. + */ + public String eoDescription() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a String containing all attribute keys and values for + * this object. Relationships are not included. + */ + public String eoShallowDescription() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns the key used to reference this object on the + * object at the other end of the specified relationship. + * This implementation calls to the class description. + */ + public String inverseForRelationshipKey(String aRelationshipKey) + { + return classDescription().inverseForRelationshipKey( aRelationshipKey ); + } + + //Object invokeRemoteMethod( + // String aMethodName, Class[] aTypeArray Object[] anArgumentArray) + + /** + * Returns whether the specified relationship key represents + * a to-many relationship. + */ + public boolean isToManyKey(String aKey) + { + return toManyRelationshipKeys().containsObject( aKey ); + } + + /** + * Returns whether the objects at the other end of the specified + * relationship should be deleted when this object is deleted. + * This implementation calls to the class description. + */ + public boolean ownsDestinationObjectsForRelationshipKey(String aKey) + { + return classDescription().ownsDestinationObjectsForRelationshipKey( aKey ); + } + + //void prepareValuesForClient() + + /** + * Called to perform the delete propagation for this object + * on the specified editing context. All relationships + * should be processed according to their corresponding + * delete rule. + * This implementation calls to the class description. + */ + public void propagateDeleteWithEditingContext(EOEditingContext aContext) + { + classDescription().propagateDeleteForObject( this, aContext ); + } + + /** + * Applies the changes from the specified snapshot to + * this object. + * @see #changesFromSnapshot(NSDictionary) + */ + public void reapplyChangesFromDictionary(NSDictionary aDeltaSnapshot) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a snapshot of the current state of this object. + * All property keys are mapped to their values; nulls are + * represented by NSNull. + */ + public NSDictionary snapshot() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a List of the to-many relationship keys + * for this object. + * This implementation calls to the class description. + */ + public NSArray toManyRelationshipKeys() + { + return classDescription().toManyRelationshipKeys(); + } + + /** + * Returns a List of the to-one relationship keys + * for this object. + * This implementation calls to the class description. + */ + public NSArray toOneRelationshipKeys() + { + return classDescription().toOneRelationshipKeys(); + } + + /** + * Applies the specified snapshot to this object, + * converting NSNulls to null and calling + * takeStoredValueForKey for each key in the Map. + */ + public void updateFromSnapshot(NSDictionary aSnapshot) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a short, stateful string representation + * of this object. + * This implementation calls to the class description. + */ + public String userPresentableDescription() + { + return classDescription().userPresentableDescriptionForObject( this ); + } + + /** + * This method should be called by each setter method + * on this object before changes are made to the + * object's internal state. This implementation calls + * EOObserverCenter.notifyObserversObjectWillChange( this ), + */ + public void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + // interface EOKeyValueCoding + + /** + * Returns the value for the specified property. + * If the property does not exist, this method should + * call handleQueryWithUnboundKey. + */ + public Object valueForKey( String aKey ) + { + return EOKeyValueCodingSupport.valueForKey( this, aKey ); + } + + /** + * Sets the property to the specified value. + * If the property does not exist, this method should + * call handleTakeValueForUnboundKey. + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey. + */ + public void takeValueForKey( Object aValue, String aKey ) + { + EOKeyValueCodingSupport.takeValueForKey( this, aValue, aKey ); + } + + /** + * Returns the value for the private field that + * corresponds to the specified property. + */ + public Object storedValueForKey( String aKey ) + { + return EOKeyValueCodingSupport.storedValueForKey( this, aKey ); + } + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value. + */ + public void takeStoredValueForKey( Object aValue, String aKey ) + { + EOKeyValueCodingSupport.takeStoredValueForKey( this, aValue, aKey ); + } + + /** + * Called by valueForKey when the specified key is + * not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + public Object handleQueryWithUnboundKey( String aKey ) + { + return EOKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + /** + * Called by takeValueForKey when the specified key + * is not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + public void handleTakeValueForUnboundKey( Object aValue, String aKey ) + { + EOKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types. Implementing + * classes should handle this case appropriately + * or otherwise throw an exception. + */ + public void unableToSetNullForKey( String aKey ) + { + EOKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + // interface EOKeyValueCodingAdditions + + /** + * Returns the value for the specified key path, which is + * a series of keys delimited by ".", for example: + * "createTime.year.length". + */ + public Object valueForKeyPath( String aKeyPath ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Sets the value for the specified key path, which is + * a series of keys delimited by ".", for example: + * "createTime.year.length". + * The value is set for the last object referenced by + * the key path. + */ + public void takeValueForKeyPath( Object aValue, String aKeyPath ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns a Map of the specified keys to their values, + * each of which might be obtained by calling valueForKey. + */ + public NSDictionary valuesForKeys( List aKeyList ) + { + return KeyValueCodingUtilities.valuesForKeys( this, aKeyList ); + } + + /** + * Takes the keys from the specified map as properties + * and applies the corresponding values, each of which + * might be set by calling takeValueForKey. + */ + public void takeValuesFromDictionary( Map aMap ) + { + KeyValueCodingUtilities.takeValuesFromDictionary( this, aMap ); + } + + // interface EOFaulting + + /** + * Called by EOFaultHandler to prepare the object to be turned into a fault. + */ + public void clearFault() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns this object's EOFaultHandler. + */ + public EOFaultHandler faultHandler() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Returns whether this object is currently a fault. + * Returns true if this object has not yet retrieved any values. + */ + public boolean isFault() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Turns this object into a fault using the specified fault handler. + */ + public void turnIntoFault( EOFaultHandler aFaultHandler ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Called to completely fire the fault, reading all attributes. + * This method may be implemented to call willRead(null). + */ + public void willRead() + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Called to fire the fault for the specified key. + * The fault manager is required to populate the specified key + * with a value, and may populate any or all of the other values + * on this object. A null key will populate all values on the object. + * NOTE: This method is not part of the specification. + */ + public void willRead( String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + // interface EODeferredFaulting + + /** + * Returns a fault for the specified deferred fault. + */ + public Object willReadRelationship( Object anObject ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + // interface EORelationshipManipulation + + /** + * Adds the specified object to the relationship on this + * object specified by the key. For to-one relationships, + * this operation is the same as valueForKey. + */ + public void addObjectToPropertyWithKey( + Object anObject, String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Removes the specified object from the relationship on + * this object specified by the key. For to-one relationships, + * this operation is the same as takeValueForKey with a null + * value. + */ + public void removeObjectFromPropertyWithKey( + Object anObject, String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * As addObjectToProperty with key, but also performs the + * reciprocal operation on the other side of the relationship. + */ + public void addObjectToBothSidesOfRelationshipWithKey( + EORelationshipManipulation anObject, String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * As removeObjectFromPropertyWithKey with key, but also performs the + * reciprocal operation on the other side of the relationship. + */ + public void removeObjectFromBothSidesOfRelationshipWithKey( + EORelationshipManipulation anObject, String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + // interface EOValidation + + /** + * Validates this object for delete. + * Throws an exception if this object cannot be deleted. + * This implementation calls to the class description. + */ + public void validateForDelete() + { + classDescription().validateObjectForDelete( this ); + } + + /** + * Validates this object for insertion into the external store. + * Throws an exception if this object cannot be inserted. + * Validations here should be specific to insertion. + * This implementation calls validateForSave(). + */ + public void validateForInsert() + { + validateForSave(); + } + + /** + * Validates this object for a commit to the external store. + * Throws an exception if this object cannot be committed. + * Validations here are not specific to either inserts or updates. + * This implementation calls to the class description. + */ + public void validateForSave() + { + classDescription().validateObjectForSave( this ); + } + + /** + * Validates this object for update to the external store. + * Throws an exception if this object cannot be updated. + * Validations here should be specific to updates. + * This implementation calls validateForSave(). + */ + public void validateForUpdate() + { + validateForSave(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2001/12/06 16:42:29 mpowers + * Added appropriate constructor. + * + * Revision 1.2 2001/11/24 17:37:29 mpowers + * Implemented static methods. + * + * Revision 1.1 2001/11/17 17:18:15 mpowers + * Initial implementation of EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODataSource.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODataSource.java new file mode 100644 index 0000000..c7e5284 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODataSource.java @@ -0,0 +1,164 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSArray; + +/** +* EODataSource is used by EODisplayGroup.fetch() to retrieve +* a list of objects to display. When a display group has a +* data source, the display group will use the data source to +* populate the object list and to create new objects to be +* displayed in the list, and will update the data source when +* objects are inserted or removed from the list.

+* +* In certain cases, as when a display group needs to populate +* a child display group to show a one-to-many relationship, +* the display group will call dataSourceQualifiedByKey to +* return a new data source that can vend objects associated +* with the specified key and then call qualifyWithRelationshipKey +* to specify the object and key that are the source of the +* child relationship.

+* +* Concrete subclasses are expected to override fetch() and +* are required to override insertObject and removeObject. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EODataSource +{ + /** + * Creates a new object. You should call + * insertObject() to insert the new object into + * this data source. + * This implementation attempts to create a new + * instance of the class returned by + * classDescriptionForObjects(). + * Override to return an object specific to + * your implementation. + * @return The newly created object, or null if + * new objects are not supported by this data source. + * @see #classDescriptionForObjects + */ + public Object createObject () + { + Object result = null; + EOClassDescription c = classDescriptionForObjects(); + if ( c != null ) + { + result = c.createInstanceWithEditingContext( editingContext(), null ); + } + return result; + } + + /** + * Inserts the specified object into this data source. + */ + public abstract void insertObject ( Object anObject ); + + /** + * Deletes the specified object from this data source. + */ + public abstract void deleteObject ( Object anObject ); + + /** + * Returns the editing context for this data source, + * or null if no editing context is used. + * This implementation returns null. + */ + public EOEditingContext editingContext () + { + return null; + } + + /** + * Returns a List containing the objects in this + * data source. This implementation returns null. + */ + public NSArray fetchObjects () + { + return null; + } + + /** + * Returns a data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * @see #qualifyWithRelationshipKey + */ + public abstract EODataSource + dataSourceQualifiedByKey ( String aKey ); + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public abstract void + qualifyWithRelationshipKey ( + String aKey, Object anObject ); + + /** + * Returns the description of the class of the + * objects that is vended by this data source, + * or null if this cannot be determined. + * This implementation returns null. + */ + public EOClassDescription + classDescriptionForObjects () + { + return null; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2001/05/21 14:02:44 mpowers + * Corrected javadoc. + * + * Revision 1.4 2001/04/27 23:37:20 mpowers + * Now using EOClassDescription in the EODataSource class, as we should. + * + * Revision 1.3 2001/02/27 23:11:07 mpowers + * Removed object registration from createObject(). + * + * Revision 1.2 2001/02/16 18:34:19 mpowers + * Implementing nested contexts. + * + * Revision 1.1.1.1 2000/12/21 15:46:38 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:34 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODatabaseDataSource.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODatabaseDataSource.java new file mode 100644 index 0000000..2e350f1 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODatabaseDataSource.java @@ -0,0 +1,339 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Collection; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSSet; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EODatabaseSource is a general-purpose implementation +* of EODataSource that is EOClassDescription-aware and +* that can vend appropriate EODetailDataSources. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EODatabaseDataSource +{ + EOQualifier auxiliaryQualifier; + EOEditingContext editingContext; + String entityName; + String fetchSpecificationName; + EOFetchSpecification fetchSpecification; + NSDictionary qualifierBindings; + EOClassDescription classDescription; + boolean fetchEnabled; + + /** + * Constructs a data source that fetches all objects of + * the specified entity type. + */ + public EODatabaseDataSource( + EOEditingContext aContext, String anEntityName) + { + this( aContext, anEntityName, null ); + } + + /** + * Constructs a data source that fetches objects of the + * specified entity type according to the fetch specification + * with the specified name. + */ + public EODatabaseDataSource( + EOEditingContext aContext, String anEntityName, String aFetchSpecName) + { + fetchEnabled = true; + editingContext = aContext; + entityName = anEntityName; + setFetchSpecificationByName( fetchSpecificationName ); + } + + /** + * Returns the qualifier that is applied to the results fetched by the fetch + * specification before objects are returned by fetch objects, or null if no + * such qualifier has been specified. + */ + public EOQualifier auxiliaryQualifier() + { + return auxiliaryQualifier; + } + + /** + * Returns the description of the class of the + * objects that is vended by this data source, + * or null if no entity name is specified. + */ + public EOClassDescription classDescriptionForObjects () + { + if ( entityName == null ) return null; + return EOClassDescription.classDescriptionForEntityName( entityName ); + } + + /** + * Returns the object store at the root of the + * editing context's editing hierarchy. + */ + public EOObjectStore databaseContext() + { + EOObjectStore store = editingContext(); + while ( store instanceof EOEditingContext ) + { + store = ((EOEditingContext)store).parentObjectStore(); + } + return store; + } + + /** + * Returns a detail data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * @see #qualifyWithRelationshipKey + */ + public EODataSource dataSourceQualifiedByKey ( String aKey ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + /** + * Deletes the specified object from this data source. + * This implementation deletes the specified object from + * the editing context. + */ + public void deleteObject ( Object anObject ) + { + editingContext.deleteObject( anObject ); + } + + /** + * Returns the editing context for this data source, + * or null if no editing context was specified. + */ + public EOEditingContext editingContext () + { + return editingContext; + } + +/* + public EOEntity entity() {} +*/ + + /** + * Returns a List containing the objects of the current + * entity type that conform to the specified fetch specification. + * If an auxiliary qualifier has been specified, that qualifier + * is applied to the objects before returning the result. + * If fetch is not enabled, this method returns null. + */ + public NSArray fetchObjects () + { + if ( ! isFetchEnabled() ) return null; + NSArray result = + editingContext.objectsWithFetchSpecification( fetchSpecification() ); + if ( auxiliaryQualifier() != null ) + { + result = EOQualifier.filteredArrayWithQualifier( result, auxiliaryQualifier() ); + } + return result; + } + + /** + * Returns the fetch specification currently used by this data + * source to fetch objects, or null if none is specified. + * If null, this fetchObjects() will return all objects of the + * specified entity type. + */ + public EOFetchSpecification fetchSpecification() + { + return fetchSpecification; + } + + /** + * Returns a copy of the fetch specification that will be used to + * determine fetch for this data source. If this data source has + * an auxiliary qualifier, that qualifier will be inserted into + * the returned fetch specification's qualifier. + */ + public EOFetchSpecification fetchSpecificationForFetch() + { + EOFetchSpecification result = (EOFetchSpecification) fetchSpecification.clone(); + if ( auxiliaryQualifier() != null ) + { + NSMutableArray join = new NSMutableArray(); + join.addObject( fetchSpecification.qualifier() ); + join.addObject( auxiliaryQualifier() ); + result.setQualifier( new EOAndQualifier( join ) ); + } + return result; + } + + /** + * Returns the name of the current fetch specification, or null + * if no name has been specified. + */ + public String fetchSpecificationName() + { + return fetchSpecificationName; + } + + /** + * Inserts the specified object into this data source. + * This implementation registers the object as an inserted + * object with the editing context. + */ + public void insertObject ( Object anObject ) + { + editingContext.insertObject( anObject ); + } + + /** + * Returns whether fetching is currently allowed. + * If false, fetchObjects() will return null. + * Default is true. + */ + public boolean isFetchEnabled() + { + return fetchEnabled; + } + + /** + * Returns a List of the union of the binding keys for the fetch spec's + * qualifier and the auxiliary qualifier. + */ + public NSArray qualifierBindingKeys() + { + NSSet union = new NSSet(); + if ( ( fetchSpecification != null ) + && ( fetchSpecification.qualifier() != null ) ) + { + union.addAll( fetchSpecification.qualifier().bindingKeys() ); + } + if ( auxiliaryQualifier() != null ) + { + union.addAll( auxiliaryQualifier().bindingKeys() ); + } + return new NSArray( (Collection) union ); + } + + /** + * Returns a Map of the bindings that will be applied against + * the fetch spec's qualifier and the auxiliary qualifier, + * or null if no bindings exist. + */ + public NSDictionary qualifierBindings() + { + if ( qualifierBindings == null ) return null; + return new NSDictionary( (Map) qualifierBindings ); + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public void qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + throw new WotonomyException( "Not implemented yet" ); + } + + /** + * Sets the auxiliary qualifier that will be applied to + * objects returned from the fetch described by the fetch specification. + */ + public void setAuxiliaryQualifier(EOQualifier aQualifier) + { + auxiliaryQualifier = aQualifier; + } + + /** + * Sets whether fetches are currently allowed. + * If false, fetchObjects() will return null. + */ + public void setFetchEnabled(boolean isFetchEnabled) + { + fetchEnabled = isFetchEnabled; + } + + /** + * Sets the fetch specification used by this data source. + * If null, all objects of the specified entity type will + * be returned by fetchObjects(). + */ + public void setFetchSpecification( EOFetchSpecification aFetchSpec) + { + fetchSpecificationName = null; + fetchSpecification = aFetchSpec; + } + + /** + * Sets the fetch specification used by this data source, + * requesting it from the class description for this data source's + * entity class description, if any. If the name cannot be resolved, + * the fetch specification will be set to null. + */ + public void setFetchSpecificationByName(String aName) + { + fetchSpecificationName = aName; + fetchSpecification = EOFetchSpecification.fetchSpecificationNamed( aName, entityName ); + } + + /* + public void setParentDataSourceRelationshipKey( EODataSource aDataSource, String aKey) + */ + + /** + * Sets the bindings to be applied to the fetch specification and the auxiliary qualifier. + */ + public void setQualifierBindings(Map aBindingMap) + { + if ( aBindingMap == null ) + { + qualifierBindings = null; + } + else + { + qualifierBindings = new NSDictionary( (Map) aBindingMap ); + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/24 17:38:00 mpowers + * Contributing EODatabaseDataSource. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODeferredFaulting.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODeferredFaulting.java new file mode 100644 index 0000000..c87a097 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODeferredFaulting.java @@ -0,0 +1,48 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* EODeferredFaulting defines a method +* to handle relationships that are deferred faults. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EODeferredFaulting extends EOFaulting +{ + /** + * Returns a fault for the specified deferred fault. + */ + Object willReadRelationship( Object anObject ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserver.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserver.java new file mode 100644 index 0000000..758fb40 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserver.java @@ -0,0 +1,152 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Observable; +import java.util.Observer; + +/** +* This is an abstract class for receiving coalesced +* notifications of changes from objects. +* This class also implements Observer for greater +* compatibility. +* The point of EODelayedObservers is that when +* they receive a willChange message, they +* queue themselves with a EODelayedObserverQueue +* so they can receive a single subjectChanged() +* after all changes from an observed object take +* place. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EODelayedObserver + implements EOObserving, Observer +{ + /** + * Notified immediately. + */ + public static final int ObserverPriorityImmediate = 0; + public static final int ObserverPriorityFirst = 1; + public static final int ObserverPrioritySecond = 2; + public static final int ObserverPriorityThird = 3; + public static final int ObserverPriorityFourth = 4; + public static final int ObserverPriorityFifth = 5; + public static final int ObserverPrioritySixth = 6; + public static final int ObserverPriorityLater = 7; + public static final int ObserverNumberOfPriorities = 8; + + /** + * Default constructor. + */ + public EODelayedObserver () + { + } + + /** + * Removes this observer from the observer queue + * for a currently pending notification. + */ + public void discardPendingNotification () + { + observerQueue().dequeueObserver( this ); + } + + /** + * Returns the observer queue to which this observer + * belongs. This implementation returns the default + * EODelayedObserverQueue. + * Override to use a different one. + */ + public EODelayedObserverQueue observerQueue () + { + return EODelayedObserverQueue.defaultObserverQueue(); + } + + /** + * Returns the priority of this observer in the queue. + * This implementation returns ObserverPriorityThird. + * Override to be notified before other observers. + */ + public int priority () + { + return ObserverPriorityThird; + } + + /** + * Notifies observer that one or more objects that + * it is observing have changed. The observer should + * check all objects it is observing for changes. + */ + public abstract void subjectChanged (); + + // interface EOObserving + + /** + * Called when the specified object is about to change. + * This implementation puts this observer on a + * notification queue. + */ + public void objectWillChange ( Object anObject ) + { + observerQueue().enqueueObserver( this ); + } + + // interface Observer + + /** + * Called when the specified object has changed, + * with the specified argument. + * This method is included for interacting with + * the java.lang.Observer pattern. + * This implementation simply objectWillChange(anObject) + * so that the observer still gets a single subjectChanged + * call in response to multiple changes. + */ + public void update ( Observable anObject, Object aValue ) + { + objectWillChange( anObject ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/10/26 18:38:10 mpowers + * Reordered priorities. + * + * Revision 1.1.1.1 2000/12/21 15:46:38 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserverQueue.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserverQueue.java new file mode 100644 index 0000000..6b9b9c3 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EODelayedObserverQueue.java @@ -0,0 +1,323 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import net.wotonomy.foundation.NSRunLoop; +import net.wotonomy.foundation.NSSelector; + +/** +* EODelayedObserverQueue allows EODelayedObservers +* to receive only one subjectChanged() message +* after numerous willChange() messages have +* been sent. Observers are then notified in order +* of their priority property, +* so that certain observers can be notified before +* others for whatever application-specific purpose. +* This class is not thread-safe and should be used +* only for single-threaded GUI clients (AWT and Swing). +*

+* +* Important note: because AWT's event queue does +* not allow for priority-based scheduling, this +* class installs a custom event queue, replacing +* the existing queue on the AWT dispatch thread. +* We know of no way around this problem. +*

+* +* Implementation note: this queue relies on the +* result of equals() for maintaining a set of +* objects on the queue. If two EODelayedObservers +* evaluate to the same value using equals(), only +* one of them will exist on the queue. If this, +* starts to suck, we can change it. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public class EODelayedObserverQueue +{ + /** + * The default run loop ordering flushes the delayed observers + * up to ObserverPrioritySixth before dispatching the AWT event + * queue. ObserverPriorityLater is run last. + */ + public static int FlushDelayedObserversRunLoopOrdering = 400000; + + private static EODelayedObserverQueue + defaultObserverQueue = null; + + private static NSSelector runLaterSelector = + new NSSelector( "flushObserverQueue", + new Class[] { Object.class } ); + + private boolean willRunLater; + private LinkedList priorityQueue; + + /** + * Default constructor. + */ + public EODelayedObserverQueue () + { + willRunLater = false; + priorityQueue = new LinkedList(); + } + + /** + * Returns the system default observer queue. + */ + public static EODelayedObserverQueue defaultObserverQueue () + { + if ( defaultObserverQueue == null ) + { + defaultObserverQueue = new EODelayedObserverQueue(); + } + return defaultObserverQueue; + } + + /** + * Removes the specified observer from the queue. + */ + public void dequeueObserver ( + EODelayedObserver anObserver ) + { +//System.out.println( "dequeueObserver: " + anObserver ); + //synchronized ( priorityQueue ) + //{ + priorityQueue.remove( anObserver ); + //} + } + + /** + * Adds the specified observer to the queue. + * An already enqueued observer will not be + * added again. + * If the observer's priority is + * ObserverPriorityImmediate, it will be + * notified immediately and not added to the + * queue. + * Otherwise, the queue sets itself up to + * call notifyObserversUpToPriority during the + * run loop as specified by + * FlushDelayedObserversRunLoopOrdering. + */ + public void enqueueObserver ( + EODelayedObserver anObserver ) + { + // syntactic glue for Runnables + final EODelayedObserver observer = anObserver; + + if ( observer.priority() == + EODelayedObserver.ObserverPriorityImmediate ) + { + // invoke immediately + observer.subjectChanged(); + } + else + { + // place in the delayed observer queue + + //synchronized ( priorityQueue ) + //{ + int i = 0; + int priority = observer.priority(); + Object o; + + Iterator iterator = priorityQueue.iterator(); + + // scan entire list to ensure we're not already queued + while ( iterator.hasNext() ) + { + o = iterator.next(); + if ( o == observer ) + { + // already queued + return; + } + if ( ((EODelayedObserver)o).priority() > priority ) + { + // insert at this index: break now + break; + } + i++; + } + + // if we broke early, we found a threshhold: + // continue scanning to ensure we're not already queued + while ( iterator.hasNext() ) + { + if ( iterator.next() == observer ) + { + // already queued + return; + } + } + + // insert before items of lower priority, + // otherwise insert at end of list. + priorityQueue.add( i, observer ); + + //} + runLater(); + } +//System.out.println( "enqueueObserver: " + anObserver + " : " + priorityQueue ); + } + + /** + * Notifies all observers with priority equal to + * or greater than the specified priority. + */ + public void notifyObserversUpToPriority ( int priority ) + { +//System.out.println( "notifyObserversUpToPriority: priorityQueue size = " + priorityQueue.size() ); + EODelayedObserver o; + while ( ! priorityQueue.isEmpty() ) + { + o = (EODelayedObserver) priorityQueue.getFirst(); + if ( o.priority() > priority ) break; + priorityQueue.removeFirst(); + + try + { + o.subjectChanged(); + } + catch ( Exception exc ) + { + System.out.println( "Error notifying observer: " + o ); + exc.printStackTrace(); + } + } + } + + /** + * Called to ensure that notifyObserversUpToPriority + * will be called on the next event loop. + */ + private void runLater() + { + if ( ! willRunLater ) + { + willRunLater = true; + NSRunLoop.currentRunLoop().performSelectorWithOrder( + runLaterSelector, this, null, FlushDelayedObserversRunLoopOrdering, null ); + } + } + + /** + * This method is called by the event queue run loop + * and calls notifyObserversUpToPriority with + * ObserverPriorityLater. + * NOTE: This method is not part of the specification. + */ + public void flushObserverQueue( Object anObject ) + { +//System.out.println( "EODelayedObserverQueue: running" ); + notifyObserversUpToPriority( EODelayedObserver.ObserverPrioritySixth ); + if ( ! priorityQueue.isEmpty() ) + { + // assumes all remaining on queue are ObserverPriorityLater + NSRunLoop.invokeLater( + new PriorityLaterRunnable( new LinkedList( priorityQueue ) ) ); + priorityQueue.clear(); + } + willRunLater = false; + } + + /** + * A runnable for dispatching remaining observers running at ObserverPriorityLater. + */ + class PriorityLaterRunnable implements Runnable + { + List observers; + + public PriorityLaterRunnable( List anObserverList ) + { + observers = anObserverList; + } + + public void run() + { + EODelayedObserver o = null; + Iterator i = observers.iterator(); + while ( i.hasNext() ) + { + try + { + o = (EODelayedObserver) i.next(); + o.subjectChanged(); + } + catch ( Exception exc ) + { + System.out.println( "Error notifying observer: " + o ); + exc.printStackTrace(); + } + } + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.7 2002/05/20 15:08:35 mpowers + * Optimization for enqueueObserver: we were scanning the entire list anyway; + * now we compare priorities and ensure we're not double-queued on same pass. + * + * Revision 1.6 2002/05/15 13:45:57 mpowers + * RunLater now appropriately runs later: at the end of the current awt queue. + * + * Revision 1.5 2002/03/11 03:18:39 mpowers + * Now properly handling ObserverChangesLater. + * + * Revision 1.4 2001/10/26 18:37:15 mpowers + * Now using NSRunLoop instead of AWT EventQueue. + * + * Revision 1.3 2001/10/22 21:54:16 mpowers + * Removed swing dependency in favor of jdk1.3 event queue. + * Optimized priority queue population. + * + * Revision 1.2 2001/10/12 18:01:59 mpowers + * Now catching exceptions before they disrupt the awt event queue. + * + * Revision 1.1.1.1 2000/12/21 15:46:42 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEditingContext.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEditingContext.java new file mode 100644 index 0000000..b01727d --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEditingContext.java @@ -0,0 +1,3247 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSRunLoop; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +// swing dependency for undo manager +//import javax.swing.undo.UndoManager; + +/** +* EOEditingContext provides transactional support for +* fetching, editing, and committing changes made on a +* collection of objects to a parent object store.

+* +* EOEditingContext is itself a subclass of EOObjectStore, +* and this means that EOEditingContexts can use other +* EOEditingContexts as their parent. However, there +* still must exist an EOObjectStore as the root of the +* editing hierarchy that can maintain persistent state. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOEditingContext + extends EOObjectStore + implements EOObserving +{ + /** + * Key for the NSNotification posted after this editing context + * saves changes. Object of the notification will be this editing + * context, and user info will contain InsertedKey, UpdatedKey, + * and DeletedKey (keys are defined in EOObjectStore). + */ + public static final String + EditingContextDidSaveChangesNotification = + "EOEditingContextDidSaveChangesNotification"; + + /** + * Key for the NSNotification posted after this editing context + * observes changes. Object of the notification will be this editing + * context, and user info will contain InsertedKey, UpdatedKey, InvalidatedKey, + * and DeletedKey (keys are defined in EOObjectStore), however + * the objects in the corresponding Lists will be the actual + * objects, not their ids. + */ + public static final String + ObjectsChangedInEditingContextNotification = + "EOObjectsChangedInEditingContextNotification"; + + /** + * The default run loop ordering processes recent changes + * before delayed observers are notified and before dispatching + * the AWT event queue. + */ + public static int + EditingContextFlushChangesRunLoopOrdering = 300000; + + private static NSSelector runLaterSelector = + new NSSelector( "flushRecentChanges", + new Class[] { Object.class } ); + + private static EOObjectStore defaultParentObjectStore = null; + private static double defaultFetchTimestampLag = 0; + private static boolean retainsRegisteredObjects = true; + + private EOObjectStore parentStore; + private WeakReference delegate; + private WeakReference messageHandler; + private List editorSet; + private double fetchTimestamp; + private boolean lockBeforeModify; + private boolean propagateDeletesAfterEvent; + private boolean stopValidationAfterError; + private NSMutableArray insertedObjects; + private NSMutableArray insertedObjectsBuffer; + private NSArray insertedObjectsProxy; + private NSMutableArray updatedObjects; + private NSMutableArray updatedObjectsBuffer; + private NSArray updatedObjectsProxy; + private NSMutableArray deletedObjects; + private NSMutableArray deletedObjectsBuffer; + private NSArray deletedObjectsProxy; + private NSMutableArray deletedIDsBuffer; + private NSMutableArray invalidatedObjectsBuffer; + private NSMutableArray invalidatedIDsBuffer; + private Registrar registrar; +// private UndoManager undoManager; + + // so we don't have to trouble EOObserverCenter + private boolean ignoreChanges; + + // for delayed handling of processRecentChanges + private boolean willRunLater; + + // for handling of notifications posted + // while we're in the saveChanges method + private boolean isInvalidating; + + // for i18n or other customization + static protected String MessageChangeConflict = + "Another user changed an object you are editing: "; + + /** + * Default constructor creates a new editing context + * that uses the default object store. If the default + * object store has not been set, an exception is thrown. + */ + public EOEditingContext() + { + this( defaultParentObjectStore() ); + } + + /** + * Creates a new editing context that uses the specified + * object store as its parent object store. + */ + public EOEditingContext( EOObjectStore anObjectStore ) + { + if ( anObjectStore == null ) + { + throw new IllegalArgumentException( + "A parent object store must be specified." ); + } + + parentStore = anObjectStore; + delegate = null; + messageHandler = null; + editorSet = new LinkedList(); + fetchTimestamp = 0; + lockBeforeModify = false; + propagateDeletesAfterEvent = true; + stopValidationAfterError = true; + insertedObjects = new NSMutableArray(); + insertedObjectsBuffer = new NSMutableArray(); + insertedObjectsProxy = NSArray.arrayBackedByList( insertedObjects ); + updatedObjects = new NSMutableArray(); + updatedObjectsBuffer = new NSMutableArray(); + updatedObjectsProxy = NSArray.arrayBackedByList( updatedObjects ); + deletedObjects = new NSMutableArray(); + deletedObjectsBuffer = new NSMutableArray(); + deletedObjectsProxy = NSArray.arrayBackedByList( deletedObjects ); + deletedIDsBuffer = new NSMutableArray(); + invalidatedObjectsBuffer = new NSMutableArray(); + invalidatedIDsBuffer = new NSMutableArray(); + + if ( instancesRetainRegisteredObjects() ) + { + registrar = new Registrar( this ); + } + else + { + registrar = new WeakRegistrar( this ); + } + + ignoreChanges = false; + willRunLater = false; + isInvalidating = false; + + // create undo manager + //TODO: this should be NSUndoManager +// undoManager = new UndoManager(); + + // register for notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + // any from parent store + NSNotificationCenter.defaultCenter().addObserver( + this, handleNotification, null, parentStore ); + // global id change from any + NSNotificationCenter.defaultCenter().addObserver( + this, handleNotification, EOGlobalID.GlobalIDChangedNotification, null ); +//new net.wotonomy.ui.swing.NotificationInspector( null, parentStore ); + } + + /** + * Registers the specified object as an editor for this + * context. The object is expected to implement + * EOEditingContext.Editor. + */ + public void addEditor ( Object anEditor ) + { + if ( anEditor == null ) return; + editorSet.add( new WeakReference( anEditor ) ); + } + + /** + * Returns a read-only List of objects associated with the object + * with the specified id for the specified property + * relationship, or may return a placeholder array that + * will defer the fetch until needed (aka an array fault). + * All objects must be registered in the specified editing context. + * This implementation calls to its parent object store's + * implementation if the requested source object is not + * registered in this editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public NSArray arrayFaultWithSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationshipKey, + EOEditingContext aContext ) + { + NSArray result = null; + Object source = registrar.objectForGlobalID( aGlobalID ); + + // if not registered in our context + if ( source == null ) + { + // get the object registered into our context + result = parentStore.arrayFaultWithSourceGlobalID( + aGlobalID, aRelationshipKey, this ); + } + else // source is registered in our context + { + // get existing value + Object value; + if ( source instanceof EOKeyValueCoding ) + { + value = ((EOKeyValueCoding)source).storedValueForKey( + aRelationshipKey ); + } + else // handle directly + { + value = EOKeyValueCodingSupport.storedValueForKey( + source, aRelationshipKey ); + } + + if ( value == null ) + { + // do the same as if the source was null + result = parentStore.arrayFaultWithSourceGlobalID( + aGlobalID, aRelationshipKey, this ); + } + else + if ( value instanceof NSArray ) + { + result = (NSArray) value; + } + else // not NSArray + if ( value instanceof Collection ) + { + // convert to NSArray + result = new NSArray( (Collection) value ); + } + else + { + throw new WotonomyException( + "Relationship key did not return a collection: " + + aGlobalID + " : " + aRelationshipKey ); + } + } + + // if our context is not the specified context + if ( aContext != this ) + { + result = (NSArray) clone( this, result, aContext ); + } + + return result; + } + + /** + * Returns a snapshot of the specified object as it + * existed when it was last read or committed to the + * parent object store. + */ + public NSDictionary committedSnapshotForObject ( + Object anObject ) + { + byte[] snapshot = (byte[]) + registrar.getCommitSnapshot( anObject ); + if ( snapshot == null ) + { + // this object not modified: take a current snapshot + snapshot = takeSnapshot( anObject ); + } + return convertSnapshotToDictionary( snapshot ); + } + + /** + * Returns a snapshot of the specified object as it + * existed before the edits triggered by the current + * event loop were processed. + */ + public NSDictionary currentEventSnapshotForObject ( + Object anObject ) + { + byte[] result = (byte[]) + registrar.getCurrentSnapshot( anObject ); + if ( result == null ) + { + return committedSnapshotForObject( anObject ); + } + return convertSnapshotToDictionary( result ); + } + + /** + * Returns the delegate for this editing context, + * or null if no delegate has been set. + */ + public Object delegate () + { + if ( delegate == null ) return null; + return delegate.get(); + } + + /** + * Deletes the specified object from this editing context. + * The editing context marks the object as deleted and + * will notify the parent store when changes are committed. + */ + public void deleteObject ( + Object anObject ) + { + willChange(); + + int i; + // remove from added objects if necessary + i = insertedObjects.indexOfIdenticalObject( anObject ); + if ( i != NSArray.NotFound ) + { + insertedObjects.removeObjectAtIndex( i ); + + // if in the inserted objects buffer + int index = insertedObjectsBuffer.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + // remove from inserted objects buffer + insertedObjectsBuffer.removeObjectAtIndex( index ); + } + + // now forget the object ever existed. + forgetObject( anObject ); + + // we're done + return; + } + else // otherwise add to deleted objects list + { + deletedObjects.addObject( anObject ); + } + + // remove from updated objects if necessary + i = updatedObjects.indexOfIdenticalObject( anObject ); + if ( i != NSArray.NotFound ) + { + updatedObjects.removeObjectAtIndex( i ); + } + + // add to buffer + deletedObjectsBuffer.addObject( anObject ); + deletedIDsBuffer.addObject( globalIDForObject( anObject ) ); + } + + /** + * Returns a read-only List of all objects marked as deleted + * in this editing context. + */ + public NSArray deletedObjects () + { + return deletedObjectsProxy; + } + + /** + * Called by child editing contexts when they no longer + * need to track the specified id. + * This implementation forwards the call to the parent store. + */ + public void editingContextDidForgetObjectWithGlobalID ( + EOEditingContext aContext, + EOGlobalID aGlobalID ) + { + parentStore.editingContextDidForgetObjectWithGlobalID( + aContext, aGlobalID ); + } + + /** + * Returns a read-only List of registered editors of this + * editing context. + */ + public NSArray editors () + { + NSMutableArray result = new NSMutableArray(); + Object o; + Iterator i = editorSet.iterator(); + while ( i.hasNext() ) + { + o = ((WeakReference)i.next()).get(); + if ( o != null ) + { + result.addObject( o ); + } + else + { + i.remove(); + } + } + return result; + } + +/* + public static void encodeObjectWithCoder ( + Object anObject, + NSCoder aCoder ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + + /** + * Returns the object for the specified id. + * If the object is registered in in this context + * but not in the specified context, + * this implementation will create a copy of the object + * and register it in the specified context. + * Otherwise it will forward the call to the parent + * object store. + */ + public /*EOEnterpriseObject*/ Object faultForGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + Object result = registrar.objectForGlobalID( aGlobalID ); + + // if not registered in our context + if ( result == null ) + { + // get the object registered into our context + result = parentStore.faultForGlobalID( aGlobalID, this ); + } + + // if our context is not the specified context + if ( aContext != this ) + { + result = registerClone( aGlobalID, this, result, aContext ); + } + + return result; + } + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. + * This implementation calls faultForRawRow + * on the parent store. + */ + public Object faultForRawRow ( + Map aDictionary, + String anEntityName ) + { + return parentStore.faultForRawRow( + aDictionary, anEntityName, this ); + } + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. The fault should + * belong to the specified editing context. + * This implementation forwards the call to + * the parent store. + */ + public /*EOEnterpriseObject*/ Object faultForRawRow ( + Map aDictionary, + String anEntityName, + EOEditingContext aContext ) + { + return parentStore.faultForRawRow( + aDictionary, anEntityName, aContext ); + } + + /** + * Returns the fetch timestamp for this editing context. + */ + public double fetchTimestamp () + { + return fetchTimestamp; + } + + /** + * Unregisters the specified object from this editing context, + * removing all references to it. Use this method to remove + * an object from the context without marking it for deletion. + */ + public void forgetObject ( + Object anObject ) + { + EOGlobalID id = registrar.globalIDForObject( anObject ); + if ( id == null ) + { + System.err.println( + "EOEditingContext.forgetObject: not registered: " + anObject ); + return; + } + + // unregister object + registrar.forgetObject( anObject ); + + // remove from all, inserted, updated, and deleted lists + int index; + index = updatedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + updatedObjects.removeObjectAtIndex( index ); + } + index = insertedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + insertedObjects.removeObjectAtIndex( index ); + return; + } + index = deletedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + deletedObjects.removeObjectAtIndex( index ); + return; + } + + // notify parent context + parentStore.editingContextDidForgetObjectWithGlobalID( this, id ); + } + + /** + * Returns the id for the specified object, or null + * if the object is not registered in this context. + */ + public EOGlobalID globalIDForObject ( + Object anObject ) + { + return registrar.globalIDForObject( anObject ); + } + + /** + * Returns an array of ids for an array of objects. + */ + private NSArray globalIDsForObjects( + List anObjectList ) + { + NSMutableArray result = new NSMutableArray(); + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + result.add( globalIDForObject( it.next() ) ); + } + return result; + } + + /** + * Returns whether this editing context has changes that + * have not yet been committed to the parent object store. + */ + public boolean hasChanges () + { + if ( updatedObjects.count() > 0 ) return true; + if ( insertedObjects.count() > 0 ) return true; + if ( deletedObjects.count() > 0 ) return true; + return false; + } + + /** + * Given a newly instantiated object, this method + * initializes its properties to values appropriate + * for the specified id. The object should already + * belong to the specified editing context. + * This method is called to populate faults. + * This implementation will try to apply the values + * from an object with a matching id in this editing + * context if possible, calling to the parent object + * store only if such an object is not found. + */ + public void initializeObject ( + /*EOEnterpriseObject*/ Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + Object existingObject = registrar.objectForGlobalID( aGlobalID ); + + // if not registered in our context + if ( existingObject == null ) + { + // get the object registered into our context + existingObject = parentStore.faultForGlobalID( aGlobalID, this ); + } + + if ( aContext == this ) + { + // initialize the object + parentStore.initializeObject( + /*(EOEnterpriseObject)*/existingObject, aGlobalID, this ); + } + else // ( aContext != this ) + { + // translates child relationships + copy( this, existingObject, aContext, anObject ); + } + + aContext.registrar.setCommitSnapshot( anObject, null ); + aContext.registrar.setCurrentSnapshot( anObject, null ); + } + + /** + * Inserts the specified object into this editing context. + * This implementation calls insertObjectWithGlobalID + * with an EOTemporaryGlobalID. + */ + public void insertObject ( Object anObject ) + { + insertObjectWithGlobalID( + anObject, new EOTemporaryGlobalID() ); + } + + /** + * Inserts the specified object into this editing context + * with the specified id, which is expected to be a + * temporary id. + */ + public void insertObjectWithGlobalID ( + Object anObject, + EOGlobalID aGlobalID ) + { + willChange(); + + // if this object was marked for deletion + int index = deletedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + // don't need to re-register: just update the lists + + // remove from deleted list + deletedObjects.removeObjectAtIndex( index ); + + // if in the deleted ids buffer + index = deletedIDsBuffer.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + // remove from deleted ids buffer + deletedIDsBuffer.removeObjectAtIndex( index ); + } + + // if in the deleted objects buffer + index = deletedObjectsBuffer.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) + { + // remove from deleted objects buffer + deletedObjectsBuffer.removeObjectAtIndex( index ); + } + else // not in the deleted objects buffer + { + // add to the inserted objects buffer + insertedObjectsBuffer.addObject( anObject ); + } + + // we're done + return; + } + + // make sure object is not already in editing context + if ( objectForGlobalID( aGlobalID ) != null ) + { + throw new WotonomyException( + "Tried to insert but object was already registered:" + + aGlobalID ); + } + + // register object + recordObject( anObject, aGlobalID ); + + // add to inserted list + insertedObjects.addObject( anObject ); + // add to buffer + insertedObjectsBuffer.addObject( anObject ); + } + + /** + * Returns a read-only List of the objects that have been + * inserted into this editing context. + */ + public NSArray insertedObjects () + { + return insertedObjectsProxy; + } + + /** + * Turn all objects in this editing context into faults, + * so that they will be fetched the next time they are + * accessed, and calls invalidateObjectsWithGlobalIDs + * on the parent object store. + */ + public void invalidateAllObjects () + { + // register change so processRecentChanges is called + willChange(); + + invalidateAllObjectsQuietly(); + + // post notification + NSNotificationCenter.defaultCenter().postNotification( + new NSNotification( + InvalidatedAllObjectsInStoreNotification, this ) ); + } + + /** + * Only refaults all objects, does not notify will change + * nor post notification, but does call parent store. + * Called by invalidateAllObjects() and handleNotification(). + */ + private void invalidateAllObjectsQuietly() + { + // remember the ids + NSMutableArray ids = new NSMutableArray( registrar.registeredGlobalIDs() ); + + // track of discarded IDs (from inserted objects) + NSMutableArray discardedIDs = new NSMutableArray(); + + // refault all objects + EOGlobalID id; + Object o; + Enumeration e = ids.objectEnumerator(); + while ( e.hasMoreElements() ) + { + id = (EOGlobalID) e.nextElement(); + o = objectForGlobalID( id ); + + // some objects may have been manually discarded + if ( o != null ) + { + // don't refault newly inserted objects + if ( insertedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound ) + { + refaultObject( o, id, this ); + } + else + { + // discard inserted objects + forgetObject( o ); + discardedIDs.add( id ); + } + invalidatedObjectsBuffer.add( o ); + } + invalidatedIDsBuffer.add( id ); + } + ids.removeAll( discardedIDs ); + + // call to parent store (should call this after posting instead?) + isInvalidating = true; + parentStore.invalidateObjectsWithGlobalIDs( ids ); + isInvalidating = false; + } + + /** + * Turns the objects with the specified ids into faults, + * so that they will be fetched the next time they are + * accessed, and forwards the call to the parent object store. + */ + public void invalidateObjectsWithGlobalIDs ( + List anArray ) + { + // register change so processRecentChanges is called + willChange(); + + // call to parent to invalidate objects + parentStore.invalidateObjectsWithGlobalIDs( anArray ); + + Object o; + EOGlobalID id; + Iterator it = anArray.iterator(); + while ( it.hasNext() ) + { + id = (EOGlobalID) it.next(); + if ( id != null ) + { + o = objectForGlobalID( id ); + if ( o != null ) + { + Object result = notifyDelegate( + "editingContextShouldInvalidateObject", + new Class[] { EOEditingContext.class, Object.class, EOGlobalID.class }, + new Object[] { this, o, id } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + // refault the object + refaultObject( o, id, this ); + invalidatedObjectsBuffer.add( o ); + invalidatedIDsBuffer.add( id ); + } + } + } + else + { + throw new WotonomyException( + "Attempted to invalidate a null global id: " + anArray ); + } + } + + } +/* + public boolean invalidatesObjectsWhenFinalized ( ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } + + public boolean invalidatesObjectsWhenFreed ( ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Returns whether the object referenced by the + * specified id is locked. + * This implementation simply forwards the call to + * the parent object store. + */ + public boolean isObjectLockedWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext) + { + return parentStore.isObjectLockedWithGlobalID( + aGlobalID, aContext ); + } + +/* + public void lock () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Locks the specified object in this editing context + * by calling lockObjectWithGlobalID on the parent store. + */ + public void lockObject ( + Object anObject ) + { + parentStore.lockObjectWithGlobalID( + globalIDForObject( anObject ), this ); + } + + /** + * Locks the object referenced by the specified id + * in the specified editing context. + * This implementation simply forwards the call to + * the parent object store. + */ + public void lockObjectWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext) + { + parentStore.lockObjectWithGlobalID( + aGlobalID, aContext ); + } + + /** + * Returns whether this editing context attempts to + * lock objects when they are first modified. + */ + public boolean locksObjectsBeforeFirstModification () + { + return lockBeforeModify; + } + + /** + * Returns the message handler for this editing context, + * or null if no message handler has been set. + */ + public Object messageHandler () + { + if ( messageHandler == null ) return null; + return messageHandler.get(); + } + + /** + * Returns the object registered in this editing context + * for the specified id, or null if that id is not + * registered. + */ + public Object objectForGlobalID ( + EOGlobalID aGlobalID ) + { + return registrar.objectForGlobalID( aGlobalID ); + } + + /** + * Returns a read-only List of objects associated with the object + * with the specified id for the specified property + * relationship. This method may not return an array fault + * because array faults call this method to fetch on demand. + * All objects must be registered the specified editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public NSArray objectsForSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationshipKey, + EOEditingContext aContext ) + { +//System.out.println( "EOEditingContext.objectsForSourceGlobalID: " +//+ aGlobalID + " : " + aRelationshipKey ); + + NSArray result = null; + +if ( aContext == this ) +{ + throw new WotonomyException( "Assert failed: calling objectsForSourceGlobalID on ourself." ); +} + Object source = registrar.objectForGlobalID( aGlobalID ); + + // if not registered in our context + if ( source == null ) + { + // get the object registered into our context + result = parentStore.objectsForSourceGlobalID( + aGlobalID, aRelationshipKey, this ); + } + else // source is registered in our context + { + // get existing value + Object value; + if ( source instanceof EOKeyValueCoding ) + { + value = ((EOKeyValueCoding)source).storedValueForKey( + aRelationshipKey ); + } + else // handle directly + { + value = EOKeyValueCodingSupport.storedValueForKey( + source, aRelationshipKey ); + } + + // if we don't have a valid value on our object + if ( ( value == null ) + || ( ( value instanceof ArrayFault ) + && ( !((ArrayFault)value).isFetched() ) ) ) + { + // do the same as if the source was null + result = parentStore.objectsForSourceGlobalID( + aGlobalID, aRelationshipKey, this ); + + // set our value since we have it + if ( source instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)source).takeStoredValueForKey( + result, aRelationshipKey ); + } + else // handle directly + { + EOKeyValueCodingSupport.takeStoredValueForKey( + source, result, aRelationshipKey ); + } + } + else + if ( ( value instanceof ArrayFault ) + && ( !((ArrayFault)value).isFetched() ) ) + { + // do the same as if the source was null + result = parentStore.objectsForSourceGlobalID( + aGlobalID, aRelationshipKey, this ); + } + else + if ( value instanceof NSArray ) + { + result = (NSArray) value; + } + else // not NSArray + if ( value instanceof Collection ) + { + // convert to NSArray + result = new NSArray( (Collection) value ); + } + else + { + throw new WotonomyException( + "Relationship key did not return a collection: " + + aGlobalID + " : " + aRelationshipKey ); + } + } + + // if our context is not the specified context + if ( aContext != this ) + { + result = (NSArray) clone( this, result, aContext ); + } + + return result; + } + + /** + * Returns a read-only List of objects the meet the criteria of + * the supplied specification. This method simply calls + * objectsWithFetchSpecification on this editing context + * with this editing context as the parameter. + */ + public NSArray objectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec ) + { + return objectsWithFetchSpecification( aFetchSpec, this ); + } + + /** + * Returns a read-only List of objects the meet the criteria of + * the supplied specification. Faults are not allowed in the array. + * If any objects are already fetched, they should not be + * refetched. All objects should belong to the specified editing context. + * This implementation forwards the call to the parent object + * store, which will register each object in the specified editing + * context only if it does not already exist. + */ + public NSArray objectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec, + EOEditingContext aContext) + { + if ( aContext == this ) + { + Object result = notifyDelegate( + "editingContextShouldFetchObjects", + new Class[] { EOEditingContext.class, EOFetchSpecification.class }, + new Object[] { aContext, aFetchSpec } ); + if ( result instanceof NSArray ) return (NSArray) result; + } + return parentStore.objectsWithFetchSpecification( aFetchSpec, aContext ); + } + + /** + * Returns the parent object store for this editing context. + * The result will not be null. + */ + public EOObjectStore parentObjectStore () + { + return parentStore; + } + + /** + * Updates the inserted, updated, and deleted objects lists, + * and posts notifications about which objects have been changed. + * This method is called at the end of an event loop in which + * objects were modified. This method is additionally called + * by saveChanges() so that any changes in the same event loop + * will be processed correctly before calling to the parent + * object store. + * This implementation updates those lists immediately, but + * only posts notifications when this method is called. + */ + public void processRecentChanges () + { // System.out.println( "EOEditingContext.processRecentChanges: " + invalidatedObjectsBuffer ); + + /* + * This implementation actually updates those lists immediately, + * but keeps a separate buffer of changes in the current event + * loop for the purposes of posting a notification. + * NOTE: to reenable buffering, uncomment lines from this method + * body and reenable the RecentChangesObserver in the constructor. + */ + + // broadcast ObjectsChangedInStoreNotification + // for the benefit of child editing contexts + + boolean postStoreInfo = + ( insertedObjectsBuffer.size() + + updatedObjectsBuffer.size() + + deletedIDsBuffer.size() + + invalidatedIDsBuffer.size() > 0 ); + + NSMutableDictionary storeInfo = new NSMutableDictionary(); + if ( postStoreInfo ) + { + storeInfo.setObjectForKey( + globalIDsForObjects( insertedObjectsBuffer ), + // globalIDsForObjects( insertedObjects ), + EOObjectStore.InsertedKey ); + storeInfo.setObjectForKey( + globalIDsForObjects( updatedObjectsBuffer ), + // globalIDsForObjects( updatedObjects ), + EOObjectStore.UpdatedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) deletedIDsBuffer ), + // globalIDsForObjects( deletedObjects ), + EOObjectStore.DeletedKey ); + storeInfo.setObjectForKey( + new NSArray( (Collection) invalidatedIDsBuffer ), + EOObjectStore.InvalidatedKey ); + } + + // broadcast ObjectsChangedInEditingContextNotification + // for the benefit of attached display groups + + boolean postContextInfo = + ( insertedObjectsBuffer.size() + + updatedObjectsBuffer.size() + + deletedObjectsBuffer.size() + + invalidatedObjectsBuffer.size() > 0 ); + + NSMutableDictionary contextInfo = new NSMutableDictionary(); + + if ( postContextInfo ) + { + + contextInfo.setObjectForKey( + new NSArray( (Collection) insertedObjectsBuffer ), + // new NSArray( (Collection) insertedObjects ), + EOObjectStore.InsertedKey ); + contextInfo.setObjectForKey( + new NSArray( (Collection) updatedObjectsBuffer ), + // new NSArray( (Collection) updatedObjects ), + EOObjectStore.UpdatedKey ); + contextInfo.setObjectForKey( + new NSArray( (Collection) deletedObjectsBuffer ), + // new NSArray( (Collection) deletedObjects ), + EOObjectStore.DeletedKey ); + contextInfo.setObjectForKey( + new NSArray( (Collection) invalidatedObjectsBuffer ), + EOObjectStore.InvalidatedKey ); + } + + // update the current snapshots + + Object o; + Iterator it; + it = insertedObjectsBuffer.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + registrar.setCurrentSnapshot( o, takeSnapshot( o ) ); + } + it = updatedObjectsBuffer.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + registrar.setCurrentSnapshot( o, takeSnapshot( o ) ); + } + + // clear buffers + + insertedObjectsBuffer.removeAllObjects(); + updatedObjectsBuffer.removeAllObjects(); + deletedObjectsBuffer.removeAllObjects(); + deletedIDsBuffer.removeAllObjects(); + invalidatedObjectsBuffer.removeAllObjects(); + invalidatedIDsBuffer.removeAllObjects(); + + // post notifications (does order matter?) + + if ( postStoreInfo ) + { + NSNotificationCenter.defaultCenter().postNotification( + new NSNotification( + ObjectsChangedInStoreNotification, this, storeInfo ) ); + } + + if ( postContextInfo ) + { + NSNotificationCenter.defaultCenter().postNotification( + new NSNotification( + ObjectsChangedInEditingContextNotification, this, contextInfo ) ); + } + + } + + /** + * Returns whether this editing context propagates deletes + * immediately after the event that triggered the delete. + * Otherwise, propagation occurs only before commit. + */ + public boolean propagatesDeletesAtEndOfEvent () + { + return propagateDeletesAfterEvent; + } + + /** + * Registers the specified object in this editing context + * for the specified id. This method is called by an object + * store when fetching objects for a display group, or when + * objects are inserted into a display group. + * This implementation will re-register the object under the + * new id if it is already registered under a different id. + */ + public void recordObject ( + Object anObject, + EOGlobalID aGlobalID ) + { + // find state for re-registration + boolean inserted = false; + boolean updated = false; + boolean deleted = false; + + // is the object already registered? + EOGlobalID existingID = globalIDForObject( anObject ); + if ( existingID != null ) + { + // remember object state + int index; + index = insertedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) inserted = true; + index = updatedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) updated = true; + index = deletedObjects.indexOfIdenticalObject( anObject ); + if ( index != NSArray.NotFound ) deleted = true; + // forget the object + forgetObject( anObject ); + } + + // is the global id already in use? + Object existingObject = objectForGlobalID( aGlobalID ); + if ( existingObject != null ) + { + // forget it (don't worry about state?) + forgetObject( existingObject ); + } + + registrar.registerObject( anObject, aGlobalID ); + + // restore state if necessary + if ( inserted ) insertedObjects.addObject( anObject ); + if ( updated ) updatedObjects.addObject( anObject ); + if ( deleted ) deletedObjects.addObject( anObject ); + } + + /** + * Undoes the last undo operation. + */ + public void redo () + { + //TODO: not supported yet + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Refaults this editing context, turning all unmodified + * objects into faults. This implementation calls + * editingContextWillSaveChanges() on all editors, and + * then calls refaultObjects(). + */ + public void refault () + { + fireWillSaveChanges(); + refaultObjects(); + } + + /** + * Refaults the specified object, turning it into a fault + * for the specified global id in the specified context. + */ + public void refaultObject ( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext) + { + aContext.registrar.setCurrentSnapshot( anObject, null ); + + ignoreChanges = true; + parentStore.refaultObject( anObject, aGlobalID, aContext ); + ignoreChanges = false; + + // remove from updated objects if necessary + int i = updatedObjects.indexOfIdenticalObject( anObject ); + if ( i != NSArray.NotFound ) + { + updatedObjects.removeObjectAtIndex( i ); + } + + // add to invalidated notification queue + invalidatedObjectsBuffer.addObject( anObject ); + invalidatedIDsBuffer.addObject( aGlobalID ); + } + + /** + * Turns all unmodified objects into faults, calling + * processRecentChanges() and then refaultObject() for + * each unmodified object. + */ + public void refaultObjects () + { + // is this call really needed? + // processRecentChanges(); + + Object o; + EOGlobalID id; + Iterator it = registeredObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( updatedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound ) + && ( insertedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound ) + && ( deletedObjects.indexOfIdenticalObject( o ) == NSArray.NotFound ) ) + { + id = globalIDForObject( o ); + refaultObject( o, id, this ); + } + } + } + + /** + * Calls editingContextWillSaveChanges() on all editors, + * and then calls invalidateAllObjects(). + */ + public void refetch () + { + fireWillSaveChanges(); + invalidateAllObjects(); + } + + /** + * Returns a read-only List of all objects registered in this + * editing context. + */ + public NSArray registeredObjects () + { + return registrar.registeredObjects(); + } + + /** + * Unregisters the specified editor with this editing context. + */ + public void removeEditor ( Object anObject ) + { + if ( anObject == null ) return; + + Object o; + Iterator i = editorSet.iterator(); + while ( i.hasNext() ) + { + o = ((WeakReference)i.next()).get(); + if ( ( o == null ) || ( o == anObject ) ) + { + i.remove(); + } + } + } + + /** + * Unregisters all objects from this editing context, + * and resets the fetch timestamp. + */ + public void reset () + { + Iterator it = registeredObjects().iterator(); + while ( it.hasNext() ) + { + forgetObject( it.next() ); + } + fetchTimestamp = 0; //FIXME: reset timestamp properly + } + + /** + * Reverts the objects in this editing context to + * their original state. + * Calls editingContextWillSaveChanges on all editors, + * discards all inserted objects, restores deleted + * objects, and applies the fetch snapshot to all + * registered objects. + */ + public void revert () + { + willChange(); + fireWillSaveChanges(); + + Iterator it; + + // forget inserted objects + it = new NSArray( insertedObjects ).iterator(); + while ( it.hasNext() ) + { + forgetObject( it.next() ); + } + + EOGlobalID id; + Object o; + byte[] snapshot; + + // re-initialize updated objects + it = new NSArray( updatedObjects ).iterator(); + while ( it.hasNext() ) + { + o = it.next(); + snapshot = (byte[]) registrar.getCommitSnapshot( o ); + if ( snapshot != null ) + { + applySnapshot( snapshot, o ); + } + registrar.setCommitSnapshot( o, null ); + updatedObjectsBuffer.addObject( o ); + } + + // re-initialize deleted objects + it = new NSArray( deletedObjects ).iterator(); + while ( it.hasNext() ) + { + o = it.next(); + snapshot = (byte[]) registrar.getCommitSnapshot( o ); + if ( snapshot != null ) + { + applySnapshot( snapshot, o ); + } + registrar.setCommitSnapshot( o, null ); + updatedObjectsBuffer.addObject( o ); + } + + // reset lists + insertedObjects.removeAllObjects(); // unneccessary? + deletedObjects.removeAllObjects(); + updatedObjects.removeAllObjects(); + + // post notification + processRecentChanges(); + } + + /** + * Returns the root object store, which is the parent + * of all parent object stores of this editing context. + */ + public EOObjectStore rootObjectStore () + { + EOObjectStore parent = parentObjectStore(); + while ( parent instanceof EOEditingContext ) + { + parent = ((EOEditingContext)parent).parentObjectStore(); + } + return parent; + } + + /** + * Calls editingContextWillSaveChanges on all editors, + * and commits all changes in this editing context to + * the parent editing context by calling + * saveChangesInEditingContext to the parent. + * Then posts EditingContextDidSaveChangeNotification. + */ + public void saveChanges () + { +//System.out.println( "EOEditingContext.saveChanges: " + this ); + willChange(); + + // process any changes + processRecentChanges(); + + // set up user info for notification to be posted. + NSMutableDictionary userInfo = new NSMutableDictionary(); + userInfo.setObjectForKey( + new NSArray( (Collection) insertedObjects ), + EOObjectStore.InsertedKey ); + userInfo.setObjectForKey( + new NSArray( (Collection) updatedObjects ), + EOObjectStore.UpdatedKey ); + userInfo.setObjectForKey( + new NSArray( (Collection) deletedObjects ), + EOObjectStore.DeletedKey ); + + // notify the editors + fireWillSaveChanges(); + + // notify the delegate + notifyDelegate( + "editingContextWillSaveChanges", + new Class[] { EOEditingContext.class }, + new Object[] { this } ); + + // needed for notification handling + isInvalidating = true; + try + { + // ask parent to save us + parentStore.saveChangesInEditingContext( this ); + } + catch ( RuntimeException e ) + { + // unset save flag and rethrow + isInvalidating = false; + throw e; + } + isInvalidating = false; + + // no exceptions: proceed! + + Object o, key; + Iterator it; + + // update the committed snapshots + it = insertedObjects.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + registrar.setCommitSnapshot( o, null ); + registrar.setCurrentSnapshot( o, null ); + } + it = updatedObjects.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + registrar.setCommitSnapshot( o, null ); + registrar.setCurrentSnapshot( o, null ); + } + + // clear the lists + updatedObjects.removeAllObjects(); + insertedObjects.removeAllObjects(); + it = new NSArray( deletedObjects() ).iterator(); + while ( it.hasNext() ) + { // parent is doing this as well? + forgetObject( it.next() ); + } + + // post notification + NSNotificationCenter.defaultCenter().postNotification( + new NSNotification( + EditingContextDidSaveChangesNotification, this, userInfo ) ); + } + + /** + * Commits all changes in the specified editing context + * to this one. Called by child editing contexts in + * their saveChanges() method. + */ + public void saveChangesInEditingContext ( + EOEditingContext aContext) + { + Object o; + Iterator it; + + // process deletes + List deletes = new NSArray( aContext.deletedObjects() ); + it = deletes.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + EOGlobalID id = aContext.globalIDForObject( o ); + Object localVersion = objectForGlobalID( id ); + if ( localVersion == null ) + { + // make a local copy and register it + localVersion = registerClone( id, aContext, o, this ); + if ( localVersion == null ) + { + throw new WotonomyException( + "Deleted object could not be serialized: " + + id + " : " + o ); + } + } + else // we have a local version, copy changes + { + copy( aContext, o, this, localVersion ); + // copy marks the object as updated: will be on both lists + } + // delete our copy -- marks context as changed + deleteObject( localVersion ); + } + + // process inserts - all inserts are new objects + List inserts = new NSArray( aContext.insertedObjects() ); + it = inserts.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + // make a local copy and register it + EOGlobalID id = aContext.globalIDForObject( o ); + willChange(); // need to mark editing context as changed + Object localVersion = registerClone( id, aContext, o, this ); + if ( localVersion == null ) + { + throw new WotonomyException( + "Inserted object could not be serialized: " + + o ); + } + // insert our copy manually so a new id is not generated + insertedObjects.addObject( localVersion ); + insertedObjectsBuffer.addObject( localVersion ); + } + + // process updates + List updates = new NSArray( aContext.updatedObjects() ); + it = updates.iterator(); + while ( it.hasNext() ) + { + willChange(); // need to mark editing context as changed + o = it.next(); + EOGlobalID id = aContext.globalIDForObject( o ); + Object localVersion = objectForGlobalID( id ); + if ( localVersion == null ) + { + // make a local copy and register it + localVersion = registerClone( id, aContext, o, this ); + if ( localVersion == null ) + { + throw new WotonomyException( + "Updated object could not be serialized: " + + id + " : " + o ); + } + if ( id.isTemporary() ) + { + // mark this object as inserted + insertedObjects.addObject( localVersion ); + insertedObjectsBuffer.addObject( localVersion ); + } + else + { + // mark this object as updated + updatedObjects.addObject( localVersion ); + + // notify of update only if not on deleted list + if ( deletedObjectsBuffer.indexOfIdenticalObject( + localVersion ) == NSArray.NotFound ) + { + updatedObjectsBuffer.addObject( localVersion ); + } + } + } + else // we have a local version, copy changes + { + copy( aContext, o, this, localVersion ); + // copy marks the object as updated + } + } + + } + + /** + * Sets the delegate for this editing context. + * Note: this implementation retains only a + * weak reference to the specified object. + */ + public void setDelegate ( + Object anObject ) + { + if ( anObject == null ) delegate = null; + delegate = new WeakReference( anObject ); + } + + /** + * Sets the fetch timestamp for this editing context. + */ + public void setFetchTimestamp ( + double aDouble ) + { + fetchTimestamp = aDouble; + } +/* + public void setInvalidatesObjectsWhenFinalized ( + boolean invalidatesObjects ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } + + public void setInvalidatesObjectsWhenFreed ( + boolean invalidatesObjects ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Sets whether this editing context attempts to + * lock objects when they are first modified. + * Default is false. + */ + public void setLocksObjectsBeforeFirstModification ( + boolean locksObjects ) + { + lockBeforeModify = locksObjects; + } + + /** + * Sets the message handler for this editing context. + * Note: this implementation retains only a + * weak reference to the specified object. + */ + public void setMessageHandler ( + Object anObject ) + { + if ( anObject == null ) messageHandler = null; + messageHandler = new WeakReference( anObject ); + } + + /** + * Sets whether this editing context propagates deletes + * immediately after the event that triggered the delete. + * Otherwise, propagation occurs only before commit. + * Default is true. + */ + public void setPropagatesDeletesAtEndOfEvent ( + boolean propagatesDeletes ) + { + propagateDeletesAfterEvent = propagatesDeletes; + } +/* + public void setSharedEditingContext ( + EOSharedEditingContext aSharedEditingContext ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Sets whether validation is stopped after the + * first error occurs. Otherwise, validation will + * continue for all other objects. + * Default is true. + */ + public void setStopsValidationAfterFirstError ( + boolean stopsValidation ) + { + stopValidationAfterError = stopsValidation; + } + + /** + * Sets the undo manager to be used for this context. + * Note: This is currently javax.swing.undo.UndoManager, + * until we have a implementation of NSUndoManager. + */ +/* + public void setUndoManager ( + UndoManager anUndoManager ) + { + undoManager = anUndoManager; + } +*/ +/* + public EOSharedEditingContext sharedEditingContext () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Returns whether validation is stopped after the + * first error occurs. Otherwise, validation will + * continue for all other objects. + */ + public boolean stopsValidationAfterFirstError () + { + return stopValidationAfterError; + } + + /** + * Reverts the last change on the undo stack. + */ + public void undo () + { + //TODO: not supported yet + throw new UnsupportedOperationException("Not implemented yet."); + } +/* + public NSUndoManager undoManager () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ +/** + public void unlock () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + /** + * Returns a read-only list of all objects marked as modified, + * but not inserted or deleted, in this editing context. + */ + public NSArray updatedObjects () + { + return updatedObjectsProxy; + } + + /** + * Notify editors of changes. + */ + private void fireWillSaveChanges() + { + Object o = null; + Iterator i = editors().iterator(); + while ( i.hasNext() ) + { + try + { + o = i.next(); + NSSelector.invoke( "editingContextWillSaveChanges", + new Class[] { EOEditingContext.class }, o, this ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( "Error while notifying editor of pending save: " + o ); + exc.printStackTrace(); + } + } + } + + /** + * Handles notifications from parent store, looking for + * InvalidatedAllObjectsInStoreNotification and + * ObjectsChangedInStoreNotification. + * The former causes all objects in this store to be + * invalidated. + * The latter refaults the invalidated ids, merges changes + * from the updated ids, and forgets the deleted ids, then + * posts a ObjectsChangedInStoreNotification. + * Note: This method is not in the public specification. + */ + public void handleNotification( NSNotification aNotification ) + { // System.out.println( "EOEditingContext: " + this + " : " + aNotification ); + + willChange(); + if ( InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { + refaultObjects(); + + // relay notification + NSNotificationCenter.defaultCenter().postNotification( + new NSNotification( + InvalidatedAllObjectsInStoreNotification, this ) ); + } + else + if ( EOGlobalID.GlobalIDChangedNotification + .equals( aNotification.name() ) ) + { + NSDictionary userInfo = aNotification.userInfo(); + + // if any keys in userInfo are registered ids, + // re-register with new permanent values. + + Object o; + EOGlobalID id; + Enumeration e = userInfo.keyEnumerator(); + while ( e.hasMoreElements() ) + { + id = (EOGlobalID) e.nextElement(); + o = objectForGlobalID( id ); + if ( o != null ) + { + // record object is assumed to handle key updates + recordObject( o, (EOGlobalID) userInfo.objectForKey( id ) ); + } + } + } + else + if ( EOObjectStore.ObjectsChangedInStoreNotification + .equals( aNotification.name() ) ) + { + //System.out.println( "EOEditingContext.handleNotification: " + aNotification + " : " + this ); + // post so child contexts are notified + willChange(); + + Object o; + EOGlobalID id; + Enumeration e; + NSDictionary userInfo = aNotification.userInfo(); + + // inserted objects are ignored + + // existing deleted objects are removed + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + e = deletes.objectEnumerator(); + while ( e.hasMoreElements() ) + { + id = (EOGlobalID) e.nextElement(); + o = objectForGlobalID( id ); + if ( o != null ) + { + //System.out.println( "EOEditingContext: deleted: " + id ); + forgetObject( o ); + deletedObjectsBuffer.addObject( o ); + deletedIDsBuffer.addObject( id ); + } + } + + // existing updated objects are merged + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + e = updates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + id = (EOGlobalID) e.nextElement(); + o = objectForGlobalID( id ); + if ( o != null ) + { + //System.out.println( "EOEditingContext: updated: " + id ); + if ( updatedObjects // only update if unchanged + .indexOfIdenticalObject( o ) == NSArray.NotFound ) + { + refaultObject( o, id, this ); + updatedObjectsBuffer.addObject( o ); + } + else + { + // notify user and/or merge + handleUpdateConflict( id, o ); + } + } + } + + // existing invalidated objects are refaulted + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + e = invalidates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + id = (EOGlobalID) e.nextElement(); + o = objectForGlobalID( id ); + if ( o != null ) + { + if ( updatedObjects // only invalidate if unchanged + .indexOfIdenticalObject( o ) == NSArray.NotFound ) + { + refaultObject( o, id, this ); + } + else + { + // notify user and/or merge + handleUpdateConflict( id, o ); + } + if ( invalidatedObjectsBuffer + .indexOfIdenticalObject( o ) == NSArray.NotFound ) + { + invalidatedObjectsBuffer.addObject( o ); + } + if ( invalidatedIDsBuffer + .indexOfIdenticalObject( id ) == NSArray.NotFound ) + { + invalidatedIDsBuffer.addObject( id ); + } + } + } + + } + } + + /** + * Called by handleNotification to resolve the case where we have + * received notification that another user or context has updated + * an object that is currently marked as edited in this context. + * This implementation first asks the delegate if it should merge. + * If true or no delegate, the changes are merged. True or false, + * the delegate is then sent editingContextDidMergeChanges. + */ + private void handleUpdateConflict( EOGlobalID anID, Object anObject ) + { + // if we're causing the invalidation, ignore + // (this is probably better handled by the caller...) + if ( isInvalidating ) + { + ignoreChanges = true; + parentStore.refaultObject( anObject, anID, this ); + ignoreChanges = false; + return; + } + + Boolean result = (Boolean) notifyDelegate( + "editingContextShouldMergeChangesForObject", + new Class[] { EOEditingContext.class, Object.class }, + new Object[] { this, anObject } ); + + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + // do merge + mergeExternalChanges( anID, anObject ); + } + else // Boolean.FALSE + { + // do nothing: don't lose the user's changes + } + + // notify merge did happen + notifyDelegate( + "editingContextDidMergeChanges", + new Class[] { EOEditingContext.class }, + new Object[] { this } ); + } + + /** + * For the currently modified object with the specified global id, + * this method merges changes with the updated version in the parent + * object store. This implementation looks at the fetch snapshot + * to determine which changes where made by the user, fetches the + * updated version of the object, and then determine what external + * changes were made. If the changes do not overlap, the original + * changes are applied to the updated version. If there is a conflict, + * notifies the user of the conflict. + */ + private boolean mergeExternalChanges( EOGlobalID anID, Object anObject ) + { + try + { + Iterator i; + Object key; + + // get fetch snapshot + Map fetchSnapshot = committedSnapshotForObject( anObject ); + + // get current snapshot + Map currentSnapshot = currentEventSnapshotForObject( anObject ); + + // diff against fetch snapshot + Map currentDiff = new HashMap(); + i = currentSnapshot.keySet().iterator(); + while ( i.hasNext() ) + { + key = i.next(); + if ( ! currentSnapshot.get( key ).equals( fetchSnapshot.get( key ) ) ) + { + currentDiff.put( key, currentSnapshot.get( key ) ); + } + } + + // refault + ignoreChanges = true; + parentStore.refaultObject( anObject, anID, this ); + ignoreChanges = false; + + // get updated snapshot + Map updatedSnapshot = convertSnapshotToDictionary( takeSnapshot( anObject ) ); + + // diff against fetch snapshot + Map updatedDiff = new HashMap(); + i = updatedSnapshot.keySet().iterator(); + while ( i.hasNext() ) + { + key = i.next(); + if ( ! updatedSnapshot.get( key ).equals( fetchSnapshot.get( key ) ) ) + { + updatedDiff.put( key, updatedSnapshot.get( key ) ); + } + } + + // determine if there's a conflict + boolean proceed = true; + Set updatedKeys = updatedDiff.keySet(); + i = currentDiff.keySet().iterator(); + while ( i.hasNext() ) + { + if ( updatedKeys.contains( i.next() ) ) + { + proceed = false; + break; + } + } + + // if no conflicts, apply original diff to current object and exit + if ( proceed ) + { + KeyValueCodingUtilities.takeStoredValuesFromDictionary( anObject, currentDiff ); + return true; // exit! + } + } + catch ( Exception exc ) + { + // log error to standard out + exc.printStackTrace(); + } + + // notify user we're unable to merge + notifyMessageHandler( MessageChangeConflict + anObject ); + return false; + } + + /** + * Sends the specified message to the message handler. + */ + private void notifyMessageHandler( String aMessage ) + { + Object handler = null; + try + { + handler = messageHandler(); + if ( handler == null ) return; + NSSelector.invoke( "editingContextPresentErrorMessage", + new Class[] { EOEditingContext.class, String.class }, + handler, this, aMessage ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( + "Error while notifying message handler: " + + handler + " : " + aMessage ); + exc.printStackTrace(); + } + } + + /** + * Sends the specified message to the delegate. + * Returns the return value of the method, + * or null if no return value or no delegate + * or no implementation. + */ + private Object notifyDelegate( + String aMethodName, Class[] types, Object[] params ) + { + try + { + Object delegate = delegate(); + if ( delegate == null ) return null; + return NSSelector.invoke( + aMethodName, types, delegate, params ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( + "Error while messaging delegate: " + + delegate + " : " + aMethodName ); + exc.printStackTrace(); + } + + return null; + } + + // interface EOObserving + + /** + * Implementation of the EOObserving interface. + * Called before objects are modified. + */ + public void objectWillChange ( + Object anObject ) + { + if ( ignoreChanges ) return; +//NSNotificationCenter.defaultCenter().postNotification( "objectWillChange", this, new NSDictionary( "object", anObject ) ); +//new RuntimeException().printStackTrace(); + + willChange(); + + // mark as updated if not marked already + int i = updatedObjects.indexOfIdenticalObject( anObject ); + if ( i == NSArray.NotFound ) + { + // don't mark inserted objects as updated + i = insertedObjects.indexOfIdenticalObject( anObject ); + if ( i == NSArray.NotFound ) + { + i = deletedObjects.indexOfIdenticalObject( anObject ); + if ( i == NSArray.NotFound ) + { + // add object + updatedObjects.addObject( anObject ); + + // record revert snapshot + registrar.setCommitSnapshot( anObject, takeSnapshot( anObject ) ); + } + } + } + + // add to buffer + if ( updatedObjectsBuffer.indexOfIdenticalObject( anObject ) + == NSArray.NotFound ) + { + updatedObjectsBuffer.addObject( anObject ); + } + } + + // static methods + + public static double defaultFetchTimestampLag () + { + return defaultFetchTimestampLag; + } + + /** + * Returns the default parent object store for all + * object stores created with the parameterless + * constructor. + */ + public static EOObjectStore defaultParentObjectStore () + { + return defaultParentObjectStore; + } + +/* + public static Object initObjectWithCoder ( + Object anObject, + NSCoder aCoder ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + + /** + * Returns whether editing contexts are configured to retain strong + * references to their registered objects. If false, editing contexts + * will only retain weak references to their registered objects. + */ + public static boolean instancesRetainRegisteredObjects() + { + return retainsRegisteredObjects; + } + + /** + * Sets the global default fetch timestamp lag. + */ + public static void setDefaultFetchTimestampLag ( + double aDouble ) + { + defaultFetchTimestampLag = aDouble; + } + + /** + * Sets the global default parent object store, + * used for the parameterless constructor. + */ + public static void setDefaultParentObjectStore ( + EOObjectStore anObjectStore ) + { + defaultParentObjectStore = anObjectStore; + } + + public static void setInstancesRetainRegisteredObjects ( + boolean retainsObjects ) + { + retainsRegisteredObjects = retainsObjects; + } + +/* + public static void setSubstitutionEditingContext ( + EOEditingContext aContext) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } + + public static void setUsesContextRelativeEncoding ( + boolean usesRelativeEncoding ) + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } + + public static EOEditingContext substitutionEditingContext () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } + + public static boolean usesContextRelativeEncoding () + { + throw new net.wotonomy.util.WotonomyException("Not implemented yet."); + } +*/ + + public String toString() + { + return "[EOEditingContext@"+Integer.toHexString(System.identityHashCode(this))+":"+ + " inserted="+idsForObjects(insertedObjects)+ + " updated="+idsForObjects(updatedObjects)+ + " deleted="+idsForObjects(deletedObjects)+ + " registered="+registrar.registeredGlobalIDs()+" ]"; + } + private List idsForObjects( List objects ) + { + List result = new LinkedList(); + Iterator i = objects.iterator(); + while ( i.hasNext() ) result.add( globalIDForObject( i.next() ) ); + return result; + } + + // snapshots + + /** + * Returns a NSDictionary containing only the mutable properties + * for the specified object and deep clones of their values. + * Nulls are represented by NSNull.nullValue(). + */ + private byte[] takeSnapshot( Object anObject ) + { // System.out.println( "takeSnapshot: " + anObject ); + return KeyValueCodingUtilities.freeze( anObject, this, anObject, true ); + } + + /** + * Applies the map of properties and values to the + * specified object. Null values for properties must + * be represented by the NSNull.nullValue(). + * Posts a willChange event before applying changes. + */ + private void applySnapshot( byte[] aSnapshot, Object anObject ) + { + // must clone snapshot to avoid changing existing snapshot + NSDictionary values = convertSnapshotToDictionary( aSnapshot ); + +//System.out.println( "applySnapshot: " + aSnapshot + " : " + values ); +//ignoreChanges = true; + willChange(); + KeyValueCodingUtilities.takeStoredValuesFromDictionary( anObject, values ); +//ignoreChanges = false; + } + + /** + * Snapshots are stored internally in binary format, + * but exposed to the user as NSDictionaries. + */ + private NSDictionary convertSnapshotToDictionary( byte[] aSnapshot ) + { + // get the object + Object clone = KeyValueCodingUtilities.thaw( aSnapshot, this, true ); + // get all keys for this object + EOClassDescription classDesc = + EOClassDescription.classDescriptionForClass( clone.getClass() ); + List keys = new LinkedList(); + keys.addAll( classDesc.attributeKeys() ); + keys.addAll( classDesc.toOneRelationshipKeys() ); + keys.addAll( classDesc.toManyRelationshipKeys() ); + + return KeyValueCodingUtilities.valuesForKeys( clone, keys ); + } + /** + * Creates a deep clone of the specified object. + * (Object.clone() only creates a shallow clone.) + * Returns null if operation fails. + */ + static private Object clone( + EOEditingContext aSourceContext, + Object aSource, + EOEditingContext aDestinationContext ) + { // System.out.println( "clone: " + aSource ); + return KeyValueCodingUtilities.clone( + aSourceContext, aSource, aDestinationContext ); + } + + /** + * Creates a deep clone of the specified object. + * but does not transpose references. This allows + * us to register an object before transposing + * references so that child objects will be able + * to resolve references to their parent. + * After recording the object, we copy the source + * object into the clone, which does transpose + * and resolve properly. + * Returns null if operation fails. + */ + static private Object registerClone( + EOGlobalID aGlobalID, + EOEditingContext aSourceContext, + Object aSource, + EOEditingContext aDestinationContext ) + { + // while we'd like to just transpose/clone at the same time + // we must record a clone without transposing: this + // avoids a endless loop if the object graph has a cycle + Object clone = KeyValueCodingUtilities.thaw( + KeyValueCodingUtilities.freeze( + aSource, aSourceContext, aSource, false ), + aDestinationContext, false ); + + aDestinationContext.recordObject( clone, aGlobalID ); + + // need to copy to transpose references into this context + // while preserving the same instance of the object + aDestinationContext.ignoreChanges = true; + copy( aSourceContext, aSource, aDestinationContext, clone ); + aDestinationContext.ignoreChanges = false; + + // return our clone + return clone; + } + + /** + * Copies values from one object to another. + * Returns the destination object, or throws exception + * if operation fails. + */ + static private Object copy( + EOEditingContext aSourceContext, + Object aSource, + EOEditingContext aDestinationContext, + Object aDestination ) + { // System.out.println( "copy: " ); + EOObserverCenter.notifyObserversObjectWillChange( aDestination ); + KeyValueCodingUtilities.copy( aSourceContext, aSource, aDestinationContext, aDestination ); + return aDestination; + } + + // process recent changes + + /** + * Called to notify observers of changes. + * Also calls runLater(). + */ + private void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + runLater(); + } + + /** + * Called to ensure that processRecentChanges + * will be called on the next event loop. + */ + private void runLater() + { + if ( ! willRunLater ) + { + willRunLater = true; + NSRunLoop.currentRunLoop().performSelectorWithOrder( + runLaterSelector, this, null, EditingContextFlushChangesRunLoopOrdering, null ); + } + } + + /** + * This method is called by the event queue run loop + * and calls processRecentChanges. + * NOTE: This method is not part of the specification. + */ + public void flushRecentChanges( Object anObject ) + { +//System.out.println( "EODelayedObserverQueue: running" ); + processRecentChanges(); + willRunLater = false; + } + + // inner classes + + /** + * Gatekeeper for all access to registered objects. + */ + static private class Registrar + { + EOEditingContext context; + NSMutableDictionary IDsToObjects; + NSMutableDictionary objectsToIDs; + NSMutableDictionary objectsToCommitSnapshots; + NSMutableDictionary objectsToCurrentSnapshots; + + ReferenceKey comparisonKey; //FIXME not thread safe! + + public Registrar( EOEditingContext aContext ) + { + context = aContext; + IDsToObjects = new NSMutableDictionary(); + objectsToIDs = new NSMutableDictionary(); + objectsToCommitSnapshots = new NSMutableDictionary(); + objectsToCurrentSnapshots = new NSMutableDictionary(); + comparisonKey = new ReferenceKey(); + } + + public NSArray registeredObjects() + { + return IDsToObjects.allValues(); + } + + public NSArray registeredGlobalIDs() + { + return IDsToObjects.allKeys(); + } + + public Object objectForGlobalID( EOGlobalID aGlobalID ) + { + return IDsToObjects.objectForKey( aGlobalID ); + } + + public EOGlobalID globalIDForObject( Object anObject ) + { + comparisonKey.set( anObject ); + return (EOGlobalID) objectsToIDs.objectForKey( comparisonKey ); + } + + public byte[] getCommitSnapshot( Object anObject ) + { + comparisonKey.set( anObject ); + return (byte[]) objectsToCommitSnapshots.objectForKey( comparisonKey ); + } + + public void setCommitSnapshot( Object anObject, byte[] aSnapshot ) + { + if ( aSnapshot == null ) + { + comparisonKey.set( anObject ); + objectsToCommitSnapshots.removeObjectForKey( comparisonKey ); + } + else + { + objectsToCommitSnapshots.setObjectForKey( + aSnapshot, new ReferenceKey( anObject ) ); + } + } + + public byte[] getCurrentSnapshot( Object anObject ) + { + comparisonKey.set( anObject ); + return (byte[]) objectsToCurrentSnapshots.objectForKey( comparisonKey ); + } + + public void setCurrentSnapshot( Object anObject, byte[] aSnapshot ) + { + if ( aSnapshot == null ) + { + comparisonKey.set( anObject ); + objectsToCurrentSnapshots.removeObjectForKey( comparisonKey ); + } + else + { + objectsToCurrentSnapshots.setObjectForKey( + aSnapshot, new ReferenceKey( anObject ) ); + } + } + + public void registerObject( Object anObject, EOGlobalID aGlobalID ) + { + IDsToObjects.setObjectForKey( anObject, aGlobalID ); + objectsToIDs.setObjectForKey( aGlobalID, new ReferenceKey( anObject ) ); + EOObserverCenter.addObserver( context, anObject ); + } + + public void forgetObject( Object anObject ) + { + comparisonKey.set( anObject ); + Object id = objectsToIDs.objectForKey( comparisonKey ); + IDsToObjects.removeObjectForKey( id ); + objectsToIDs.removeObjectForKey( comparisonKey ); + EOObserverCenter.removeObserver( context, anObject ); + } + + public void disposeSnapshots( Object anObject ) + { + setCommitSnapshot( anObject, null ); + setCurrentSnapshot( anObject, null ); + } + + } + + /** + * Registrar that uses only WeakReferences. + * Used if retainsRegisteredObjects is false. + */ + static private class WeakRegistrar extends Registrar + { + private WeakReferenceKey weakComparisonKey; //FIXME not thread safe! + + public WeakRegistrar( EOEditingContext aContext ) + { + super( aContext ); + weakComparisonKey = new WeakReferenceKey(); + } + + public NSArray registeredObjects() + { + Object object; + WeakReferenceKey weakKey; + NSMutableArray result = new NSMutableArray(); + Enumeration e = new NSArray( objectsToIDs.allKeys() ).objectEnumerator(); + while ( e.hasMoreElements() ) + { + weakKey = (WeakReferenceKey) e.nextElement(); + object = weakKey.get(); + if ( object != null ) + { + result.addObject( object ); + } + else + { + // object has been released: perform cleanup + disposeObject( null, weakKey ); + } + } + return result; + } + + public Object objectForGlobalID( EOGlobalID aGlobalID ) + { + WeakReference ref = (WeakReference) super.objectForGlobalID( aGlobalID ); + if ( ref == null ) return null; + Object result = ref.get(); + if ( result == null ) + { + // clean up manually + IDsToObjects.removeObjectForKey( aGlobalID ); + Iterator i = new LinkedList( objectsToIDs.allKeysForObject( ref ) ).iterator(); + while ( i.hasNext() ) + { + objectsToIDs.removeObjectForKey( i.next() ); + } + disposeSnapshots( aGlobalID ); + } + return result; + } + + public byte[] getCommitSnapshot( Object anObject ) + { + weakComparisonKey.set( anObject ); + return (byte[]) objectsToCommitSnapshots.objectForKey( weakComparisonKey ); + } + + public void setCommitSnapshot( Object anObject, byte[] aSnapshot ) + { + if ( aSnapshot == null ) + { + weakComparisonKey.set( anObject ); + objectsToCommitSnapshots.removeObjectForKey( weakComparisonKey ); + } + else + { + objectsToCommitSnapshots.setObjectForKey( + aSnapshot, new WeakReferenceKey( anObject ) ); + } + } + + public byte[] getCurrentSnapshot( Object anObject ) + { + weakComparisonKey.set( anObject ); + return (byte[]) objectsToCurrentSnapshots.objectForKey( weakComparisonKey ); + } + + public void setCurrentSnapshot( Object anObject, byte[] aSnapshot ) + { + if ( aSnapshot == null ) + { + weakComparisonKey.set( anObject ); + objectsToCurrentSnapshots.removeObjectForKey( weakComparisonKey ); + } + else + { + objectsToCurrentSnapshots.setObjectForKey( + aSnapshot, new WeakReferenceKey( anObject ) ); + } + } + + public void registerObject( Object anObject, EOGlobalID aGlobalID ) + { // new net.wotonomy.ui.swing.ReferenceInspector( anObject ); + IDsToObjects.setObjectForKey( new WeakReference( anObject ), aGlobalID ); + objectsToIDs.setObjectForKey( aGlobalID, new WeakReferenceKey( anObject ) ); + EOObserverCenter.addObserver( context, anObject ); + } + + public void forgetObject( Object anObject ) + { + disposeObject( anObject, null ); + } + + // must specify one or the other + private void disposeObject( Object anObject, WeakReferenceKey key ) + { + if ( key == null ) key = new WeakReferenceKey( anObject ); + EOGlobalID id = (EOGlobalID) objectsToIDs.objectForKey( key ); + if ( id != null ) IDsToObjects.removeObjectForKey( id ); + objectsToIDs.removeObjectForKey( key ); + disposeSnapshots( id ); + if ( anObject != null ) + { + EOObserverCenter.removeObserver( context, anObject ); + } + } + } + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + */ + static private class ReferenceKey + { + int hashCode; + Object referent; + + public ReferenceKey() + { + referent = null; + hashCode = -1; + } + + public ReferenceKey( Object anObject ) + { + set( anObject ); + } + + public Object get() + { + return referent; + } + + public void set( Object anObject ) + { + referent = anObject; + hashCode = anObject.hashCode(); + } + + /** + * Returns the actual key's hash code. + */ + public int hashCode() + { + return hashCode; + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( anObject == this ) return true; + if ( anObject instanceof ReferenceKey ) + { + return ((ReferenceKey)anObject).get() == referent; + } + return false; + } + } + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + */ + static private class WeakReferenceKey extends ReferenceKey + { + public WeakReferenceKey() + { + super(); + } + + public WeakReferenceKey( Object anObject ) + { + super( anObject ); + } + + public Object get() + { + return ((WeakReference)referent).get(); + } + + public void set( Object anObject ) + { + referent = new WeakReference( anObject ); + hashCode = anObject.hashCode(); + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( anObject == this ) return true; + if ( anObject instanceof ReferenceKey ) + { + return ((ReferenceKey)anObject).get() == get(); + } + return false; + } + } + + /** + * Key combining an object with a string. + * Object is compared by reference. + */ + static private class CompoundKey + { + private Object object; + private String string; + private int hashCode; + + /** + * Creates compound key. + * Neither name nor object may be null. + */ + public CompoundKey ( + Object anObject, String aString ) + { + object = anObject; + string = aString; + hashCode = object.hashCode() + string.hashCode(); + } + + public int hashCode() + { + return hashCode; + } + + public boolean equals( Object anObject ) + { + if ( anObject instanceof CompoundKey ) + { + CompoundKey key = (CompoundKey) anObject; + return ( ( key.object == object ) && ( key.string.equals( string ) ) ); + } + return false; + } + + public String toString() + { + return "[CompoundKey:"+object+":"+string+"]"; + } + } + + /** + * Used by EditingContext to delegate behavior to another class. + * Note that EditingContext doesn't require its delegates to implement + * this interface: rather, this interface defines the methods that + * EditingContext will attempt to invoke dynamically on its delegate. + * The delegate may choose to implement only a subset of the methods + * on the interface. + */ + public interface Delegate + { + /** + * Called after the editing context has completed merge operations + * on one or more objects after receiving an ObjectChangedInStore + * notification. + */ + void editingContextDidMergeChanges( + EOEditingContext anEditingContext ); + + /** + * Called by objectsWithFetchSpecification. + * If null, the editing context will pass the fetch specification + * on to its parent store, as normal. Otherwise, the context + * will use the returned array to service the request. + */ + NSArray editingContextShouldFetchObjects( + EOEditingContext anEditingContext, + EOFetchSpecification fetchSpecification ); + + /** + * Called to determine whether an object should be invalidated. + * Return false to prevent the object from being invalidated. + * Default is true. + */ + boolean editingContextShouldInvalidateObject( + EOEditingContext anEditingContext, + Object anObject, + EOGlobalID aGlobalID ); + + /** + * Called to determine whether the editing context should attempt + * to merge changes in the specified object that the parent store + * says has changed via an ObjectChangedInStore notification. + * Default is true. Return false if you wish to handle the merge + * yourself, by extracting the values in the object now and comparing + * them to the values when editingContextDidMergeChanges is called. + */ + boolean editingContextShouldMergeChangesForObject( + EOEditingContext anEditingContext, + Object anObject ); + + /** + * Returns whether the editing context should ask its message handler + * to display a message. Return false if the delegate will display the error. + * Default is true. + */ + boolean editingContextShouldPresentException( + EOEditingContext anEditingContext, + Throwable exception ); + + /** + * Returns whether the editing context should undo the most + * recent set of changes that resulted in a validation failure. + * Default is true. + */ + boolean editingContextShouldUndoUserActionsAfterFailure( + EOEditingContext anEditingContext ); + + /** + * Returns whether the editing context should validate the + * most recent set of changes. Default is true. + */ + boolean editingContextShouldValidateChanges( + EOEditingContext anEditingContext ); + + /** + * Called before the editing context saves its changes + * to the parent object store. + */ + void editingContextWillSaveChanges( + EOEditingContext anEditingContext ); + + } + + /** + * Editors register themselves with the editing context + * so that they may receive notification before the context + * commits changes. This is useful for associations whose + * components do not immediately commit their changes to + * the object they are editing. + */ + public interface Editor + { + /** + * Called before the editing context saves its changes + * to the parent object store. + */ + void editingContextWillSaveChanges( + EOEditingContext anEditingContext ); + + /** + * Called to determine whether this editor has changes + * that have not been committed to the object in the context. + */ + boolean editorHasChangesForEditingContext( + EOEditingContext anEditingContext ); + + } + + /** + * Used by EditingContext to delegate messaging handling to another class, + * typically the display group that has the currently active association. + * Note that EditingContext doesn't require its message handlers to implement + * this interface: rather, this interface defines the methods that + * EditingContext will attempt to invoke dynamically on its delegate. + * The delegate may choose to implement only a subset of the methods + * on the interface. + */ + public interface MessageHandler + { + /** + * Called to display a message for an error that occurred + * in the specified editing context. + */ + void editingContextPresentErrorMessage( + EOEditingContext anEditingContext, + String aMessage ); + + /** + * Called by the specified object store to determine whether + * fetching should continue, where count is the current count + * and limit is the limit as specified by the fetch specification. + * Default is false. + */ + boolean editingContextShouldContinueFetching( + EOEditingContext anEditingContext, + int count, + int limit, + EOObjectStore anObjectStore ); + + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.86 2003/12/18 15:37:38 mpowers + * Changes to retain ability to work with objects that don't necessarily + * implement EOEnterpriseObject. I would still like to preserve this case + * for general usage, however the access package is free to assume that + * those objects will be EOs and cast appropriately. + * + * Revision 1.85 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.84 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.83 2003/02/13 15:24:33 mpowers + * hasChanges is now derived, not tracked. + * refaultObject now more consistently removes object from updated list. + * + * Revision 1.82 2002/12/16 15:46:00 mpowers + * Major refactoring to implement setInstancesRetainRegisteredObjects(). + * + * Revision 1.81 2002/11/18 22:10:58 mpowers + * Now resetting hasChanges flag on reset. + * + * Revision 1.80 2002/10/24 21:15:33 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.79 2002/10/24 18:18:12 mpowers + * NSArray's are now considered read-only, so we can return our internal + * representation to reduce unnecessary object allocation. + * + * Revision 1.78 2002/06/21 21:44:33 mpowers + * No longer marking deleted objects as updated (thanks to dwang). + * + * Revision 1.77 2002/05/20 15:10:17 mpowers + * No longer refaulting if delegate does not wish to handle the merge. + * + * Revision 1.76 2002/03/26 21:46:06 mpowers + * Contributing EditingContext as a java-friendly convenience. + * + * Revision 1.75 2002/03/06 16:14:57 mpowers + * More attempts at ignoring update conflicts that come from ourself. + * + * Revision 1.74 2002/02/21 21:57:50 mpowers + * Implemented default merge behavior. + * + * Revision 1.73 2002/02/20 16:46:54 mpowers + * Implemented better support for EOEditingContext.Delegate. + * + * Revision 1.70 2002/02/19 22:26:05 mpowers + * Implemented EOEditingContext.MessageHandler support. + * + * Revision 1.69 2002/02/19 16:33:42 mpowers + * Implemented support for EditingContext.Editor + * + * Revision 1.68 2002/02/13 22:00:34 mpowers + * Fixed: invalidateAllObjects tries to invalidate inserted objects, + * typically causing class cast exceptions involving EOTemporaryGlobalID. + * + * Revision 1.67 2002/02/06 21:15:35 mpowers + * No longer refaulting a dirty object when we receive an invalidation notif. + * + * Revision 1.66 2002/01/08 19:31:03 mpowers + * refaultObject now correctly refaults the object. + * + * Revision 1.65 2001/12/20 18:56:15 mpowers + * Refinements to snapshotting and calling processRecentChanges. + * + * Revision 1.64 2001/12/10 15:11:41 mpowers + * Now only tracking a commit snapshot after an object has been modified. + * + * Revision 1.63 2001/11/14 00:08:10 mpowers + * Now marking context changed when objects are inserted or deleted + * and when child contexts save their changes into this context. + * + * Revision 1.62 2001/11/07 14:49:31 mpowers + * invalidateAllObjects now handles objects manually discarded in the course + * of invalidation. + * + * Revision 1.61 2001/10/26 20:02:49 mpowers + * No longer using NSNotificationQueue: all notifications are posted immed. + * + * Revision 1.60 2001/10/26 18:37:50 mpowers + * Now using NSRunLoop to correctly flush recent changes before delayed + * observers and AWT events. + * + * Revision 1.59 2001/10/23 22:29:59 mpowers + * Now posting notifications immediately. + * Recent changes are flushed at ObserverPrioritySixth, soon to change. + * + * Revision 1.58 2001/09/10 14:16:51 mpowers + * EditingContexts now relay InvalidatedAllObjectsInStore notifications. + * + * Revision 1.57 2001/06/18 14:11:15 mpowers + * Inserting a deleted object simply cancels the delete operation. + * + * Revision 1.56 2001/06/07 22:07:59 mpowers + * Now handling delete notifications before update notifications. + * Eliminated the case where deleted objects were also being put + * on the updated list when notifying child contexts. + * + * Revision 1.55 2001/05/18 21:04:33 mpowers + * Reimplemented EditingContext.initializeObject. + * + * Revision 1.54 2001/05/05 23:05:42 mpowers + * Implemented Array Faults. + * + * Revision 1.53 2001/05/05 15:00:06 mpowers + * Tested load-on-demand: still works. + * Now using registerClone for consistency. + * Editing context is temporarily posting notification on objectWillChange. + * + * Revision 1.52 2001/05/05 14:11:48 mpowers + * Implemented registerClone. + * + * Revision 1.51 2001/05/04 16:57:56 mpowers + * Now correctly transposing references to editing contexts when + * cloning/copying between editing contexts. + * + * Revision 1.50 2001/05/02 17:58:41 mpowers + * Removed debugging code, added comments. + * + * Revision 1.49 2001/05/02 15:47:40 mpowers + * Fixed the pernicious problem with reverts: recordObject was recording + * a snapshot of the clone before the transposition-copy happened, + * so the revert object would lose all of its transposed relationships. + * + * Revision 1.48 2001/05/02 12:39:05 mpowers + * Fixed a nasty problem with transpose-cloning and faultForGlobalID. + * Now we're forced to create a deep clone, registered it, and then + * transpose it after it has been registered. + * + * Revision 1.47 2001/04/30 13:15:24 mpowers + * Child contexts re-initializing objects invalidated in parent now + * propery transpose relationships. + * + * Revision 1.46 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.45 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.44 2001/04/28 16:18:44 mpowers + * Implementing relationships. + * + * Revision 1.43 2001/04/28 14:12:23 mpowers + * Refactored cloning/copying into KeyValueCodingUtilities. + * + * Revision 1.42 2001/04/27 00:27:11 mpowers + * Provided description to not-implemented exceptions. + * + * Revision 1.41 2001/04/26 01:16:44 mpowers + * Major bug fix so we no longer accumulate objects in the all objects + * list with every InvalidateAllObjectsInStore. + * + * Revision 1.40 2001/04/21 23:07:49 mpowers + * Now only broadcasts notifications if there's actually a change. + * + * Revision 1.39 2001/04/13 16:33:11 mpowers + * Corrected the refaulting behavior. + * + * Revision 1.38 2001/04/09 21:42:10 mpowers + * Debugging and optimizing notifications. + * + * Revision 1.37 2001/04/08 20:59:47 mpowers + * objectsForFetchSpecification now relies on faultForGlobalID. + * + * Revision 1.36 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.35 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.34 2001/03/28 14:06:29 mpowers + * Implemented snapshots. Revert now uses snapshots. + * + * Revision 1.33 2001/03/20 23:20:33 mpowers + * invalidating all objects now sets the dirty flag to false. + * + * Revision 1.32 2001/03/19 21:44:36 mpowers + * Reverts reinitialize for now. + * Testing for inserted objects instead of temp id when invalidating object. + * + * Revision 1.31 2001/03/15 21:10:26 mpowers + * Implemented global id re-registration for newly saved inserts. + * + * Revision 1.30 2001/03/13 21:41:34 mpowers + * Broadcasting willChange for any change to hasChanges. + * Fixed major bug with inserted objects treated as updated objects + * in child display groups. + * + * Revision 1.29 2001/03/09 22:10:30 mpowers + * Fine tuned initializeObject. + * + * Revision 1.28 2001/03/06 23:23:55 mpowers + * objectForGlobalID now returns null if not found. + * objectsForFetchSpecification again does things the old way, for now. + * + * Revision 1.27 2001/03/02 16:31:45 mpowers + * Trying to better handle fetches from child contexts. + * No longer trying to invalidate temporary objects. + * + * Revision 1.26 2001/02/28 16:25:19 mpowers + * Now calling globalIDForObject internally. + * + * Revision 1.25 2001/02/27 17:36:55 mpowers + * Objects inserted from child now preserve the existing temporary id. + * + * Revision 1.24 2001/02/27 02:11:17 mpowers + * Now throwing exception when cloning fails. + * Removed debugging printlns. + * + * Revision 1.23 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * Revision 1.22 2001/02/26 21:18:45 mpowers + * Now marking edited objects from child contexts that were not already + * recorded in parent as changed in saveChangesInEditingContext. + * + * Revision 1.21 2001/02/26 15:53:22 mpowers + * Fine-tuning notification firing. + * Child display groups now update properly after parent save or invalidate. + * + * Revision 1.20 2001/02/24 17:03:22 mpowers + * Implemented the notification queue, and changed editing context to use it. + * + * Revision 1.19 2001/02/23 23:44:15 mpowers + * Fine-tuning notification handling. + * + * Revision 1.18 2001/02/22 22:56:57 mpowers + * Only refaulting edited objects on parent store invalidateAll. + * + * Revision 1.17 2001/02/22 20:54:39 mpowers + * Implemented notification handling. + * + * Revision 1.16 2001/02/21 22:10:55 mpowers + * Editing context is now posting appropriate notifications. + * + * Revision 1.15 2001/02/21 21:17:32 mpowers + * Now retaining a reference to the recent changes observer. + * Better documented need to retain reference. + * Started implementing notifications. + * + * Revision 1.14 2001/02/20 17:24:22 mpowers + * Now using reference keys in objectToID. + * + * Revision 1.13 2001/02/20 16:45:36 mpowers + * Child data sources now accept a data source instead of an editing context + * for more flexibility. Child data sources now forward relationship + * methods to parent source. + * + * Revision 1.12 2001/02/16 22:51:29 mpowers + * Now deep-cloning objects passed between editing contexts. + * + * Revision 1.11 2001/02/16 18:34:19 mpowers + * Implementing nested contexts. + * + * Revision 1.9 2001/02/15 21:13:30 mpowers + * First draft implementation is complete. Now on to debugging. + * + * Revision 1.7 2001/02/13 23:24:29 mpowers + * Implementing more of editing context. + * + * Revision 1.4 2001/02/09 22:09:34 mpowers + * Completed implementation of EOObjectStore. + * + * Revision 1.3 2001/02/06 15:24:11 mpowers + * Widened parameters on abstract method to fix build. + * + * Revision 1.2 2001/02/05 03:45:37 mpowers + * Starting work on EOEditingContext. + * + * Revision 1.1.1.1 2000/12/21 15:46:42 mpowers + * Contributing wotonomy. + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEnterpriseObject.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEnterpriseObject.java new file mode 100644 index 0000000..112531c --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOEnterpriseObject.java @@ -0,0 +1,219 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* EOEnterpriseObject defines the required methods a data object +* must implement to take full advantage of the control package. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public interface EOEnterpriseObject + extends EOKeyValueCodingAdditions, + EORelationshipManipulation, + EODeferredFaulting, + EOValidation +{ + /** + * Returns a List of all property keys defined on this object. + * This includes both attributes and relationships. + */ + NSArray allPropertyKeys(); + + /** + * Returns a list of all attributes defined on this object. + * Attributes are all properties that are not relationships. + */ + NSArray attributeKeys(); + + //void awakeFromClientUpdate(EOEditingContext aContext) + + /** + * Called when the object has first been fetched into the + * specified editing context. + */ + void awakeFromFetch(EOEditingContext anEditingContext); + + /** + * Called when the object has been inserted into the + * specified editing context. + */ + void awakeFromInsertion(EOEditingContext anEditingContext); + + /** + * Returns a Map representing the delta of the current state + * from the state represented in the specified snapshot. + * The result will contain only the keys that have changed + * and their values. Relationship keys will map to an NSArray + * that contains an NSArray of added objects and an NSArray + * of removed objects, in that order. + */ + NSDictionary changesFromSnapshot(NSDictionary snapshot); + + /** + * Returns a class description for this object. + */ + EOClassDescription classDescription(); + + /** + * Returns a class description for the object at the + * other end of the specified relationship key. + */ + EOClassDescription classDescriptionForDestinationKey(String aKey); + + /** + * Clears all property values for this object. + * This method is called to clean-up an object that + * will no longer be used, and implementations should + * ensure that all references are set to null to + * prevent problems with garbage-collection. + */ + void clearProperties(); + + /** + * Returns the delete rule constant defined on EOClassDescription + * for the relationship defined by the specified key. + */ + int deleteRuleForRelationshipKey(String aRelationshipKey); + + /** + * Returns the editing context in which this object is registered. + */ + EOEditingContext editingContext(); + + /** + * Returns the name of the entity that this object represents. + */ + String entityName(); + + /** + * Returns a String containing all property keys and values for + * this object. Relationships should be represented by calling + * eoShallowDescription() on the object. + */ + String eoDescription(); + + /** + * Returns a String containing all attribute keys and values for + * this object. Relationships are not included. + */ + String eoShallowDescription(); + + /** + * Returns the key used to reference this object on the + * object at the other end of the specified relationship. + */ + String inverseForRelationshipKey(String aRelationshipKey); + + //Object invokeRemoteMethod( + // String aMethodName, Class[] aTypeArray Object[] anArgumentArray) + + /** + * Returns whether the specified relationship key represents + * a to-many relationship. + */ + boolean isToManyKey(String aKey); + + /** + * Returns whether the objects at the other end of the specified + * relationship should be deleted when this object is deleted. + */ + boolean ownsDestinationObjectsForRelationshipKey(String aKey); + + //void prepareValuesForClient() + + /** + * Called to perform the delete propagation for this object + * on the specified editing context. All relationships + * should be processed according to their corresponding + * delete rule. + */ + void propagateDeleteWithEditingContext(EOEditingContext aContext); + + /** + * Applies the changes from the specified snapshot to + * this object. + * @see #changesFromSnapshot(NSDictionary) + */ + void reapplyChangesFromDictionary(NSDictionary aDeltaSnapshot); + + /** + * Returns a snapshot of the current state of this object. + * All property keys are mapped to their values; nulls are + * represented by NSNull. + */ + NSDictionary snapshot(); + + /** + * Returns a List of the to-many relationship keys + * for this object. + */ + NSArray toManyRelationshipKeys(); + + /** + * Returns a List of the to-one relationship keys + * for this object. + */ + NSArray toOneRelationshipKeys(); + + /** + * Applies the specified snapshot to this object, + * converting NSNulls to null and calling + * takeStoredValueForKey for each key in the Map. + */ + void updateFromSnapshot(NSDictionary aSnapshot); + + /** + * Returns a short, stateful string representation + * of this object. + */ + String userPresentableDescription(); + + /** + * This method should be implemented to call + * EOObserverCenter.objectWillChange( this ), + * and it should be called by each setter method + * on this object before changes are made to the + * object's internal state. + */ + void willChange(); +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaultHandler.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaultHandler.java new file mode 100644 index 0000000..19dc2df --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaultHandler.java @@ -0,0 +1,102 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* EOFaultHandler defines the contract for objects that can +* create and populate faults. In wotonomy, this interface is +* currently only a marker interface. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public abstract class EOFaultHandler { + + protected Class _targetClass; + + public EOFaultHandler() { + super(); + } + + public static EOFaultHandler handlerForFault(Object obj) { + if (!(obj instanceof EOFaulting)) + throw new IllegalArgumentException("Object must implement EOFaulting"); + return ((EOFaulting)obj).faultHandler(); + } + + public static boolean isFault(Object obj) { + if (obj == null) + return false; + boolean isit = (obj instanceof EOFaulting); + if (isit) + isit = ((EOFaulting)obj).isFault(); + return isit; + } + + public static void makeObjectIntoFault(Object obj, EOFaultHandler handler) { + if (!(obj instanceof EOFaulting)) + throw new IllegalArgumentException("Object must implement EOFaulting"); + ((EOFaulting)obj).turnIntoFault(handler); + } + + public static void clearFault(Object obj) { + if (!(obj instanceof EOFaulting)) + throw new IllegalArgumentException("Object must implement EOFaulting"); + ((EOFaulting)obj).clearFault(); + } + + public Class targetClass() { + return _targetClass; + } + + public Object createFaultForDeferredFault(Object fault, EOEnterpriseObject source) { + return fault; + } + + public String descriptionForObject(Object obj) { + if (obj == null) + return ""; + return obj.toString(); + } + + public String eoShallowDescription(Object obj) { + return null; + } + + public abstract void completeInitializationOfObject(Object obj); + + public abstract void faultWillFire(Object obj); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaulting.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaulting.java new file mode 100644 index 0000000..95b3e35 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFaulting.java @@ -0,0 +1,79 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* EOFaulting defines the requirements for objects that +* can be faulted by an EOFaultManager. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOFaulting +{ + /** + * Called by EOFaultHandler to prepare the object to be turned into a fault. + */ + void clearFault(); + + /** + * Returns this object's EOFaultHandler. + */ + EOFaultHandler faultHandler(); + + /** + * Returns whether this object is currently a fault. + * Returns true if this object has not yet retrieved any values. + */ + boolean isFault(); + + /** + * Turns this object into a fault using the specified fault handler. + */ + void turnIntoFault( EOFaultHandler aFaultHandler ); + + /** + * Called to completely fire the fault, reading all values. + * This method may be implemented to call willRead(null). + */ + void willRead(); + + /** + * Called to fire the fault for the specified key. + * The fault manager is required to populate the specified key + * with a value, and may populate any or all of the other values + * on this object. A null key will populate all values on the object. + * NOTE: This method is not part of the specification. + */ + void willRead( String aKey ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFetchSpecification.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFetchSpecification.java new file mode 100644 index 0000000..71f3b78 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOFetchSpecification.java @@ -0,0 +1,565 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOFetchSpecification defines the parameters used to request +* objects from an EOObjectStore. They are commonly created +* and passed to a EODataSource which fetches from its +* EOEditingContext, which passes the call up to its root +* EOObjectStore's objectsWithFetchSpecification method. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOFetchSpecification implements EOKeyValueArchiving { + private boolean fetchesRawRows; + private String entityName; + private NSDictionary hints; + private boolean deep; + private int fetchLimit; + private boolean locksObjects; + private NSArray prefetchingRelationshipKeyPaths; + private boolean promptsAfterFetchLimit; + private EOQualifier qualifier; + private NSArray rawRowKeyPaths; + private boolean refreshesRefetchedObjects; + private boolean requiresAllQualifierBindingVariables; + private NSArray sortOrderings; + private boolean distinct; + + /** + * Default constructor initializes internal state. + */ + public EOFetchSpecification() + { + fetchesRawRows = false; + entityName = null; + hints = null; + deep = true; + fetchLimit = 0; + locksObjects = false; + prefetchingRelationshipKeyPaths = null; + promptsAfterFetchLimit = false; + qualifier = null; + rawRowKeyPaths = null; + refreshesRefetchedObjects = false; + requiresAllQualifierBindingVariables = false; + sortOrderings = null; + distinct = false; + } + + /** + * Constructs a fetch specification for the specified entity type using + * the specified qualifier and sort ordering. + */ + public EOFetchSpecification( String anEntityName, EOQualifier aQualifier, List aSortOrderingList ) + { + this(); + entityName = anEntityName; + qualifier = aQualifier; + sortOrderings = new NSArray( (Collection) aSortOrderingList ); + } + + /** + * Constructs a fetch specification for the specified entity type using + * the specified qualifier and sort ordering, distinct flag, deep flag, + * and hints dictionary. + */ + public EOFetchSpecification( String anEntityName, EOQualifier aQualifier, NSArray aSortOrderingList, + boolean usesDistinct, boolean isDeep, Map aHintMap) + { + this(); + entityName = anEntityName; + qualifier = aQualifier; + sortOrderings = new NSArray( (Collection) aSortOrderingList ); + distinct = usesDistinct; + deep = isDeep; + hints = new NSMutableDictionary( (Map) aHintMap ); + } + + /** + * Convenience to return the named fetch specification from the class description + * corresponding to the specified entity name. Returns null if either entityName + * or spec name cannot be resolved. + */ + public static EOFetchSpecification fetchSpecificationNamed( String name, String entityName) + { + EOClassDescription classDesc = EOClassDescription.classDescriptionForEntityName( entityName ); + if ( classDesc == null ) return null; + return classDesc.fetchSpecificationNamed( name ); + } + + /** + * Implemented to return a new fetch specification that is a deep copy of this one. + */ + public Object clone() + { + EOFetchSpecification clone = new EOFetchSpecification(); + + clone.fetchesRawRows = this.fetchesRawRows; + clone.entityName = this.entityName; + if ( this.hints != null ) + clone.hints = new NSDictionary( (Map) this.hints ); + clone.deep = this.deep; + clone.locksObjects = this.locksObjects; + if ( this.prefetchingRelationshipKeyPaths != null ) + clone.prefetchingRelationshipKeyPaths = + new NSArray( (List) prefetchingRelationshipKeyPaths ); + clone.promptsAfterFetchLimit = this.promptsAfterFetchLimit; + if ( this.qualifier != null ) + clone.qualifier = this.qualifier; //FIXME: probably should clone? + if ( this.rawRowKeyPaths != null ) + clone.rawRowKeyPaths = new NSArray( (List) this.rawRowKeyPaths ); + clone.refreshesRefetchedObjects = this.refreshesRefetchedObjects; + clone.requiresAllQualifierBindingVariables = + this.requiresAllQualifierBindingVariables; + if ( this.sortOrderings != null ) + clone.sortOrderings = new NSArray( (List) this.sortOrderings ); + clone.distinct = this.distinct; + + return clone; + } + + /** + * Returns the name of the entity fetched by this fetch spec. + */ + public String entityName() + { + return entityName; + } + + /** + * Returns the current fetch limit. + * A fetch limit of zero indicates no fetch limit. + * Zero is the default. + */ + public int fetchLimit() + { + return fetchLimit; + } + + /** + * Returns whether this fetch spec will fetch raw rows. + * Default is false. + */ + public boolean fetchesRawRows() + { + return fetchesRawRows; + } + + /** + * Returns a fetch specification that resolves the bindings + * in the specified map. + */ + public EOFetchSpecification + fetchSpecificationWithQualifierBindings(Map aBindingMap) + { + throw new WotonomyException( "Not implemented yet" ); + } + + /** + * Returns a Map containing the hints used by this fetch specification, + * or null if no hints have been specified. + */ + public NSDictionary hints() + { + if ( hints == null ) return null; + return new NSDictionary( (NSDictionary) hints ); + } + + /** + * Returns whether entities related to the primary + * entities are fetched by this fetch spec. If true, all relationships + * whose destinations meet the qualifier criteria will be returned + * in addition to primary results. If false, only the primary entities + * will be returned. Default is true. + */ + public boolean isDeep() + { + return deep; + } + + /** + * Returns whether this data source should lock objects that + * are fetched. Default is false. + */ + public boolean locksObjects() + { + return locksObjects; + } + + /*** + * Returns a List of relationships for the fetched objects that + * should also be fetched, or null if no such list has been specified. + * Use this to avoid additional calls to the server to fetch + * relationships that you know you will use. + * NOTE: wotonomy allows you to specify non-relational keys + * as well. + */ + public NSArray prefetchingRelationshipKeyPaths() + { + return prefetchingRelationshipKeyPaths; + } + + /** + * Returns whether the user should be prompted to continue + * when the fetch limit has been exceeded. + * Default is false. + */ + public boolean promptsAfterFetchLimit() + { + return promptsAfterFetchLimit; + } + + /** + * Returns the qualifier used by this fetch specification, + * or null if none has been specified. + */ + public EOQualifier qualifier() + { + return qualifier; + } + + /** + * Returns a List of keys or key paths for which + * values should be returned when fetching raw rows, + * or null if no raw row key paths have been specified. + */ + public NSArray rawRowKeyPaths() + { + return rawRowKeyPaths; + } + + /** + * Returns whether fetched objects should replace + * modified versions already fetched into an editing context. + * If true, those changes will be lost. + * Default is false. + */ + public boolean refreshesRefetchedObjects() + { + return refreshesRefetchedObjects; + } + + /** + * Returns whether all qualifier bindings must be specified + * in order to fetch. If true, an exception is thrown if + * unspecified bindings exist. If false, unspecified bindings + * will be removed from the qualifier. Default is false. + */ + public boolean requiresAllQualifierBindingVariables() + { + return requiresAllQualifierBindingVariables; + } + + /** + * Sets the name of the entity fetched by this spec. + */ + public void setEntityName(String aName) + { + entityName = aName; + } + + /** + * Sets whether this fetch spec will return raw rows. + */ + public void setFetchesRawRows(boolean shouldFetchRawRows) + { + fetchesRawRows = shouldFetchRawRows; + } + + /** + * Sets the limit on the number of records returned for this fetch spec. + * Zero indicates no limit on fetches. + */ + public void setFetchLimit(int aLimit) + { + fetchLimit = aLimit; + } + + /** + * Sets the hints passed by this fetch spec. + */ + public void setHints(Map aHintMap) + { + if ( aHintMap == null ) + { + hints = null; + } + else + { + hints = new NSDictionary( (Map) aHintMap ); + } + } + + /** + * Sets whether this fetch specification fetches deeply. + */ + public void setIsDeep(boolean isDeep) + { + deep = isDeep; + } + + /** + * Sets whether this fetch spec locks objects that + * are returned by the fetch. + */ + public void setLocksObjects(boolean shouldLockObjects) + { + locksObjects = shouldLockObjects; + } + + /** + * Sets the prefetch key paths that should be used as an optimization + * hint to the server. NOTE: wotonomy allows you to specify non-relationship + * keys as well. + */ + public void setPrefetchingRelationshipKeyPaths(List aKeyPathList) + { + if ( aKeyPathList == null ) + { + prefetchingRelationshipKeyPaths = null; + } + else + { + prefetchingRelationshipKeyPaths = new NSArray( (List) aKeyPathList ); + } + } + + /** + * Sets whether the user should be prompted when the fetch limit has been + * reached. + */ + public void setPromptsAfterFetchLimit(boolean shouldPrompt) + { + promptsAfterFetchLimit = shouldPrompt; + } + + /** + * Sets the qualifier used by this fetch specification. + */ + public void setQualifier(EOQualifier aQualifier) + { + qualifier = aQualifier; + } + + /** + * Sets the key paths to be returned if this fetch spec + * is returning raw rows. + */ + public void setRawRowKeyPaths(List aKeyPathList) + { + if ( aKeyPathList == null ) + { + rawRowKeyPaths = null; + } + else + { + rawRowKeyPaths = new NSArray( (List) aKeyPathList ); + } + } + + /** + * Sets whether modified objects in an editing context should + * be replaced by newer versions returned by this fetch spec. + */ + public void setRefreshesRefetchedObjects(boolean shouldRefresh) + { + refreshesRefetchedObjects = shouldRefresh; + } + + /** + * Sets whether this fetch spec should require all bindings to be + * resolved before executing. + */ + public void setRequiresAllQualifierBindingVariables(boolean shouldRequireAll) + { + requiresAllQualifierBindingVariables = shouldRequireAll; + } + + /** + * Sets the sort orderings used by this fetch spec. + */ + public void setSortOrderings(List aSortList) + { + if ( aSortList == null ) + { + sortOrderings = null; + } + else + { + sortOrderings = new NSArray( (List) aSortList ); + } + } + + /** + * Sets whether this fetch spec should return only distinct + * objects. + */ + public void setUsesDistinct(boolean shouldUseDistinct) + { + distinct = shouldUseDistinct; + } + + /** + * Returns a List of the sort orderings used by this fetch spec, + * or null if none have been specified. + */ + public NSArray sortOrderings() + { + return sortOrderings; + } + + /** + * Returns a string representation of this fetch specification. + */ + public String toString() + { + return "[FetchSpecification:qualifier=("+qualifier+"),sortOrderings="+sortOrderings+"]"; + } + + /** + * Returns whether this fetch specification will return only one + * reference to each distinct object returned by the fetch. + * Default is false. + */ + public boolean usesDistinct() + { + return distinct; + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOFetchSpecification", "class"); + arch.encodeObject(entityName(), "entityName"); + arch.encodeInt(fetchLimit(), "fetchLimit"); + + //flags + if (isDeep()) + arch.encodeObject("YES", "isDeep"); + arch.encodeObject(qualifier(), "qualifier"); + if (refreshesRefetchedObjects()) + arch.encodeObject("YES", "refreshesRefetchedObjects"); + if (locksObjects()) + arch.encodeObject("YES", "locksObjects"); + if (fetchesRawRows()) + arch.encodeObject("YES", "fetchesRawRows"); + if (promptsAfterFetchLimit()) + arch.encodeObject("YES", "promptsAfterFetchLimit"); + if (requiresAllQualifierBindingVariables()) + arch.encodeObject("YES", "requiresAllQualifierBindingVariables"); + if (usesDistinct()) + arch.encodeObject("YES", "usesDistinct"); + + //encode arrays + if (sortOrderings() != null) { + NSMutableArray arr = new NSMutableArray(sortOrderings().count()); + for (int i = 0; i < sortOrderings.count(); i++) { + EOSortOrdering so = (EOSortOrdering)sortOrderings().objectAtIndex(i); + EOKeyValueArchiver ar2 = new EOKeyValueArchiver(); + so.encodeWithKeyValueArchiver(ar2); + arr.addObject(ar2.dictionary()); + } + arch.encodeObject(arr, "sortOrderings"); + } + if (rawRowKeyPaths != null && rawRowKeyPaths.count() > 0) + arch.encodeObject(rawRowKeyPaths, "rawRowKeyPaths"); + if (prefetchingRelationshipKeyPaths != null && prefetchingRelationshipKeyPaths.count() > 0) + arch.encodeObject(rawRowKeyPaths, "prefetchingRelationshipKeyPaths"); + if (hints != null && hints.count() > 0) + arch.encodeObject(hints, "hints"); + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver unarch) { + EOFetchSpecification fs = new EOFetchSpecification(); + fs.setEntityName((String)unarch.decodeObjectForKey("entityName")); + fs.setFetchLimit(unarch.decodeIntForKey("fetchLimit")); + fs.setIsDeep(unarch.decodeBoolForKey("isDeep")); + fs.setRefreshesRefetchedObjects(unarch.decodeBoolForKey("refreshesRefetchedObjects")); + + //Sort orderings + NSArray arr = (NSArray)unarch.decodeObjectForKey("sortOrderings"); + if (arr != null && arr.count() > 0) { + NSMutableArray orderings = new NSMutableArray(arr.count()); + for (int i = 0; i < arr.count(); i++) { + NSDictionary so = (NSDictionary)arr.objectAtIndex(i); + String selname = (String)so.objectForKey("selectorName"); + NSSelector selector = EOSortOrdering.CompareAscending; + if (selname.startsWith("compareDescending")) + selector = EOSortOrdering.CompareDescending; + else if (selname.startsWith("compareCaseInsensitiveAscending")) + selector = EOSortOrdering.CompareCaseInsensitiveAscending; + else if (selname.startsWith("compareCaseInsensitiveDescending")) + selector = EOSortOrdering.CompareCaseInsensitiveDescending; + EOSortOrdering eoso = new EOSortOrdering((String)so.objectForKey("key"), selector); + orderings.addObject(eoso); + } + fs.setSortOrderings(orderings); + } + //raw rows + arr = (NSArray)unarch.decodeObjectForKey("rawRowKeyPaths"); + if (arr != null && arr.count() > 0) { + fs.setFetchesRawRows(true); + fs.setRawRowKeyPaths(arr); + } + //qualifier + fs.setQualifier((EOQualifier)unarch.decodeObjectForKey("qualifier")); + return fs; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/11 18:19:01 chochos + * encoding/decoding with EOKeyValueArchiving now works properly + * + * Revision 1.3 2003/08/09 01:22:20 chochos + * implements EOKeyValueArchiving (and unarchiving) + * + * Revision 1.2 2001/11/24 17:32:57 mpowers + * We now have a real implementation. + * + * Revision 1.1 2001/02/05 03:45:37 mpowers + * Starting work on EOEditingContext. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGenericRecord.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGenericRecord.java new file mode 100644 index 0000000..4358ecf --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGenericRecord.java @@ -0,0 +1,151 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNull; + +/** +* EOGenericRecord extends EOCustomObject to provide a +* general-purpose implementation, relying entirely on the +* class description for entity-specific behavior, and +* storing data in a dictionary. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOGenericRecord extends EOCustomObject +{ + private EOClassDescription classDescription; + private NSMutableDictionary dataDictionary; + + /** + * Default constructor. + */ + protected EOGenericRecord() + { + classDescription = null; + dataDictionary = new NSMutableDictionary(); + } + + /** + * Preferred constructor. + */ + public EOGenericRecord( EOClassDescription aDescription ) + { + this(); + classDescription = aDescription; + } + + /** + * Overridden to return true so that deferred faults are used. + */ + public static boolean usesDeferredFaultCreation() + { + return true; + } + + /** + * Compatibility constructor: aContext and anID are ignored. + */ + public EOGenericRecord( + EOEditingContext aContext, EOClassDescription aDescription, EOGlobalID anID ) + { + this( aDescription ); + } + + /** + * Returns a class description for this object. + * Overridden to return the class description passed + * into the constructor. + */ + public EOClassDescription classDescription() + { + return classDescription; + } + + // interface EOKeyValueCoding + + /** + * Calls storedValueForKey. + */ + public Object valueForKey( String aKey ) + { + return storedValueForKey( aKey ); + } + + /** + * Calls willChange and then calls takeStoredValueForKey. + */ + public void takeValueForKey( Object aValue, String aKey ) + { + willChange(); + takeStoredValueForKey( aValue, aKey ); + } + + /** + * Calls willRead for the specified key, + * and returns the corresponding value from + * the data dictionary. Keys that do not + * exist will return null. + */ + public Object storedValueForKey( String aKey ) + { + willRead( aKey ); + Object result = dataDictionary.objectForKey( aKey ); + if ( NSNull.nullValue().equals( result ) ) result = null; + return result; + } + + /** + * Writes the specified value into the data dictionary + * for the specified key. Nulls are stored as NSNulls + * in the dictionary. + * No checking is performed to determine whether the + * key is a valid attribute key. + */ + public void takeStoredValueForKey( Object aValue, String aKey ) + { + if ( aValue == null ) aValue = NSNull.nullValue(); + dataDictionary.setObjectForKey( aValue, aKey ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/11/18 18:57:10 mpowers + * Implemented EOGenericRecord. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGlobalID.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGlobalID.java new file mode 100644 index 0000000..1b4ccbe --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOGlobalID.java @@ -0,0 +1,83 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.io.Serializable; + +/** +* A pure java implementation of EOGlobalID. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public abstract class EOGlobalID implements Cloneable, Serializable +{ + /** + * ObjectStores broadcast this notification when they + * replace a temporary global id with a permanent one. + * EditingContexts listen for these notifications to + * update their mapping of global ids to objects. + * The object of the notification is null, and the user + * info contains a mapping of the old temporary ids to + * the new permanent ids. + */ + public static final String GlobalIDChangedNotification + = "GlobalIDChangedNotification"; + + /** + * Returns whether this id is a temporary id. + * Temporary ids are generated for newly created + * objects that have not been persisted. When + * persisted, the temporary id is discarded in favor + * of the one generated by the object store. + */ + public abstract boolean isTemporary(); + + /** + * Returns a copy of this object. + * This implementation calls super.clone(). + */ + public Object clone() throws CloneNotSupportedException + { + return super.clone(); + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.3 2001/03/15 21:10:26 mpowers + * Implemented global id re-registration for newly saved inserts. + * + * Revision 1.2 2001/02/15 21:13:30 mpowers + * First draft implementation is complete. Now on to debugging. + * + * Revision 1.1 2001/02/05 03:45:37 mpowers + * Starting work on EOEditingContext. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOIntegralKeyGlobalID.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOIntegralKeyGlobalID.java new file mode 100644 index 0000000..ee7aac1 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOIntegralKeyGlobalID.java @@ -0,0 +1,72 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.control; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOIntegralKeyGlobalID extends EOKeyGlobalID { + + protected Number keyValue; + + public EOIntegralKeyGlobalID(String entityName, Number value) { + super(entityName, 0); + keyValue = value; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#keyValues() + */ + public Object[] keyValues() { + return new Object[]{ keyValue }; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#_keyValuesNoCopy() + */ + public Object[] _keyValuesNoCopy() { + return new Object[]{ keyValue }; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#keyCount() + */ + public int keyCount() { + return 1; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOGlobalID#isTemporary() + */ + public boolean isTemporary() { + return false; + } + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/19 01:59:01 chochos + * Added the wotonomy headers + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyComparisonQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyComparisonQualifier.java new file mode 100644 index 0000000..97baaab --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyComparisonQualifier.java @@ -0,0 +1,151 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.control; + +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOKeyComparisonQualifier + extends EOQualifier + implements EOKeyValueArchiving, EOQualifierEvaluation { + + private String _leftKey; + private String _rightKey; + private NSSelector _selector; + + public EOKeyComparisonQualifier(String leftKey, NSSelector selector, String rightKey) { + super(); + _leftKey = leftKey; + _rightKey = rightKey; + _selector = selector; + } + + public String leftKey() { + return _leftKey; + } + + public String rightKey() { + return _rightKey; + } + + public NSSelector selector() { + return _selector; + } + + /** + * Evaluates this qualifier for the specified object, + * and returns whether the object is qualified. + * selector() is invoked on the value for key() on the + * specified object, with value() as the parameter. + */ + public boolean evaluateWithObject(Object eo) { + try { + Object lvalue, rvalue; + if ( eo instanceof EOKeyValueCoding) { + lvalue = ((EOKeyValueCoding)eo).valueForKey(leftKey()); + rvalue = ((EOKeyValueCoding)eo).valueForKey(rightKey()); + } else { + lvalue = EOKeyValueCodingSupport.valueForKey(eo, leftKey()); + rvalue = EOKeyValueCodingSupport.valueForKey(eo, rightKey()); + } + return ((Boolean)_selector.invoke( lvalue, rvalue)).booleanValue(); + } catch (Exception exc) { + throw new WotonomyException( exc ); + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyValueArchiving#encodeWithKeyValueArchiver(net.wotonomy.control.EOKeyValueArchiver) + */ + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOKeyComparisonQualifier", "class"); + arch.encodeObject(_leftKey, "key"); + NSMutableDictionary d = new NSMutableDictionary(2); + arch.encodeObject(_rightKey, "value"); + String selname = null; + if (_selector.equals(EOQualifier.QualifierOperatorCaseInsensitiveLike)) + selname = "caseInsensitiveLike:"; + else if (_selector.equals(EOQualifier.QualifierOperatorContains)) + selname = "contains:"; + else if (_selector.equals(EOQualifier.QualifierOperatorEqual)) + selname = "isEqualTo:"; + else if (_selector.equals(EOQualifier.QualifierOperatorGreaterThan)) + selname = "greaterThan:"; + else if (_selector.equals(EOQualifier.QualifierOperatorGreaterThanOrEqualTo)) + selname = "greaterThanOrEqualTo:"; + else if (_selector.equals(EOQualifier.QualifierOperatorLessThan)) + selname = "lessThan:"; + else if (_selector.equals(EOQualifier.QualifierOperatorLessThanOrEqualTo)) + selname = "lessThanOrEqualTo:"; + else if (_selector.equals(EOQualifier.QualifierOperatorLike)) + selname = "like:"; + else if (_selector.equals(EOQualifier.QualifierOperatorNotEqual)) + selname = "isNotEqualTo:"; + else + selname = _selector.name() + ":"; + arch.encodeObject(selname, "selectorName"); + } + + public static Object decodeWithKeyValueArchiver(EOKeyValueUnarchiver arch) { + String k = (String)arch.decodeObjectForKey("key"); + String v = (String)arch.decodeObjectForKey("value"); + NSSelector sel = null; + String sname = (String)arch.decodeObjectForKey("selectorName"); + if (sname.equals("isEqualTo:")) + sel = EOQualifier.QualifierOperatorEqual; + else if (sname.equals("isNotEqualTo:")) + sel = EOQualifier.QualifierOperatorNotEqual; + else if (sname.equals("caseInsensitiveLike:")) + sel = EOQualifier.QualifierOperatorCaseInsensitiveLike; + else if (sname.equals("contains:")) + sel = EOQualifier.QualifierOperatorContains; + else if (sname.equals("greaterThan:")) + sel = EOQualifier.QualifierOperatorGreaterThan; + else if (sname.equals("greaterThanOrEqualTo:")) + sel = EOQualifier.QualifierOperatorGreaterThanOrEqualTo; + else if (sname.equals("lessThan:")) + sel = EOQualifier.QualifierOperatorLessThan; + else if (sname.equals("lessThanOrEqualTo:")) + sel = EOQualifier.QualifierOperatorLessThanOrEqualTo; + else if (sname.equals("like:")) + sel = EOQualifier.QualifierOperatorLike; + EOKeyComparisonQualifier q = new EOKeyComparisonQualifier(k, sel, v); + return q; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/12 01:42:36 chochos + * this qualifier was missing + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyGlobalID.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyGlobalID.java new file mode 100644 index 0000000..76c4d05 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyGlobalID.java @@ -0,0 +1,146 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: EOKeyGlobalID.java 894 2006-02-16 16:47:14Z cgruber $ + +*/ + +package net.wotonomy.control; + +import java.io.ObjectStreamException; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSCoder; +import net.wotonomy.foundation.NSCoding; + +/** +* The model object which represents the mapping between database +* fields and object properties, lists available entities, lists +* connection dictionaries, and has fetch specifications. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public abstract class EOKeyGlobalID extends EOGlobalID + implements NSCoding { + + protected String _entityName; + protected int _hash; + + protected EOKeyGlobalID(String entityName, int hashCode) { + super(); + _entityName = entityName; + _hash = hashCode; + } + + protected void _prepClone(EOKeyGlobalID gid) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static EOKeyGlobalID globalIDWithEntityName(String s, Object aobj[]) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static EOKeyGlobalID _defaultGlobalIDWithEntityName(String s, Object aobj[]) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String entityName() { + return _entityName; + } + + public String _literalEntityName() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean _isFinal() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void _setGuessedEntityName(String s) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String _guessedEntityName() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void _setSubEntityName(String s) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public String _subEntityName() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public int hashCode() { + return _hash; + } + + public abstract Object[] keyValues(); + + public abstract Object[] _keyValuesNoCopy(); + + public abstract int keyCount(); + + public NSArray keyValuesArray() { + return new NSArray(keyValues()); + } + + public static Object decodeObject(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static EOKeyGlobalID _adjustForInheritance(EOKeyGlobalID eokeyglobalid, String s, String s1) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void encodeWithCoder(NSCoder nscoder) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public Class classForCoder() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + protected Object readResolve() throws ObjectStreamException { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/19 01:50:54 chochos + * added two concrete implementations of EOKeyGlobalID. + * + * Revision 1.1 2002/07/14 21:59:06 mpowers + * Contributions from cgruber. + * + * Revision 1.1 2002/06/25 00:11:09 cgruber + * Add EOKeyGlobalID, but it won't work without NSCoder. + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiver.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiver.java new file mode 100644 index 0000000..3e2f808 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiver.java @@ -0,0 +1,106 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.control; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; + +/** +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOKeyValueArchiver { + + //for the delegate + public static final NSSelector sel = new NSSelector("referenceToEncodeForObject", + new Class[]{ Object.class }); + NSMutableDictionary dict = new NSMutableDictionary(); + private Object _delegate; + + public EOKeyValueArchiver() { + super(); + } + + public void encodeBool(boolean flag, String key) { + String v = flag ? "true" : "false"; + dict.setObjectForKey(v, key); + } + + public void encodeInt(int value, String key) { + dict.setObjectForKey(new Integer(value), key); + } + + public void encodeObject(Object obj, String key) { + if (obj == null) + return; + if (obj instanceof NSArray || obj instanceof NSDictionary) { + dict.setObjectForKey(obj, key); + return; + } + if (obj instanceof EOKeyValueArchiving) { + EOKeyValueArchiver arch = new EOKeyValueArchiver(); + ((EOKeyValueArchiving)obj).encodeWithKeyValueArchiver(arch); + dict.setObjectForKey(arch.dictionary(), key); + return; + } + if (obj instanceof String) + dict.setObjectForKey(obj, key); + } + + public void encodeReferenceToObject(Object obj, String key) { + if (_delegate != null) { + if (sel.implementedByObject(obj)) { + try { + Object ref = sel.invoke(_delegate, obj); + if (ref != null) + dict.setObjectForKey(ref, key); + } catch (Exception e) { + } + } + } + } + + public NSDictionary dictionary() { + return new NSDictionary(dict); + } + + public void setDelegate(Object delegate) { + _delegate = delegate; + } + public Object delegate() { + return _delegate; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/09 01:17:53 chochos + * Part of the EOKeyValueArchiving protocol (archives objects into NSDictionaries and vice-versa) + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiving.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiving.java new file mode 100644 index 0000000..0d9f78c --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueArchiving.java @@ -0,0 +1,41 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.control; + +/** + * Remember that this method is also part of the interface: + * public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver unarchiver) + * +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOKeyValueArchiving { + + public abstract void encodeWithKeyValueArchiver(EOKeyValueArchiver archiver); + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/09 01:17:53 chochos + * Part of the EOKeyValueArchiving protocol (archives objects into NSDictionaries and vice-versa) + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCoding.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCoding.java new file mode 100644 index 0000000..b3cf926 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCoding.java @@ -0,0 +1,129 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSKeyValueCoding; + +/** +* EOKeyValueCoding defines an interface for classes that +* need to have more control over the wotonomy's property +* introspection facilities.

+* +* On an object that implements this interface, wotonomy +* will call these methods, and otherwise use the static +* methods on EOKeyValueCodingSupport.

+* +* EOKeyValueCodingSupport implements the default behaviors +* for each of these methods, so classes implementing this +* interface can call those methods to acheive the same +* behavior.

+* +* valueForKey and takeValueForKey are called in response +* to user actions, like viewing an object or updating its +* value in a user interface. These should call the public +* getter and setter methods on the object itself and the +* operations should be subject to validation.

+* +* storedValueForKey and takeStoredValueForKey are called +* in response to wotonomy actions, like snapshotting, +* faulting, commits, and reverts. These operations should +* bypass the public methods and directly modify the internal +* state of the object without validation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOKeyValueCoding extends NSKeyValueCoding +{ + /** + * Returns the value for the specified property. + * If the property does not exist, this method should + * call handleQueryWithUnboundKey. + */ + Object valueForKey( String aKey ); + + /** + * Sets the property to the specified value. + * If the property does not exist, this method should + * call handleTakeValueForUnboundKey. + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey. + */ + void takeValueForKey( Object aValue, String aKey ); + + /** + * Returns the value for the private field that + * corresponds to the specified property. + */ + Object storedValueForKey( String aKey ); + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value. + */ + void takeStoredValueForKey( Object aValue, String aKey ); + + /** + * Called by valueForKey when the specified key is + * not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + Object handleQueryWithUnboundKey( String aKey ); + + /** + * Called by takeValueForKey when the specified key + * is not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + void handleTakeValueForUnboundKey( Object aValue, String aKey ); + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types. Implementing + * classes should handle this case appropriately + * or otherwise throw an exception. + */ + void unableToSetNullForKey( String aKey ); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.2 2001/03/28 16:12:30 mpowers + * Documented interface. + * + * Revision 1.1 2001/03/27 23:25:05 mpowers + * Contributing interface, no docs yet. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingAdditions.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingAdditions.java new file mode 100644 index 0000000..bc48f58 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingAdditions.java @@ -0,0 +1,176 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCodingAdditions; + +/** +* EOKeyValueCodingAdditions defines an interface for classes +* that need to have more control over the wotonomy's bulk +* property copying and cloning facilities.

+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public interface EOKeyValueCodingAdditions extends EOKeyValueCoding, NSKeyValueCodingAdditions +{ + + /** + * Static utilities methods that + * call the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public class Utility extends NSKeyValueCodingAdditions.Utility + { + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static void takeValuesFromDictionary( + Object object, Map dictionary) + { + if (object instanceof NSKeyValueCodingAdditions) { + ((NSKeyValueCodingAdditions)object).takeValuesFromDictionary(dictionary); + } else { + DefaultImplementation.takeValuesFromDictionary(object, dictionary); + } + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ +/* + public static void takeValuesFromDictionaryWithMapping( + Object object, NSDictionary dictionary, NSDictionary mapping) + { + if (object instanceof NSKeyValueCodingAdditions) { + ((NSKeyValueCodingAdditions)object).takeValuesFromDictionaryWithMapping(dictionary, mapping); + } else { + DefaultImplementation.takeValuesFromDictionaryWithMapping(object, dictionary, mapping); + } + } +*/ + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ + public static NSDictionary valuesForKeys( + Object object, List keys) + { + if (object instanceof NSKeyValueCodingAdditions) { + return ((NSKeyValueCodingAdditions)object).valuesForKeys(keys); + } else { + return DefaultImplementation.valuesForKeys(object, keys); + } + } + + /** + * Calls the appropriate method if the object implements + * NSKeyValueCodingAdditions, otherwise calls the method + * on DefaultImplementation. + */ +/* + public static NSDictionary valuesForKeysWithMapping( + Object object, NSDictionary mapping) + { + if (object instanceof NSKeyValueCodingAdditions) { + return ((NSKeyValueCodingAdditions)object).valuesForKeysWithMapping(mapping); + } else { + return DefaultImplementation.valuesForKeysWithMapping(object, mapping); + } + } +*/ + } + + /** + * Provides a reflection-based implementation for classes that + * don't implement NSKeyValueCodingAdditions. + */ + public class DefaultImplementation extends NSKeyValueCodingAdditions.DefaultImplementation + { + public static void takeValuesFromDictionary( + Object object, NSDictionary dictionary) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public static void takeValuesFromDictionaryWithMapping( + Object object, NSDictionary dictionary, NSDictionary mapping) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public static NSDictionary valuesForKeys( + Object object, List keys) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public static NSDictionary valuesForKeysWithMapping( + Object object, Map mapping) + { + throw new RuntimeException( "Not implemented yet." ); + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.3 2001/12/10 15:25:11 mpowers + * Now properly extending EOKeyValueCoding. + * + * Revision 1.2 2001/04/28 14:12:23 mpowers + * Refactored cloning/copying into KeyValueCodingUtilities. + * + * Revision 1.1 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.2 2001/03/28 16:12:30 mpowers + * Documented interface. + * + * Revision 1.1 2001/03/27 23:25:05 mpowers + * Contributing interface, no docs yet. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingSupport.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingSupport.java new file mode 100644 index 0000000..89e3e91 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueCodingSupport.java @@ -0,0 +1,235 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.IntrospectorException; +import net.wotonomy.foundation.internal.MissingPropertyException; +import net.wotonomy.foundation.internal.NullPrimitiveException; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOKeyValueCodingSupport defines default behavior for +* classes implementing EOKeyValueSupport.

+* +* On an object that does not implement EOKeyValueCoding, +* wotonomy will call the methods on this class directly. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOKeyValueCodingSupport +{ + /** + * Returns the value for the specified property key + * on the specified object.

+ * + * If the property does not exist, this method calls + * handleQueryWithUnboundKey on the object if it + * implements EOKeyValueCoding, otherwise calls + * handleQueryWithUnboundKey on this class.

+ */ + static public Object valueForKey( + Object anObject, String aKey ) + { + //TODO: may need to handle "." nesting here so + // that handleQueryWithUnboundKey gets called for + // for the nested object, not the parent object + + //Correction: need to handle key paths in + // KeyValueCodingAdditionsSupport. + + try + { + return Introspector.get( anObject, aKey ); + } + catch ( IntrospectorException exc ) + { + if ( anObject instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)anObject).handleQueryWithUnboundKey( aKey ); + } + return handleQueryWithUnboundKey( anObject, aKey ); + } + } + + /** + * Sets the property to the specified value on + * the specified object. + * + * If the property does not exist, this method calls + * handleTakeValueForUnboundKey on the object if it + * implements EOKeyValueCoding, otherwise calls + * handleTakeValueForUnboundKey on this class. + * + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey + * on the object if it implements EOKeyValueCoding, + * otherwise calls unableToSetNullForKey on this class. + */ + static public void takeValueForKey( + Object anObject, Object aValue, String aKey ) + { + //TODO: may need to handle "." nesting here so + // that handleTakeValueForUnboundKey gets called for + // for the nested object, not the parent object + + try + { + Introspector.set( anObject, aKey, aValue ); + } + catch ( NullPrimitiveException exc ) + { + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).unableToSetNullForKey( aKey ); + } + else + { + unableToSetNullForKey( anObject, aKey ); + } + } + catch ( MissingPropertyException exc ) + { + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).handleTakeValueForUnboundKey( + aValue, aKey ); + } + else + { + handleTakeValueForUnboundKey( anObject, aValue, aKey ); + } + } + + } + + /** + * Returns the value for the private field that + * corresponds to the specified property on + * the specified object. + * + * This implementation currently calls valueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public Object storedValueForKey( + Object anObject, String aKey ) + { + //TODO: this currently just calls valueForKey + return valueForKey( anObject, aKey ); + } + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value on the + * specified object. + * + * This implementation currently calls takeValueForKey, + * because java security currently prevents us from + * accessing the fields of another object. + */ + static public void takeStoredValueForKey( + Object anObject, Object aValue, String aKey ) + { + //TODO: this currently just calls takeValueForKey + takeValueForKey( anObject, aValue, aKey ); + } + + /** + * Called by valueForKey when the specified key is + * not found on the specified object, if that object + * does not implement EOKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public Object handleQueryWithUnboundKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Key not found for object: " + + aKey + " : " + anObject ); + } + + /** + * Called by takeValueForKey when the specified key + * is not found on the specified object, if that object + * does not implement EOKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void handleTakeValueForUnboundKey( + Object anObject, Object aValue, String aKey ) + { + throw new WotonomyException( + "Key not found for object while setting value: " + + aKey + " : " + anObject + " : " + aValue ); + } + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types, if the specified + * object does not implement EOKeyValueCoding. + * + * This implementation throws a WotonomyException. + */ + static public void unableToSetNullForKey( + Object anObject, String aKey ) + { + throw new WotonomyException( + "Tried to key on object to null: " + + aKey + " : " + anObject ); + } + +} + + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.4 2001/05/18 21:04:33 mpowers + * Reimplemented EditingContext.initializeObject. + * + * Revision 1.3 2001/04/27 00:28:29 mpowers + * Fixed a return value. + * + * Revision 1.2 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.1 2001/03/28 17:49:33 mpowers + * Implemented EOKeyValueCodingSupport. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueQualifier.java new file mode 100644 index 0000000..799955e --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueQualifier.java @@ -0,0 +1,233 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOKeyValueQualifier performs a property-based +* comparison against a specified value. The comparison +* is specified in the form of a NSSelector. The +* selector is expected to take two arguments, the +* property value on an object and the comparison value, +* and return a Boolean indicating whether the object +* is qualified. EOQualifier defines selectors that +* may be used in creating EOKeyValueQualifiers. +* +* @author michael@mpowers.net +* @author yjcheung@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOKeyValueQualifier extends EOQualifier + implements EOKeyValueArchiving, EOQualifierEvaluation +{ + private String key; + private NSSelector selector; + private Object value; + + /** + * Constructor specifying a property key, a selector, + * and a value for comparison. The selector may be + * one of the constant selectors defined on EOQualifier. + */ + public EOKeyValueQualifier( + String aKey, + NSSelector aSelector, + Object aValue ) + { + key = aKey; + selector = aSelector; + value = aValue; + } + + /** + * Returns the key for this qualifier. + */ + public String key() + { + return key; + } + + /** + * Returns the selector for this qualifier. + */ + public NSSelector selector() + { + return selector; + } + + /** + * Returns the value for this qualifier. + */ + public Object value() + { + return value; + } + + /** + * Evaluates this qualifier for the specified object, + * and returns whether the object is qualified. + * selector() is invoked on the value for key() on the + * specified object, with value() as the parameter. + */ + public boolean evaluateWithObject( Object anObject ) + { + try + { + Object value; + if ( anObject instanceof EOKeyValueCoding ) + { + value = ((EOKeyValueCoding)anObject).valueForKey( key() ); + } + else + { + value = EOKeyValueCodingSupport.valueForKey( anObject, key() ); + } + return ((Boolean)selector.invoke( value(), value)).booleanValue(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + /** + * Returns a string representation of this qualifier. + */ + public String toString() + { + return "( " + key + " " + selector + " " + value + " )"; + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOKeyValueQualifier", "class"); + arch.encodeObject(key, "key"); + NSMutableDictionary d = new NSMutableDictionary(2); + if (value instanceof String) + d.setObjectForKey("NSString", "class"); + else if (value instanceof java.math.BigDecimal) + d.setObjectForKey("NSDecimalNumber", "class"); + else if (value instanceof Number) + d.setObjectForKey("NSNumber", "class"); + else if (value instanceof NSKeyValueCoding.Null) + d.setObjectForKey("EONull", "class"); + if (value != null && !(value instanceof NSKeyValueCoding.Null)) + d.setObjectForKey(value.toString(), "value"); + arch.encodeObject(d, "value"); + String selname = null; + if (selector.equals(EOQualifier.QualifierOperatorCaseInsensitiveLike)) + selname = "caseInsensitiveLike:"; + else if (selector.equals(EOQualifier.QualifierOperatorContains)) + selname = "contains:"; + else if (selector.equals(EOQualifier.QualifierOperatorEqual)) + selname = "isEqualTo:"; + else if (selector.equals(EOQualifier.QualifierOperatorGreaterThan)) + selname = "greaterThan:"; + else if (selector.equals(EOQualifier.QualifierOperatorGreaterThanOrEqualTo)) + selname = "greaterThanOrEqualTo:"; + else if (selector.equals(EOQualifier.QualifierOperatorLessThan)) + selname = "lessThan:"; + else if (selector.equals(EOQualifier.QualifierOperatorLessThanOrEqualTo)) + selname = "lessThanOrEqualTo:"; + else if (selector.equals(EOQualifier.QualifierOperatorLike)) + selname = "like:"; + else if (selector.equals(EOQualifier.QualifierOperatorNotEqual)) + selname = "isNotEqualTo:"; + else + selname = selector.name() + ":"; + arch.encodeObject(selname, "selectorName"); + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver arch) { + String k = (String)arch.decodeObjectForKey("key"); + Object v = arch.decodeObjectForKey("value"); + NSSelector sel = null; + String sname = (String)arch.decodeObjectForKey("selectorName"); + if (sname.equals("isEqualTo:")) + sel = EOQualifier.QualifierOperatorEqual; + else if (sname.equals("isNotEqualTo:")) + sel = EOQualifier.QualifierOperatorNotEqual; + else if (sname.equals("caseInsensitiveLike:")) + sel = EOQualifier.QualifierOperatorCaseInsensitiveLike; + else if (sname.equals("contains:")) + sel = EOQualifier.QualifierOperatorContains; + else if (sname.equals("greaterThan:")) + sel = EOQualifier.QualifierOperatorGreaterThan; + else if (sname.equals("greaterThanOrEqualTo:")) + sel = EOQualifier.QualifierOperatorGreaterThanOrEqualTo; + else if (sname.equals("lessThan:")) + sel = EOQualifier.QualifierOperatorLessThan; + else if (sname.equals("lessThanOrEqualTo:")) + sel = EOQualifier.QualifierOperatorLessThanOrEqualTo; + else if (sname.equals("like:")) + sel = EOQualifier.QualifierOperatorLike; + EOKeyValueQualifier q = new EOKeyValueQualifier(k, sel, v); + return q; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.10 2003/08/12 01:43:04 chochos + * formally implement EOQualifierEvaluation + * + * Revision 1.9 2003/08/11 19:39:30 chochos + * special conditions for NSKeyValueCoding.NullValue -> EONull + * + * Revision 1.8 2003/08/09 01:22:51 chochos + * qualifiers implement EOKeyValueArchiving + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2001/10/31 15:26:06 mpowers + * Fixed typo. + * + * Revision 1.5 2001/10/31 15:25:14 mpowers + * Cleanup of qualifiers. + * + * Revision 1.4 2001/10/30 22:57:28 mpowers + * EOQualifier framework is now working. + * + * Revision 1.3 2001/09/13 15:25:56 mpowers + * Started implementation of the EOQualifier framework. + * + * Revision 1.2 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.1 2001/02/27 03:33:04 mpowers + * Initial draft of the key-value qualifier. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueUnarchiver.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueUnarchiver.java new file mode 100644 index 0000000..caed8a4 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOKeyValueUnarchiver.java @@ -0,0 +1,165 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.control; + +import java.lang.reflect.Method; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCoding; + +/** Creates objects from dictionaries that were created with + * EOKeyValueArchiver. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOKeyValueUnarchiver { + + NSDictionary dict; + Object _delegate; + protected static final Class[] METHOD_ARGS = new Class[]{ EOKeyValueUnarchiver.class }; + + public EOKeyValueUnarchiver(NSDictionary archive) { + super(); + dict = archive; + } + + public void awakeObjects() { + } + + public boolean decodeBoolForKey(String key) { + Object x = dict.objectForKey(key); + if (x == null) + return false; + return (x.equals("true") || x.equals("YES") || x.equals("Y")); + } + + public int decodeIntForKey(String key) { + Object x = dict.objectForKey(key); + if (x == null) + return 0; + if (x instanceof Number) + return ((Number)x).intValue(); + try { + int i = Integer.parseInt(x.toString()); + return i; + } catch (NumberFormatException ex) { + return 0; + } + } + + public Object decodeObjectForKey(String key) { + Object x = dict.objectForKey(key); + if (x == null) + return null; + if (x instanceof NSDictionary) { + NSDictionary d = (NSDictionary)x; + if (d.objectForKey("class") != null) { + String cname = d.objectForKey("class").toString(); + Class _class = null; + if (cname.equals("NSNumber")) { + if (d.objectForKey("value") != null) + return new Long(d.objectForKey("value").toString()); + } else if (cname.equals("NSDecimalNumber")) { + if (d.objectForKey("value") != null) + return new java.math.BigDecimal(d.objectForKey("value").toString()); + } else if (cname.equals("NSString")) { + if (d.objectForKey("value") != null) + return d.objectForKey("value").toString(); + } else if (cname.equals("EONull")) { + return NSKeyValueCoding.NullValue; + } else if (cname.equals("EOFetchSpecification")) { + _class = EOFetchSpecification.class; + } else if (cname.equals("EOKeyValueQualifier")) { + _class = EOKeyValueQualifier.class; + } else if (cname.equals("EONotQualifier")) { + _class = EONotQualifier.class; + } else if (cname.equals("EOAndQualifier")) { + _class = EOAndQualifier.class; + } else if (cname.equals("EOOrQualifier")) { + _class = EOOrQualifier.class; + } else if (cname.equals("EOSortOrdering")) { + _class = EOSortOrdering.class; + } else if (cname.indexOf(".") < 0) { //Load a class without package + try { + _class = Class.forName("net.wotonomy.control." + cname); + } catch (ClassNotFoundException ex) { + } + //search for the class in access + if (_class == null) { + try { + _class = Class.forName("net.wotonomy.access." + cname); + } catch (ClassNotFoundException ex) { + } + } + } else { + try { + _class = Class.forName(cname); + } catch (ClassNotFoundException ex) { + } + } + if (_class == null) + return x; + try { + Method met = _class.getMethod("decodeWithKeyValueUnarchiver", METHOD_ARGS); + return met.invoke(null, new Object[]{ new EOKeyValueUnarchiver(d) }); + } catch (Exception ex) { + ex.printStackTrace(); + return x; + } + } + } + return x; + } + + public Object decodeObjectReferenceForKey(String key) { + return null; + } + + public void ensureObjectAwake(Object obj) { + } + + public void finishInitializationOfObjects() { + } + + public Object parent() { + return null; + } + + public void setDelegate(Object del) { + _delegate = del; + } + public Object delegate() { + return _delegate; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/11 18:17:41 chochos + * decoding of objects now works properly + * + */ \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONotQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONotQualifier.java new file mode 100644 index 0000000..d086840 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONotQualifier.java @@ -0,0 +1,129 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EONotQualifiier negates a specified qualifier, +* evaluating to the opposite of the specified qualifier. +* +* @author michael@mpowers.net +* @author yjcheung@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EONotQualifier extends EOQualifier + implements EOKeyValueArchiving, EOQualifierEvaluation +{ + private EOQualifier qualifier; + + public EONotQualifier( + EOQualifier aQualifier ) + { + qualifier = aQualifier; + } + + /** + * Returns the qualifier that this qualifier negates. + */ + public EOQualifier qualifier() + { + return qualifier; + } + + /** + * Evaluates this qualifier for the specified object, + * and returns whether the object is qualified. + * evaluateWithObject is invoked on qualifier + * and the result is negated and returned. + */ + public boolean evaluateWithObject( Object anObject ) + { + return !(qualifier.evaluateWithObject(anObject)); + } + + /** + * Returns a string representation of this qualifier. + */ + public String toString() + { + return (new StringBuffer("Not ").append(qualifier.toString()).toString()); + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver arch) { + EOQualifier q = (EOQualifier)arch.decodeObjectForKey("qualifier"); + if (q == null) + return null; + return new EONotQualifier(q); + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EONotQualifier", "class"); + if (qualifier instanceof EOKeyValueArchiving) { + EOKeyValueArchiver ar2 = new EOKeyValueArchiver(); + ((EOKeyValueArchiving)qualifier).encodeWithKeyValueArchiver(ar2); + arch.encodeObject(ar2.dictionary(), "qualifiers"); + } else + throw new WotonomyException("Cannot archive instance of " + qualifier.getClass().getName()); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2003/08/12 01:43:04 chochos + * formally implement EOQualifierEvaluation + * + * Revision 1.8 2003/08/11 19:39:30 chochos + * special conditions for NSKeyValueCoding.NullValue -> EONull + * + * Revision 1.7 2003/08/09 01:24:19 chochos + * implements EOKeyValueArchiving + * + * Revision 1.6 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.5 2001/10/31 15:25:14 mpowers + * Cleanup of qualifiers. + * + * Revision 1.4 2001/10/30 22:57:28 mpowers + * EOQualifier framework is now working. + * + * Revision 1.3 2001/09/13 15:42:20 mpowers + * Fixed another cut/paste typo. + * + * Revision 1.2 2001/09/13 15:41:34 mpowers + * Fixed typo. + * + * Revision 1.1 2001/09/13 15:38:19 mpowers + * Started implementation of the EOQualifier framework. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONullValue.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONullValue.java new file mode 100644 index 0000000..3b4544e --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EONullValue.java @@ -0,0 +1,113 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.io.Serializable; + +import net.wotonomy.foundation.NSNull; + +/** +* EONullValue is used to represent null in Collections classes +* because List and Map do not specify whether null values +* are allowed and because NSArray and NSDictionary explicitly +* do not allow null values.

+* +* Use of the static singleton method nullValue() is required +* by this implementation because Java cannot return a singleton +* instance from a constructor.

+* +* This implementation duplicates NSNull, but the singleton instances +* are of course different. Be careful. I have no idea why this +* class was even created, given that NSNull exists. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EONullValue implements Serializable +{ + private static final EONullValue instance = new EONullValue(); + + /** + * Create a new instance of EONullValue. + */ + private EONullValue () + { + } + + /** + * Constructor specifying name and object. + */ + public static EONullValue nullValue () + { + return instance; + } + + /** + * Returns a human-readable string representation. + */ + public String toString() + { + return "[null]"; + } + + /** + * Hashcode of all instances is zero. + */ + public int hashCode() + { + return 0; + } + + /** + * Implemented to return true for any instance of EONullValue + * and for any instance of NSNull. + */ + public boolean equals( Object anObject ) + { + if ( anObject instanceof EONullValue ) return true; + if ( anObject instanceof NSNull ) return true; + return false; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/03/01 20:35:38 mpowers + * Implemented equals and hashCode. + * + * Revision 1.1 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStore.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStore.java new file mode 100644 index 0000000..cd18e36 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStore.java @@ -0,0 +1,320 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; + +/** +* EOObjectStore defines an object repository that tracks +* object creations, deletions, and updates made by +* EOEditingContexts.

+* +* A concrete implementation would probably write these +* changes to some kind of persistent storage, like a +* database.

+* +* EOEditingContext is itself a subclass of EOObjectStore +* that requires an EOObjectStore parent for committing +* its changes. This means that EOEditingContexts can +* use other EOEditingContexts as their parent, but there +* still must exist an EOObjectStore as the root of the +* editing graph. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EOObjectStore +{ + /** + * Key for the user info of ObjectsChangedInStoreNotifications. + * The key should retrieve an array of deleted EOGlobalIDs. + */ + public static final String DeletedKey = "deleted"; + + /** + * Key for the user info of ObjectsChangedInStoreNotifications. + * The key should retrieve an array of inserted EOGlobalIDs. + */ + public static final String InsertedKey = "inserted"; + + /** + * Key for the user info of ObjectsChangedInStoreNotifications. + * The key should retrieve an array of updated EOGlobalIDs. + * EOEditingContexts should refault their copies of these objects. + */ + public static final String UpdatedKey = "updated"; + + /** + * Key for the user info of ObjectsChangedInStoreNotification. + * The key should retrieve an array of EOGlobalIDs. + */ + public static final String InvalidatedKey = "invalidated"; + + /** + * Key for the NSNotification posted when this object store + * is asked to invalidate all objects. Object of the notification + * will be this object store, and user info will contain the + * InvalidatedKey. + */ + public static final String + InvalidatedAllObjectsInStoreNotification = + "EOInvalidatedAllObjectsInStoreNotification"; + + /** + * Key for the NSNotification posted when this object store + * is changed. Object of the notification will be this object + * store, and user info will contain InsertedKey, UpdatedKey, + * DeletedKey, and InvalidatedKey. + */ + public static final String + ObjectsChangedInStoreNotification = + "EOObjectsChangedInStoreNotification"; + + /** + * Default constructor is responsible for initializing + * internal state. + */ + public EOObjectStore () + { + } + + /** + * Called by editing contexts when they no longer + * need to track the specified id. You will not need + * to call this method, but you use use it for a hint + * that the specified global id is not in use by that + * child editing context. + */ + public void editingContextDidForgetObjectWithGlobalID ( + EOEditingContext aContext, + EOGlobalID aGlobalID ) + { + } + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship, or may return a placeholder array that + * will defer the fetch until accessed (an array fault). + * All objects must be registered the specified editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public abstract NSArray arrayFaultWithSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ); + + /** + * Returns the object for the specified id. + * The returned object may be a fault. + * The object will be registered in the + * specified editing context. + */ + public abstract /*EOEnterpriseObject*/ Object faultForGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ); + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. The fault should + * belong to the specified editing context. + */ + public abstract /*EOEnterpriseObject*/ Object faultForRawRow ( + Map aDictionary, + String anEntityName, + EOEditingContext aContext ); + + /** + * Given a newly instantiated object, this method + * initializes its properties to values appropriate + * for the specified id. The object should already + * belong to the specified editing context. + * This method is called to populate faults. + */ + public abstract void initializeObject ( + /*EOEnterpriseObject*/ Object eo, + EOGlobalID aGlobalID, + EOEditingContext aContext ); + + /** + * Remove all values from all objects in memory, + * turning them into faults, and posts an NSNotification + * that all objects have been invalidated. + * The notification should be named with the string + * constant InvalidatedAllObjectsInStoreNotification + * with this object store as the object and no user info. + */ + public abstract void invalidateAllObjects (); + + /** + * Removes values with the specified ids from memory, + * turning them into faults, and posts a notification + * that those objects have been invalidated. + * The notification should be named with the string + * constant ObjectsChangedInStoreNotification + * with this object store as the object and user info + * containing a key named InvalidateKey that returns + * a List of the EOGlobalIDs of the invalidated objects. + */ + public abstract void invalidateObjectsWithGlobalIDs ( + List aList ); + + /** + * Returns whether the object corresponding to the + * specified id is locked. The concept of object + * locking is implementation-specific. + */ + public abstract boolean isObjectLockedWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ); + + /** + * Locks the object corresponding to the + * specified id is locked. The concept of object + * locking is implementation-specific. + * The lock may be released when objects are + * invalidated or commited, but this behavior + * is not required. + */ + public abstract void lockObjectWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ); + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship. This method may not return an array fault + * because array faults call this method to fetch on demand. + * All objects must be registered the specified editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public abstract NSArray objectsForSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ); + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. Faults are not allowed in the array. + * Each object is registered with the specified editing context. + * If any object is already fetched in the specified context, + * it is not refetched and that object should be used in the array. + */ + public abstract NSArray objectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec, + EOEditingContext aContext ); + + /** + * Removes all values from the specified object, + * converting it into a fault for the specified id. + * New or deleted objects should not be refaulted. + */ + public abstract void refaultObject ( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext ); + + /** + * Writes all changes in the specified editing context + * to the respository. The object store is expected to + * post a notification that should be named with the string + * constant ObjectsChangedInStoreNotification + * with this object store as the object and user info + * containing keys named UpdatedKey, InsertedKey, and + * DeletedKey that return Lists of the EOGlobalIDs of the + * corresponding objects. + */ + public abstract void saveChangesInEditingContext ( + EOEditingContext aContext ); +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.15 2003/12/18 15:37:38 mpowers + * Changes to retain ability to work with objects that don't necessarily + * implement EOEnterpriseObject. I would still like to preserve this case + * for general usage, however the access package is free to assume that + * those objects will be EOs and cast appropriately. + * + * Revision 1.14 2003/08/19 01:53:12 chochos + * EOObjectStore had some incompatible return types (Object instead of EOEnterpriseObject, in fault methods mostly). It's internally consistent but I hope it doesn't break anything based on this, even though fault methods mostly throw exceptions for now. + * + * Revision 1.13 2002/02/13 21:20:15 mpowers + * Updated comments. + * + * Revision 1.12 2001/05/05 23:05:42 mpowers + * Implemented Array Faults. + * + * Revision 1.11 2001/02/21 21:17:32 mpowers + * Now retaining a reference to the recent changes observer. + * Better documented need to retain reference. + * Started implementing notifications. + * + * Revision 1.10 2001/02/16 22:51:29 mpowers + * Now deep-cloning objects passed between editing contexts. + * + * Revision 1.9 2001/02/16 18:34:19 mpowers + * Implementing nested contexts. + * + * Revision 1.8 2001/02/15 21:13:30 mpowers + * First draft implementation is complete. Now on to debugging. + * + * Revision 1.7 2001/02/14 23:03:02 mpowers + * A near-complete first draft of EOEditingContext. + * + * Revision 1.6 2001/02/13 23:24:29 mpowers + * Implementing more of editing context. + * + * Revision 1.5 2001/02/12 20:36:36 mpowers + * Documented methods. + * + * Revision 1.4 2001/02/09 22:09:34 mpowers + * Completed implementation of EOObjectStore. + * + * Revision 1.3 2001/02/06 15:24:11 mpowers + * Widened parameters on abstract method to fix build. + * + * Revision 1.2 2001/02/06 14:57:42 mpowers + * Defined abstract methods. + * + * Revision 1.1.1.1 2000/12/21 15:46:42 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStoreCoordinator.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStoreCoordinator.java new file mode 100644 index 0000000..e9a3a6a --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObjectStoreCoordinator.java @@ -0,0 +1,204 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Israfil consulting Services Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org + +$Id: EOObjectStoreCoordinator.java 894 2006-02-16 16:47:14Z cgruber $ + +*/ + +package net.wotonomy.control; + +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSNotification; +/** +* A representation of a channel of communication to the database. +* +* @author cgruber@israfil.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ + +public class EOObjectStoreCoordinator extends EOObjectStore { + + public static final String CooperatingObjectStoreWasAddedNotification = "EOCooperatingObjectStoreWasAddedNotification"; + public static final String CooperatingObjectStoreWasRemovedNotification = "EOCooperatingObjectStoreWasRemovedNotification"; + public static final String CooperatingObjectStoreNeededNotification = "EOCooperatingObjectStoreNeededNotification"; + public static final String GlobalIDKey = "globalID"; + public static final String FetchSpecificationKey = "fetchSpecification"; + public static final String ObjectKey = "object"; + + public static synchronized EOObjectStoreCoordinator defaultCoordinator() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public static synchronized void setDefaultCoordinator(EOObjectStoreCoordinator eoobjectstorecoordinator) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public EOObjectStoreCoordinator() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void dispose() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + private NSArray _sources() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSArray cooperatingObjectStores() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void addCooperatingObjectStore(EOCooperatingObjectStore eocooperatingobjectstore) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void removeCooperatingObjectStore(EOCooperatingObjectStore eocooperatingobjectstore) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public EOCooperatingObjectStore objectStoreForGlobalID(EOGlobalID eoglobalid) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public EOCooperatingObjectStore objectStoreForObject(EOEnterpriseObject eoenterpriseobject) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public EOCooperatingObjectStore objectStoreForFetchSpecification(EOFetchSpecification eofetchspecification) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + EOCooperatingObjectStore objectStoreForEntityNamed(String s) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void forwardUpdateForObject(EOEnterpriseObject eoenterpriseobject, NSDictionary nsdictionary) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSDictionary valuesForKeys(NSArray nsarray, EOEnterpriseObject eoenterpriseobject) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void saveChangesInEditingContext(EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSArray objectsWithFetchSpecification(EOFetchSpecification eofetchspecification, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public boolean isObjectLockedWithGlobalID(EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void lockObjectWithGlobalID(EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public /*EOEnterpriseObject*/ Object faultForGlobalID(EOGlobalID eoglobalid, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public /*EOEnterpriseObject*/ Object faultForRawRow(NSDictionary nsdictionary, String s, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSArray arrayFaultWithSourceGlobalID(EOGlobalID eoglobalid, String s, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void editingContextDidForgetObjectWithGlobalID(EOEditingContext eoeditingcontext, EOGlobalID eoglobalid) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSArray objectsForSourceGlobalID(EOGlobalID eoglobalid, String s, EOEditingContext eoeditingcontext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void invalidateObjectsWithGlobalIDs(NSArray nsarray) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void invalidateAllObjects() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void _objectsChangedInSubStore(NSNotification nsnotification) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void _invalidatedAllObjectsInSubStore(NSNotification nsnotification) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public void setUserInfo(NSDictionary nsdictionary) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + public NSDictionary userInfo() { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + /** + * @see net.wotonomy.control.EOObjectStore#faultForRawRow(Map, String, EOEditingContext) + */ + public /*EOEnterpriseObject*/ Object faultForRawRow( + Map aDictionary, + String anEntityName, + EOEditingContext aContext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + + /** + * @see net.wotonomy.control.EOObjectStore#initializeObject(Object, EOGlobalID, EOEditingContext) + */ + public void initializeObject( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + + /** + * @see net.wotonomy.control.EOObjectStore#invalidateObjectsWithGlobalIDs(List) + */ + public void invalidateObjectsWithGlobalIDs(List aList) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + + /** + * @see net.wotonomy.control.EOObjectStore#refaultObject(Object, EOGlobalID, EOEditingContext) + */ + public void refaultObject( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext) { + throw new UnsupportedOperationException("Not Yet Implemented"); + } + + +} diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverCenter.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverCenter.java new file mode 100644 index 0000000..d42130c --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverCenter.java @@ -0,0 +1,547 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Observable; +import java.util.Observer; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; + +/** +* EOObserverCenter is a static singleton-like class +* that manages EOObservings that want to be notified +* of changes to those objects that call +* notifyObserversObjectWillChange() before their +* properties change.

+* +* Implementation note: Because Java implements the +* Observer pattern in java.util.Observable, this +* class knows how to register itself as an Observer +* with Observables as well. However, users should +* note that Observables only notify their Observers +* of changes after their property has changed. +* EODelayedObservers would see no difference because +* they always receive their notification after all +* changes have taken place.

+* +* This implementation uses weak references for observers +* and observables. The advantage to this approach is +* that you do not need to explicitly unregister observers +* or observables; they will be unregistered when they are +* garbage-collected. Note that you will need to retain +* a reference to any objects you register or they may +* become unregistered if no other object references them. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOObserverCenter implements Observer +{ + /** + * Would much rather use a WeakHashMap, but that class + * compares by value, and we need to compare by reference. + * This means we need to recreate a weak hashmap with + * the ReferenceKey class below. Using hashtable for + * thread safety. + */ + private static Map observableToObservers = new Hashtable(); + + private static List omniscients = new LinkedList(); + + // suppression count + private static int suppressions = 0; + + // singleton instance - needed for Observer + private static EOObserverCenter instance = null; + + // optimization: remember last request and result + private static Object lastRequest; + private static NSArray lastResult; + + /** + * Registers the specified EOObserving for + * notifications from the specified object. + * An EOObserving can only be registered + * once for a given object. + */ + public static void addObserver( + EOObserving anObserver, Object anObject ) + { + // atomic operation + synchronized ( instance() ) + { + // find observer list + List observers = (List) + observableToObservers.get( new ReferenceKey( anObject ) ); + + // if observer list not found, create and add item + if ( observers == null ) + { + observers = new ArrayList(); + observers.add( new WeakReference( anObserver ) ); + processQueue(); + observableToObservers.put( new ReferenceKey( anObject, queue ), observers ); + + // support for java.util.Observable + if ( anObject instanceof Observable ) + { + ((Observable)anObject).addObserver( instance() ); + } + } + else // observer list found - scan for observer + if ( indexOf( observers, anObserver ) < 0 ) + { + // observer not found, register it + observers.add( new WeakReference( anObserver ) ); + } + + lastRequest = null; + lastResult = null; + } + } + + /** + * Registers the specified EOObserving for notifications + * on all object changes. An EOObserving can be + * registered as an omniscient observer at most once. + * Use omniscient observers with caution. + */ + public static void addOmniscientObserver( + EOObserving anObserver ) + { + if ( indexOf( omniscients, anObserver ) < 0 ) + { + omniscients.add( anObserver ); + } + } + + /** + * Notifies all EOObservings registered for + * notifications for the specified object. + * This method is typically called by objects + * that wish to broadcast a notification before + * a property change takes place, passing itself + * as the argument. + */ + public static void notifyObserversObjectWillChange( + Object anObject ) + { + if ( observerNotificationSuppressCount() == 0 ) + { + List observers = observersForObject( anObject ); + EOObserving o; + Iterator it = observers.iterator(); + while ( it.hasNext() ) + { + o = (EOObserving) it.next(); + o.objectWillChange( anObject ); + } + } + } + + /** + * Returns an observer that is an instance of + * the specified class and + * that is registered for notifications about + * the specified object. If more than one + * such observer exists, which observer is + * returned is undetermined - use + * observersForObject instead. If no observer + * exists, returns null. + */ + public static EOObserving observerForObject( + Object anObject, Class aClass ) + { + List result = observersForObject( anObject ); + if ( result.size() == 0 ) return null; + + Object o; + Iterator it = result.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( aClass.isAssignableFrom( o.getClass() ) ) + { + return (EOObserving) o; + } + } + return null; + } + + /** + * Returns the number of times that notifications + * have been suppressed. This is also the number + * of times that enableObserverNotification must + * be called to allow notifications to take place. + */ + public static int observerNotificationSuppressCount() + { + return suppressions; + } + + /** + * Returns a List of observers for the + * specified object. Returns an empty List + * if no observer has registered for that + * object. + */ + public static NSArray observersForObject( + Object anObject ) + { + synchronized ( instance() ) + { + // optimization: this is called very frequently + // from the same object calling willChange() a + // number of times in a row as it is updating. + if ( lastRequest == anObject ) + { + return lastResult; + } + + NSArray result; + + List references = observerListForObject( anObject ); + if ( references == null ) + { + result = NSArray.EmptyArray; + } + else + { + result = new NSMutableArray(); + Object observer; + Iterator it = references.iterator(); + while ( it.hasNext() ) + { + observer = ((Reference)it.next()).get(); + if ( observer != null ) + { + result.add( observer ); + } + else // reference has expired + { + processQueue(); + it.remove(); // remove from list + // if last observer, unregister observable + if ( references.size() == 0 ) + { + observableToObservers.remove( new ReferenceKey( anObject ) ); + } + } + } + } + + lastRequest = anObject; + lastResult = result; + + return result; + } + + } + + /** + * Returns a reference to the actual list of + * observers for the given object, or null if it + * doesn't exist. + */ + private static List observerListForObject( Object anObject ) + { + return (List) observableToObservers.get( new ReferenceKey( anObject ) ); + } + + /** + * Unregisters the specified observer for + * notifications from the specified object. + */ + public static void removeObserver( + EOObserving anObserver, Object anObject ) + { + // atomic operation + synchronized ( instance() ) + { + lastRequest = null; + lastResult = null; + + List result = observerListForObject( anObject ); + if ( result == null ) return; + int index = indexOf( result, anObserver ); + if ( index == -1 ) return; + + // remove observer from list + result.remove( index ); + + // if last observer, unregister observable + if ( result.size() == 0 ) + { + processQueue(); + observableToObservers.remove( new ReferenceKey( anObject ) ); + } + } + } + + /** + * Unregisters the specified omniscient observer. + */ + public static void removeOmniscientObserver( + EOObserving anObserver ) + { + int index = indexOf( omniscients, anObserver ); + if ( index != -1 ) + { + omniscients.remove( index ); + } + } + + /** + * Enables notifications after they have been + * suppressed by suppressObserverNotification. + * If notifications have been suppressed + * multiple times, this method must be called + * an equal number of times to resume notifications. + * If notifications are not currently suppressed, + * this method does nothing. + */ + public static void enableObserverNotification() + { + if ( suppressions > 0 ) suppressions--; + } + + /** + * Causes notifications to be suppressed until + * the next matching call to enableObserverNotification. + * If this method is called more than once, + * enableObserverNotification must be called an + * equal number of times for notifications to resume. + * This method always causes notifications to cease + * immediately. + */ + public static void suppressObserverNotification() + { + suppressions++; + } + + /** + * Because we're comparing by reference, we need to + * test for the existence of the object directly. + * @return the index or -1 if not found. + */ + private static int indexOf( List aList, Object anObject ) + { + if ( anObject == null ) return -1; + + synchronized ( aList ) + { + int len = aList.size(); + for ( int i = 0; i < len; i++ ) + { + // compare by reference + if ( anObject == ((Reference)aList.get(i)).get() ) + { + return i; + } + } + } + return -1; + } + + /** + * Private singleton instance, so we can be an observer. + */ + private static EOObserverCenter instance() + { + if ( instance == null ) + { + instance = new EOObserverCenter(); + } + return instance; + } + + /** + * Interface Observer + */ + public void update( Observable o, Object arg ) + { + notifyObserversObjectWillChange( o ); + } + + /* Reference queue for cleared WeakKeys */ + private static ReferenceQueue queue = new ReferenceQueue(); + + /* Remove all invalidated entries from the map, that is, remove all entries + whose keys have been discarded. This method should be invoked once by + each public mutator in this class. We don't invoke this method in + public accessors because that can lead to surprising + ConcurrentModificationExceptions. */ + private static void processQueue() + { + synchronized ( instance() ) + { + ReferenceKey rk; + while ((rk = (ReferenceKey)queue.poll()) != null) + { + //System.out.println( "EOObserverCenter.processQueue: removing object" ); + observableToObservers.remove(rk); + } + } + } + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + * Retains a weak reference just like WeakHashMap. + */ + static private class ReferenceKey extends WeakReference + { + private int hashCode; + + /** + * Called to create a disposable reference key, + * used for retrieving values from the hashtable. + */ + public ReferenceKey( Object anObject ) + { + super( anObject ); + hashCode = anObject.hashCode(); + } + + /** + * Called to create a reference key that will be + * used as a key in the hashtable, so we need the + * reference queue to later remove the key from the + * table when referred object is no longer in use. + */ + public ReferenceKey( Object anObject, ReferenceQueue aQueue ) + { + super( anObject, aQueue ); + hashCode = anObject.hashCode(); + } + + /** + * Passes through to actual key's hash code. + */ + public int hashCode() + { + return hashCode; + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( ! ( anObject instanceof ReferenceKey ) ) return false; + Object key = get(); + if ( key == null ) return false; + return ( key == ((ReferenceKey)anObject).get() ); + } + } + + private static String debugString() + { + String result = ""; + int count = 0; + synchronized ( instance() ) + { + Object anObject; + Iterator it = observableToObservers.keySet().iterator(); + while ( it.hasNext() ) + { + result += ((Reference)it.next()).get() + " : "; + count++; + /* + Iterator values = ((List)it.next()).iterator(); + while ( values.hasNext() ) + { + anObject = ((Reference)values.next()).get(); + if ( anObject != null ) + { +// if ( anObject instanceof net.wotonomy.ui.MasterDetailAssociation ) + result += anObject.getClass().toString() + " : "; + count++; + } + } + */ + } + result += "["+count+"]"; + } + return result; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2002/10/24 18:18:12 mpowers + * NSArray's are now considered read-only, so we can return our internal + * representation to reduce unnecessary object allocation. + * + * Revision 1.8 2001/02/21 21:17:32 mpowers + * Now retaining a reference to the recent changes observer. + * Better documented need to retain reference. + * Started implementing notifications. + * + * Revision 1.7 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.6 2001/02/05 18:45:45 mpowers + * Reduced access back to private for utility methods. + * + * Revision 1.5 2001/02/05 18:42:32 mpowers + * Updated documentation throughout project. + * + * Revision 1.4 2001/01/18 16:57:47 mpowers + * Added debug facility. + * + * Revision 1.3 2001/01/10 16:28:53 mpowers + * Implemented a compare-by-reference weak hashtable + * because WeakHashMap compared by value and we can't + * assume that display groups won't contain objects + * whose values are equivalent. + * + * Revision 1.2 2001/01/09 20:10:19 mpowers + * Now using weak references to track observables and their observers. + * + * Revision 1.1.1.1 2000/12/21 15:46:44 mpowers + * Contributing wotonomy. + * + * Revision 1.8 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverProxy.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverProxy.java new file mode 100644 index 0000000..e616a33 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserverProxy.java @@ -0,0 +1,104 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import net.wotonomy.foundation.NSSelector; + +/** +* A convenience observer for objects that do not or cannot +* subclass EODelayedObserver. EOObserverProxy will invoke +* an NSSelector on an object when it receives a subjectChanged +* message. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOObserverProxy + extends EODelayedObserver +{ + protected Object target; + protected NSSelector selector; + protected int priority; + + /** + * Constructs an EODelayedObserver that will invoke the specified selector + * on the specified object, and will run at the specified priority. + */ + public EOObserverProxy ( + Object anObject, NSSelector aSelector, int aPriority ) + { + target = anObject; + selector = aSelector; + priority = aPriority; + } + + /** + * Constructs an EODelayedObserver that will invoke the specified selector + * on the specified object, and will run at ObserverPriorityThird priority, + * which is the default. + */ + public EOObserverProxy ( + Object anObject, NSSelector aSelector ) + { + this( anObject, aSelector, ObserverPriorityThird ); + } + + /** + * Returns the priority of this observer in the queue. + * This implementation returns the priority specified + * in the constructor. + */ + public int priority () + { + return priority; + } + + /** + * Notifies observer that one or more objects that + * it is observing have changed. The observer should + * check all objects it is observing for changes. + */ + public void subjectChanged () + { + try + { + selector.invoke( target ); + } + catch ( Exception exc ) + { + System.out.println( "Error notifying observer: " ); + exc.printStackTrace(); + } + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/10/22 21:55:32 mpowers + * This turns out to be a really useful class. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserving.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserving.java new file mode 100644 index 0000000..8ec6e5c --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOObserving.java @@ -0,0 +1,51 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* A pure java implementation of EOObserving. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOObserving +{ + /** + * Called when the specified object is about to change. + */ + void objectWillChange ( Object anObject ); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:46:44 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOOrQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOOrQualifier.java new file mode 100644 index 0000000..7f4b1c6 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOOrQualifier.java @@ -0,0 +1,169 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOOrQualifier contains other EOQualifiers, +* evaluating as true if any of the contained +* qualifiers evaluate as true. +* +* @author michael@mpowers.net +* @author yjcheung@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOOrQualifier extends EOQualifier + implements EOKeyValueArchiving, EOQualifierEvaluation +{ + private List qualifiers; + + public EOOrQualifier( + List aQualifierList ) + { + qualifiers = new LinkedList( aQualifierList ); + } + + /** + * Returns a List of qualifiers contained by this qualifier. + */ + public NSArray qualifiers() + { + return new NSArray( qualifiers ); + } + + /** + * Add a new qualifier to the list. + */ + public void addQualifier(EOQualifier qualifier) + { + qualifiers.add(qualifier); + } + + /** + * Evaluates this qualifier for the specified object, + * and returns whether the object is qualified. + * selector() is invoked on the value for key() on the + * specified object, with value() as the parameter. + * + * Note: this has a lazy "or" implementation. Ex. Qal1 or Qal2. + * If Qal1 is evaluated to be true, then it returns true without + * further evaluate Qal2. + */ + public boolean evaluateWithObject( Object anObject ) + { + Iterator it = qualifiers.iterator(); + while( it.hasNext() ) + { + if ( ((EOQualifier) it.next()).evaluateWithObject(anObject) ) + { + return true; + } + } + return false; + } + + /** + * Returns a string representation of this qualifier. + */ + public String toString() + { + StringBuffer myBuf = new StringBuffer("("); + Iterator it = qualifiers.iterator(); + while (it.hasNext()) + { + myBuf = myBuf.append(((EOQualifier) it.next()).toString()).append(" or "); + } + String myStr = myBuf.toString(); + myStr = myStr.substring(0, myStr.lastIndexOf(" or ")).concat(")"); + return myStr; + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver arch) { + NSArray a = (NSArray)arch.decodeObjectForKey("qualifiers"); + if (a == null) + return null; + NSMutableArray l = new NSMutableArray(); + for (int i = 0; i < a.count(); i++) { + NSDictionary d = (NSDictionary)a.objectAtIndex(i); + EOKeyValueUnarchiver ua = new EOKeyValueUnarchiver(d); + EOQualifier q = (EOQualifier)EOQualifier.decodeWithKeyValueUnarchiver(ua); + if (q != null) + l.addObject(q); + } + return new EOAndQualifier(l); + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOOrQualifier", "class"); + NSMutableArray arr = new NSMutableArray(qualifiers.size()); + for (int i = 0; i < qualifiers.size(); i++) { + EOQualifier q = (EOQualifier)qualifiers.get(i); + if (q instanceof EOKeyValueArchiving) { + EOKeyValueArchiver ar2 = new EOKeyValueArchiver(); + ((EOKeyValueArchiving)q).encodeWithKeyValueArchiver(ar2); + arr.addObject(ar2.dictionary()); + } else + throw new WotonomyException("Cannot archive instance of " + q.getClass().getName()); + } + arch.encodeObject(arr, "qualifiers"); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/12 01:43:04 chochos + * formally implement EOQualifierEvaluation + * + * Revision 1.5 2003/08/09 01:22:51 chochos + * qualifiers implement EOKeyValueArchiving + * + * Revision 1.4 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.3 2001/10/31 15:25:14 mpowers + * Cleanup of qualifiers. + * + * Revision 1.2 2001/10/30 22:57:28 mpowers + * EOQualifier framework is now working. + * + * Revision 1.1 2001/09/13 15:25:56 mpowers + * Started implementation of the EOQualifier framework. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifier.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifier.java new file mode 100644 index 0000000..6fffea4 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifier.java @@ -0,0 +1,680 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.NSSet; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EOQualifiers are used to perform property-based +* qualifications on objects: for a set of criteria, +* a qualifier either qualifies or disqualifies an +* given object. EOKeyValueQualifiers can be joined +* by EOAndQualifier and EOOrQualifier, and so can +* form a tree of qualifications.

+* +* Certain qualifiers +* can accept a variable in place of a value; variable +* names are marked by a "$", as in "$name". Variables +* are resolved with the qualifierWithBindings() method. +* +* @author michael@mpowers.net +* @author yjcheung@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public abstract class EOQualifier +{ + public static final NSSelector + QualifierOperatorCaseInsensitiveLike = new OperatorCaseInsensitiveLike(); + public static final NSSelector + QualifierOperatorContains = new OperatorContains(); + public static final NSSelector + QualifierOperatorEqual = new OperatorEqual(); + public static final NSSelector + QualifierOperatorGreaterThan = new OperatorGreaterThan(); + public static final NSSelector + QualifierOperatorGreaterThanOrEqualTo = new OperatorGreaterThanOrEqualTo(); + public static final NSSelector + QualifierOperatorLessThan = new OperatorLessThan(); + public static final NSSelector + QualifierOperatorLessThanOrEqualTo = new OperatorLessThanOrEqualTo(); + public static final NSSelector + QualifierOperatorLike = new OperatorLike(); + public static final NSSelector + QualifierOperatorNotEqual = new OperatorNotEqual(); + + /** + * Default constructor. + */ + public EOQualifier () + { + } + + /** + * Adds all qualifier keys in this qualifier to + * the specified Set, which is expected to be + * mutable. The tree of qualifiers is traversed + * and the left-hand-side of each expression is + * added to the set. + */ + public void addQualifierKeysToSet ( Set aSet ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Set of all property names used for + * comparisons by this qualifier. The tree of + * qualifiers is traversed and the left-hand-side + * of each expression is added to the set. + */ + public NSSet allQualifierKeys () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a List containing the variables used + * at compare-time by this qualifier. Each variable + * will appear only once in the list. + */ + public NSArray bindingKeys () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether the specified object meets the + * criteria defined by this qualifier. + */ + public boolean evaluateWithObject ( Object anObject ) + { + return true; + } + + /** + * Returns the key (which can be a key path) that + * is tested against the specified binding variable. + * The tree is traversed looking for the first instance + * of the specified variable, and the corresponding + * left-hand-side of the expression is returned. + */ + public String keyPathForBindingKey ( String aVariable ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a qualifier that is like this qualifier, + * except all variables will be replaced with values + * from the specified Map whose keys match the variable + * names. If requireAll is true, an exception will be + * thrown if there is no key that matches on of the + * variables in the tree; otherwise, the qualifier + * containing the unmatched variable is removed. + */ + public EOQualifier qualifierWithBindings ( + Map aMap, + boolean requireAll ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + + /** + * Tests whether all the keys in this qualifier can + * be applied to an object of the specified class. + * @return A Throwable if the validation fails, + * otherwise returns null if the class can be used + * with this qualifier. + */ + public Throwable validateKeysWithRootClassDescription ( + Class aClass ) + { + throw new WotonomyException( "Not implemented yet." ); + } + + // statics + + /** + * Convenience to retain only those objects from the specified + * List that meet the specified qualifier's requirements. + */ + public static void filterArrayWithQualifier ( + List anObjectList, EOQualifier aQualifier ) + { + ListIterator iterator = anObjectList.listIterator(); + while ( iterator.hasNext() ) + { + if ( ! aQualifier.evaluateWithObject( iterator.next() ) ) + { + iterator.remove(); + } + } + } + + /** + * Convenience to return a List consisting only + * of those objects in the specified List that meet + * the specified qualifier's requirements. + */ + public static NSArray filteredArrayWithQualifier ( + List anObjectList, EOQualifier aQualifier ) + { + Object o; + List result = new LinkedList(); + Iterator iterator = anObjectList.iterator(); + while ( iterator.hasNext() ) + { + o = iterator.next(); + if ( aQualifier.evaluateWithObject( o ) ) + { + result.add( o ); + } + } + return new NSArray( (Collection) result ); + } + + /** + * Convenience to create a set of EOKeyValueQualifiers + * joined by an EOAndQualifier. Each pair of keys and + * values are used to create EOKeyValueQualifiers. + */ + public static EOQualifier + qualifierToMatchAllValues ( Map aMap ) + { + Object key, value; + List qualifierList = new LinkedList(); + Iterator iterator = aMap.keySet().iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next(); + value = aMap.get( key ); + qualifierList.add( new EOKeyValueQualifier( + key.toString(), QualifierOperatorEqual, value ) ); + } + return new EOAndQualifier( qualifierList ); + } + + /** + * Convenience to create a set of EOKeyValueQualifiers + * joined by an EOOrQualifier. Each pair of keys and + * values are used to create EOKeyValueQualifiers. + */ + public static EOQualifier + qualifierToMatchAnyValue ( Map aMap ) + { + Object key, value; + List qualifierList = new LinkedList(); + Iterator iterator = aMap.keySet().iterator(); + while ( iterator.hasNext() ) + { + key = iterator.next(); + value = aMap.get( key ); + qualifierList.add( new EOKeyValueQualifier( + key.toString(), QualifierOperatorEqual, value ) ); + } + return new EOOrQualifier( qualifierList ); + } + + /** + * Returns an EOQualifier that meets the criteria + * represented by the specified string and variable + * length argument list. This method parses the string + * and returns a tree of qualifiers. Each token beginning + * with "%" is replaced with the corresponding object + * from the argument list. The parser recognizes the + * operation tokens returned from the allQualifierOperators() + * method. + */ + public static EOQualifier qualifierWithQualifierFormat ( + String aString, List anArgumentList ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a List of operators that are supported for + * relational operations. This excludes string comparison + * operators. + */ + public static NSArray relationalQualifierOperators () + { + NSMutableArray result = new NSMutableArray(); + BaseSelector selector; + Iterator iterator = allQualifierOperators().iterator(); + while ( iterator.hasNext() ) + { + selector = (BaseSelector) iterator.next(); + if ( selector.isRelationalOperator() ) + { + result.addObject( selector ); + } + } + return result; + } + + /** + * Returns a List of valid operators. + */ + public static NSArray allQualifierOperators () + { + return operators.allKeys(); + } + + /** + * Returns a selector the corresponds to the operation + * represented by the specified string. For example, + * ">" represents QualifierOperatorGreaterThan. + */ + public static NSSelector operatorSelectorForString ( + String anOperatorString ) + { + return (NSSelector) + operators.objectForKey( anOperatorString ); + } + + /** + * Returns a string the corresponds to the operation + * represented by the specified selector. For example, + * QualifierOperatorGreaterThan is represented with ">". + */ + public static String stringForOperatorSelector ( + NSSelector aSelector ) + { + return (String) + operators.allKeysForObject( aSelector ).lastObject(); + } + + /** + * Returns a string representation of this qualifier. + */ + public String toString() + { + //TODO: implement this + return super.toString(); + } + + // built-in qualifiers + + private static NSMutableDictionary operators; + static + { + operators = new NSMutableDictionary(); + operators.setObjectForKey( + QualifierOperatorCaseInsensitiveLike, + QualifierOperatorCaseInsensitiveLike.toString() ); + operators.setObjectForKey( + QualifierOperatorContains, + QualifierOperatorContains.toString() ); + operators.setObjectForKey( + QualifierOperatorEqual, + QualifierOperatorEqual.toString() ); + operators.setObjectForKey( + QualifierOperatorGreaterThan, + QualifierOperatorGreaterThan.toString() ); + operators.setObjectForKey( + QualifierOperatorGreaterThanOrEqualTo, + QualifierOperatorGreaterThanOrEqualTo.toString() ); + operators.setObjectForKey( + QualifierOperatorLessThan, + QualifierOperatorLessThan.toString() ); + operators.setObjectForKey( + QualifierOperatorLessThanOrEqualTo, + QualifierOperatorLessThanOrEqualTo.toString() ); + operators.setObjectForKey( + QualifierOperatorLike, + QualifierOperatorLike.toString() ); + operators.setObjectForKey( + QualifierOperatorNotEqual, + QualifierOperatorNotEqual.toString() ); + } + + static private abstract class BaseSelector extends NSSelector + { + public String name () + { + return "BaseSelector"; + } + + public Class[] parameterTypes () + { + return new Class[] { Object.class, Object.class }; + } + + public Method methodOnClass (Class aClass) + throws NoSuchMethodException + { + throw new NoSuchMethodException(); + } + + public Method methodOnObject (Object anObject) + throws NoSuchMethodException + { + throw new NoSuchMethodException(); + } + + public boolean implementedByClass (Class aClass) + { + return true; + } + + public boolean implementedByObject (Object anObject) + { + return true; + } + + public boolean isRelationalOperator() + { + return true; + } + + public Object invoke (Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return qualify( anObject, parameters[0] ); + } + + abstract protected Boolean qualify( Object target, Object parameter ); + + protected int doCompare(Object o1, Object o2) + { + Class firstClass = o1.getClass(); + Class secondClass = o2.getClass(); + + if ( ! ( secondClass.equals( firstClass ) ) ) + { + Object converted = null; + if ( o2 instanceof Comparable ) + { + converted = ValueConverter.convertObjectToClass( o1, secondClass ); + if (converted != null) + { + o1 = converted; + } + } + + if (converted == null && (o1 instanceof Comparable)) + { + converted = ValueConverter.convertObjectToClass( o2, firstClass ); + if ( converted != null ) + { + o2 = converted; + } + } + + if (converted == null) + { + throw new WotonomyException("Qualifier: Not Comparable Objects"); + // no way to compare + } + } + + return ((Comparable)o2).compareTo( o1 ); + } + + } + + + static class OperatorCaseInsensitiveLike extends BaseSelector { + + public boolean isRelationalOperator() + { + return false; + } + + protected Boolean qualify( Object o1, Object o2 ) + { + String myString1 = o1.toString(); + String myString2 = o2.toString(); + myString1 = myString1.toLowerCase(); + myString2 = myString2.toLowerCase(); + StringTokenizer st = new StringTokenizer(myString1, "%"); + + while (st.hasMoreTokens()) { + String part = st.nextToken(); + int index = myString2.indexOf(part); + if (index > -1) + { + myString2 = myString2.substring(index + part.length()); + } + else + { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + + } + + public String toString() + { + return "caseInsensitiveLike"; + } + } + + static class OperatorContains extends BaseSelector { + + public boolean isRelationalOperator() + { + return false; + } + + protected Boolean qualify( Object o1, Object o2 ) + { + String myString1 = o1.toString(); + String myString2 = o2.toString(); + return new Boolean( + myString2.indexOf(myString1) > -1 ); + } + + public String toString() + { + return "contains"; + } + } + + static class OperatorEqual extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean( doCompare(o1, o2) == 0 ); + } + + public String toString() + { + return "="; + } + } + + static class OperatorGreaterThan extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean( doCompare(o1, o2) > 0 ); + } + + public String toString() + { + return ">"; + } + } + + static class OperatorGreaterThanOrEqualTo extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean( doCompare(o1, o2) >= 0 ); + } + + public String toString() + { + return new String(" >= "); + } + } + + static class OperatorLessThan extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean( doCompare(o1, o2) < 0 ); + } + + public String toString() + { + return ">"; + } + } + + static class OperatorLessThanOrEqualTo extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean (doCompare(o1, o2) <= 0); + } + + public String toString() + { + return "<="; + } + } + + static class OperatorLike extends BaseSelector { + + public boolean isRelationalOperator() + { + return false; + } + + protected Boolean qualify( Object o1, Object o2 ) + { + String myString1 = o1.toString(); + String myString2 = o2.toString(); + StringTokenizer st = new StringTokenizer(myString1, "%"); + while (st.hasMoreTokens()) { + String part = st.nextToken(); + int index = myString2.indexOf(part); + if (index > -1) + { + myString2 = myString2.substring(index + part.length()); + } + else + { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + } + + public String toString() + { + return "like"; + } + } + + static class OperatorNotEqual extends BaseSelector { + + protected Boolean qualify( Object o1, Object o2 ) + { + return new Boolean(!(o1.equals(o2))); + } + + public String toString() + { + return "!="; + } + } + + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver ua) { + String cname = (String)ua.decodeObjectForKey("class"); + if (cname.equals("EOKeyValueQualifier")) + return (EOQualifier)EOKeyValueQualifier.decodeWithKeyValueUnarchiver(ua); + if (cname.equals("EOAndQualifier")) + return (EOQualifier)EOAndQualifier.decodeWithKeyValueUnarchiver(ua); + if (cname.equals("EOOrQualifier")) + return (EOQualifier)EOOrQualifier.decodeWithKeyValueUnarchiver(ua); + if (cname.equals("EONotQualifier")) + return (EOQualifier)EONotQualifier.decodeWithKeyValueUnarchiver(ua); + return null; + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.10 2003/08/09 01:22:51 chochos + * qualifiers implement EOKeyValueArchiving + * + * Revision 1.9 2001/11/04 18:29:11 mpowers + * Better handling for non-string types used with non-relational operators. + * + * Revision 1.8 2001/10/31 15:25:14 mpowers + * Cleanup of qualifiers. + * + * Revision 1.7 2001/10/30 22:57:28 mpowers + * EOQualifier framework is now working. + * + * Revision 1.6 2001/10/30 22:16:37 mpowers + * Implemented operators as selectors. + * + * Revision 1.5 2001/09/14 14:21:28 mpowers + * Updated javadoc. + * + * Revision 1.3 2001/09/13 15:25:56 mpowers + * Started implementation of the EOQualifier framework. + * + * Revision 1.2 2001/02/27 03:33:04 mpowers + * Initial draft of the key-value qualifier. + * + * Revision 1.1.1.1 2000/12/21 15:46:47 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifierEvaluation.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifierEvaluation.java new file mode 100644 index 0000000..87769b8 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOQualifierEvaluation.java @@ -0,0 +1,42 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.control; + +/** +* EOQualifiers that want to perform in-memory +* evaluation should implement this interface.

+* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOQualifierEvaluation { + + public boolean evaluateWithObject(Object eo); + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/08/12 01:42:17 chochos + * formally declare this interface + * + */ + \ No newline at end of file diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EORelationshipManipulation.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EORelationshipManipulation.java new file mode 100644 index 0000000..568b555 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EORelationshipManipulation.java @@ -0,0 +1,76 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* EORelationshipManipulation provides methods for generically +* adding and removing relationships between objects, handling +* both one-way and reciprocal relationships. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EORelationshipManipulation +{ + /** + * Adds the specified object to the relationship on this + * object specified by the key. For to-one relationships, + * this operation is the same as valueForKey. + */ + void addObjectToPropertyWithKey( + Object anObject, String aKey ); + + /** + * Removes the specified object from the relationship on + * this object specified by the key. For to-one relationships, + * this operation is the same as takeValueForKey with a null + * value. + */ + void removeObjectFromPropertyWithKey( + Object anObject, String aKey ); + + /** + * As addObjectToProperty with key, but also performs the + * reciprocal operation on the other side of the relationship. + */ + void addObjectToBothSidesOfRelationshipWithKey( + EORelationshipManipulation anObject, String aKey ); + + /** + * As removeObjectFromPropertyWithKey with key, but also performs the + * reciprocal operation on the other side of the relationship. + */ + void removeObjectFromBothSidesOfRelationshipWithKey( + EORelationshipManipulation anObject, String aKey ); + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOSortOrdering.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOSortOrdering.java new file mode 100644 index 0000000..789b6da --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOSortOrdering.java @@ -0,0 +1,406 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSSelector; + +/** +* EOSortOrdering defines a sort key and operation. +* DisplayGroups use lists of EOSortOrdering to determine +* how to order their items. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EOSortOrdering implements Serializable, EOKeyValueArchiving +{ + /** + * Sorts items in ascending order. + */ + public static final + NSSelector CompareAscending = new CompareAscendingComparator(); + + /** + * Sorts items in descending order. + */ + public static final + NSSelector CompareDescending = new CompareDescendingComparator(); + + /** + * Sorts items' string representations in ascending order + * in a case insensitive manner. + */ + public static final + NSSelector CompareCaseInsensitiveAscending = + new CompareCaseInsensitiveAscendingComparator(); + + /** + * Sorts items' string representations in descending order + * in a case insensitive manner. + */ + public static final + NSSelector CompareCaseInsensitiveDescending = + new CompareCaseInsensitiveDescendingComparator(); + + protected String key; + protected NSSelector selector; + + /** + * Factory-style constructor returns a new EOSortOrdering instance + * with the specified key and selector. Neither may be null. + */ + public static EOSortOrdering sortOrderingWithKey(String key, NSSelector selector) + { + return new EOSortOrdering( key, selector ); + } + + /** + * Constructor creates an EOSortOrdering that uses the + * specified key and selector. Neither may be null. + */ + public EOSortOrdering( String aKey, NSSelector aSelector ) + { + key = aKey; + selector = aSelector; + } + + /** + * Constructor creates an EOSortOrdering that uses the + * specified key and comparator. Neither may be null. + * Not in the spec. + */ + public EOSortOrdering( String aKey, Comparator aComparator ) + { + key = aKey; + selector = new NSSelector( aKey, aComparator ); + } + + /** + * Returns the property key. + */ + public String key() + { + return key; + } + + /** + * Returns the selector. + */ + public NSSelector selector() + { + return selector; + } + + public String toString() + { + return "[EOSortOrdering: key='"+key+"' selector='"+selector+"']"; + } + + public boolean equals( Object anObject ) + { + if ( anObject instanceof EOSortOrdering ) + { + EOSortOrdering x = (EOSortOrdering) anObject; + if ( selector().equals( x.selector() ) ) + { + if ( key().equals( x.key() ) ) + { + return true; + } + } + } + return false; + } + + /** + * Sorts the specified list in place according to the specified + * list of EOSortOrderings. The items will be sorted first by the + * first ordering, and items with equal values for that property + * will be sorted by the next ordering, and so on. + */ + public static void sortArrayUsingKeyOrderArray( + List anObjectList, List aSortOrderingList ) + { + List keys = new ArrayList( aSortOrderingList ); + Collections.reverse( keys ); + Iterator it = keys.iterator(); + EOSortOrdering sortOrdering; + while ( it.hasNext() ) + { + sortOrdering = (EOSortOrdering) it.next(); + Collections.sort( anObjectList, + new DelegatingComparator( + sortOrdering.key(), sortOrdering.selector() ) ); + } + } + + /** + * Sorts the specified list in place according to the specified + * list of EOSortOrderings. The items will be sorted first by the + * first ordering, and items with equal values for that property + * will be sorted by the next ordering, and so on. + */ + public static NSArray sortedArrayUsingKeyOrderArray( + List anObjectList, List aSortOrderingList ) + { + NSArray result = new NSMutableArray(); + result.addAll( anObjectList ); + sortArrayUsingKeyOrderArray( result, aSortOrderingList ); + return result; + } + + public static Object decodeWithKeyValueUnarchiver(EOKeyValueUnarchiver arch) { + String k = (String)arch.decodeObjectForKey("key"); + String sname = (String)arch.decodeObjectForKey("selectorName"); + NSSelector sel = null; + if (sname.equals("compareAscending:")) + sel = CompareAscending; + else if (sname.equals("compareDescending:")) + sel = CompareDescending; + else if (sname.equals("compareCaseInsensitiveAscending:")) + sel = CompareCaseInsensitiveAscending; + else if (sname.equals("compareCaseInsensitiveDescending:")) + sel = CompareCaseInsensitiveAscending; + else { + if (sname.endsWith(":")) + sname = sname.substring(0, sname.length()-1); + sel = new NSSelector(sname, new Class[]{ Object.class }); + } + return new EOSortOrdering(k, sel); + } + + public void encodeWithKeyValueArchiver(EOKeyValueArchiver arch) { + arch.encodeObject("EOSortOrdering", "class"); + arch.encodeObject(key(), "key"); + if (selector.equals(CompareAscending)) + arch.encodeObject("compareAscending:", "selectorName"); + else if (selector.equals(CompareDescending)) + arch.encodeObject("compareDescending:", "selectorName"); + else if (selector.equals(CompareCaseInsensitiveAscending)) + arch.encodeObject("compareCaseInsensitiveAscending:", "selectorName"); + else if (selector.equals(CompareCaseInsensitiveAscending)) + arch.encodeObject("compareCaseInsensitiveDescending:", "selectorName"); + else + arch.encodeObject(selector.name() + ":", "selectorName"); + } + + private static class CompareAscendingComparator + extends NSSelector + { + public int compare(Object o1, Object o2) + { + if ( o1 instanceof Comparable ) + { + if ( o2 instanceof Comparable ) + { + return ((Comparable)o1).compareTo( o2 ); + } + } + + // null handling: null is less than any object + + if ( o1 == null ) + { + if ( o2 == null ) + { + return 0; + } + else + { + return -1; + } + } + else // o1 != null + if ( o2 == null ) + { + return 1; + } + + // fall back on string representation comparison + + return o1.toString().compareTo( o2.toString() ); + } + + public boolean equals(Object obj) + { + return ( this == obj ); + } + } + + private static class CompareDescendingComparator + extends CompareAscendingComparator + { + public int compare(Object o1, Object o2) + { + return -1 * super.compare( o1, o2 ); + } + } + + private static class CompareCaseInsensitiveAscendingComparator + extends NSSelector + { + public int compare(Object o1, Object o2) + { + // null handling: null is less than any object + + if ( o1 == null ) + { + if ( o2 == null ) + { + return 0; + } + else + { + return -1; + } + } + else // o1 != null + if ( o2 == null ) + { + return 1; + } + + return o1.toString().toLowerCase().compareTo( + o2.toString().toLowerCase() ); + } + + public boolean equals(Object obj) + { + return ( this == obj ); + } + } + + private static class CompareCaseInsensitiveDescendingComparator + extends CompareCaseInsensitiveAscendingComparator + { + public int compare(Object o1, Object o2) + { + return -1 * super.compare( o1, o2 ); + } + } + + private static class DelegatingComparator implements Comparator + { + private String key; + private Comparator comparator; + + public DelegatingComparator( String aKey, Comparator aComparator ) + { + key = aKey; + comparator = aComparator; + } + + public int compare(Object o1, Object o2) + { + Object v1, v2; + if ( o1 instanceof EOKeyValueCoding ) + { + v1 = ((EOKeyValueCoding)o1).valueForKey( key ); + } + else + { + v1 = EOKeyValueCodingSupport.valueForKey( o1, key ); + } + if ( o2 instanceof EOKeyValueCoding ) + { + v2 = ((EOKeyValueCoding)o2).valueForKey( key ); + } + else + { + v2 = EOKeyValueCodingSupport.valueForKey( o2, key ); + } + return comparator.compare( v1, v2 ); + } + + public boolean equals(Object obj) + { + return ( this == obj ); + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.14 2003/08/11 19:39:52 chochos + * now encodes/decodes correctly. + * + * Revision 1.13 2003/08/09 01:29:56 chochos + * implements EOKeyValueArchiving + * + * Revision 1.12 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.11 2003/02/07 20:23:23 mpowers + * Provided backwards compatibility for comparators. + * + * Revision 1.10 2003/01/18 23:46:58 mpowers + * EOSortOrdering is now correctly using NSSelectors. + * + * Revision 1.9 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.8 2002/03/01 20:23:27 mpowers + * Implemented equals. + * + * Revision 1.7 2002/02/06 21:15:04 mpowers + * Added null handling to CompareCaseInsensitiveAscendingComparator. + * + * Revision 1.6 2001/12/01 23:51:24 mpowers + * Made serializable. + * + * Revision 1.5 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.4 2001/01/24 22:15:52 mpowers + * Fixed npe when comparing nulls. + * + * Revision 1.3 2001/01/12 19:11:56 mpowers + * Fixed table column click sorting. + * + * Revision 1.2 2001/01/12 17:23:46 mpowers + * Inner classes are now private. + * + * Revision 1.1 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOTemporaryGlobalID.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOTemporaryGlobalID.java new file mode 100644 index 0000000..44ba855 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOTemporaryGlobalID.java @@ -0,0 +1,219 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.net.InetAddress; + +/** +* EOTemporaryGlobalID is a network-wide unique key. +* This is used by EOEditingContext to construct temporary +* ids when new objects are created.

+* +* The specified format of the key is a byte array: +* < Sequence [2], ProcessID [2], Time [4], IP Addr [4] >, +* but because java does not allow access to the process id, +* the timestamp of when this class is first loaded is used +* to simulate a process id. +* +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOTemporaryGlobalID + extends EOGlobalID +{ + /** + * Holds the length in bytes of the key that is generated. + */ + public static final int UniqueBinaryKeyLength = 12; + + private static int sequence; + private static byte[] processid; // 2 bytes + private static byte[] ipaddr; // 4 bytes + + static // static initializer + { + // init sequence + sequence = 0; + + // init processid + processid = new byte[2]; + long time = System.currentTimeMillis(); + processid[1] = (byte) time; + time = time >> 8; + processid[0] = (byte) time; + + // init ipaddr + ipaddr = new byte[4]; + try + { + ipaddr = InetAddress.getLocalHost().getAddress(); + } + catch ( Exception exc ) + { + // could not obtain ip address - use pid twice + ipaddr[0] = processid[0]; + ipaddr[1] = processid[1]; + ipaddr[2] = processid[0]; + ipaddr[3] = processid[1]; + } + } + + private byte[] key; + private int hashCode; + + /** + * Generates a new id with a unique key. + */ + public EOTemporaryGlobalID() + { + key = new byte[UniqueBinaryKeyLength]; + + // init sequence (important byte first) + key[0] = (byte) ( sequence ); + key[1] = (byte) ( sequence >> 8 ); + sequence++; + + // populate pid (important byte first) + key[2] = processid[1]; + key[3] = processid[0]; + + // init time (important byte first) + long time = System.currentTimeMillis(); + key[4] = (byte) time; + time = time >> 8; + key[5] = (byte) time; + time = time >> 8; + key[6] = (byte) time; + time = time >> 8; + key[7] = (byte) time; + + // populate ipaddr + key[8] = ipaddr[0]; + key[9] = ipaddr[1]; + key[10] = ipaddr[2]; + key[11] = ipaddr[3]; + + // use string's hash code + hashCode = new String( key ).hashCode(); + } + + /** + * Private constructor for cloning. + */ + private EOTemporaryGlobalID( byte[] aKey ) + { +// key = aKey; // this might be faster + + // make copy of key - might be safer + key = new byte[ UniqueBinaryKeyLength ]; + for ( int i = 0; i < UniqueBinaryKeyLength; i++ ) + { + key[i] = aKey[i]; + } + + // use string's hash code + hashCode = new String( aKey ).hashCode(); + } + + /** + * Returns true. + */ + public boolean isTemporary() + { + return true; + } + + /** + * Returns whether the keys are equal. + */ + public boolean equals( Object anObject ) + { + if ( ! ( anObject instanceof EOTemporaryGlobalID ) ) + return false; + + byte[] otherKey = ((EOTemporaryGlobalID)anObject).key; + + for ( int i = 0; i < UniqueBinaryKeyLength; i++ ) + { + if ( key[i] != otherKey[i] ) return false; + } + return true; + } + + /** + * Returns a copy of this object. + */ + public Object clone() + { + // faster than super.clone() + return new EOTemporaryGlobalID( key ); + } + + public int hashCode() + { + return hashCode; + } + + /** + * Returns a string representation of this key. + * This is a 24-character string with each pair + * of characters holding a hexadecimal value that + * is 128 more than the value of the corresponding + * byte (to account for two's complement). + */ + public String toString() + { + String hex; + StringBuffer buffer = new StringBuffer(); + for ( int i = 0; i < key.length; i++ ) + { + // get string: adjust for two's complement + hex = Integer.toHexString( key[i]+128 ); + // pad with zero so we take two characters + if ( hex.length() == 1 ) hex = "0" + hex; + // append hex code + buffer.append( hex ); + } + return buffer.toString(); + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.3 2001/02/15 21:13:30 mpowers + * First draft implementation is complete. Now on to debugging. + * + * Revision 1.2 2001/02/14 23:03:02 mpowers + * A near-complete first draft of EOEditingContext. + * + * Revision 1.1 2001/02/13 23:24:29 mpowers + * Implementing more of editing context. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOValidation.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOValidation.java new file mode 100644 index 0000000..a0aa4db --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOValidation.java @@ -0,0 +1,72 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* EOValidation provides methods for validating a operation +* on an object as a whole, rather than on an individual property. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface EOValidation +{ + /** + * Validates this object for delete. + * Throws an exception if this object cannot be deleted. + */ + void validateForDelete(); + + /** + * Validates this object for insertion into the external store. + * Throws an exception if this object cannot be inserted. + * Validations here should be specific to insertion. + * Implementations may call validateForSave(). + */ + void validateForInsert(); + + /** + * Validates this object for a commit to the external store. + * Throws an exception if this object cannot be committed. + * Validations here are not specific to either inserts or updates. + */ + void validateForSave(); + + /** + * Validates this object for update to the external store. + * Throws an exception if this object cannot be updated. + * Validations here should be specific to updates. + * Implementations may call validateForSave(). + */ + void validateForUpdate(); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/11/13 04:13:59 mpowers + * Added interfaces needed to begin work on EOCustomObject. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOVectorKeyGlobalID.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOVectorKeyGlobalID.java new file mode 100644 index 0000000..cc91fd7 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EOVectorKeyGlobalID.java @@ -0,0 +1,79 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2001 Michael Powers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ +package net.wotonomy.control; + +/** +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class EOVectorKeyGlobalID extends EOKeyGlobalID { + + protected Object[] _keyValues; + + public EOVectorKeyGlobalID(String entityName, Object[] values) { + super(entityName, 0); + _keyValues = new Object[values.length]; + for (int i = 0; i < values.length; i++) { + _keyValues[i] = values[i]; + } + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#keyValuesNoCopy() + */ + public Object[] _keyValuesNoCopy() { + return _keyValues; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#_keyValues() + */ + public Object[] keyValues() { + Object[] v = new Object[_keyValues.length]; + for (int i = 0; i < _keyValues.length; i++) { + v[i] = _keyValues[i]; + } + return v; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOKeyGlobalID#keyCount() + */ + public int keyCount() { + return _keyValues.length; + } + + /* (non-Javadoc) + * @see net.wotonomy.control.EOGlobalID#isTemporary() + */ + public boolean isTemporary() { + return false; + } + +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/19 01:59:01 chochos + * Added the wotonomy headers + * + */ diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EditingContext.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EditingContext.java new file mode 100644 index 0000000..ee40f23 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/EditingContext.java @@ -0,0 +1,283 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.List; +import java.util.Map; + +// swing dependency for undo manager +//import javax.swing.undo.UndoManager; + +/** +* EditingContext provides transactional support for +* fetching, editing, and committing changes made on a +* collection of objects to a parent object store. +* This subclasses EOEditingContext to provide +* java-friendly conveniences. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class EditingContext extends EOEditingContext +{ + /** + * Default constructor creates a new editing context + * that uses the default object store. If the default + * object store has not been set, an exception is thrown. + */ + public EditingContext() + { + this( defaultParentObjectStore() ); + } + + /** + * Creates a new editing context that uses the specified + * object store as its parent object store. + */ + public EditingContext( EOObjectStore anObjectStore ) + { + super( anObjectStore ); + } + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship, or may return a placeholder array that + * will defer the fetch until needed (aka an array fault). + * All objects must be registered in the specified editing context. + * This implementation calls to its parent object store's + * implementation if the requested source object is not + * registered in this editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public List getArrayFaultWithSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationshipKey, + EOEditingContext aContext ) + { + return arrayFaultWithSourceGlobalID( + aGlobalID, aRelationshipKey, aContext ); + } + + /** + * Returns a snapshot of the specified object as it + * existed when it was last read or committed to the + * parent object store. + */ + public Map getCommittedSnapshotForObject ( + Object anObject ) + { + return committedSnapshotForObject( anObject ); + } + + /** + * Returns a snapshot of the specified object as it + * existed before the edits triggered by the current + * event loop were processed. + */ + public Map getCurrentEventSnapshotForObject ( + Object anObject ) + { + return currentEventSnapshotForObject( anObject ); + } + + /** + * Returns the delegate for this editing context, + * or null if no delegate has been set. + */ + public Object getDelegate () + { + return delegate(); + } + + /** + * Returns a List of all objects marked as deleted + * in this editing context. + */ + public List getDeletedObjects () + { + return deletedObjects(); + } + + /** + * Returns a List of registered editors of this + * editing context. + */ + public List getEditors() + { + return editors(); + } + + /** + * Returns the object for the specified id. + * If the object's data has not been fetched, + * it will be fetched when needed. + */ + public Object getFaultForGlobalID ( + EOGlobalID aGlobalID ) + { + return faultForGlobalID( aGlobalID, this ); + } + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. + */ + public Object getFaultForRawRow ( + Map aDictionary, + String anEntityName ) + { + return faultForRawRow( aDictionary, anEntityName ); + } + + /** + * Returns the fetch timestamp for this editing context. + */ + public double getFetchTimestamp() + { + return fetchTimestamp(); + } + + /** + * Returns the id for the specified object, or null + * if the object is not registered in this context. + */ + public EOGlobalID getGlobalIDForObject ( + Object anObject ) + { + return globalIDForObject( anObject ); + } + + /** + * Returns a List of the objects that have been + * inserted into this editing context. + */ + public List getInsertedObjects () + { + return insertedObjects(); + } + + /** + * Returns the message handler for this editing context, + * or null if no message handler has been set. + */ + public Object getMessageHandler () + { + return messageHandler(); + } + + /** + * Returns the object registered in this editing context + * for the specified id, or null if that id is not + * registered. + */ + public Object getObjectForGlobalID ( + EOGlobalID aGlobalID ) + { + return objectForGlobalID( aGlobalID ); + } + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. + */ + public List getObjectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec ) + { + return objectsWithFetchSpecification( aFetchSpec ); + } + + /** + * Returns the parent object store for this editing context. + * The result will not be null. + */ + public EOObjectStore getParentObjectStore () + { + return parentObjectStore(); + } + + /** + * Returns a List of all objects registered in this + * editing context. + */ + public List geRegisteredObjects () + { + return registeredObjects(); + } + + /** + * Returns the root object store, which is the parent + * of all parent object stores of this editing context. + */ + public EOObjectStore getRootObjectStore () + { + return rootObjectStore(); + } + + /** + * Returns a list of all objects marked as modified, + * but not inserted or deleted, in this editing context. + */ + public List getUpdatedObjects () + { + return updatedObjects(); + } + + // static methods + + public static double getDefaultFetchTimestampLag() + { + return defaultFetchTimestampLag(); + } + + /** + * Returns the default parent object store for all + * object stores created with the parameterless + * constructor. + */ + public static EOObjectStore getDefaultParentObjectStore() + { + return defaultParentObjectStore(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2002/03/26 21:46:36 mpowers + * Contributing EditingContext as a java-friendly convenience. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java new file mode 100644 index 0000000..1aa2147 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/KeyValueCodingUtilities.java @@ -0,0 +1,740 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.internal.Duplicator; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* KeyValueCodingUtilities implements what +* EOKeyValueCodingSupport leaves out. Importantly, +* this class implements the deep clone and deep copy +* operations that are essential to the functioning of +* nested editing contexts. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 900 $ +*/ +public class KeyValueCodingUtilities +{ + /** + * Returns a Map of the specified keys to their values, + * each of which is obtained by calling valueForKey + * on the specified object if it implements EOKeyValueCoding, + * and otherwise falling back on EOKeyValueCodingSupport. + * Null values must be represented by NSNull.nullValue(). + */ + static public NSDictionary valuesForKeys( + Object anObject, List aKeyList ) + { + return valuesForKeys( anObject, aKeyList, false ); + } + + /** + * Returns a Map of the specified keys to their values, + * each of which is obtained by calling storedValueForKey + * on the specified object if it implements EOKeyValueCoding, + * and otherwise falling back on EOKeyValueCodingSupport. + * Null values must be represented by NSNull.nullValue(). + */ + static public NSDictionary storedValuesForKeys( + Object anObject, List aKeyList ) + { + return valuesForKeys( anObject, aKeyList, true ); + } + + /** + * Called by valuesForKeys and storedValuesForKeys. + * This uses storedValueForKey if isStored is true, + * otherwise uses valueForKey. + */ + static private NSDictionary valuesForKeys( + Object anObject, List aKeyList, boolean isStored ) + { + EOKeyValueCoding coding; + if ( anObject instanceof EOKeyValueCoding ) + { + coding = (EOKeyValueCoding) anObject; + } + else + { + coding = null; + } + + String key; + Object value; + NSMutableDictionary result = new NSMutableDictionary(); + Iterator it = aKeyList.iterator(); + while ( it.hasNext() ) + { + //TODO: get rid of this try/catch - exceptions should be fatal (?) + try + { + key = it.next().toString(); + if ( coding != null ) + { + if ( isStored ) + value = coding.storedValueForKey( key ); + else + value = coding.valueForKey( key ); + } + else + { + if ( isStored ) + value = EOKeyValueCodingSupport.storedValueForKey( anObject, key ); + else + value = EOKeyValueCodingSupport.valueForKey( anObject, key ); + } + if ( value == null ) + { + value = EONullValue.nullValue(); + } + result.setObjectForKey( value, key ); + } + catch ( RuntimeException exc ) + { + System.out.println( + "KeyValueCodingUtilities.valuesForKeys: " + + isStored + " : " + exc ); + } + } + return result; + } + + /** + * Takes the keys from the specified Map as properties + * and applies the corresponding values, each of which + * might be set by calling takeValueForKey on the + * specified object if it implements EOKeyValueCoding, + * and otherwise falling back on EOKeyValueCodingSupport. + * Null values must be represented by NSNull.nullValue(). + */ + static public void takeValuesFromDictionary( + Object anObject, Map aMap ) + { + takeStoredValuesFromDictionary( anObject, aMap, false ); + } + + /** + * Takes the keys from the specified Map as properties + * and applies the corresponding values, each of which + * might be set by calling takeStoredValueForKey on the + * specified object if it implements EOKeyValueCoding, + * and otherwise falling back on EOKeyValueCodingSupport. + * Null values must be represented by NSNull.nullValue(). + */ + static public void takeStoredValuesFromDictionary( + Object anObject, Map aMap ) + { + takeStoredValuesFromDictionary( anObject, aMap, true ); + } + + /** + * Called by takeValuesFromDictionary and takeStoredValuesFromDictionary. + * This uses takeStoredValueForKey if isStored is true, + * otherwise uses takeValueForKey. + */ + static private void takeStoredValuesFromDictionary( + Object anObject, Map aMap, boolean isStored ) + { + EOKeyValueCoding coding; + if ( anObject instanceof EOKeyValueCoding ) + { + coding = (EOKeyValueCoding) anObject; + } + else + { + coding = null; + } + + String key; + Object value; + NSMutableDictionary result = new NSMutableDictionary(); + Iterator it = aMap.keySet().iterator(); + while ( it.hasNext() ) + { + //TODO: get rid of this try/catch - exceptions should be fatal (?) + try + { + key = it.next().toString(); + value = aMap.get( key ); + if ( value instanceof EONullValue ) + // can't use == nullValue() because of cloning/serialization + { + value = null; + } + if ( coding != null ) + { + if ( isStored ) + coding.takeStoredValueForKey( value, key ); + else + coding.takeValueForKey( value, key ); + } + else + { + if ( isStored ) + EOKeyValueCodingSupport.takeStoredValueForKey( + anObject, value, key ); + else + EOKeyValueCodingSupport.takeValueForKey( + anObject, value, key ); + } + } + catch ( WotonomyException exc ) + { + System.out.println( + "KeyValueCodingUtilities.takeStoredValuesFromDictionary: " + + isStored + " : " + exc ); + } + } + } + + /** + * Creates a deep clone of the specified object. + * (Object.clone() only creates a shallow clone.) + * Returns null if operation fails. + */ + static public Object clone( Object aSource ) + { + return Duplicator.deepClone( aSource ); + } + + /** + * Creates a deep clone of the specified object, + * registered in the specified source editing context, + * transposing it into the specified destination + * editing context. + * Returns null if operation fails. + */ + static public Object clone( + EOEditingContext aSourceContext, Object aSource, + EOEditingContext aDestinationContext ) + { + return clone( aSourceContext, aSource, aDestinationContext, aSource ); + } + + /** + * Called by clone and copy. + * The specified root object will not be replaced + * by an object in the destination editing context: + * this should be the same as the source object for + * cloning, but should be null for copying. + * Returns null if operation fails. + */ + static private Object clone( + EOEditingContext aSourceContext, Object aSource, + EOEditingContext aDestinationContext, + Object aRootObject ) + { + +//System.out.println(); +//System.out.println( "clone: " + aSourceContext ); +//System.out.println( " : " + aSource ); +//System.out.println( " : " + aDestinationContext ); +//System.out.println(); + + // the only known way to deep copy in + // java without native code is serialization + + return thaw( + freeze( aSource, aSourceContext, aRootObject, true ), + aDestinationContext, true ); + } + + /** + * Serializes an object to a byte array containing + * GlobalIDMarkers in place of references to other objects + * registered in the specified context. + * The specified root object will be serialized, + * even if it is registered in the specified context: + * this is typically the root object you're trying to + * serialize. + * Package access, as this method is used by editing + * context for snapshots. + */ + static public byte[] freeze( + Object anObject, EOEditingContext aContext, Object aRootObject, boolean transpose ) + { + try + { +//long t = System.currentTimeMillis(); + ByteArrayOutputStream byteOutput = + new ByteArrayOutputStream();// CloneBufferSize ); + ObjectOutputStream objectOutput; + if ( transpose ) + { + objectOutput = + new TransposingContextObjectOutputStream( + byteOutput, aContext, aRootObject ); + } + else + { + objectOutput = + new ContextObjectOutputStream( + byteOutput, aContext ); + } + + objectOutput.writeObject( anObject ); + objectOutput.flush(); + objectOutput.close(); + + return byteOutput.toByteArray(); + +// profiling +/* +byte[] result = byteOutput.toByteArray(); +long size = result.length; +long time = ( System.currentTimeMillis() - t ); +maxSize = Math.max( size, maxSize ); +minSize = Math.min( size, minSize ); +totSize += size; +maxTime = Math.max( time, maxTime ); +minTime = Math.min( time, minTime ); +totTime += time; +nTime++; +System.out.println( "freeze: size = [ " + size + " : " + minSize + " : " + ( (float)totSize / (float)nTime ) + " : " + maxSize ++ " ] time = [ " + time + " : " + minTime + " : " + ( (float)totTime / (float)nTime ) + " : " + maxTime + " ]" ); +return result; +*/ +// end profiling + + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + +//static long maxTime, minTime, totTime, nTime, maxSize, minSize, totSize; +//static long maxTimeThaw, minTimeThaw, totTimeThaw, nTimeThaw; + + /** + * De-serializes an object from the specified byte + * array, replacing GlobalIDMarkers with reference + * to objects registered in the specified editing + * context. + * Package access, as this method is used by editing + * context for snapshots. + */ + static public Object thaw( + byte[] aByteArray, EOEditingContext aContext, boolean transpose ) + { + return thaw( aByteArray, aContext, null, transpose ); + } + + /** + * De-serializes an object from the specified byte + * array, replacing GlobalIDMarkers with reference + * to objects registered in the specified editing + * context. + * Package access, as this method is used by editing + * context for snapshots. + */ + static public Object thaw( + byte[] aByteArray, EOEditingContext aContext, ClassLoader aLoader, boolean transpose ) + { + try + { +//long t = System.currentTimeMillis(); + ByteArrayInputStream byteInput = + new ByteArrayInputStream( aByteArray ); + ObjectInputStream objectInput; + if ( transpose ) + { + objectInput = + new TransposingContextObjectInputStream( + byteInput, aContext, aLoader ); + } + else + { + objectInput = + new ContextObjectInputStream( + byteInput, aContext, aLoader ); + } + + return objectInput.readObject(); +// profiling +/* +Object result = objectInput.readObject(); +long timeThaw = ( System.currentTimeMillis() - t ); +maxTimeThaw = Math.max( timeThaw, maxTimeThaw ); +minTimeThaw = Math.min( timeThaw, minTimeThaw ); +totTimeThaw += timeThaw; +nTimeThaw++; +System.out.println( "thaw: size = " + aByteArray.length + ", time = [ " + timeThaw + " : " + minTimeThaw + " : " + ( (float)totTimeThaw / (float)nTimeThaw ) + " : " + maxTimeThaw + " ]" ); +return result; +*/ +// end profiling + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + /** + * Copies values from one object registered in the + * specified origin context to the specified destination + * object + * The values themselves are cloned, so this is a deep copy. + * Returns the destination object, or throws exception + * if operation fails. + */ + static public Object copy( Object aSource, Object aDestination ) + { + NSDictionary values = (NSDictionary) + clone( valuesForKeys( aSource, + EOClassDescription.classDescriptionForClass( + aSource.getClass() ).attributeKeys() ) ); + + takeStoredValuesFromDictionary( aDestination, values ); + return aDestination; + } + + /** + * Copies values from one object registered in the + * specified origin context to the specified destination + * object + * The values themselves are cloned, so this is a deep copy. + * Returns the destination object, or throws exception + * if operation fails. + */ + static public Object copy( + EOEditingContext aSourceContext, Object aSource, + EOEditingContext aDestinationContext, Object aDestination ) + { + // get all keys for this object + EOClassDescription classDesc = + EOClassDescription.classDescriptionForClass( aSource.getClass() ); + List keys = new LinkedList(); + keys.addAll( classDesc.attributeKeys() ); + keys.addAll( classDesc.toOneRelationshipKeys() ); + keys.addAll( classDesc.toManyRelationshipKeys() ); + + // transpose all objects registered in source context + NSDictionary values = storedValuesForKeys( aSource, keys ); + values = (NSDictionary) + clone( aSourceContext, values, aDestinationContext, null ); + + // apply to destination object + takeStoredValuesFromDictionary( aDestination, values ); + return aDestination; + } + + // inner classes + + /** + * An ObjectOutputStream that serializes objects with references + * to an editing context. The specified context will not be + * serialized but referenced, so that a ContextObjectInputStream + * can replace the reference with another editing context. + */ + static private class ContextObjectOutputStream extends ObjectOutputStream + { + private EditingContextMarker marker = new EditingContextMarker(); + protected EOEditingContext editingContext; + + /** + * Specifies the output stream to wrap, + * and the source context that should be + * referenced but not serialized. + */ + public ContextObjectOutputStream( + OutputStream anOutputStream, + EOEditingContext aContext ) + throws IOException + { + super( anOutputStream ); + editingContext = aContext; + try + { + enableReplaceObject(true); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + protected Object replaceObject(Object anObject) throws IOException + { +// if ( anObject == editingContext ) return marker; +//FIXME: this should be more strict as above + if ( anObject instanceof EOEditingContext ) return marker; + return anObject; + } + + } + + /** + * A ContextObjectOutputStream that replaces any objects registered + * in the source editing context with markers to be used in + * ContextObjectInputStream. + */ + static private class TransposingContextObjectOutputStream + extends ContextObjectOutputStream + { + protected Object rootObject; + + /** + * Specifies the output stream to wrap, + * the source context containing objects that + * should be replaced if found, + * and the object which should not be re-registered, + * which is typically the object being cloned, but + * may be null. + */ + public TransposingContextObjectOutputStream( + OutputStream anOutputStream, + EOEditingContext aContext, + Object anObject ) + throws IOException + { + super( anOutputStream, aContext ); + rootObject = anObject; + } + + protected Object replaceObject(Object anObject) throws IOException + { + if ( anObject == rootObject ) return anObject; + if ( editingContext != null ) + { + EOGlobalID id = editingContext.globalIDForObject( anObject ); + if ( id != null ) + { + Object result = new GlobalIDMarker( id ); + //System.out.println( "KeyValueCodingUtilities.replaceObject: returning: " + result ); + return result; + } + } + return super.replaceObject( anObject ); + } + + } + + /** + * A marker class so references to objects registered in editing + * contexts get transposed rather than cloned. + */ + static private class GlobalIDMarker implements Serializable + { + private EOGlobalID id; + + public GlobalIDMarker( EOGlobalID anID ) + { + id = anID; + } + + public EOGlobalID getID() + { + return id; + } + + public String toString() + { + return "[GlobalIDMarker:"+id+"]"; + } + } + + /** + * A marker class so references an object's editing context + * gets transposed rather than cloned. + */ + static private class EditingContextMarker implements Serializable + { + // just a marker class - no implementation necessary + } + + /** + * An ObjectInputStream that replaces any markers from + * ContextObjectOutputStream with objects registered + * in the destination editing context. + */ + static private class ContextObjectInputStream extends ObjectInputStream + { + protected EOEditingContext editingContext; + protected ClassLoader classLoader; + + /** + * Specifies the output stream to wrap, + * the source context containing objects that + * should be to replace any markers. + * The class loader may be null. + */ + public ContextObjectInputStream( + InputStream anInputStream, + EOEditingContext aContext, + ClassLoader aClassLoader ) + throws IOException + { + super( anInputStream ); + editingContext = aContext; + classLoader = aClassLoader; + if ( classLoader == null ) + { + classLoader = + KeyValueCodingUtilities.class.getClassLoader(); + } + try + { + enableResolveObject(true); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + } + + protected Object resolveObject(Object anObject) throws IOException + { + if ( anObject instanceof EditingContextMarker ) + { + return editingContext; + } + return anObject; + } + + protected Class resolveClass(ObjectStreamClass v) + throws IOException, ClassNotFoundException + { + return classLoader.loadClass( v.getName() ); + } + } + + /** + * A ContextObjectInputStream that replaces any markers from + * TransposingContextObjectOutputStream with objects registered + * in the destination editing context. + */ + static private class TransposingContextObjectInputStream + extends ContextObjectInputStream + { + /** + * Specifies the output stream to wrap, + * the source context containing objects that + * should be to replace any markers. + */ + public TransposingContextObjectInputStream( + InputStream anInputStream, + EOEditingContext aContext, + ClassLoader aClassLoader ) + throws IOException + { + super( anInputStream, aContext, aClassLoader ); + } + + protected Object resolveObject(Object anObject) throws IOException + { + if ( anObject instanceof GlobalIDMarker ) + { + return editingContext.faultForGlobalID( + ((GlobalIDMarker)anObject).getID(), editingContext ); + } + return super.resolveObject( anObject ); + } + } + +} + +/* + * $Log$ + * Revision 1.3 2006/02/18 22:46:44 cgruber + * Add Surrogate map from .util into control's internal package, and fix imports. + * + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.15 2003/01/21 22:30:10 mpowers + * thaw() now allows you to pass in a class loader. + * + * Revision 1.14 2002/05/15 13:46:35 mpowers + * Exposed freeze and thaw as public. + * + * Revision 1.13 2001/08/22 19:25:13 mpowers + * Added (and commented out) profiling code for freeze. + * + * Revision 1.12 2001/05/06 18:27:10 mpowers + * More broadly catching editing contexts for now. + * + * Revision 1.11 2001/05/05 13:18:49 mpowers + * Fixed: transposing output stream was not returning the object to replace. + * + * Revision 1.10 2001/05/04 16:57:56 mpowers + * Now correctly transposing references to editing contexts when + * cloning/copying between editing contexts. + * + * Revision 1.9 2001/05/04 14:42:58 mpowers + * Now getting stored values in KeyValueCoding. + * MasterDetail now marks dirty based on whether it's an attribute + * or relation. + * Implemented editing context marker. + * + * Revision 1.8 2001/05/02 15:47:40 mpowers + * Fixed the pernicious problem with reverts: recordObject was recording + * a snapshot of the clone before the transposition-copy happened, + * so the revert object would lose all of its transposed relationships. + * + * Revision 1.7 2001/04/30 12:33:17 mpowers + * Fixed problem with use of EONullValue.nullValue(), which can't be used + * when we're serializably duplicating objects. + * + * Revision 1.6 2001/04/30 02:14:25 mpowers + * Copying should call takeStoredValueForKeys. + * + * Revision 1.5 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.4 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.3 2001/04/28 16:18:44 mpowers + * Implementing relationships. + * + * Revision 1.2 2001/04/28 14:12:23 mpowers + * Refactored cloning/copying into KeyValueCodingUtilities. + * + * Revision 1.1 2001/04/27 23:41:12 mpowers + * Contributing file for KeyValueCodingUtilities. + * + * + */ + + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ObservableArray.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ObservableArray.java new file mode 100644 index 0000000..19d39ff --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/ObservableArray.java @@ -0,0 +1,346 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSRange; + +/** +* A package class that extends NSMutableArray but makes use +* of the fact that wotonomy's implementation extends ArrayList +* to intercept insertions and deletion and register and +* unregister objects for change notifications as appropriate. +* Since we can't be sure of ArrayList's implementation, we're +* forced to override each and every add and remove method, +* some of which probably call each other. However, +* EOObserverCenter will only register us once per object. +*/ +class ObservableArray extends NSMutableArray +{ + EOObserving observer; + + ObservableArray( EOObserving anObserver ) + { + observer = anObserver; + } + + /** + * Removes the last object from the array. + */ + public void removeLastObject () + { + remove( count() - 1 ); + } + + /** + * Removes the object at the specified index. + */ + public void removeObjectAtIndex (int index) + { + remove( index ); + } + + /** + * Adds all objects in the specified collection. + */ + public void addObjectsFromArray (Collection aCollection) + { + addAll( aCollection ); + } + + /** + * Removes all objects from the array. + */ + public void removeAllObjects () + { + clear(); + } + + /** + * Removes all objects equivalent to the specified object + * within the range of specified indices. + */ + public void removeObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( get( i ) ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all instances of the specified object within the + * range of specified indices, comparing by reference. + */ + public void removeIdenticalObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == get( i ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all objects in the specified collection from the array. + */ + public void removeObjectsInArray (Collection aCollection) + { + removeAll( aCollection ); + } + + /** + * Removes all objects in the indices within the specified range + * from the array. + */ + public void removeObjectsInRange (NSRange aRange) + { + if ( aRange == null ) return; + + for ( int i = 0; i < aRange.length(); i++ ) + { + remove( aRange.location() ); + } + } + + /** + * Replaces objects in the current range with objects from + * the specified range of the specified array. If currentRange + * is larger than otherRange, the extra objects are removed. + * If otherRange is larger than currentRange, the extra objects + * are added. + */ + public void replaceObjectsInRange (NSRange currentRange, + List otherArray, NSRange otherRange) + { + if ( ( currentRange == null ) || ( otherArray == null ) || + ( otherRange == null ) ) return; + + // transform otherRange if out of bounds for array + if ( otherRange.maxRange() > otherArray.size() ) + { + // TODO: Test this logic. + int loc = Math.min( otherRange.location(), otherArray.size() - 1 ); + otherRange = new NSRange( loc, otherArray.size() - loc ); + } + + Object o; + List subList = subList( + currentRange.location(), currentRange.maxRange() ); + int otherIndex = otherRange.location(); + // TODO: Test this logic. + for ( int i = 0; i < subList.size(); i++ ) + { + if ( otherIndex < otherRange.maxRange() ) + { // set object + subList.set( i, otherArray.get( otherIndex ) ); + } + else + { // remove extra elements from currentRange + subList.remove( i ); + i--; + } + otherIndex++; + } + // TODO: Test this logic. + for ( int i = otherIndex; i < otherRange.maxRange(); i++ ) + { + add( otherArray.get( i ) ); + } + } + + /** + * Clears the current array and then populates it with the + * contents of the specified collection. + */ + public void setArray (Collection aCollection) + { + clear(); + addAll( aCollection ); + } + + /** + * Removes all objects equivalent to the specified object. + */ + public void removeObject (Object anObject) + { + remove( anObject ); + } + + /** + * Removes all occurences of the specified object, + * comparing by reference. + */ + public void removeIdenticalObject (Object anObject) + { + EOObserverCenter.removeObserver( observer, anObject ); + super.removeIdenticalObject( anObject ); + } + + /** + * Inserts the specified object into this array at the + * specified index. + */ + public void insertObjectAtIndex (Object anObject, int anIndex) + { + add( anIndex, anObject ); + } + + /** + * Replaces the object at the specified index with the + * specified object. + */ + public void replaceObjectAtIndex (int anIndex, Object anObject) + { + set( anIndex, anObject ); + } + + /** + * Adds the specified object to the end of this array. + */ + public void addObject (Object anObject) + { + add( anObject ); + } + + // interface List: mutators + + public void add(int index, Object element) + { + EOObserverCenter.addObserver( observer, element ); + super.add( index, element ); + } + + public boolean add(Object o) + { + EOObserverCenter.addObserver( observer, o ); + return super.add(o); + } + + public boolean addAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll( index, c ); + } + + public void clear() + { + Iterator it = iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + super.clear(); + } + + public Object remove(int index) + { + EOObserverCenter.removeObserver( observer, get(index) ); + return super.remove( index ); + } + + public boolean remove(Object o) + { + EOObserverCenter.removeObserver( observer, o ); + return super.remove(o); + } + + public boolean removeAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + return super.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + throw new UnsupportedOperationException(); + } + + public Object set(int index, Object element) + { + EOObserverCenter.removeObserver( observer, get(index) ); + EOObserverCenter.addObserver( observer, element ); + return super.set( index, element ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2002/10/24 21:15:35 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.1 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.1 2001/01/24 14:37:24 mpowers + * Contributing a delegate useful for debugging. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/OrderedDataSource.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/OrderedDataSource.java new file mode 100644 index 0000000..8b88615 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/OrderedDataSource.java @@ -0,0 +1,57 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +/** +* A simple extension of EODataSource that +* allows for indexed insertion. The wotonomy +* implementation of EODisplayGroup supports +* this and will use it if possible. This is +* useful for classes like the PropertyDataSource, +* where the ordering of items in an indexed +* property may be important. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public abstract class OrderedDataSource extends EODataSource +{ + /** + * Inserts the specified object into this data source, + * at the specified index. + */ + public abstract void insertObjectAtIndex ( + Object anObject, int anIndex ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/01/24 14:10:53 mpowers + * Contributing OrderedDataSource, and PropertyDataSource extends it. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/PropertyDataSource.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/PropertyDataSource.java new file mode 100644 index 0000000..76f7219 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/PropertyDataSource.java @@ -0,0 +1,549 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control; + +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeSet; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* A data source that reads and writes to an indexed +* property of a java object. This class is used by +* MasterDetailAssociation to retreive objects from +* the master display group. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 894 $ +*/ +public class PropertyDataSource extends OrderedDataSource +{ + protected Object source; + protected String key; + protected Class lastKnownType; // for best-guessing + protected EOClassDescription classDesc; + protected EOEditingContext context; + + /** + * Creates a new PropertyDataSource with no editing context + * and will try to guess the appropriate class description + * when trying to create objects. + */ + public PropertyDataSource() + { + this( null, (EOClassDescription) null ); + } + + /** + * Creates a new PropertyDataSource that uses the specified + * editing context, but will try to guess the appropriate + * class description when trying to create objects. + */ + public PropertyDataSource( EOEditingContext aContext ) + { + this( aContext, (EOClassDescription) null ); + } + + /** + * Creates a new PropertyDataSource that uses the specified + * editing context and vends objects of the specified class. + */ + public PropertyDataSource( + EOEditingContext aContext, Class aClass ) + { + this( aContext, EOClassDescription.classDescriptionForClass( aClass ) ); + } + + /** + * Creates a new PropertyDataSource that uses the specified + * editing context and vends objects of the specified + * class description. + */ + public PropertyDataSource( + EOEditingContext aContext, EOClassDescription aClassDesc ) + { + source = null; + key = null; + lastKnownType = null; + classDesc = aClassDesc; + context = aContext; + } + + /** + * Provides the master object for detail display groups. + */ + public Object source() + { + return source; + } + + /** + * Allows a detail display group to set the master object. + */ + public void setSource( Object anObject ) + { + source = anObject; + } + + /** + * Provides the detail key for detail display groups. + */ + public String key() + { + return key; + } + + /** + * Allows a detail display group to set the detail key. + */ + public void setKey( String aKey ) + { + key = aKey; + } + + /** + * Inserts the specified object into this data source. + * Calls insertObjectAtIndex and appends to the end + * of the list. + */ + public void insertObject ( Object anObject ) + { + insertObjectAtIndex( anObject, -1 ); // trick to force to end + } + + /** + * Inserts the specified object into this data source, + * at the specified index. + */ + public void insertObjectAtIndex ( + Object anObject, int anIndex ) + { + if ( source == null ) return; + List list = readAsList(); + if ( anIndex == -1 ) anIndex = list.size(); // force to end + if ( anIndex > list.size() ) anIndex = list.size(); // force to end + list.add( anIndex, anObject ); + writeAsList( list ); + } + + /** + * Deletes the specified object from this data source. + */ + public void deleteObject ( Object anObject ) + { + if ( source == null ) return; + List list = readAsList(); + list.remove( anObject ); + writeAsList( list ); + } + + public EOEditingContext editingContext () + { + return context; + } + + /** + * Returns a List containing the objects in this + * data source. + */ + public NSArray fetchObjects () + { + if ( source == null ) return NSArray.EmptyArray; + return readAsList(); + } + + /** + * Returns a new instance of this class. + */ + public EODataSource + dataSourceQualifiedByKey ( String aKey ) + { + // determine the target class desc if possible + EOClassDescription keyClassDesc = null; + if ( classDesc != null ) + { + keyClassDesc = classDesc.classDescriptionForDestinationKey( aKey ); + } + return new PropertyDataSource( editingContext(), keyClassDesc ); + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public void + qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + source = anObject; + key = aKey; + } + + /** + * Returns the class description passed to the + * constructor, if any. If no class description and + * if the bound property is an indexed property, + * the type of the array is returned, otherwise + * this method returns null. This method is called + * by createObject(). + */ + public EOClassDescription + classDescriptionForObjects () + { + // just return the class description if we have one + if ( classDesc != null ) return classDesc; + + // otherwise, try to do some guesswork + EOClassDescription result = null; + + // lastKnownType is not updated here + Class type = lastKnownType; + + // if no last known type + if ( type == null ) + { + // if source and key were specified + if ( ( source != null ) && ( key != null ) ) + { + // try to get an array type + Method m = Introspector.getPropertyReadMethod( + source.getClass(), key, new Class[0] ); + if ( m != null ) + { + Class returnType = m.getReturnType(); + if ( returnType.isArray() ) + { + type = returnType.getComponentType(); + } + } + else + { + throw new WotonomyException( "Key does not exist for object: " + key + " : " + source ); + } + } + + // does not update lastKnownType because + // we prefer to get that info from a fetch. + } + + // if type has been determined + if ( type != null ) + { + result = + EOClassDescription.classDescriptionForClass( type ); + } + + return result; + } + + /** + * Calls getValue() and returns the result as a List. + * Sets lastKnownType to the retrieved type. + */ + protected NSMutableArray readAsList() + { + Object value = getValue(); + if ( value == null ) + { + return new NSMutableArray(); + } + + Object o; + NSMutableArray result = new NSMutableArray(); + boolean hasReadType = false; + lastKnownType = null; + + // if instance of array, convert to list + if ( value.getClass().isArray() ) + { + int count = Array.getLength( value ); + for ( int i = 0; i < count; i++ ) + { + o = Array.get( value, i ); + if ( o != null ) + { + // we've already found a type + if ( hasReadType ) + { + // check that this matches the last known type + if ( o.getClass() != lastKnownType ) + { + // not all of the same type: set to null + lastKnownType = null; + } + } + else // this is the first type we've found + { + // remember it + hasReadType = true; + lastKnownType = o.getClass(); + } + } + result.add( o ); + } + } + else + if ( value instanceof Collection ) + { + // convert to list so we handle sets, etc. + Iterator i = ((Collection)value).iterator(); + while ( i.hasNext() ) + { + o = i.next(); + if ( o != null ) + { + // we've already found a type + if ( hasReadType ) + { + // check that this matches the last known type + if ( o.getClass() != lastKnownType ) + { + // not all of the same type: set to null + lastKnownType = null; + } + } + else // this is the first type we've found + { + // remember it + hasReadType = true; + lastKnownType = o.getClass(); + } + } + result.add( o ); + } + } + else + { + lastKnownType = null; + throw new WotonomyException( "PropertyDataSource: " + + "bound property was not an indexed property: " + key ); + } + + return result; + } + + /** + * Converts the specified List to lastKnownType + * and calls setValue(). + */ + protected void writeAsList( List anObjectList ) + { + if ( source == null ) + { + throw new WotonomyException( "PropertyDataSource: " + + "no source object: " + key ); + } + + Class c = source.getClass(); + Method m = Introspector.getPropertyReadMethod( c, key, new Class[0] ); + if ( m == null ) + { + throw new WotonomyException( "Could not read property for object: " + + key + " : " + source + " (" + c + ")" ); + } + + Class returnType = m.getReturnType(); + + int count = anObjectList.size(); + Object result = null; + + if ( returnType.isArray() ) + { + Class type = returnType.getComponentType(); + result = Array.newInstance( type, count ); + for ( int i = 0; i < count; i++ ) + { + Array.set( result, i, anObjectList.get( i ) ); + } + } + else + { + Collection collection = null; + + if ( ! returnType.isInterface() ) + { + try + { + collection = (Collection) returnType.newInstance(); + } + catch ( Exception exc ) + { + // no default constructor, leave null + } + } + + // try to find an acceptable collections type + if ( collection == null ) + { + if ( returnType.isAssignableFrom( NSMutableArray.class ) ) + { + collection = new NSMutableArray(); + } + else + if ( returnType.isAssignableFrom( LinkedList.class ) ) + { + collection = new LinkedList(); + } + else + if ( returnType.isAssignableFrom( ArrayList.class ) ) + { + collection = new ArrayList(); + } + else + if ( returnType.isAssignableFrom( HashSet.class ) ) + { + collection = new HashSet(); + } + else + if ( returnType.isAssignableFrom( TreeSet.class ) ) + { + collection = new TreeSet(); + } + } + + if ( collection == null ) + { + throw new WotonomyException( "Could not create a collection of type: " + returnType ); + } + + collection.addAll( anObjectList ); + result = collection; + } + + setValue( result ); + } + + /** + * Returns the value of the indexed property + * specified by qualifyWithRelationshipKey. + */ + protected Object getValue() + { + if ( source instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)source).valueForKey( key ); + } + return EOKeyValueCodingSupport.valueForKey( source, key ); + } + + /** + * Sets the value of the indexed property + * specified by qualifyWithRelationshipKey. + * The argument is assumed to be of appropriate + * type for the property. EOObserverCenter is + * notified that the object will change. + */ + protected void setValue( Object aValue ) + { + EOClassDescription sourceDesc = + EOClassDescription.classDescriptionForClass( source.getClass() ); + + + // if we're not editing a relationship (?) +// if ( ! sourceDesc.toManyRelationshipKeys().containsObject( key ) ) + { + // mark the parent as changed + EOObserverCenter.notifyObserversObjectWillChange( source ); + } + + + if ( source instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)source).takeValueForKey( aValue, key ); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( source, aValue, key ); + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/16 16:47:14 cgruber + * Move some classes in to "internal" packages and re-work imports, etc. + * + * Also use UnsupportedOperationExceptions where appropriate, instead of WotonomyExceptions. + * + * Revision 1.1 2006/02/16 13:19:57 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.14 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.13 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.12 2002/10/24 18:18:12 mpowers + * NSArray's are now considered read-only, so we can return our internal + * representation to reduce unnecessary object allocation. + * + * Revision 1.11 2002/04/15 21:55:33 mpowers + * Catching a condition where the get may not return the value passed to set. + * + * Revision 1.10 2002/03/08 23:20:37 mpowers + * insertObject now calls insertObjectAtIndex. + * + * Revision 1.9 2001/06/05 19:10:41 mpowers + * Better handling of null properties. + * + * Revision 1.8 2001/05/21 14:03:35 mpowers + * Added a convenience constructor for java classes. + * + * Revision 1.7 2001/04/30 13:15:24 mpowers + * Child contexts re-initializing objects invalidated in parent now + * propery transpose relationships. + * + * Revision 1.6 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.5 2001/04/28 22:17:51 mpowers + * Revised PropertyDataSource to be EOClassDescription-aware. + * + * Revision 1.4 2001/04/27 23:37:20 mpowers + * Now using EOClassDescription in the EODataSource class, as we should. + * + * Revision 1.3 2001/03/29 03:29:49 mpowers + * Now using KeyValueCoding and Support instead of Introspector. + * + * Revision 1.2 2001/01/24 14:10:53 mpowers + * Contributing OrderedDataSource, and PropertyDataSource extends it. + * + * Revision 1.1.1.1 2000/12/21 15:46:50 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:35 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/internal/Surrogate.java b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/internal/Surrogate.java new file mode 100644 index 0000000..e12fda0 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/internal/Surrogate.java @@ -0,0 +1,259 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.control.internal; + +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.internal.Introspector; + +/** +* A Surrogate is a special object that can be used in a display +* group when you wish to emulate other objects or modify their +* behaviors. Because it is a Map, it makes use of Introspector's +* ability to treat keys in a map as if they were properties to +* implement the following features. +*
    +*
  • By default, Surrogate works like a Map, and reading and +* writing properties to a Surrogate gets and puts values in the +* Map.
  • +*
  • If one or more delegate objects are specified, property keys +* that do not exist in the map are read from and written to the +* delegate object.
  • +*
  • If a default value is specified, that value will be returned +* for all property reads that do not exist in the map or in the +* delegate object. (Subsequent writes to those properties will +* create a key in the map and then subsequent reads will read not +* read the default object.)
  • +*
  • Subclasses can override the get(Object) method to further +* customize the behavior of a Surrogate. +*
+* +* @author michael@mpowers.net +* @date $Date: 2006-02-18 17:46:44 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 900 $ +*/ +public class Surrogate extends NSMutableDictionary +{ + protected Object[] delegates; + protected Object defaultValue; + + /** + * Default constructor with no delegate object and no default value. + */ + public Surrogate() + { + delegates = null; + defaultValue = null; + } + + /** + * Constructor specifying a delegate object. + */ + public Surrogate( Object[] aDelegateArray ) + { + setDelegates( aDelegateArray ); + } + + /** + * Constructor specifying a default value. + */ + public Surrogate( Object aDefault ) + { + setDefaultValue( aDefault ); + } + + /** + * Constructor specifying a delegate object and a default value. + */ + public Surrogate( Object[] aDelegateArray, Object aDefault ) + { + setDelegates( aDelegateArray ); + setDefaultValue( aDefault ); + } + + /** + * Returns the first delegate object, or null if no delegates exist. + */ + public Object getDelegate() + { + if ( delegates == null ) return null; + if ( delegates.length == 0 ) return null; + return delegates[0]; + } + + /** + * Sets the delegate object list to contain only the + * specified object. + */ + public void setDelegate( Object aDelegate ) + { + setDelegates( new Object[] { aDelegate } ); + } + + /** + * Returns the list of delegates in the order in which + * they are consulted. + */ + public Object[] getDelegates() + { + if ( delegates == null ) delegates = new Object[0]; + return delegates; + } + + /** + * Sets the list of delegates in the order in which they + * will be consulted. + */ + public void setDelegates( Object[] aDelegateArray ) + { + delegates = aDelegateArray; + } + + /** + * Returns the current default value, or null if no default exists. + */ + public Object getDefaultValue() + { + return defaultValue; + } + + /** + * Sets the default value. + */ + public void setDefaultValue( Object aDefault ) + { + defaultValue = aDefault; + } + + /** + * Called by get to retrieve a value from the internal map. + * This implementation simply calls super.get(). + */ + public Object directGet( Object aKey ) + { + return super.get( aKey ); + } + + /** + * Called by put to retrieve a value from the internal map. + * This implementation simply calls super.put(). + */ + public Object directPut( Object aKey, Object aValue ) + { + return super.put( aKey, aValue ); + } + + /** + * Overridden to consult each delegate before + * checking the internal list of keys. No matching + * key is found, returns the default object, or + * null if no default object exists. + */ + public Object get( Object aKey ) + { + // check all delegates in order + int i, j; + Object[] list = getDelegates(); + String[] properties; + for ( i = 0; i < list.length; i++ ) + { + // for each delegate + properties = + Introspector.getReadPropertiesForObject( list[i] ); + for ( j = 0; j < properties.length; j++ ) + { + // if delegate has property + if ( properties[j].equals( aKey ) ) + { + // use this delegate + return Introspector.get( list[i], aKey.toString() ); + } + } + } + + // return from internal map + Object result = directGet( aKey ); + if ( result == null ) + { + // if not in map, return default object + result = getDefaultValue(); + } + return result; + } + + /** + * Overridden to attempt to write each delegate, writing to + * only the first successful delegate, before storing the + * value in the internal map. + */ + public Object put( Object aKey, Object aValue ) + { + // check all delegates in order + int i, j; + Object[] list = getDelegates(); + String[] properties; + for ( i = 0; i < list.length; i++ ) + { + // for each delegate + properties = + Introspector.getWritePropertiesForObject( list[i] ); + for ( j = 0; j < properties.length; j++ ) + { + // if delegate has property + if ( properties[j].equals( aKey ) ) + { + // use this delegate + EOObserverCenter.notifyObserversObjectWillChange( list[i] ); + return Introspector.set( list[i], aKey.toString(), aValue ); + } + } + } + + // set on internal map + EOObserverCenter.notifyObserversObjectWillChange( this ); + return directPut( aKey, aValue ); + } + + /** + * Overridden to compare by reference. + */ + public boolean equals( Object anObject ) + { + return ( this == anObject ); + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/18 22:46:44 cgruber + * Add Surrogate map from .util into control's internal package, and fix imports. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:52:21 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/package.html b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/package.html new file mode 100644 index 0000000..c32d3c7 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/main/java/net/wotonomy/control/package.html @@ -0,0 +1,6 @@ + +

+Support classes needed by the ui and web packages. +This package roughly corresponds to the eocontrol package. +

+ diff --git a/projects/net.wotonomy.persistence/src/test/java/net/wotonomy/access/EOEntityTest.java b/projects/net.wotonomy.persistence/src/test/java/net/wotonomy/access/EOEntityTest.java new file mode 100644 index 0000000..84f0721 --- /dev/null +++ b/projects/net.wotonomy.persistence/src/test/java/net/wotonomy/access/EOEntityTest.java @@ -0,0 +1,14 @@ +package net.wotonomy.access; + +import junit.framework.TestCase; + +public class EOEntityTest extends TestCase { + + /* + * Test method for 'net.wotonomy.access.EOEntity.addAttribute(EOAttribute)' + */ + public void testAddAttribute() { + + } + +} diff --git a/projects/net.wotonomy.test/.classpath b/projects/net.wotonomy.test/.classpath new file mode 100644 index 0000000..dce51f2 --- /dev/null +++ b/projects/net.wotonomy.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/projects/net.wotonomy.test/.cvsignore b/projects/net.wotonomy.test/.cvsignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/projects/net.wotonomy.test/.cvsignore @@ -0,0 +1 @@ +target diff --git a/projects/net.wotonomy.test/.project b/projects/net.wotonomy.test/.project new file mode 100644 index 0000000..12cd40d --- /dev/null +++ b/projects/net.wotonomy.test/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.test/pom.xml b/projects/net.wotonomy.test/pom.xml new file mode 100644 index 0000000..9fe9730 --- /dev/null +++ b/projects/net.wotonomy.test/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-test + Wotonomy - System Tests + jar + + + junit + junit + 3.8.1 + test + + + ${project.groupId} + wotonomy-web + ${project.version} + + + ${project.groupId} + wotonomy-ui-swing + ${project.version} + + + ${project.groupId} + wotonomy-datastore + ${project.version} + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + org.codehaus.mojo + cobertura-maven-plugin + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.test + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.test + + diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingController.java new file mode 100644 index 0000000..be83eb4 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingController.java @@ -0,0 +1,35 @@ +package net.wotonomy.test; + +//import net.wotonomy.foundation.*; +import javax.swing.JDialog; + +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TreeAssociation; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* A simple editor panel with a few textfields. +*/ +public class BindingController +{ + public BindingController( EODisplayGroup titlesGroup, EODisplayGroup childGroup ) + { + BindingPanel bindingPanel = new BindingPanel(); + + EOAssociation ta; + ta = new TreeAssociation( bindingPanel.treeChooser, "People" ); + ta.bindAspect( EOAssociation.TitlesAspect, titlesGroup, "lastName" ); + ta.bindAspect( EOAssociation.ChildrenAspect, childGroup, "children" ); + ta.bindAspect( EOAssociation.IsLeafAspect, titlesGroup, "childCount" ); + ta.establishConnection(); + + JDialog d = new JDialog(); + d.getContentPane().add( bindingPanel ); + d.setTitle( "Chooser Panel" ); + d.pack(); + WindowUtilities.cascade( d ); + d.show(); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingPanel.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingPanel.java new file mode 100644 index 0000000..624dc37 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/BindingPanel.java @@ -0,0 +1,172 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; +import javax.swing.JPanel; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; + +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.components.TreeChooser; + +/** +* BindingPanel is a FileChooser-like panel that +* uses a TreeModel as a data source. It basically +* provides an alternative to JTree for rendering +* and manipulating tree-like data. +*/ +public class BindingPanel extends JPanel +{ + protected TreeChooser treeChooser; + protected ButtonPanel okPanel; + + public BindingPanel() + { + init(); + } + + protected void init() + { + this.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + this.setLayout( new BorderLayout( 10, 10 ) ); + + this.add( treeChooser = new TreeChooser(), BorderLayout.CENTER ); + + okPanel = new ButtonPanel( new String[] { "OK", "Cancel" } ); + this.add( okPanel, BorderLayout.SOUTH ); + } + + /** + * Creates a new folder. + */ + protected class NewFolderAction extends AbstractAction + { + protected NewFolderAction() + { + super("New Folder", UIManager.getIcon("FileChooser.newFolderIcon") ); + } + public void actionPerformed(ActionEvent e) + { + } + } + + /** + * Acts on the "home" key event or equivalent event. + */ + protected class GoHomeAction extends AbstractAction + { + protected GoHomeAction() + { + super("Go Home", UIManager.getIcon("FileChooser.homeFolderIcon") ); + } + public void actionPerformed(ActionEvent e) + { + } + } + + protected class ChangeToParentDirectoryAction extends AbstractAction + { + protected ChangeToParentDirectoryAction() + { + super("Go Up", UIManager.getIcon("FileChooser.upFolderIcon") ); + } + public void actionPerformed(ActionEvent e) + { + } + } + + /** + * Responds to an Open or Save request + */ + protected class ApproveSelectionAction extends AbstractAction { + public void actionPerformed(ActionEvent e) + { + } + } + + + /** + * Responds to a cancel request. + */ + protected class CancelSelectionAction extends AbstractAction { + public void actionPerformed(ActionEvent e) + { + } + } + + /** + * Rescans the files in the current directory + */ + protected class UpdateAction extends AbstractAction { + public void actionPerformed(ActionEvent e) + { + } + } + + // + // Renderer for DirectoryComboBox + // +/* + class DirectoryComboBoxRenderer extends DefaultListCellRenderer { + IndentIcon ii = new IndentIcon(); + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, + boolean cellHasFocus) { + + super.getListCellRendererComponent(list, value, index, + isSelected, cellHasFocus); + File directory = (File) value; + if(directory == null) { + setText(""); + return this; + } + + String fileName = getFileChooser().getName(directory); + setText(fileName); + + // Find the depth of the directory + int depth = 0; + if(index != -1) { + File f = directory; + while(f.getParent() != null) { + depth++; + f = getFileChooser().getFileSystemView().createFileObject( + f.getParent() + ); + } + } + + Icon icon = getFileChooser().getIcon(directory); + + ii.icon = icon; + ii.depth = depth; + + setIcon(ii); + + return this; + } + } + + final static int space = 10; + class IndentIcon implements Icon { + + Icon icon = null; + int depth = 0; + + public void paintIcon(Component c, Graphics g, int x, int y) { + icon.paintIcon(c, g, x+depth*space, y); + } + + public int getIconWidth() { + return icon.getIconWidth() + depth*space; + } + + public int getIconHeight() { + return icon.getIconHeight(); + } + + } +*/ +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataKeyID.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataKeyID.java new file mode 100644 index 0000000..fca6c98 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataKeyID.java @@ -0,0 +1,88 @@ +package net.wotonomy.test; + +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.datastore.DataKey; + +/** +* A test implementation of EOGlobalID that +* wraps a DataKey. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class DataKeyID extends EOGlobalID +{ + private DataKey key; + + /** + * Constructor takes a data key. + */ + public DataKeyID( DataKey aKey ) + { + key = aKey; + } + + /** + * Returns the wrapped data key. + */ + public DataKey getKey() + { + return key; + } + + public boolean isTemporary() + { + return false; + } + + public Object clone() + { + return new DataKeyID( (DataKey) key.clone() ); + } + + public String toString() + { + return "[DataKeyID:"+key.toString()+"]"; + } + + public int hashCode() + { + return key.hashCode(); + } + + public boolean equals( Object anObject ) + { + if ( anObject instanceof DataKeyID ) + { + if ( ((DataKeyID)anObject).key.equals( key ) ) + { + return true; + } + } + return false; + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2001/02/23 23:44:44 mpowers + * Fixes for hashcode to ensure proper key comparison. + * + * Revision 1.2 2001/02/22 20:56:07 mpowers + * Tests of notification handling. + * + * Revision 1.1 2001/02/15 21:14:45 mpowers + * Test suite now using a persistent object store with editing context. + * + * Revision 1.1 2001/02/05 03:45:37 mpowers + * Starting work on EOEditingContext. + * + * + */ + + diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataObjectStore.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataObjectStore.java new file mode 100644 index 0000000..d175f6e --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/DataObjectStore.java @@ -0,0 +1,483 @@ +package net.wotonomy.test; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import net.wotonomy.control.ArrayFault; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.KeyValueCodingUtilities; +import net.wotonomy.datastore.DataKey; +import net.wotonomy.datastore.DataSoup; +import net.wotonomy.datastore.DataView; +import net.wotonomy.datastore.XMLFileSoup; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationQueue; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An object store that wraps a datastore for vending test objects. +*/ +public class DataObjectStore extends EOObjectStore +{ + DataSoup soup; + + /** + * Constructor specifies path to datastore. + */ + public DataObjectStore( String aPath ) + { + soup = new XMLFileSoup( aPath ); + } + + /** + * This implementation returns an appropriately configured array fault. + */ + public NSArray arrayFaultWithSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ) + { + return new ArrayFault( + aGlobalID, aRelationship, aContext ); + } + + /** + * This implementation returns the actual + * object for the specified id. + */ + public Object faultForGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { +System.out.println( "DataObjectStore.faultForGlobalID: * reading object * : " + aGlobalID ); + Object result = soup.getObjectByKey( + ((DataKeyID)aGlobalID).getKey() ); + + if ( result == null ) return null; + + //! transpose keys to objects + convertRelationKeysToObjects( aContext, result, aGlobalID ); + //! + + aContext.recordObject( result, aGlobalID ); + return result; + } + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. The fault should + * belong to the specified editing context. + */ + public Object faultForRawRow ( + Map aDictionary, + String anEntityName, + EOEditingContext aContext ) + { + //TODO: faults are not yet supported + throw new WotonomyException( + "Faults are not yet supported." ); + } + + /** + * Given a newly instantiated object, this method + * initializes its properties to values appropriate + * for the specified id. The object should belong + * to the specified editing context. + * This method is called to populate faults. + */ + public void initializeObject(Object anObject, EOGlobalID aGlobalID, + EOEditingContext aContext) { + if (aGlobalID.isTemporary()) { + // TODO: this should never happen, but it does now until we get + // faults. + + // do not reinit an uncommitted object + return; + } + + System.out.println("DataObjectStore.initializeObject: * reading object * : " + + aGlobalID); + //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + Object original = soup.getObjectByKey(((DataKeyID) aGlobalID).getKey()); + + // ! transpose keys to objects + convertRelationKeysToObjects(aContext, original, aGlobalID); + // ! + EOObserverCenter.notifyObserversObjectWillChange(anObject); + KeyValueCodingUtilities.copy(aContext, original, aContext, anObject); + } + + /** + * Remove all values from all objects in memory, turning them into faults, + * and posts a notification that all objects have been invalidated. + */ + public void invalidateAllObjects () + { + // does nothing except post notification + + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( + InvalidatedAllObjectsInStoreNotification, this ), + NSNotificationQueue.PostNow ); + } + + /** + * Removes values with the specified ids from memory, + * turning them into faults, and posts a notification + * that those objects have been invalidated. + */ + public void invalidateObjectsWithGlobalIDs ( + List aList ) + { + // does nothing + } + + /** + * Returns false because locking is not permitted. + */ + public boolean isObjectLockedWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + return false; + } + + /** + * Does nothing because locking is not permitted. + */ + public void lockObjectWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + // does nothing + } + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship. This method may not return an array fault + * because array faults call this method to fetch on demand. + * All objects must be registered the specified editing context. + * The specified relationship key must produce a result of + * type Collection for the source object or an exception is thrown. + */ + public NSArray objectsForSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ) + { + System.out.println( "DataObjectStore.objectsForSourceGlobalID: * reading object * : " + aGlobalID ); + Object object = soup.getObjectByKey(((DataKeyID)aGlobalID).getKey() ); + + if ( object == null ) return null; + + Object fault; + EOGlobalID id; + NSMutableArray result = new NSMutableArray(); + + Iterator it = ((TestObject)object).getChildList().iterator(); + while ( it.hasNext() ) + { + id = new DataKeyID((DataKey)it.next()); + fault = aContext.faultForGlobalID( id, aContext ); + + // if key still exists + if ( fault != null ) + { +//System.out.println( "objectsForSourceGlobalID: found: " + id + " : " + fault ); + result.add( fault ); + +// for testing purposes +((TestObject)fault).setParent( (TestObject) object ); + } + else // key no longer exists + { + // do not add +System.out.println( "objectsForSourceGlobalID: could not find fault for id: " + id ); + } + } + return result; + + } + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. + * Each object is registered with the specified editing context. + * If any object is already registered in the specified context, + * it is not refetched and that object should be used in the array. + */ + public NSArray objectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec, + EOEditingContext aContext ) + { + //TODO: fetch specs are not yet supported + + DataView view = soup.queryObjects( null, null ); +System.out.println( "DataObjectStore: ** querying all objects **" ); + + // we've changed this implementation so that + // it simply calls faultForGlobalID on the context + // for each id in the result set. + // this way, child contexts inherit parent's state. + // however, it's unclear if the specification allows + // faults in the resulting array. sounds like it doesn't. + NSMutableArray result = new NSMutableArray(); + DataKeyID id; + Iterator it = view.iterator(); + while ( it.hasNext() ) + { + id = new DataKeyID( view.getKeyForObject( it.next() ) ); + result.addObject( aContext.faultForGlobalID( id, aContext ) ); + } + return result; + } + + /** + * Removes all values from the specified object, + * converting it into a fault for the specified id. + * New or deleted objects should not be refaulted. + */ + public void refaultObject ( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + //TODO: faults are not yet supported + // just re-initialize the object + initializeObject( anObject, aGlobalID, aContext ); + } + + /** + * Writes all changes in the specified editing context + * to the respository. + */ + public void saveChangesInEditingContext ( + EOEditingContext aContext ) + { + Object o; + DataKeyID id; + Iterator it; + + // process deletes + it = aContext.deletedObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + id = (DataKeyID) aContext.globalIDForObject( o ); +System.out.println( "DataObjectStore: * deleting object * : " + id ); + soup.removeObject( id.getKey() ); + // remove object from editing context + aContext.forgetObject( o ); + } + + // process inserts + NSMutableDictionary userInfo = null; + it = aContext.insertedObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + EOGlobalID oldId = aContext.globalIDForObject( o ); + + //! transpose objects to keys + convertRelationObjectsToKeys( aContext, (TestObject) o ); + id = new DataKeyID( soup.addObject( o ) ); + convertRelationKeysToObjects( aContext, (TestObject) o, oldId ); + //! + +System.out.println( "DataObjectStore: * adding object * : " + id ); + + // save mapping of old id to new id + if ( userInfo == null ) + { + userInfo = new NSMutableDictionary(); + } + userInfo.setObjectForKey( id, oldId ); + } + + // broadcast inserted objects' new ids if necessary + if ( userInfo != null ) + { + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( + EOGlobalID.GlobalIDChangedNotification, null, userInfo ), + NSNotificationQueue.PostNow ); + } + + System.out.println( aContext.updatedObjects() ); + + // process updates + it = aContext.updatedObjects().iterator(); + while ( it.hasNext() ) + { +//if ( true ) // test validation error message handling +//throw new RuntimeException( "Update not allowed." ); + o = it.next(); + id = (DataKeyID) aContext.globalIDForObject( o ); +System.out.println( "DataObjectStore: * updating object * : " + id ); + + //! transpose objects to keys + convertRelationObjectsToKeys( aContext, (TestObject) o ); + soup.updateObject( id.getKey(), o ); + convertRelationKeysToObjects( aContext, (TestObject) o, id ); + //! + + } + } + + private void convertRelationKeysToObjects( + EOEditingContext aContext, Object anObject, EOGlobalID aGlobalID ) + { // System.out.println( "convertRelationKeysToObjects: " + anObject ); +// set editing context for testing +((TestObject)anObject).editingContext = aContext; + + Object fault; + DataKeyID id; + List result = new LinkedList(); + Iterator it = ((TestObject)anObject).getChildList().iterator(); + while ( it.hasNext() ) + { + id = new DataKeyID((DataKey)it.next()); + fault = aContext.faultForGlobalID( id, aContext ); + + // if key still exists + if ( fault != null ) + { +//System.out.println( "convertRelationObjectsToKeys: found: " + id + " : " + fault ); + result.add( fault ); + +// for testing purposes +((TestObject)fault).setParent( (TestObject) anObject ); + } + else // key no longer exists + { + // do not add +System.out.println( "convertRelationObjectsToKeys: could not find fault for id: " + id ); + } + } + // this tests loading manually on-demand +// ((TestObject)anObject).setChildList( null ); + // this tests loading immediately + ((TestObject)anObject).setChildList( result ); + // this tests loading array faults +// ((TestObject)result).setChildList( null ); + ((TestObject)anObject).setChildList( + aContext.arrayFaultWithSourceGlobalID( + aGlobalID, "childList", aContext ) ); + + } + + private void convertRelationObjectsToKeys( + EOEditingContext aContext, Object anObject ) + { // System.out.println( "convertRelationObjectsToKeys: " + anObject ); + Object o; + DataKeyID id; + List result = new LinkedList(); + Iterator it = ((TestObject)anObject).getChildList().iterator(); +// for testing purposes +((TestObject)anObject).setParent( null ); +((TestObject)anObject).editingContext = null; + while ( it.hasNext() ) + { + o = it.next(); +//System.out.println( "convertRelationObjectsToKeys: " + o + " : " + aContext.globalIDForObject( o ) ); + id = (DataKeyID)aContext.globalIDForObject( o ); + + // if object still exists in context + if ( id != null ) + { + result.add( id.getKey() ); + } + else // object was deleted + { + // do not add +System.out.println( "convertRelationObjectsToKeys: could not find id for object: " + o ); +System.out.println( aContext.registeredObjects() ); + } + + } + ((TestObject)anObject).setChildList( result ); + } + + +/* + * $Log$ + * Revision 1.1 2006/02/19 16:30:25 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:18:56 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.18 2002/03/11 03:18:39 mpowers + * Now properly handling ObserverChangesLater. + * + * Revision 1.17 2001/10/26 18:39:44 mpowers + * Posting notifications immediately, rather than delayed. + * + * Revision 1.16 2001/05/06 18:27:10 mpowers + * More broadly catching editing contexts for now. + * + * Revision 1.15 2001/05/05 23:05:43 mpowers + * Implemented Array Faults. + * + * Revision 1.14 2001/05/05 15:00:06 mpowers + * Tested load-on-demand: still works. + * Now using registerClone for consistency. + * Editing context is temporarily posting notification on objectWillChange. + * + * Revision 1.13 2001/05/04 23:24:30 mpowers + * Changes to test code. + * + * Revision 1.12 2001/05/04 16:57:56 mpowers + * Now correctly transposing references to editing contexts when + * cloning/copying between editing contexts. + * + * Revision 1.11 2001/05/02 17:33:28 mpowers + * More changes for testing. + * + * Revision 1.10 2001/04/30 13:15:24 mpowers + * Child contexts re-initializing objects invalidated in parent now + * propery transpose relationships. + * + * Revision 1.9 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.8 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.7 2001/04/28 22:17:51 mpowers + * Revised PropertyDataSource to be EOClassDescription-aware. + * + * Revision 1.6 2001/04/28 16:18:44 mpowers + * Implementing relationships. + * + * Revision 1.5 2001/04/13 16:33:36 mpowers + * Now broadcasting notifications. + * + * Revision 1.4 2001/04/08 21:00:54 mpowers + * Changes to support new objectsForFetchSpecification scheme. + * + * Revision 1.3 2001/03/22 21:37:52 mpowers + * Testing new features. + * + * Revision 1.2 2001/03/15 21:10:41 mpowers + * Implemented global id re-registration for newly saved inserts. + * + * Revision 1.1 2001/03/05 22:12:11 mpowers + * Created the control package for a datastore-specific implementation + * of EOObjectStore. + * + * + */ +} + diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditController.java new file mode 100644 index 0000000..b304ade --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditController.java @@ -0,0 +1,214 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.AbstractButton; +import javax.swing.BorderFactory; +import javax.swing.JDialog; +import javax.swing.JFrame; + +import net.wotonomy.control.ChildDataSource; +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.MasterDetailAssociation; +import net.wotonomy.ui.swing.DisplayGroupActionAssociation; +import net.wotonomy.ui.swing.ListAssociation; +import net.wotonomy.ui.swing.RadioPanelAssociation; +import net.wotonomy.ui.swing.TextAssociation; +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* A simple editor panel with a few textfields. +*/ +public class EditController +{ + EODisplayGroup group; + JDialog dialog; + + public EditController( EODataSource aDataSource ) + { + EditPanel editPanel = new EditPanel(); + editPanel.infoPanel.setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createRaisedBevelBorder(), + BorderFactory.createEmptyBorder( 10, 10, 10, 10 ) ) ); + editPanel.setBorder( + BorderFactory.createEmptyBorder( 0, 0, 0, 0 ) ); + ButtonPanel okPanel = new ButtonPanel( + new String[] { "Revert", "Refault", "Refresh", "Commit" } ); + editPanel.add( okPanel, BorderLayout.SOUTH ); + + group = new EODisplayGroup(); + group.setDataSource( aDataSource ); + group.fetch(); + group.selectNext(); + + // text associations + + EOAssociation ta; + + ta = new TextAssociation( editPanel.firstNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "firstName" ); + ta.establishConnection(); + + ta = new TextAssociation( editPanel.middleNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "middleName" ); + ta.establishConnection(); + + ta = new TextAssociation( editPanel.lastNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "lastName" ); + ta.establishConnection(); + + // radio panels + + ta = new RadioPanelAssociation( editPanel.yearRadioPanel ); + ta.bindAspect( EOAssociation.ValueAspect, group, "createDate.year" ); + + EODisplayGroup yearTitles = new EODisplayGroup(); + yearTitles.setObjectArray( new NSArray( + new Object[] { "1999", "2000", "2001" } ) ); + ta.bindAspect( EOAssociation.TitlesAspect, yearTitles, "" ); + + EODisplayGroup yearObjects = new EODisplayGroup(); + yearObjects.setObjectArray( new NSArray( + new Object[] { new Integer( 99 ), new Integer( 100 ), new Integer( 101 ) } ) ); + ta.bindAspect( EOAssociation.ObjectsAspect, yearObjects, "" ); + + ta.establishConnection(); + + // detail group + + final EODisplayGroup detailGroup = new EODisplayGroup(); + detailGroup.setDataSource( new PropertyDataSource( + aDataSource.editingContext(), TestObject.class ) ); + + ta = new MasterDetailAssociation( detailGroup ); + ta.bindAspect( EOAssociation.ParentAspect, group, "childList" ); + ta.establishConnection(); + + ta = new ListAssociation( editPanel.list ); + ta.bindAspect( EOAssociation.TitlesAspect, detailGroup, "fullName" ); + ta.establishConnection(); + + // display group action associations + + AbstractButton button = (AbstractButton) + editPanel.addPanel.getButton( "Add" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + detailGroup.insertNewObjectAtIndex( 0 ); + } + } ); + + ta = new DisplayGroupActionAssociation( + editPanel.addPanel.getButton( "Remove" ) ); + ta.bindAspect( EOAssociation.ActionAspect, detailGroup, "deleteSelection" ); + ta.establishConnection(); + + // ok / cancel buttons + + button = (AbstractButton) + okPanel.getButton( "Commit" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + group.dataSource().editingContext().saveChanges(); + } + } ); + + button = (AbstractButton) + okPanel.getButton( "Refresh" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + group.dataSource().editingContext().invalidateAllObjects(); + } + } ); + + button = (AbstractButton) + okPanel.getButton( "Refault" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { +/* + Object o = group.displayedObjects().objectAtIndex( 0 ); + group.dataSource().editingContext().refaultObject( + o, + group.dataSource().editingContext().globalIDForObject( o ), + group.dataSource().editingContext() ); +*/ + group.dataSource().editingContext().revert(); + group.dataSource().editingContext().refaultObjects(); + } + } ); + + button = (AbstractButton) + okPanel.getButton( "Revert" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + group.dataSource().editingContext().revert(); + } + } ); + + // add mouse listener for list + + editPanel.list.addMouseListener( new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if ( e.getClickCount() == 2 ) + { + Object item = detailGroup.selectedObject(); + if ( item != null ) + { +// new InspectorController( item ); + + new EditController( + new ChildDataSource( + group.dataSource(), item ) ); + } + } + } + }); + + // launch + + dialog = new JDialog(); + // add WindowListener for frame + dialog.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + dialog.getContentPane().add( editPanel ); + dialog.setTitle( "Edit Panel" ); + dialog.pack(); +// dialog.setSize( 300, dialog.getSize().height ); + WindowUtilities.cascade( dialog ); + dialog.show(); + + // workaround for memory issues on jdk1.2.2 + dialog.addWindowListener( new WindowAdapter() + { + // exit on close + public void windowClosing(WindowEvent e) + { + ((JDialog)e.getWindow()).getContentPane().removeAll(); + } + }); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditPanel.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditPanel.java new file mode 100644 index 0000000..63b1317 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/EditPanel.java @@ -0,0 +1,61 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; + +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.text.JTextComponent; + +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.components.InfoPanel; +import net.wotonomy.ui.swing.components.RadioButtonPanel; + +/** +* A simple editor panel with a few textfields. +*/ +public class EditPanel extends JPanel +{ + public JTextComponent firstNameField; + public JTextField middleNameField, lastNameField; + public RadioButtonPanel yearRadioPanel; + public InfoPanel infoPanel; + public JList list; + public ButtonPanel addPanel; + + + public EditPanel() + { + this.setLayout( new BorderLayout() ); + this.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + + infoPanel = new InfoPanel(); + + // name fields + firstNameField = new JTextField(); + infoPanel.addPair( "First Name", firstNameField ); + middleNameField = new JTextField(); + infoPanel.addPair( "Middle Name", middleNameField ); + lastNameField = new JTextField(); + infoPanel.addPair( "Last Name", lastNameField ); + yearRadioPanel = new RadioButtonPanel(); + infoPanel.addPair( "Year", yearRadioPanel ); + + list = new JList(); + JPanel containerPanel = new JPanel(); + containerPanel.setLayout( new BorderLayout( 0, 5 ) ); + JScrollPane scrollPane = new JScrollPane( list ); + scrollPane.setPreferredSize( new java.awt.Dimension( 100, 100 ) ); + addPanel = new ButtonPanel( new String[] { "Add", "Remove" } ); + addPanel.setAlignment( FlowLayout.CENTER ); + containerPanel.add( scrollPane, BorderLayout.CENTER ); + containerPanel.add( addPanel, BorderLayout.SOUTH ); + infoPanel.addRow( "Children", containerPanel ); + + this.add( infoPanel, BorderLayout.CENTER ); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/InspectorController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/InspectorController.java new file mode 100644 index 0000000..58e2d9a --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/InspectorController.java @@ -0,0 +1,137 @@ +package net.wotonomy.test; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.AbstractButton; +import javax.swing.JDialog; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.MasterDetailAssociation; +import net.wotonomy.ui.swing.DisplayGroupActionAssociation; +import net.wotonomy.ui.swing.ListAssociation; +import net.wotonomy.ui.swing.RadioPanelAssociation; +import net.wotonomy.ui.swing.TextAssociation; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* A simple editor panel with a few textfields. +*/ +public class InspectorController +{ + public InspectorController( Object o ) + { + EditPanel editPanel = new EditPanel(); + + EODisplayGroup group = new EODisplayGroup(); + group.setDataSource( new TestDataSource() ); + group.setObjectArray( new NSArray( o ) ); + group.selectNext(); + + // text associations + + EOAssociation ta; + + ta = new TextAssociation( editPanel.firstNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "firstName" ); + ta.establishConnection(); + + ta = new TextAssociation( editPanel.middleNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "middleName" ); + ta.establishConnection(); + + ta = new TextAssociation( editPanel.lastNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "lastName" ); + ta.establishConnection(); + + // radio panels + + ta = new RadioPanelAssociation( editPanel.yearRadioPanel ); + ta.bindAspect( EOAssociation.ValueAspect, group, "createDate.year" ); + + EODisplayGroup yearTitles = new EODisplayGroup(); + yearTitles.setObjectArray( new NSArray( + new Object[] { "1999", "2000", "2001" } ) ); + ta.bindAspect( EOAssociation.TitlesAspect, yearTitles, "" ); + + EODisplayGroup yearObjects = new EODisplayGroup(); + yearObjects.setObjectArray( new NSArray( + new Object[] { new Integer( 99 ), new Integer( 100 ), new Integer( 101 ) } ) ); + ta.bindAspect( EOAssociation.ObjectsAspect, yearObjects, "" ); + + ta.establishConnection(); + + // detail group + + final EODisplayGroup detailGroup = new EODisplayGroup(); + + ta = new MasterDetailAssociation( detailGroup ); + ta.bindAspect( EOAssociation.ParentAspect, group, "childList" ); + ta.establishConnection(); + + ta = new ListAssociation( editPanel.list ); + ta.bindAspect( EOAssociation.TitlesAspect, detailGroup, "fullName" ); + ta.establishConnection(); + + // display group action associations + + AbstractButton button = (AbstractButton) + editPanel.addPanel.getButton( "Add" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + detailGroup.insertNewObjectAtIndex( 0 ); + } + } ); + + ta = new DisplayGroupActionAssociation( + editPanel.addPanel.getButton( "Remove" ) ); + ta.bindAspect( EOAssociation.ActionAspect, detailGroup, "deleteSelection" ); + ta.establishConnection(); + + // add mouse listener for list + + editPanel.list.addMouseListener( new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if ( e.getClickCount() == 2 ) + { + Object item = detailGroup.selectedObject(); + if ( item != null ) + { + new InspectorController( item ); + } + } + } + }); + + // launch + + JDialog dialog = new JDialog(); + dialog.getContentPane().add( editPanel ); + dialog.setTitle( "Inspector Panel" ); + dialog.pack(); + dialog.setSize( 300, dialog.getSize().height ); + WindowUtilities.cascade( dialog ); + dialog.show(); + + // workaround for memory issues on jdk1.2.2 + dialog.addWindowListener( new WindowAdapter() + { + // exit on close + public void windowClosing(WindowEvent e) + { + ((JDialog)e.getWindow()).getContentPane().removeAll(); + } + }); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/Test.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/Test.java new file mode 100644 index 0000000..ba6a1dc --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/Test.java @@ -0,0 +1,120 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; +import javax.swing.UIManager; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObjectStore; + +/** +* A simple test-bed for wotonomy. +* Shows a JFrame containing the TestPanel +* which is controlled by the TestController. +*/ +public class Test +{ + static EOObjectStore objectStore; + static EOEditingContext editingContext; + static public void main( String[] argv ) + { +// NSRunLoop.currentRunLoop(); + + // system l&f + try + { +// UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel"); + UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); +// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); +// UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } + catch (Exception e) + { + // no system l&f - fail silently + } + + // launch notification monitor if desired + for ( int i = 0; i < argv.length; i++ ) + { + if ( argv[i].indexOf( "monitor" ) != -1 ) + { + new net.wotonomy.ui.swing.NotificationInspector(); + } + } + new net.wotonomy.ui.swing.NotificationInspector(); + + // set up editing context hierarchy + objectStore = new DataObjectStore( "data" ); + editingContext = new EOEditingContext( objectStore ); + + // connect panel to controller + TestPanel testPanel = new TestPanel(); + final TestController controller = new TestController( testPanel ); + + // create frame and show + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE ); + + // setup menus + JMenu menu; + JMenuItem menuItem; + JMenuBar menuBar = new JMenuBar(); + menu = new JMenu( "File" ); + menu.add( "New" ); + menuItem = new JMenuItem( "Save" ); + menuItem.setAccelerator( KeyStroke.getKeyStroke( KeyEvent.VK_S, KeyEvent.CTRL_MASK ) ); + menuItem.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + controller.displayGroup.dataSource().editingContext().saveChanges(); + } + }); + + menu.add( menuItem ); + menu.add( "Close" ); + menuBar.add( menu ); + menu = new JMenu( "Edit" ); + menu.add( "Cut" ); + menu.add( "Copy" ); + menu.add( "Paste" ); + menuBar.add( menu ); + frame.setJMenuBar( menuBar ); + + frame.getContentPane().add( testPanel, BorderLayout.CENTER ); + frame.setTitle( "Test Frame" ); + frame.setBounds( 50, 50, 750, 500 ); + frame.show(); // comment out this to avoid memory leak from jdk1.2.2 bug + + // add WindowListener for frame + frame.addWindowListener( new WindowAdapter() + { + // exit on close + public void windowClosing(WindowEvent e) + { + System.exit( 0 ); + } + }); + + /* uncomment this to avoid memory leak from jdk1.2.2 bug + frame.getContentPane().removeAll(); + */ +/* + NSNotificationCenter.defaultCenter().addObserver( + frame, + new NSSelector( "hitMe", new Class[] { NSNotification.class } ), + null, null ); +*/ + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestController.java new file mode 100644 index 0000000..8bbb452 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestController.java @@ -0,0 +1,395 @@ +package net.wotonomy.test; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.text.DateFormat; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.AbstractButton; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.UIManager; +import javax.swing.table.TableColumn; + +import net.wotonomy.control.ChildDataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOKeyValueQualifier; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.ButtonAssociation; +import net.wotonomy.ui.swing.ComboBoxAssociation; +import net.wotonomy.ui.swing.DisplayGroupActionAssociation; +import net.wotonomy.ui.swing.SliderAssociation; +import net.wotonomy.ui.swing.TableColumnAssociation; +import net.wotonomy.ui.swing.TextAssociation; +import net.wotonomy.ui.swing.TreeColumnAssociation; +import net.wotonomy.ui.swing.components.AlternatingRowCellRenderer; +import net.wotonomy.ui.swing.components.FormattedCellRenderer; +import net.wotonomy.ui.swing.components.IconCellRenderer; +import net.wotonomy.ui.swing.components.KeyableCellEditor; + +/** +* Controller for the TestPanel. +*/ +public class TestController implements ActionListener +{ + EODisplayGroup displayGroup; + TestPanel panel; + + public TestController( TestPanel aPanel ) + { + panel = aPanel; + + // setup display group + + displayGroup = new EODisplayGroup(); + displayGroup.setSortOrderings( new NSArray( new Object[] + { "firstName", "middleName", "lastName" } ) ); + + // fetch the data +// displayGroup.setUsesOptimisticRefresh( true ); + displayGroup.setDataSource( new TestDataSource() ); +// displayGroup.setSelectsFirstObjectAfterFetch( true ); + displayGroup.fetch(); + displayGroup.selectNext(); + +displayGroup.setDelegate( this ); + + // set up associations + + EOAssociation assoc; + + // table association + + TableColumn column; + + column = new TableColumn(); + column.setHeaderValue( "First" ); + IconCellRenderer iconRenderer = new IconCellRenderer() + { + private Icon icon = UIManager.getIcon("FileChooser.homeFolderIcon"); + public Icon getIconForContext( + JComponent container, Object value, + int row, int col, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + return icon; + } + }; + iconRenderer.addActionListener( this ); + column.setCellRenderer( new AlternatingRowCellRenderer( iconRenderer ) ); +// column.setCellEditor( iconRenderer ); +// assoc = new TableColumnAssociation( column ); + assoc = new TreeColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "firstName" ); +//new net.wotonomy.ui.swing.DisplayGroupInspector( displayGroup ); +//displayGroup = new EODisplayGroup(); +// assoc.bindAspect( EOAssociation.ChildrenAspect, displayGroup, "childList" ); + assoc.bindAspect( EOAssociation.EditableAspect, null, "true" ); + assoc.bindAspect( EOAssociation.IsLeafAspect, null, "childCount" ); + ((TableColumnAssociation)assoc).setTable( panel.table ); + assoc.establishConnection(); + ((TreeColumnAssociation)assoc).getTreeModelAssociation().setInsertingAfter( false ); + ((TreeColumnAssociation)assoc).getTreeModelAssociation().setInsertingChild( false ); + +// column.setCellRenderer( new AlternatingRowCellRenderer( column.getCellRenderer() ) ); + +/* + // test the standalone mode of the icon cell renderer + panel.add( iconRenderer, java.awt.BorderLayout.SOUTH ); + iconRenderer.setText( "Hello World!" ); + iconRenderer.setIcon( UIManager.getIcon("FileChooser.homeFolderIcon") ); +*/ + + column = new TableColumn(); + column.setHeaderValue( "Middle" ); + column.setCellRenderer( new AlternatingRowCellRenderer() ); + assoc = new TableColumnAssociation( column ); + ((TableColumnAssociation)assoc).setSortCaseSensitive( true ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "middleName" ); + ((TableColumnAssociation)assoc).setTable( panel.table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Last" ); + column.setCellRenderer( new AlternatingRowCellRenderer() ); + column.setCellEditor( new KeyableCellEditor() ); + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "lastName" ); + assoc.bindAspect( EOAssociation.EditableAspect, null, "true" ); + ((TableColumnAssociation)assoc).setTable( panel.table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Created" ); + FormattedCellRenderer renderer = new FormattedCellRenderer(); + renderer.setFormat( DateFormat.getDateInstance() ); + column.setCellRenderer( new AlternatingRowCellRenderer( renderer ) ); + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "createDate" ); + ((TableColumnAssociation)assoc).setTable( panel.table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Special" ); + column.setCellRenderer( new AlternatingRowCellRenderer( + panel.table.getDefaultRenderer( Boolean.class ) ) ); + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "special" ); + assoc.bindAspect( EOAssociation.EditableAspect, null, "true" ); + ((TableColumnAssociation)assoc).setTable( panel.table ); + assoc.establishConnection(); + + // text associations + + assoc = new TextAssociation( panel.firstNameField ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "firstName" ); +//EODisplayGroup controllerDisplayGroup = new EODisplayGroup(); +//controllerDisplayGroup.setObjectArray( new NSArray( this ) ); +//controllerDisplayGroup.selectNext(); +//assoc.bindAspect( EOAssociation.ValueAspect, controllerDisplayGroup, "filter" ); + assoc.establishConnection(); + + assoc = new TextAssociation( panel.middleNameField ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "middleName" ); + assoc.establishConnection(); + + assoc = new TextAssociation( panel.lastNameField ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "lastName" ); + assoc.bindAspect( EOAssociation.LabelAspect, displayGroup, "special" ); + assoc.establishConnection(); + + assoc = new ButtonAssociation( panel.checkbox ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "special" ); +// assoc.bindAspect( EOAssociation.EnabledAspect, displayGroup, "special" ); +// assoc.bindAspect( EOAssociation.VisibleAspect, displayGroup, "special" ); + assoc.establishConnection(); + + // combo associations + + assoc = new ComboBoxAssociation( panel.dateBox ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "createDate.date" ); + // no titles aspect: uses existing combobox options + assoc.establishConnection(); + + assoc = new ComboBoxAssociation( panel.monthBox ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "createDate.month" ); + EODisplayGroup monthTitlesGroup = new EODisplayGroup(); + monthTitlesGroup.setObjectArray( new NSArray( + new Object[] { "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" } ) ); + assoc.bindAspect( EOAssociation.TitlesAspect, monthTitlesGroup, "" ); + + EODisplayGroup monthObjectsGroup = new EODisplayGroup(); + monthObjectsGroup.setObjectArray( new NSArray( + new Object[] { new Integer( 0 ), + new Integer( 1 ), new Integer( 2 ), new Integer( 3 ), + new Integer( 4 ), new Integer( 5 ), new Integer( 6 ), + new Integer( 7 ), new Integer( 8 ), new Integer( 9 ), + new Integer( 10 ), new Integer( 11 ) } ) ); + assoc.bindAspect( EOAssociation.ObjectsAspect, monthObjectsGroup, "" ); + + assoc.establishConnection(); + + + assoc = new ComboBoxAssociation( panel.yearBox ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "createDate.year" ); + + EODisplayGroup yearTitlesGroup = new EODisplayGroup(); + yearTitlesGroup.setObjectArray( new NSArray( + new Object[] { "1999", "2000", "2001" } ) ); + assoc.bindAspect( EOAssociation.TitlesAspect, yearTitlesGroup, "" ); + + EODisplayGroup yearObjectsGroup = new EODisplayGroup(); + yearObjectsGroup.setObjectArray( new NSArray( + new Object[] { new Integer( 99 ), new Integer( 100 ), new Integer( 101 ) } ) ); + assoc.bindAspect( EOAssociation.ObjectsAspect, yearObjectsGroup, "" ); + + assoc.establishConnection(); + + assoc = new SliderAssociation( panel.slider ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "createDate.date" ); + assoc.bindAspect( EOAssociation.VisibleAspect, displayGroup, "special" ); + assoc.establishConnection(); + + assoc = new TextAssociation( panel.infoPanel.getLabelForKey( "Day of Month" ) ); + assoc.bindAspect( EOAssociation.VisibleAspect, displayGroup, "special" ); + assoc.establishConnection(); + + // display group action associations + AbstractButton button; + + button = (AbstractButton) + panel.savePanel.getButton( "Refresh All" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { // panel.lastNameField.setText( panel.lastNameField.getText().trim() ); + + // test all three ways + + displayGroup.dataSource().editingContext().invalidateAllObjects(); +// displayGroup.dataSource().editingContext().parentObjectStore().invalidateAllObjects(); +// displayGroup.dataSource().editingContext().revert(); + } + } ); + + button = (AbstractButton) + panel.savePanel.getButton( "Commit" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + try + { + displayGroup.dataSource().editingContext().saveChanges(); + } + catch ( RuntimeException exc ) + { + JOptionPane.showMessageDialog( + (java.awt.Component)evt.getSource(), exc.getMessage() ); + exc.printStackTrace(); + } + } + } ); + + button = (AbstractButton) + panel.buttonPanel.getButton( "Add" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + displayGroup.insertNewObjectAtIndex( 0 ); + } + } ); + + assoc = new DisplayGroupActionAssociation( + panel.buttonPanel.getButton( "Remove" ) ); + assoc.bindAspect( EOAssociation.ActionAspect, displayGroup, "deleteSelection" ); + assoc.establishConnection(); +/* + assoc = new DisplayGroupActionAssociation( + panel.infoPanel.getButtonPanel().getButton( "Refresh" ) ); + assoc.bindAspect( EOAssociation.ActionAspect, displayGroup, "updateDisplayedObjects" ); + assoc.establishConnection(); + + assoc = new DisplayGroupActionAssociation( + panel.infoPanel.getButtonPanel().getButton( "Commit" ) ); + assoc.bindAspect( EOAssociation.ActionAspect, displayGroup, "updateDisplayedObjects" ); + assoc.establishConnection(); +*/ + // add MouseListener for table + panel.table.addMouseListener( new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if ( e.getClickCount() == 2 ) + { + Object o = displayGroup.selectedObject(); + if ( o != null ) + { +// new InspectorController( o ); + new EditController( + new ChildDataSource( + displayGroup.dataSource(), o ) ); + } + } + } + }); + + // add ActionListener for tree button + ((JButton) panel.buttonPanel.getButton( + "Tree View" ) ).addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + EOEditingContext parentContext = Test.editingContext; + EOEditingContext childContext = new EOEditingContext( parentContext ); + + // transpose objects to ids to faults + List ids = new LinkedList(); + Iterator i = displayGroup.selectedObjects().iterator(); + while ( i.hasNext() ) + { + ids.add( parentContext.globalIDForObject( i.next() ) ); + } + List objects = new LinkedList(); + i = ids.iterator(); + while( i.hasNext() ) + { + objects.add( childContext.faultForGlobalID( (EOGlobalID) i.next(), childContext ) ); + } + + EODisplayGroup treeGroup = new EODisplayGroup(); + treeGroup.setSortOrderings( new NSArray( "lastName" ) ); + treeGroup.setObjectArray( objects ); + + EODisplayGroup childGroup = new EODisplayGroup(); + + //childGroup.setDelegate( new DebuggingDelegate() ); + //new TreeInspectorController( treeGroup, childGroup ); + //new BindingController( treeGroup, childGroup ); + + new TreeController( childContext, treeGroup, childGroup ); + + //NOTE: ChildDataSource is fundamentally broken +// new TreeController( new ChildDataSource( +// displayGroup.dataSource(), displayGroup.selectedObjects() ) ); + } + }); + +/* + NSNotificationCenter.defaultCenter().addObserver( + this, + new NSSelector( "hitMe", new Class[] { NSNotification.class } ), + null, null ); +*/ + } + + private String filter; + public String getFilter() + { + return filter; + } + + public void setFilter( String aFilter ) + { + filter = aFilter; + + EOQualifier qualifier = null; + if ( ! "".equals( aFilter ) ) + { + qualifier = new EOKeyValueQualifier( + "firstName", EOQualifier.QualifierOperatorContains, filter ); + } + displayGroup.setQualifier( qualifier ); + displayGroup.updateDisplayedObjects(); + } + + public void hitMe( NSNotification aNote ) + { + System.out.println( aNote ); + } + + public void actionPerformed( ActionEvent evt ) + { + Object o = displayGroup.selectedObject(); + if ( o != null ) + { +// new ObjectInspector( o ); + new InspectorController( o ); + } + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestDataSource.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestDataSource.java new file mode 100644 index 0000000..1d36bef --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestDataSource.java @@ -0,0 +1,115 @@ +package net.wotonomy.test; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.foundation.NSArray; + +/** +* A custom DataSource that works with +* the datastore package for persistence. +*/ +public class TestDataSource extends EODataSource +{ + private EOEditingContext context; + private Object source; + private String key; + + public TestDataSource() + { + this( Test.editingContext ); + } + + public TestDataSource( EOEditingContext aContext ) + { + context = aContext; + } + + public EOEditingContext editingContext() + { + return context; + } + + /** + * This implementation does nothing. + */ + public void insertObject ( Object anObject ) + { + // creates are handled by createObject(). + } + + /** + * Deletes the specified object from this data source. + */ + public void deleteObject ( Object anObject ) + { + editingContext().deleteObject( anObject ); + } + + /** + * Returns a List containing the objects in this + * data source. This implementation returns all + * TestObjects that have been persisted to the + * datastore in the data directory. + */ + public NSArray fetchObjects () + { + if ( source == null ) + { + NSArray result = editingContext().objectsWithFetchSpecification( + new EOFetchSpecification() ); + if ( result.size() > 0 ) + { + result = new NSArray( result.objectAtIndex( 0 ) ); +//result.add( result.objectAtIndex( 0 ) ); + } + return result; + } + else + { + return new NSArray( + ((TestObject)source).getChildList() ); + } + } + + /** + * Returns a data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * @see #qualifyWithRelationshipKey + */ + public EODataSource + dataSourceQualifiedByKey ( String aKey ) + { + return new TestDataSource( editingContext() ); + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public void + qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + key = aKey; + source = anObject; + } + + /** + * Returns the description of the class of the + * objects that is vended by this data source, + * or null if this cannot be determined. + * This implementation returns TestObject. + */ + public EOClassDescription + classDescriptionForObjects () + { + return EOClassDescription.classDescriptionForClass( + TestObject.class ); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestMap.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestMap.java new file mode 100644 index 0000000..8a88e68 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestMap.java @@ -0,0 +1,164 @@ +package net.wotonomy.test; + +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.StringTokenizer; + +import net.wotonomy.datastore.DataSoup; +import net.wotonomy.datastore.SerializedFileSoup; +import net.wotonomy.datastore.XMLFileSoup; +import net.wotonomy.foundation.internal.ValueConverter; + +public class TestMap extends HashMap +{ + public TestMap() + { + put( "date", new Date() ); + put( "firstName", randomParse( + "Bert|Ernie|Elmo|Zoe|Arthur|Emily|DJ|Grover|Oscar|Max|Big|Twinkle") ); + put( "middleName", new StringBuffer( randomParse( + "Rufus|Remy|Martin|Josephus|Ulysses|Homer|Bart|Tip|Onegin|Meredith|Jay") ) ); + put( "lastName", randomParse( + "Alejandro|Alexander|Bird|Gosling|Joy|Van Hoff|Pedia|Marr|McNealy|Ping") ); + put( "address", randomParse( "1|2|3|4" ) + randomParse( "0|1|00|10|5|50" ) + + randomParse( "0|00|1|01|5|05|9|09||000" ) + " " + randomParse( + "Merry|Berry|Perry|Jerry|Meadow|Falls|Elm|Raspberry|Strawberry") + " " + + randomParse( "Road|Lane|Court|Drive|Parkway|Terrace" ) ); + put( "city", randomParse( + "Springfield|Sterling|Cascades|Vienna|Reston|Paris|London|Runnymeade") ); + put( "state", randomParse( + "TX|NJ|NY|VA|DC|MD|NC|SC|WV|AR|FL|CA|TN" ) ); + put( "zip", ValueConverter.getInteger( + randomParse( "1|2|3|4" ) + "0" + randomParse( "0|1|2|3|5" ) + + randomParse( "6|7|8|9" ) + randomParse( "6|7|8|9" ) ) ); + put( "age", new Short( (short) ( new Random().nextDouble() * 40 + 18 ) ) ); + childCount = -1; + } + + protected int childCount; + public int getChildCount() + { + if ( childCount == -1 ) + { + //childCount = (int) ( random.nextDouble() * 6 ) - 3; // + 100; // tree scalability test + if ( childCount < 0 ) childCount = 0; + } + return childCount; + }; + + protected TestMap[] children; + public TestMap[] getChildren() + { + if ( get( "children" ) == null ) + { + int n = getChildCount(); + TestMap[] children = new TestMap[ n ]; + for ( int i = 0; i < n; i++ ) + { + children[i] = new TestMap(); + } + put( "children", children ); + } + return (TestMap[]) get( "children" ); + } + public void setChildren( TestMap[] aChildArray ) + { + put( "children", aChildArray ); + } + public List getChildList() + { + List result = new LinkedList(); + TestMap[] childArray = getChildren(); + for ( int i = 0; i < childArray.length; i++ ) + { + result.add( childArray[i] ); + } + return result; + } + public void setChildList( List aChildList ) + { + TestMap[] children = new TestMap[ aChildList.size() ]; + for ( int i = 0; i < children.length; i++ ) + { + children[i] = (TestMap) aChildList.get( i ); + } + setChildren( children ); + } + + public String getFullName() + { + return get( "firstName" ) + " " + get( "middleName" ) + " " + get( "lastName" ); + } + + public boolean equals( Object anObject ) + { + return anObject == this; + } + + public String toString() + { + return "[" + getClass().getName() + ":" + getFullName() + "]"; + } + + // statics + + private static Random random = new Random(); + private static String randomParse( String aString ) + { + String result = ""; + StringTokenizer tokens = new StringTokenizer( aString, "|" ); + int n = (int) ( random.nextDouble() * tokens.countTokens() ); + for ( int i = 0; i <= n; i++ ) + { + result = tokens.nextToken(); + } + return result; + } + + public static void main( String[] argv ) + { + int count = 100; + boolean xmlMode = false; + if ( argv.length > 0 ) + { + Integer parsed = ValueConverter.getInteger( argv[0] ); + if ( parsed != null ) count = parsed.intValue(); + + if ( argv.length > 1 ) + { + if ( argv[1].indexOf( "xml" ) > -1 ) + { + xmlMode = true; + } + } + } + +long millis = System.currentTimeMillis(); + + DataSoup store = null; + if ( xmlMode ) + { + store = new XMLFileSoup( "testMaps-xml" ); + } + else + { + store = new SerializedFileSoup( "testMaps-java" ); + } + + Object o; + for ( int i = 0; i < count; i++ ) + { + store.addObject( new TestMap() ); + } + /* + store.addIndex( "age", "age" ); + store.addIndex( "zipCode", "zipCode" ); + store.addIndex( "firstName", "firstName" ); + store.addIndex( "lastName", "lastName" ); +*/ +System.out.println( System.currentTimeMillis() - millis + " milliseconds" ); + } +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObject.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObject.java new file mode 100644 index 0000000..72a3dbc --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObject.java @@ -0,0 +1,362 @@ +package net.wotonomy.test; + +import java.io.Serializable; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.StringTokenizer; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.datastore.DataSoup; +import net.wotonomy.datastore.SerializedFileSoup; +import net.wotonomy.datastore.XMLFileSoup; +import net.wotonomy.foundation.internal.ValueConverter; + +public class TestObject implements Serializable // , EOKeyValueCoding +{ + static final long serialVersionUID = -5482454640042392838L; + +// for testing manual array faulting +public EOEditingContext editingContext; +public EOEditingContext getEditingContext() { return editingContext; }; + + public TestObject() + { + date = new Date(); + firstName = randomParse( + "Bert|Ernie|Elmo|Zoe|Arthur|Emily|DJ|Grover|Oscar|Max|Big|Twinkle"); + middleName = new StringBuffer( randomParse( + "Rufus|Remy|Martin|Josephus|Ulysses|Homer|Bart|Tip|Onegin|Meredith|Jay") ); + lastName = randomParse( + "Alejandro|Alexander|Bird|Gosling|Joy|Van Hoff|Pedia|Marr|McNealy|Ping"); + address = randomParse( "1|2|3|4" ) + randomParse( "0|1|00|10|5|50" ) + + randomParse( "0|00|1|01|5|05|9|09||000" ) + " " + randomParse( + "Merry|Berry|Perry|Jerry|Meadow|Falls|Elm|Raspberry|Strawberry") + " " + + randomParse( "Road|Lane|Court|Drive|Parkway|Terrace" ); + city = randomParse( + "Springfield|Sterling|Cascades|Vienna|Reston|Paris|London|Runnymeade"); + state = randomParse( + "TX|NJ|NY|VA|DC|MD|NC|SC|WV|AR|FL|CA|TN" ); + zip = ValueConverter.getIntValue( + randomParse( "1|2|3|4" ) + "0" + randomParse( "0|1|2|3|5" ) + + randomParse( "6|7|8|9" ) + randomParse( "6|7|8|9" ) ); + age = (short) ( new Random().nextDouble() * 40 + 18 ); + childCount = -1; +// children = null; + childList = null; + } + + protected Date date; + public Date getCreateDate() { return date; } + public void setCreateDate( Date aDate ) { date = aDate; } + + protected String firstName; + public String getFirstName() { return firstName; } + public void setFirstName( String aName ) { firstName = aName; } + + protected String lastName; + public String getLastName() { return lastName; } + public void setLastName( String aName ) { + if ( "Jones".equals( aName ) ) throw new RuntimeException( "Jones not allowed" ) ; + lastName = aName; + } + + protected StringBuffer middleName; + public StringBuffer getMiddleName() { return middleName; } + public void setMiddleName( StringBuffer aName ) { middleName = aName; } + + protected String address; + public String getAddress() { return address; } + public void setAddress( String anAddress ) { address = anAddress; } + + protected String city; + public String getCity() { return city; } + public void setCity( String aCity ) { city = aCity; } + + protected String state; + public String getState() { return state; } + public void setState( String aState ) { state = aState; } + + protected int zip; + public int getZipCode() { return zip; } + public void setZipCode( int aZipCode ) { zip = aZipCode; } + + protected short age; + public short getAge() { return age; } + public void setAge( short anAge ) { age = anAge; } + + protected boolean special; + public Boolean isSpecial() { return new Boolean( special ); } + public void setSpecial( Boolean isSpecial ) { special = isSpecial.booleanValue(); } + +/* + protected Object[] children; + + private Object[] getChildren() + { + if ( children == null ) + { + int n = getChildCount(); + children = new Object[ n ]; + for ( int i = 0; i < n; i++ ) + { + children[i] = new TestObject(); + } + //System.out.println( "TestObject.getChildren: " + toString() + " : " + getChildCount() ); + } + return children; + } + private void setChildren( Object[] aChildArray ) + { + children = aChildArray; + childCount = aChildArray.length; + } + + // following child list implementation wraps child array + + public List getChildList() + { + List result = new LinkedList(); + Object[] childArray = getChildren(); + for ( int i = 0; i < childArray.length; i++ ) + { + result.add( childArray[i] ); + } + return result; + } + public void setChildList( List aChildList ) + { + children = new Object[ aChildList.size() ]; + for ( int i = 0; i < children.length; i++ ) + { + children[i] = (TestObject) aChildList.get( i ); + } + childCount = children.length; + } +*/ + protected int childCount; + public int getChildCount() + { + if ( childCount == -1 ) + { +// uncomment this to enable random children +// childCount = (int) ( random.nextDouble() * 6 ) - 3; // + 100; // tree scalability test + if ( childCount < 0 ) childCount = 0; + } + +// this tests internal count +// return childCount; +// this tests deferred count + if ( childList != null ) + { + return childList.size(); + } + else + { + return 0; + } + }; + + // following child list implementation stands alone + + protected List childList; + public List getChildList() + { +/* + // this tests random child population + if ( childList == null ) + { + int n = getChildCount(); + childList = new LinkedList(); + for ( int i = 0; i < n; i++ ) + { + childList.add( new TestObject() ); + } + } +*/ + // this tests manual loading + if ( childList == null ) + { + childList = new LinkedList(); + } + return childList; + } + public void setChildList( List aChildList ) + { + childList = aChildList; + } + + protected TestObject parent; + public TestObject getParent() { return parent; } + public void setParent( TestObject anObject ) { parent = anObject; } + + public String getHash() { return Integer.toHexString( System.identityHashCode( this ) ); } + + public String getFullName() + { +// return getHash() + ": " + firstName + " " + middleName + " " + lastName; + return firstName + " " + middleName + " " + lastName; + } + + public boolean equals( Object anObject ) + { + return anObject == this; + } + + public String toString() + { + return "[" + getClass().getName() + + "@" + Integer.toHexString( System.identityHashCode(this) ) + + ":" + getFullName() + "]"; + } + + // statics + + private static Random random = new Random(); + private static String randomParse( String aString ) + { + String result = ""; + StringTokenizer tokens = new StringTokenizer( aString, "|" ); + int n = (int) ( random.nextDouble() * tokens.countTokens() ); + for ( int i = 0; i <= n; i++ ) + { + result = tokens.nextToken(); + } + return result; + } + + + // interface EOKeyValueCoding: + // disable this interface by commenting out the "implements" declaration + + /** + * Returns the value for the specified property. + * If the property does not exist, this method should + * call handleQueryWithUnboundKey. + */ + public Object valueForKey( String aKey ) + { + System.out.println( "valueForKey: " + aKey ); + return EOKeyValueCodingSupport.valueForKey( this, aKey ); + } + + /** + * Sets the property to the specified value. + * If the property does not exist, this method should + * call handleTakeValueForUnboundKey. + * If the property is of a type that cannot allow + * null (e.g. primitive types) and aValue is null, + * this method should call unableToSetNullForKey. + */ + public void takeValueForKey( Object aValue, String aKey ) + { + System.out.println( "takeValueForKey: " + aValue + " : " + aKey ); + EOKeyValueCodingSupport.takeValueForKey( this, aValue, aKey ); + } + + /** + * Returns the value for the private field that + * corresponds to the specified property. + */ + public Object storedValueForKey( String aKey ) + { + System.out.println( "storedValueForKey: " + aKey ); + return EOKeyValueCodingSupport.storedValueForKey( this, aKey ); + } + + /** + * Sets the the private field that corresponds to the + * specified property to the specified value. + */ + public void takeStoredValueForKey( Object aValue, String aKey ) + { + System.out.println( "takeStoredValueForKey: " + aValue + " : " + aKey ); + EOKeyValueCodingSupport.takeStoredValueForKey( this, aValue, aKey ); + } + + /** + * Called by valueForKey when the specified key is + * not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + public Object handleQueryWithUnboundKey( String aKey ) + { + System.out.println( "handleQueryWithUnboundKey: " + aKey ); + return EOKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + /** + * Called by takeValueForKey when the specified key + * is not found on this object. Implementing classes + * should handle the specified value or otherwise + * throw an exception. + */ + public void handleTakeValueForUnboundKey( Object aValue, String aKey ) + { + System.out.println( "handleTakeValueForUnboundKey: " + aValue + " : " + aKey ); + EOKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + /** + * Called by takeValueForKey when the type of the + * specified key is not allowed to be null, as is + * the case with primitive types. Implementing + * classes should handle this case appropriately + * or otherwise throw an exception. + */ + public void unableToSetNullForKey( String aKey ) + { + System.out.println( "unableToSetNullForKey: " + aKey ); + EOKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + + // main entry point + + public static void main( String[] argv ) + { + int count = 100; + boolean xmlMode = false; + if ( argv.length > 0 ) + { + Integer parsed = ValueConverter.getInteger( argv[0] ); + if ( parsed != null ) count = parsed.intValue(); + + if ( argv.length > 1 ) + { + if ( argv[1].indexOf( "xml" ) > -1 ) + { + xmlMode = true; + } + } + } + +long millis = System.currentTimeMillis(); + + DataSoup store = null; + if ( xmlMode ) + { + store = new XMLFileSoup( "testObjects-xml" ); + } + else + { + store = new SerializedFileSoup( "testObjects-java" ); + } + + Object o; + for ( int i = 0; i < count; i++ ) + { + store.addObject( new TestObject() ); + } + /* + store.addIndex( "age", "age" ); + store.addIndex( "zipCode", "zipCode" ); + store.addIndex( "firstName", "firstName" ); + store.addIndex( "lastName", "lastName" ); +*/ +System.out.println( System.currentTimeMillis() - millis + " milliseconds" ); + } +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectClassDesc.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectClassDesc.java new file mode 100644 index 0000000..beb852c --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectClassDesc.java @@ -0,0 +1,31 @@ +package net.wotonomy.test; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.foundation.NSArray; + +/** +* A simple class description for testing. +*/ +public class TestObjectClassDesc extends EOClassDescription +{ + public TestObjectClassDesc() + { + super( TestObject.class ); + } + + public EOClassDescription classDescriptionForDestinationKey( + String detailKey ) + { + if ( "childList".equals( detailKey ) ) + { + return this; + } + return null; + } + + public NSArray toManyRelationshipKeys() + { + return new NSArray( "childList" ); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectStore.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectStore.java new file mode 100644 index 0000000..8fcb8a0 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestObjectStore.java @@ -0,0 +1,258 @@ +package net.wotonomy.test; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOFetchSpecification; +import net.wotonomy.control.EOGlobalID; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.test.DataKeyID; +import net.wotonomy.datastore.DataSoup; +import net.wotonomy.datastore.DataView; +import net.wotonomy.datastore.XMLFileSoup; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.Duplicator; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An object store that wraps a datastore +* for vending test objects. +*/ +public class TestObjectStore extends EOObjectStore +{ + DataSoup soup; + + /** + * Constructor specifies path to datastore. + */ + public TestObjectStore( String aPath ) + { + soup = new XMLFileSoup( aPath ); + } + + /** + * This implementation simply returns + * objectsWithSourceGlobalID. + */ + public NSArray arrayFaultWithSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ) + { + return objectsForSourceGlobalID( + aGlobalID, aRelationship, aContext ); + } + + /** + * This implementation returns the actual + * object for the specified id. + */ + public Object faultForGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { +System.out.println( "TestObjectStore: * reading object * : " + aGlobalID ); + return soup.getObjectByKey( + ((DataKeyID)aGlobalID).getKey() ); + } + + /** + * Returns a fault representing an object of + * the specified entity type with values from + * the specified dictionary. The fault should + * belong to the specified editing context. + */ + public Object faultForRawRow ( + Map aDictionary, + String anEntityName, + EOEditingContext aContext ) + { + //TODO: faults are not yet supported + throw new WotonomyException( + "Faults are not yet supported." ); + } + + /** + * Given a newly instantiated object, this method + * initializes its properties to values appropriate + * for the specified id. The object should belong + * to the specified editing context. + * This method is called to populate faults. + */ + public void initializeObject ( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { +System.out.println( "TestObjectStore: * reading object * : " + aGlobalID ); + Object original = soup.getObjectByKey( + ((DataKeyID)aGlobalID).getKey() ); + EOObserverCenter.notifyObserversObjectWillChange( anObject ); + Duplicator.deepCopy( original, anObject ); + //TODO: need to handle child object registration in aContext + } + + /** + * Remove all values from all objects in memory, + * turning them into faults, and posts a notification + * that all objects have been invalidated. + */ + public void invalidateAllObjects () + { + // does nothing + } + + /** + * Removes values with the specified ids from memory, + * turning them into faults, and posts a notification + * that those objects have been invalidated. + */ + public void invalidateObjectsWithGlobalIDs ( + List aList ) + { + // does nothing + } + + /** + * Returns false because locking is not permitted. + */ + public boolean isObjectLockedWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + return false; + } + + /** + * Does nothing because locking is not permitted. + */ + public void lockObjectWithGlobalID ( + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + // does nothing + } + + /** + * Returns a List of objects associated with the object + * with the specified id for the specified property + * relationship. Faults are not allowed in the array. + * All objects should belong to the specified editing context. + */ + public NSArray objectsForSourceGlobalID ( + EOGlobalID aGlobalID, + String aRelationship, + EOEditingContext aContext ) + { + //TODO: relationships are not yet supported + throw new WotonomyException( + "Relationships are not yet supported." ); + } + + /** + * Returns a List of objects the meet the criteria of + * the supplied specification. Faults are not allowed in the array. + * Each object is registered with the specified editing context. + * If any object is already registered in the specified context, + * it is not refetched and that object should be used in the array. + */ + public NSArray objectsWithFetchSpecification ( + EOFetchSpecification aFetchSpec, + EOEditingContext aContext ) + { + //TODO: fetch specs are not yet supported + + DataView view = soup.queryObjects( null, null ); +System.out.println( "TestObjectStore: ** querying all objects **" ); + NSMutableArray result = new NSMutableArray(); + Object o; + Object existing; + DataKeyID id; + Iterator it = view.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + id = new DataKeyID( view.getKeyForObject( o ) ); + existing = aContext.objectForGlobalID( id ); + if ( existing != null ) + { + o = existing; + } + else + { + aContext.recordObject( o, id ); + } + result.addObject( o ); + } + return result; + } + + /** + * Removes all values from the specified object, + * converting it into a fault for the specified id. + * New or deleted objects should not be refaulted. + */ + public void refaultObject ( + Object anObject, + EOGlobalID aGlobalID, + EOEditingContext aContext ) + { + //TODO: faults are not yet supported + // just re-initialize the object + initializeObject( anObject, aGlobalID, aContext ); + } + + /** + * Writes all changes in the specified editing context + * to the respository. + */ + public void saveChangesInEditingContext ( + EOEditingContext aContext ) + { + Object o; + DataKeyID id; + Iterator it; + + System.out.println( aContext.updatedObjects() ); + + // process updates + it = aContext.updatedObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + id = (DataKeyID) aContext.globalIDForObject( o ); +System.out.println( "TestObjectStore: * updating object * : " + id ); + soup.updateObject( id.getKey(), o ); + } + + // process deletes + it = aContext.deletedObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + id = (DataKeyID) aContext.globalIDForObject( o ); +System.out.println( "TestObjectStore: * deleting object * : " + id ); + soup.removeObject( id.getKey() ); + // remove object from editing context + aContext.forgetObject( o ); + } + + // process inserts + it = aContext.insertedObjects().iterator(); + while ( it.hasNext() ) + { + o = it.next(); + id = new DataKeyID( soup.addObject( o ) ); +System.out.println( "TestObjectStore: * adding object * : " + id ); + // register object in editing context with new id + aContext.forgetObject( o ); + aContext.recordObject( o, id ); + } + + } +} + diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestPanel.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestPanel.java new file mode 100644 index 0000000..7e868d0 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TestPanel.java @@ -0,0 +1,110 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Insets; +import java.util.Vector; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.border.EmptyBorder; +import javax.swing.text.JTextComponent; + +import net.wotonomy.ui.swing.components.BetterFlowLayout; +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.components.InfoPanel; + +/** +* A master-detail panel with a list, some +* textfields and some buttons. +*/ +public class TestPanel extends JPanel +{ +// public JList list; + public JTable table; + public InfoPanel infoPanel; + public ButtonPanel savePanel; + public ButtonPanel buttonPanel; + public JTextComponent firstNameField; + public JTextComponent middleNameField, lastNameField; + public JComboBox dateBox, monthBox, yearBox; + public JSlider slider; + public JCheckBox checkbox; + + public TestPanel() + { + this.setLayout( new BorderLayout( 10, 10 ) ); + this.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + + JPanel overviewPanel = new JPanel(); + overviewPanel.setLayout( new BorderLayout() ); + + //list = new JList(); + //JScrollPane scrollPane = new JScrollPane( list ); + + table = new JTable(); + JScrollPane scrollPane = new JScrollPane( table ); + + overviewPanel.add( scrollPane, BorderLayout.CENTER ); + + this.add( overviewPanel, BorderLayout.CENTER ); + + infoPanel = new InfoPanel(); + infoPanel.setColumns( 1 ); + + // name fields + firstNameField = new JTextField(); +// infoPanel.addPair( "First Name", firstNameField ); + middleNameField = new JTextField(); +// infoPanel.addPair( "Middle Name", middleNameField ); + lastNameField = new JTextField(); +// infoPanel.addPair( "Last Name", lastNameField ); + checkbox = new JCheckBox(); + + infoPanel.addRow( "Name", new Component[] { + firstNameField, middleNameField, lastNameField, checkbox } ); + + // date comboboxen + Vector datesList = new Vector(); + for ( int i = 1; i < 32; i++ ) datesList.add( new Integer( i ) ); + dateBox = new JComboBox( datesList ); + dateBox.setEditable( true ); + monthBox = new JComboBox(); + yearBox = new JComboBox(); + infoPanel.addRow( "Create Date", + dateBox, monthBox, yearBox ); + + // year slider + infoPanel.addRow( "Day of Month", slider = new JSlider( + JSlider.HORIZONTAL, 1, 31, 1 ) ); + + // navigation buttons + + JPanel navigationPanel = new JPanel(); + navigationPanel.setLayout( new BorderLayout() ); + + buttonPanel = new ButtonPanel( new String[] { "Tree View", "Add", "Remove" } ); + buttonPanel.setAlignment( BetterFlowLayout.LEFT ); + buttonPanel.setInsets( new Insets( 0, 0, 0, 0 ) ); + navigationPanel.add( buttonPanel, BorderLayout.WEST ); + + savePanel = new ButtonPanel( new String[] { "Refresh All", "Commit" } ); + savePanel.setAlignment( BetterFlowLayout.RIGHT ); + navigationPanel.add( savePanel, BorderLayout.EAST ); + + // bottom panel layout + + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout( new BorderLayout() ); + bottomPanel.add( infoPanel, BorderLayout.NORTH ); + bottomPanel.add( navigationPanel, BorderLayout.SOUTH ); + + this.add( bottomPanel, BorderLayout.SOUTH ); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeController.java new file mode 100644 index 0000000..d61d8ff --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeController.java @@ -0,0 +1,285 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.AbstractButton; +import javax.swing.BorderFactory; +import javax.swing.JDialog; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.MasterDetailAssociation; +import net.wotonomy.ui.swing.ListAssociation; +import net.wotonomy.ui.swing.RadioPanelAssociation; +import net.wotonomy.ui.swing.TextAssociation; +import net.wotonomy.ui.swing.TreeAssociation; +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.util.ObjectInspector; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* A simple editor panel with a few textfields. +*/ +public class TreeController implements ActionListener +{ + final EODisplayGroup group; + final EODisplayGroup titlesGroup; + final EODisplayGroup detailGroup; + final EOEditingContext editingContext; + + public TreeController( EODataSource aDataSource ) + { + titlesGroup = new EODisplayGroup(); + group = new EODisplayGroup(); + detailGroup = new EODisplayGroup(); + titlesGroup.setDataSource( aDataSource ); + editingContext = aDataSource.editingContext(); + init(); + } + + public TreeController( EOEditingContext aContext, + EODisplayGroup aTitlesGroup, EODisplayGroup aChildGroup ) + { + titlesGroup = aTitlesGroup; + group = aChildGroup; + detailGroup = new EODisplayGroup(); + editingContext = aContext; + init(); + } + + public void init() + { + final TreePanel treePanel = new TreePanel(); + treePanel.panel.setBorder( + BorderFactory.createCompoundBorder( + BorderFactory.createRaisedBevelBorder(), + BorderFactory.createEmptyBorder( 10, 10, 10, 10 ) ) ); + treePanel.setBorder( + BorderFactory.createEmptyBorder( 0, 0, 0, 0 ) ); + ButtonPanel okPanel = new ButtonPanel( + new String[] { "Refresh", "Commit" } ); + treePanel.add( okPanel, BorderLayout.SOUTH ); +/* + + // set up renderer + IconCellRenderer iconRenderer = new IconCellRenderer() + { + private Icon icon = UIManager.getIcon("FileChooser.homeFolderIcon"); + public Icon getIconForContext( + JComponent container, Object value, + int row, int col, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + return icon; + } + }; + treePanel.tree.setCellRenderer( iconRenderer ); + + // enable icon clicking + treePanel.tree.setCellEditor( iconRenderer ); + iconRenderer.addActionListener( this ); +*/ + + treePanel.tree.setEditable( true ); + + // set up display groups + + titlesGroup.fetch(); + + // text associations + EOAssociation ta; + + ta = new TreeAssociation( treePanel.tree, "People" ); + ta.bindAspect( EOAssociation.TitlesAspect, titlesGroup, "fullName" ); + ta.bindAspect( EOAssociation.ChildrenAspect, group, "childList" ); + ta.bindAspect( EOAssociation.IsLeafAspect, titlesGroup, "childCount" ); +// ta.bindAspect( EOAssociation.IsLeafAspect, null, "false" ); + ta.establishConnection(); + treePanel.tree.setEditable( true ); + + ta = new TextAssociation( treePanel.editPanel.firstNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "firstName" ); + ta.establishConnection(); + + ta = new TextAssociation( treePanel.editPanel.middleNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "middleName" ); + ta.establishConnection(); + + ta = new TextAssociation( treePanel.editPanel.lastNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "lastName" ); + ta.establishConnection(); + + ta = new RadioPanelAssociation( treePanel.editPanel.yearRadioPanel ); + ta.bindAspect( EOAssociation.ValueAspect, group, "createDate.year" ); + + EODisplayGroup yearTitles = new EODisplayGroup(); + yearTitles.setObjectArray( new NSArray( + new Object[] { "1999", "2000", "2001" } ) ); + ta.bindAspect( EOAssociation.TitlesAspect, yearTitles, "" ); + + EODisplayGroup yearObjects = new EODisplayGroup(); + yearObjects.setObjectArray( new NSArray( + new Object[] { new Integer( 99 ), new Integer( 100 ), new Integer( 101 ) } ) ); + ta.bindAspect( EOAssociation.ObjectsAspect, yearObjects, "" ); + + ta.establishConnection(); + + // detail group + + ta = new MasterDetailAssociation( detailGroup ); + ta.bindAspect( EOAssociation.ParentAspect, group, "childList" ); + ta.establishConnection(); + + ta = new ListAssociation( treePanel.editPanel.list ); + ta.bindAspect( EOAssociation.TitlesAspect, detailGroup, "fullName" ); + ta.establishConnection(); + + // display group action associations + + AbstractButton addButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Add" ); + addButton.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + Object testObject = new TestObject(); + editingContext.insertObject( testObject ); + detailGroup.insertObjectAtIndex( testObject, 0 ); + } + } ); + + AbstractButton removeButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Remove" ); + removeButton.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + if ( detailGroup.selectedObject() != null ) + detailGroup.deleteSelection(); + } + } ); + + // ok / cancel buttons + + AbstractButton button; + + button = (AbstractButton) + okPanel.getButton( "Commit" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + editingContext.saveChanges(); + } + } ); + + button = (AbstractButton) + okPanel.getButton( "Refresh" ); + button.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + editingContext.revert(); +// editingContext.invalidateAllObjects(); + } + } ); + + +/* + AbstractButton refreshButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Refresh" ); + refreshButton.addActionListener( new ActionListener() + { + int count = 0; + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup displayGroup = (EODisplayGroup) + treePanel.tree.getSelectionPath().getLastPathComponent(); +// displayGroup.insertObjectAtIndex( new TestObject(), 0 ); +// displayGroup.deleteObjectAtIndex( 0 ); + List sortOrderings = new LinkedList(); + if ( count++ % 2 == 0 ) + { + sortOrderings.add( new EOSortOrdering( + "lastName", EOSortOrdering.CompareAscending ) ); + } + else + { + sortOrderings.add( new EOSortOrdering( + "lastName", EOSortOrdering.CompareDescending ) ); + } + displayGroup.setSortOrderings( sortOrderings ); + displayGroup.updateDisplayedObjects(); + } + } ); +*/ +/* + ta = new DisplayGroupActionAssociation( + treePanel.editPanel.addPanel.getButton( "Remove" ) ); + ta.bindAspect( EOAssociation.ActionAspect, detailGroup, "deleteSelection" ); + ta.establishConnection(); + + ta = new DisplayGroupActionAssociation( + treePanel.editPanel.addPanel.getButton( "Refresh" ) ); + ta.bindAspect( EOAssociation.ActionAspect, group, "updateDisplayedObjects" ); + ta.establishConnection(); +*/ + + // add mouse listener for list + + treePanel.editPanel.list.addMouseListener( new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if ( e.getClickCount() == 2 ) + { + Object item = detailGroup.selectedObject(); + if ( item != null ) + { + new InspectorController( item ); + } + } + } + }); + + // launch + + JDialog d = new JDialog(); + d.getContentPane().add( treePanel ); + d.setTitle( "Tree Panel" ); + d.pack(); + WindowUtilities.cascade( d ); + d.show(); + + // workaround for memory issues on jdk1.2.2 + d.addWindowListener( new WindowAdapter() + { + // exit on close + public void windowClosing(WindowEvent e) + { + ((JDialog)e.getWindow()).getContentPane().removeAll(); + } + }); + } + + public void actionPerformed( ActionEvent evt ) + { + Object item = group.selectedObject(); + if ( item != null ) + { +// new InspectorController( item ); + new ObjectInspector( item ); + } + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeInspectorController.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeInspectorController.java new file mode 100644 index 0000000..8ec9554 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreeInspectorController.java @@ -0,0 +1,184 @@ +package net.wotonomy.test; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.AbstractButton; +import javax.swing.JDialog; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.MasterDetailAssociation; +import net.wotonomy.ui.swing.ListAssociation; +import net.wotonomy.ui.swing.RadioPanelAssociation; +import net.wotonomy.ui.swing.TextAssociation; +import net.wotonomy.ui.swing.TreeAssociation; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* A simple editor panel with a few textfields. +*/ +public class TreeInspectorController +{ + public TreeInspectorController( EODisplayGroup titlesGroup, EODisplayGroup childrenGroup ) + { + final EODisplayGroup group = childrenGroup; +// final EODisplayGroup group = titlesGroup; + final TreePanel treePanel = new TreePanel(); + + // text associations + + EOAssociation ta; + + ta = new TreeAssociation( treePanel.tree, "People" ); + ta.bindAspect( EOAssociation.TitlesAspect, titlesGroup, "lastName" ); + ta.bindAspect( EOAssociation.ChildrenAspect, group, "childList" ); + ta.bindAspect( EOAssociation.IsLeafAspect, titlesGroup, "childCount" ); +// ta.bindAspect( EOAssociation.IsLeafAspect, null, "false" ); + ta.establishConnection(); + treePanel.tree.setEditable( true ); + + ta = new TextAssociation( treePanel.editPanel.firstNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "firstName" ); + ta.establishConnection(); + + ta = new TextAssociation( treePanel.editPanel.middleNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "middleName" ); + ta.establishConnection(); + + ta = new TextAssociation( treePanel.editPanel.lastNameField ); + ta.bindAspect( EOAssociation.ValueAspect, group, "lastName" ); + ta.establishConnection(); + + ta = new RadioPanelAssociation( treePanel.editPanel.yearRadioPanel ); + ta.bindAspect( EOAssociation.ValueAspect, group, "createDate.year" ); + + EODisplayGroup yearTitles = new EODisplayGroup(); + yearTitles.setObjectArray( new NSArray( + new Object[] { "1999", "2000", "2001" } ) ); + ta.bindAspect( EOAssociation.TitlesAspect, yearTitles, "" ); + + EODisplayGroup yearObjects = new EODisplayGroup(); + yearObjects.setObjectArray( new NSArray( + new Object[] { new Integer( 99 ), new Integer( 100 ), new Integer( 101 ) } ) ); + ta.bindAspect( EOAssociation.ObjectsAspect, yearObjects, "" ); + + ta.establishConnection(); + + // detail group + + final EODisplayGroup detailGroup = new EODisplayGroup(); + + ta = new MasterDetailAssociation( detailGroup ); + ta.bindAspect( EOAssociation.ParentAspect, group, "childList" ); + ta.establishConnection(); + + ta = new ListAssociation( treePanel.editPanel.list ); + ta.bindAspect( EOAssociation.TitlesAspect, detailGroup, "fullName" ); + ta.establishConnection(); + + // display group action associations + + AbstractButton addButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Add" ); + addButton.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + detailGroup.insertObjectAtIndex( new TestObject(), 0 ); + } + } ); + + AbstractButton removeButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Remove" ); + removeButton.addActionListener( new ActionListener() + { + public void actionPerformed( ActionEvent evt ) + { + if ( detailGroup.selectedObject() != null ) + detailGroup.deleteSelection(); + } + } ); +/* + AbstractButton refreshButton = (AbstractButton) + treePanel.editPanel.addPanel.getButton( "Refresh" ); + refreshButton.addActionListener( new ActionListener() + { + int count = 0; + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup displayGroup = (EODisplayGroup) + treePanel.tree.getSelectionPath().getLastPathComponent(); +// displayGroup.insertObjectAtIndex( new TestObject(), 0 ); +// displayGroup.deleteObjectAtIndex( 0 ); + List sortOrderings = new LinkedList(); + if ( count++ % 2 == 0 ) + { + sortOrderings.add( new EOSortOrdering( + "lastName", EOSortOrdering.CompareAscending ) ); + } + else + { + sortOrderings.add( new EOSortOrdering( + "lastName", EOSortOrdering.CompareDescending ) ); + } + displayGroup.setSortOrderings( sortOrderings ); + displayGroup.updateDisplayedObjects(); + } + } ); +*/ +/* + ta = new DisplayGroupActionAssociation( + treePanel.editPanel.addPanel.getButton( "Remove" ) ); + ta.bindAspect( EOAssociation.ActionAspect, detailGroup, "deleteSelection" ); + ta.establishConnection(); + + ta = new DisplayGroupActionAssociation( + treePanel.editPanel.addPanel.getButton( "Refresh" ) ); + ta.bindAspect( EOAssociation.ActionAspect, group, "updateDisplayedObjects" ); + ta.establishConnection(); +*/ + + // add mouse listener for list + + treePanel.editPanel.list.addMouseListener( new MouseAdapter() + { + public void mouseClicked(MouseEvent e) + { + if ( e.getClickCount() == 2 ) + { + Object item = detailGroup.selectedObject(); + if ( item != null ) + { + new InspectorController( item ); + } + } + } + }); + + // launch + + JDialog d = new JDialog(); + d.getContentPane().add( treePanel ); + d.setTitle( "Tree Panel" ); + d.pack(); + WindowUtilities.cascade( d ); + d.show(); + + // workaround for memory issues on jdk1.2.2 + d.addWindowListener( new WindowAdapter() + { + // exit on close + public void windowClosing(WindowEvent e) + { + ((JDialog)e.getWindow()).getContentPane().removeAll(); + } + }); + } + +} diff --git a/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreePanel.java b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreePanel.java new file mode 100644 index 0000000..48ebd81 --- /dev/null +++ b/projects/net.wotonomy.test/src/main/java/net/wotonomy/test/TreePanel.java @@ -0,0 +1,39 @@ +package net.wotonomy.test; + +import java.awt.BorderLayout; +import java.awt.Dimension; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.border.EmptyBorder; + +/** +* A simple editor panel with a few textfields. +*/ +public class TreePanel extends JPanel +{ + public JTree tree; + public EditPanel editPanel; + public JPanel panel; + + public TreePanel() + { + panel = new JPanel(); + panel.setLayout( new BorderLayout() ); + panel.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + + tree = new JTree(); + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + JScrollPane scrollPane = new JScrollPane( tree ); + scrollPane.setPreferredSize( new Dimension( 150, 200 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + editPanel = new EditPanel(); + panel.add( editPanel, BorderLayout.EAST ); + + this.setLayout( new BorderLayout() ); + this.add( panel, BorderLayout.CENTER ); + } + +} diff --git a/projects/net.wotonomy.ui.swing/.classpath b/projects/net.wotonomy.ui.swing/.classpath new file mode 100644 index 0000000..dce51f2 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/projects/net.wotonomy.ui.swing/.cvsignore b/projects/net.wotonomy.ui.swing/.cvsignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/.cvsignore @@ -0,0 +1 @@ +target diff --git a/projects/net.wotonomy.ui.swing/.project b/projects/net.wotonomy.ui.swing/.project new file mode 100644 index 0000000..50e145d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.ui.swing + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.ui.swing/pom.xml b/projects/net.wotonomy.ui.swing/pom.xml new file mode 100644 index 0000000..5732a7d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-ui-swing + Wotonomy - GUI - Swing Implementation + jar + + + ${project.groupId} + wotonomy-ui + ${project.version} + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.ui.swing + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.ui.swing + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java new file mode 100644 index 0000000..cc3af69 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ActionAssociation.java @@ -0,0 +1,335 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Enumeration; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ActionAssociation binds any ActionEvent broadcaster +* (typically Buttons and the like) to a display group. +* Actions are invoked on all selected objects in the +* display group bound to the action aspect. +* Bindings are: +*
    +*
  • action: a method to be invoked on selected objects. +* If the argument aspect is bound, the method must take +* one argument. Otherwise, the method must take no arguments.
  • +*
  • argument: the attribute of the selected object(s) (possibly +* from a different display group) that will be used as an argument +* to the action method
  • +*
  • enabled: a boolean property that determines whether +* the controlled component is enabled
  • +*
  • visible: a boolean property that determines whether +* the controlled component is visible
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ActionAssociation extends EOAssociation + implements ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ActionAspect, ArgumentAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "target" + } ); + + static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public ActionAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return + ( addActionListener.implementedByObject( anObject ) ) + && ( removeActionListener.implementedByObject( anObject ) ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. + */ + public static String primaryAspect () + { + return ActionAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + try + { + addActionListener.invoke( object(), this ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOActionAssociation: " + + "could not add action listener to object:" + object() ); + } + super.establishConnection(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + try + { + removeActionListener.invoke( object(), this ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOActionAssociation: " + + "could not add action listener to object:" + object() ); + } + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + * This implementation does nothing. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + + if ( component instanceof Component ) + { + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != + ((Component)component).isEnabled() ) + { + ((Component)component).setEnabled( + converted.booleanValue() ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( VisibleAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + if ( converted.booleanValue() != + ((Component)component).isVisible() ) + { + ((Component)component).setVisible( + converted.booleanValue() ); + } + } + } + } + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup actionDisplayGroup = null; + String actionKey = null; + + // action aspect + actionDisplayGroup = displayGroupForAspect( ActionAspect ); + if ( actionDisplayGroup != null ) + { + actionKey = displayGroupKeyForAspect( ActionAspect ); + + //TODO: argument aspect not implemented + + try + { + + NSSelector selector = new NSSelector( actionKey ); + Enumeration e = + actionDisplayGroup.selectedObjects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + selector.invoke( e.nextElement() ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "ActionAssociation: error invoking action: " + actionKey, exc ); + } + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:48:28 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java new file mode 100644 index 0000000..2dc7fec --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/AdjustableAssociation.java @@ -0,0 +1,327 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Adjustable; +import java.awt.Component; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* AdjustableAssociation binds any Adjustable component to +* a display group. Components implementing the adjustable +* interface include: ScrollBar and JScrollBar. +* Bindings are: +*
    +*
  • value: a property convertable to/from a string
  • +*
  • enabled: a boolean property that determines whether +* the user can select the text in the field
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class AdjustableAssociation extends EOAssociation + implements AdjustmentListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "value" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public AdjustableAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof Adjustable ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener to the specified object. + */ + public void establishConnection () + { + component().addAdjustmentListener( this ); + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + component().removeAdjustmentListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + Adjustable component = component(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + } + value = displayGroup.selectedObjectValueForKey( key ); + + // convert value to int + value = ValueConverter.convertObjectToClass( + value, Integer.class ); + + int intValue; + if ( value == null ) + { + intValue = 0; + } + else + { + intValue = ((Integer)value).intValue(); + } + + if ( component.getValue() != intValue ) + { + component.setValue( intValue ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( ( displayGroup != null ) || ( key != null ) ) + && ( component instanceof Component ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != ((Component)component).isEnabled() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Integer( component().getValue() ); + return displayGroup.setSelectedObjectValue( value, key ); + } + return false; + } + + // interface AdjustmentListener + + /** + * Updates object on action performed. + */ + public void adjustmentValueChanged(AdjustmentEvent e) + { + writeValueToDisplayGroup(); + } + + private Adjustable component() + { + return (Adjustable) object(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:48:35 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java new file mode 100644 index 0000000..38cf38b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ButtonAssociation.java @@ -0,0 +1,444 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.util.Iterator; + +import javax.swing.ButtonModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ButtonAssociation binds any component that uses a ButtonModel +* (all Swing button classes) to a display group. This association +* should be used to handle individual JRadioButtons and JCheckBoxes. +* Bindings are: +*
    +*
  • value: a boolean property that determines the +* selected state of the button model. This will set +* the value for radio buttons and check boxes.
  • +*
  • enabled: a boolean property that determines the +* enabled state of the button model.
  • +*
  • visible: a boolean property that determines the +* visible state of the button model.
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ButtonAssociation extends EOAssociation + implements ChangeListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "model.selected" + } ); + + static NSSelector getModel = + new NSSelector( "getModel", new Class[] {} ); + + protected ButtonModel buttonModel; + protected boolean lastKnownValue; + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + * This implementation expects a ButtonModel or a class + * that has a "getModel" method that returns a ButtonModel. + */ + public ButtonAssociation ( Object anObject ) + { + super( anObject ); + + if ( anObject instanceof ButtonModel ) + { + buttonModel = (ButtonModel) anObject; + } + else + { + try + { + buttonModel = (ButtonModel) getModel.invoke( anObject ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "EOButtonAssociation: " + + "could not retrieve a button model from object:" + anObject ); + } + } + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + * This implementation expects a ButtonModel or a class + * that has a "getModel" method that returns a ButtonModel. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return + ( anObject instanceof ButtonModel ) + || ( getModel.implementedByObject( anObject ) ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + buttonModel.addChangeListener( this ); + super.establishConnection(); + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + buttonModel.removeChangeListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + * This implementation does nothing. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + } + + Object value; + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } + else // displayGroup has only one object + { + value = + displayGroup.selectedObjectValueForKey( key ); + } // end checking size of displayGroup + + buttonModel.setArmed( false ); + buttonModel.setPressed( false ); + + if ( value != null ) + { + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + lastKnownValue = converted.booleanValue(); + if ( converted.booleanValue() != + buttonModel.isSelected() ) + { + buttonModel.removeChangeListener( this ); + buttonModel.setSelected( + converted.booleanValue() ); + buttonModel.addChangeListener( this ); + } + } // end checking converted == null + } + else + { + buttonModel.setArmed( true ); + buttonModel.setPressed( true ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != + buttonModel.isEnabled() ) + { + buttonModel.removeChangeListener( this ); + buttonModel.setEnabled( + converted.booleanValue() ); + buttonModel.addChangeListener( this ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( VisibleAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + if ( converted != null ) + { + if ( converted.booleanValue() != + ((Component)component).isVisible() ) + { + ((Component)component).setVisible( + converted.booleanValue() ); + } + } + } + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + boolean returnValue = true; + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Boolean( buttonModel.isSelected() ); + + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + + } + return false; + } + // interface ChangeListener + + public void stateChanged(ChangeEvent e) + { + if ( buttonModel.isSelected() != lastKnownValue ) + { + lastKnownValue = buttonModel.isSelected(); + writeValueToDisplayGroup(); + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.5 2001/06/29 22:28:19 mpowers + * Tabs to spaces. + * + * Revision 1.4 2001/06/29 22:17:31 mpowers + * Now updating the component on establishConnection. + * + * Revision 1.3 2001/02/27 02:10:38 mpowers + * No longer updating values to the display group if the value + * has not changed. + * + * Revision 1.2 2001/02/21 20:33:01 mpowers + * Fixed bug with change listener. + * + * Revision 1.1.1.1 2000/12/21 15:48:38 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java new file mode 100644 index 0000000..d0a087e --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ComboBoxAssociation.java @@ -0,0 +1,700 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; +import javax.swing.JComboBox; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ComboBoxAssociation binds JComboBoxes to +* display groups. Bindings are: +*
    +* +*
  • value: optional - a property of the selected object in the +* display group that will be bind to the item the user +* selects or the text that the user enters in the field. +* If the value aspect is not bound, then the combo box works +* as an "overview" assocation and changing the selected object +* in the combobox will modify the selection of the display group +* bound to the objects or the titles display groups (in that order).
  • +* +*
  • titles: optional - a property of the objects in the bound +* display group that will appear in the list. If the +* objects aspect is not bound, this property is also +* used to populate the value binding. If the titles +* aspect itself is not bound, the items already in the +* combobox will be used to update the value in the +* selected object in the bound display group.
  • +* +*
  • objects: optional - if specified, when the user +* selects a title in the list, the property of the +* object at the corresponding index of the bound display +* group will be used to populate the value binding. +* If the objects aspect is used with an editable combo +* box, any value entered that does not match one of the +* titles in the list will produce a null value.
  • +* +*
  • enabled: optional - a boolean property of the +* selected object in the display group that determines whether +* the user can edit the field.
  • +* +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ComboBoxAssociation extends EOAssociation + implements FocusListener, ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ValueAspect, + ObjectsAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text" + } ); + + private boolean wasNull; + private static final String EMPTY_STRING = ""; + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public ComboBoxAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JComboBox ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + super.establishConnection(); + + // prepopulate titles + EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + addAsListener(); + subjectChanged(); + } + + protected void addAsListener() + { + component().addActionListener( this ); + component().addFocusListener( this ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void removeAsListener() + { + component().removeActionListener( this ); + component().removeFocusListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + removeAsListener(); + + JComboBox component = component(); + EODisplayGroup displayGroup; + String key; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + ComboBoxModel model = component().getModel(); + // if first time, or if backing group has changed +// if ( ( ! ( model instanceof ComboBoxAssociationModel ) ) +// || ( displayGroup.contentsChanged() ) ) +// { + key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); +// } + } + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + component.setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + //Object value = displayGroup.selectedObjectValueForKey( key ); + Object value; + + + if ( displayGroup.selectedObjects().size() > 1 ) + { + + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } else { + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + + // objects aspect + EODisplayGroup objectsDisplayGroup = + displayGroupForAspect( ObjectsAspect ); + if ( ( objectsDisplayGroup != null ) && ( value != null ) ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + Object match; + int index = NSArray.NotFound; + int count = objectsDisplayGroup.displayedObjects().count(); + for ( int i = 0; i < count; i++ ) + { + match = objectsDisplayGroup.valueForObjectAtIndex( i, objectKey ); + if ( value.equals( match ) ) + { + index = i; + } + } + if ( index == NSArray.NotFound ) + { + if ( component.getSelectedItem() != null ) + { + component.setSelectedItem( null ); + } + } + else + { + if ( component.getSelectedIndex() != index ) + { + component.setSelectedIndex( index ); + } + } + } + else + { + component.setSelectedItem( value ); + } + } + else // values aspect not bound + { + // use objects group if specified + EODisplayGroup sourceGroup = + displayGroupForAspect( ObjectsAspect ); + if ( sourceGroup == null ) + { + // fall back on titles group + sourceGroup = displayGroupForAspect( TitlesAspect ); + } + + if ( sourceGroup != null ) + { + List selection = sourceGroup.selectionIndexes(); + if ( ( selection != null ) && ( selection.size() > 0 ) ) + { + component.setSelectedIndex( ((Integer)selection.get(0)).intValue() ); + } + else + { + // the combo box model decides what to do with this value + component.setSelectedItem( null ); + } + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != component.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + addAsListener(); + } + + /** + * Called to repopulate the title list from the + * specified display group. + */ + protected void populateTitles( + EODisplayGroup displayGroup, String key ) + { + component().setModel( + new ComboBoxAssociationModel( displayGroup, key ) ); + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + JComboBox component = component(); + EODisplayGroup displayGroup; + String key; + + // selected title aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + Object value = null; + + // selected object aspect, if any + EODisplayGroup objectsGroup = + displayGroupForAspect( ObjectsAspect ); + if ( objectsGroup != null ) + { + try + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + int index = component.getSelectedIndex(); + if ( index != -1 ) + { + value = objectsGroup + .valueForObjectAtIndex( index, objectKey ); + } + else // selected index is -1 + { + // the combo box is probably editable, + // so there is no corresponding object. + value = null; + } + } + catch ( NullPointerException npe ) + { + // catches NPE on line 436 of JComboBox.java: + // this is a common developer error + throw new WotonomyException( "ComboBoxAssociation: " + + "The object in the VALUE property may not have been found in the " + + "objects in the TITLES group.", npe ); + } + } + else // just use the selected item + { + value = component.getSelectedItem(); + } + + boolean returnValue = true; + if ( displayGroup.selectedObjects().size() == 1 ) + { // displayGroup has only one object + // only set value if changed + Object existingValue = displayGroup.selectedObjectValueForKey( key ); + if ( value == existingValue ) return true; + if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true; + + // value has changed: update the value. + return displayGroup.setSelectedObjectValue( value, key ); + } + else if ( displayGroup.selectedObjects().size() > 1 ) + { + // displayGroup has more than one object + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + + } // end checking size of displayGroup + + } + else // values aspect not bound + { + // use objects group if specified + EODisplayGroup sourceGroup = + displayGroupForAspect( ObjectsAspect ); + if ( sourceGroup == null ) + { + // fall back on titles group + sourceGroup = displayGroupForAspect( TitlesAspect ); + } + + if ( sourceGroup != null ) + { + int index = component.getSelectedIndex(); + if ( index != -1 ) + { + sourceGroup.setSelectionIndexes( new NSArray( new Integer( index ) ) ); + } + else + { + sourceGroup.setSelectedObject( null ); + } + return true; + } + } + + return false; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + writeValueToDisplayGroup(); + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( component().isEditable() ) + { + if ( endEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + } + + // convenience + + private JComboBox component() + { + return (JComboBox) object(); + } + + /** + * Used as the data model for the controlled combo box. + */ + private class ComboBoxAssociationModel extends AbstractListModel + implements ComboBoxModel + { + EODisplayGroup displayGroup; + String key; + Object selectedItem; + + ComboBoxAssociationModel( + EODisplayGroup aDisplayGroup, String aKey ) + { + displayGroup = aDisplayGroup; + key = aKey; + selectedItem = null; + } + + public Object getElementAt(int index) + { + return displayGroup.valueForObjectAtIndex( index, key ); + } + + public int getSize() + { + return displayGroup.displayedObjects().count(); + } + + public void setSelectedItem(Object anItem) + { //System.out.println( "setSelectedItem: " + anItem ); + selectedItem = anItem; + + // must do this to notify an editable combo, + // otherwise the wrong value appears. + fireContentsChanged( this, -1, -1 ); + } + + public Object getSelectedItem() + { //System.out.println( "getSelectedItem: " + selectedItem ); + return selectedItem; + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.13 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.12 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.11 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.10 2001/07/23 20:17:56 mpowers + * Now works as an overview association if the values aspect is not bound. + * + * Revision 1.9 2001/06/30 14:57:29 mpowers + * Removed a println. + * + * Revision 1.8 2001/06/29 22:28:19 mpowers + * Tabs to spaces. + * + * Revision 1.7 2001/06/29 22:17:31 mpowers + * Now updating the component on establishConnection. + * + * Revision 1.6 2001/05/14 15:24:49 mpowers + * Only updating if change was made. Feels like I had fixed this here before. + * + * Revision 1.5 2001/04/09 21:41:08 mpowers + * Fixed a bug I thought that I had fixed before. + * + * Revision 1.4 2001/03/01 20:37:17 mpowers + * Updated docs to emphasize that titles aspect is optional. + * + * Revision 1.3 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.2 2001/01/10 17:01:08 mpowers + * Caught a common developer error. + * + * Revision 1.1.1.1 2000/12/21 15:48:43 mpowers + * Contributing wotonomy. + * + * Revision 1.8 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java new file mode 100644 index 0000000..ba50879 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DateAssociation.java @@ -0,0 +1,613 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.Iterator; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* DateAssociation binds any component that has a set and get Date methods and +* fire actions events when the date has been changed. Bindings are: +*
    +*
  • value: a property convertable to/from a date
  • +*
  • editable: a boolean property that determines whether +* the user can edit the date in the component
  • +*
  • enabled: a boolean property that determines whether +* the component is enabled or disabled
  • +*
+* +* @author rob@yahoo.com +* @version $Revision: 904 $ +*/ +public class DateAssociation extends EOAssociation + implements ActionListener, FocusListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, EditableAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "date", "enabled", "editable" + } ); + + private final static NSSelector getDate = + new NSSelector( "getDate" ); + private final static NSSelector setDate = + new NSSelector( "setDate", + new Class[] { Date.class } ); + private final static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector addFocusListener = + new NSSelector( "addFocusListener", + new Class[] { FocusListener.class } ); + private final static NSSelector removeFocusListener = + new NSSelector( "removeFocusListener", + new Class[] { FocusListener.class } ); + private final static NSSelector setEditable = + new NSSelector( "setEditable", + new Class[] { boolean.class } ); + + // dirty handling + private boolean needsUpdate; + private Date nullValue; // placeholder for null value flag + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public DateAssociation ( Object anObject ) + { + super( anObject ); + needsUpdate = false; + nullValue = null; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return setDate.implementedByObject( anObject ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and Focus Listener to the specified object. + */ + public void establishConnection () + { + Object component = object(); + try + { + if ( addActionListener.implementedByObject( component ) ) + { + addActionListener.invoke( component, this ); + } + if ( addFocusListener.implementedByObject( component ) ) + { + addFocusListener.invoke( component, this ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while establishing connection", exc ); + } + + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + Object component = object(); + try + { + if ( removeActionListener.implementedByObject( component ) ) + { + removeActionListener.invoke( component, this ); + } + if ( removeFocusListener.implementedByObject( component ) ) + { + removeFocusListener.invoke( component, this ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while breaking connection", exc ); + } + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + } + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } else { + + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking size of displayGroup + + // convert value to date + try + { + Date dateValue = null; + // (Date) ValueConverter.convertObjectToClass( value, Date.class ); + + if ( value instanceof Date ) + { + dateValue = (Date) value; + } + + if ( ( dateValue == null ) && ( value instanceof Calendar ) ) + { + dateValue = ( ( Calendar )value ).getTime(); + } + + if ( dateValue == null ) + { + // current time (placeholder) + nullValue = new Date(); + dateValue = nullValue; + } + else + { + nullValue = null; + } + + if ( !dateValue.equals( getDate.invoke( component ) ) ) + { // No need to update if there is no change. + setDate.invoke( component, dateValue ); + needsUpdate = false; + } + + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( ( displayGroup != null ) || ( key != null ) ) + && ( component instanceof Component ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != ((Component)component).isEnabled() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + // editable aspect + displayGroup = displayGroupForAspect( EditableAspect ); + key = displayGroupKeyForAspect( EditableAspect ); + if ( ( ( displayGroup != null ) || ( key != null ) ) + && ( setEditable.implementedByObject( component ) ) ) + { + try + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + setEditable.invoke( component, converted ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection (editable aspect)", exc ); + } + } + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + if ( !needsUpdate ) return true; + + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( ValueAspect ); + Object component = object(); + Object value = null; + try + { + if ( getDate.implementedByObject( component ) ) + { + value = getDate.invoke( component ); + } + if ( nullValue != null ) + { + if ( nullValue.equals( value ) ) + { + value = null; + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + needsUpdate = false; + + boolean returnValue = true; + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + + } + return false; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + needsUpdate = true; + writeValueToDisplayGroup(); // TODO: Should we do this here or on focus lost? + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( endEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + else + { + // probably should notify of a validation error here, + // but how to also handle actionPerformed without copying code? +/* + Object value = null; + try + { + if ( getText.implementedByObject( object() ) ) + { + value = getText.invoke( object() ); + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + String key = displayGroupKeyForAspect( ValueAspect ); + if ( displayGroup != null ) + { + if ( displayGroup.associationFailedToValidateValue( + this, (String) value, key, object(), + "That format was not recognized." ) ) + { + new net.wotonomy.ui.swing.util.StackTraceInspector(); + } + if ( object() instanceof Component ) + { + ((Component)object()).requestFocus(); + } + } +*/ + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.6 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.5 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.4 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.3 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.2 2001/01/17 16:25:26 mpowers + * Now catching null values from data object. + * + * Revision 1.1 2001/01/10 22:26:32 mpowers + * Contributing DateAssociation. + * + * Revision 1.1 2001/01/10 21:30:27 rglista + * Initial checkin + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java new file mode 100644 index 0000000..290480d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupActionAssociation.java @@ -0,0 +1,134 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; + +/** +* ActionAssociation binds any ActionEvent broadcaster +* (typically Buttons and the like) to a display group, +* but invokes actions directly on the bound display +* group rather than the selected objects. +* Bindings are: +*
    +*
  • action: a method to be invoked on the bound display group. +* If the argument aspect is bound, the method must take +* one argument. Otherwise, the method must take no arguments.
  • +*
  • argument: the attribute of the selected object(s) (possibly +* from a different display group) that will be used as an argument +* to the action method
  • +*
  • enabled: a boolean property that determines whether +* the controlled component is enabled
  • +*
  • visible: a boolean property that determines whether +* the controlled component is visible
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class DisplayGroupActionAssociation extends ActionAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ActionAspect, ArgumentAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "target" + } ); + + static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public DisplayGroupActionAssociation ( Object anObject ) + { + super( anObject ); + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + EODisplayGroup actionDisplayGroup = null; + String actionKey = null; + + // action aspect + actionDisplayGroup = displayGroupForAspect( ActionAspect ); + if ( actionDisplayGroup != null ) + { + actionKey = displayGroupKeyForAspect( ActionAspect ); + + //TODO: argument aspect not implemented + + try + { + NSSelector.invoke( actionKey, actionDisplayGroup ); + } + catch ( Exception exc ) + { + throw new WotonomyException( "DisplayGroupActionAssociation: " + + "error invoking action: " + actionKey, exc ); + } + } + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:48:46 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java new file mode 100644 index 0000000..c8ecd36 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupInspector.java @@ -0,0 +1,120 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.border.EmptyBorder; + +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.util.ObjectInspector; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* The DisplayGroupInspector displays a JFrame that +* shows allows you to view and manipulate a display group. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class DisplayGroupInspector +{ + protected JList list; + protected EODisplayGroup displayGroup; + +/** +* Displays and manipulats the specified display group. +*/ + public DisplayGroupInspector( EODisplayGroup aDisplayGroup ) + { + displayGroup = aDisplayGroup; + + list = new JList(); + list.addMouseListener( new MouseAdapter() + { + public void mouseClicked( MouseEvent e ) + { + if ( e.getClickCount() == 2 ) + { + Object selection = displayGroup.selectedObject(); + if ( selection != null ) + { + new ObjectInspector( selection ); + } + } + } + } ); + + EOAssociation assoc = new ListAssociation( list ); + assoc.bindAspect( EOAssociation.TitlesAspect, displayGroup, "" ); + assoc.establishConnection(); + + initLayout(); + + } + + protected void initLayout() + { + JPanel panel = new JPanel(); + panel.setLayout( new BorderLayout() ); + panel.setBorder( new EmptyBorder( 10, 10, 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( list ); + scrollPane.setPreferredSize( new Dimension( 200, 200 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Display Group Inspector" ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2003/06/26 23:28:00 mpowers + * Added double click. + * + * Revision 1.1 2001/05/29 19:57:47 mpowers + * Added some neglected files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java new file mode 100644 index 0000000..1756285 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/DisplayGroupNode.java @@ -0,0 +1,1518 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import javax.swing.tree.TreePath; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource; + +/** +* DisplayGroupNodes are used as nodes in the +* TreeModelAssociation's implementation of TreeModel, +* and is tightly coupled with TreeModelAssociation +* and MasterDetailAssociation.

+* +* Even though it is no longer package access, +* don't rely on this class because we want to +* have the option of completely replacing this +* approach in the future. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + abstract public class DisplayGroupNode + extends EODisplayGroup + { + protected TreeModelAssociation parentAssociation; + protected EODelayedObserver targetObserver; + protected NSMutableDictionary childNodes; + protected EODisplayGroup parentGroup; + protected Object target; + protected boolean isFetched; + protected boolean isFetchNeeded; + protected boolean useParentOrderings; + protected boolean useParentQualifier; + + /** + * Constructor for all nodes. + * Root node must have a null target. + */ + public DisplayGroupNode( + TreeModelAssociation aParentAssociation, + EODisplayGroup aParentGroup, + Object aTarget ) + { +//new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget ); +//System.out.println( "DisplayGroupNode.new: " + aTarget ); + parentAssociation = aParentAssociation; + target = null; + targetObserver = null; + parentGroup = aParentGroup; + childNodes = new NSMutableDictionary(); + isFetched = false; + isFetchNeeded = false; + useParentOrderings = true; + useParentQualifier = true; + + EODataSource parentSource = null; + if ( parentGroup != null ) + { + parentSource = parentGroup.dataSource(); + } + else + if ( parentAssociation.titlesDisplayGroup != null ) + { + parentSource = parentAssociation.titlesDisplayGroup.dataSource(); + } + + // create child datasource + if ( aTarget != null ) // not root node + { + if ( parentAssociation.childrenKey != null ) + { + if ( parentSource == null ) + { + throw new WotonomyException( + "Need a data source when children aspect is bound." ); + } + + NSArray displayedObjects = parentGroup.displayedObjects(); + EODataSource childSource = parentSource.dataSourceQualifiedByKey( + parentAssociation.childrenKey ); + childSource.qualifyWithRelationshipKey( + parentAssociation.childrenKey, aTarget ); + + // create new display group using child data source + this.setDataSource( childSource ); + + // establish observer for target object + setTarget( aTarget ); + } + else // only titles is bound + { + // establish observer for target object + setTarget( aTarget ); + + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + return new NSArray(); + } + } ); + } + } + else // else root node + { + // root node uses PropertyDataSource by default + if ( parentSource == null ) + { + setDataSource( new PropertyDataSource() + { + public NSArray fetchObjects() + { + if ( parentGroup != null ) + { + return parentGroup.displayedObjects(); + } + return null; + } + } ); + } + else + { + // root node uses parent source directly + setDataSource( parentSource ); + } + } + } + + /** + * Overridden to unregister as an editor of the editing context, + * since we don't directly present a user interface. + */ + public void setDataSource ( EODataSource aDataSource ) + { + super.setDataSource( aDataSource ); + if ( ( aDataSource != null ) + && ( aDataSource.editingContext() != null ) ) + { + aDataSource.editingContext().removeEditor( this ); + } + } + + /** + * Returns whether the node should call fetch(). + */ + protected boolean isFetched() + { + if ( isFetchNeeded() ) + { + setFetchNeeded( false ); + fetch(); + } + return isFetched; + } + + /** + * Sets whether the node should call fetch(). + */ + protected void setFetched( boolean fetched ) + { +//System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetched = fetched; + } + + /** + * Returns whether the node is in need of a refetch. + */ + protected boolean isFetchNeeded() + { + return isFetchNeeded; + } + + /** + * Returns whether the node should call fetch(). + */ + protected void setFetchNeeded( boolean fetchNeeded ) + { +//System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); + isFetchNeeded = fetchNeeded; + } + + /** + * Subclasses should override this method to fire an appropriate insertion event. + */ + protected void fireNodesInserted( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesInserted: " + this ); + parentAssociation.fireTreeNodesInserted( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate change event. + */ + protected void fireNodesChanged( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesChanged: " + this ); + parentAssociation.fireTreeNodesChanged( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate deletion event. + */ + protected void fireNodesRemoved( Object[] path, int[] indexes, Object[] objects ) + { +//System.out.println( "fireNodesRemoved: " + this ); + parentAssociation.fireTreeNodesRemoved( + this, path, indexes, objects ); + } + + /** + * Subclasses should override this method to fire an appropriate event. + */ + protected void fireStructureChanged( Object[] path, int[] indexes, Object[] objects ) + { + parentAssociation.fireTreeStructureChanged( + this, path, indexes, objects ); + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + int count = getChildCount(); // gets old count + if ( target == null ) + { + // if root node, forward to parent: + // circumventing delegating data source, if any + EODataSource dataSource = parentGroup.dataSource(); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( + ((DelegatingTreeDataSource)dataSource).delegateDataSource ); + } + parentGroup.insertObjectAtIndex( anObject, anIndex ); + if ( dataSource instanceof DelegatingTreeDataSource ) + { + parentGroup.setDataSource( dataSource ); + } + return; // prevent event from firing (?) + } + else // not root node + { + super.insertObjectAtIndex( anObject, anIndex ); + } + } + + /** + * Overridden to broadcast a tree event after super executes. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + boolean result; + Object node = getChildNodeAt( anIndex ); + if ( target == null ) + { + // if root node, forward to parent: + result = parentGroup.deleteObjectAtIndex( anIndex ); + } + else // not root node + { + result = super.deleteObjectAtIndex( anIndex ); + } + + return result; + } + + /** + * Returns the child node that corresponds to the + * specified index, creating it if necessary. + * The index must be within bounds or an exception + * is thrown. + */ + public DisplayGroupNode getChildNodeAt( int anIndex ) + { + boolean wasFetched = isFetched(); + if ( ! wasFetched ) fetch(); + Object o = displayedObjects.objectAtIndex( anIndex ); + DisplayGroupNode result = getChildNodeForObject( o ); + if ( result == null ) + { + result = createChildNodeForObject( o ); + } + return result; + } + + /** + * Returns a child node that corresponds to the + * specified object, returning null if not found. + */ + protected DisplayGroupNode getChildNodeForObject( Object anObject ) + { + return (DisplayGroupNode) + childNodes.objectForKey( new ReferenceKey( anObject ) ); + } + + /** + * Creates a child node that corresponds to the + * specified object. + */ + private DisplayGroupNode createChildNodeForObject( Object anObject ) + { + DisplayGroupNode result = parentAssociation.createNode( this, anObject ); + childNodes.setObjectForKey( result, new ReferenceKey( anObject ) ); + return result; + } + + /** + * Returns a tree path of all DisplayGroupNodes leading + * to this node, including the root node (but excluding the + * titles display group). + */ + public TreePath treePath() + { + List path = new LinkedList(); + EODisplayGroup node = this; + while ( node instanceof DisplayGroupNode ) + { + // insert at head of list + path.add( 0, node ); + node = ((DisplayGroupNode)node).parentGroup; + } + return new TreePath( path.toArray() ); + } + + /** + * Overridden to return the parent group's + * sort ordering if useParentOrderings is true. + * useParentOrderings is true by default. + */ + public NSArray sortOrderings() + { + if ( ( useParentOrderings ) + && ( parentGroup != null ) ) + { + return parentGroup.sortOrderings(); + } + return super.sortOrderings(); + } + + /** + * Overridden to set useParentOrderings to false, + * or true if aList is null. + */ + public void setSortOrderings ( List aList ) + { + if ( aList == null ) + { + useParentOrderings = true; + } + else + { + useParentOrderings = false; + super.setSortOrderings( aList ); + } + } + + /** + * Overridden to return the parent group's + * qualifier if useParentQualifier is true. + * useParentQualifier is true by default. + */ + public EOQualifier qualifier() + { + if ( ( useParentQualifier ) + && ( parentGroup != null ) ) + { + return parentGroup.qualifier(); + } + return super.qualifier(); + } + + /** + * Overridden to set useParentQualifier to false, + * or true if aList is null. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + if ( aQualifier == null ) + { + useParentQualifier = true; + } + else + { + useParentQualifier = false; + super.setQualifier( aQualifier ); + } + } + + /** + * Overridden to set isFetched to true. + */ + public boolean fetch() + { +//System.out.println( "DisplayGroupNode.fetch: " + this + " : " ); +//if ( getClass().getName().indexOf( "Activity" ) != -1 ) +//{ +// new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() ); +//} + // set flag + setFetched( true ); + + // skip root node + if ( target == null ) return true; + + // requalify + dataSource().qualifyWithRelationshipKey( + parentAssociation.childrenKey, target ); + + // call to super + return super.fetch(); + +//boolean result = super.fetch(); +//System.out.println( displayedObjects() ); +//return result; + } + + /** + * Returns the object at the appropriate index + * in the parent display group. + */ + public Object object() + { + // if root node + if ( target == null ) + { + return parentAssociation.rootLabel(); + } + return target; + } + + /** + * Returns the string value of the title property + * on the object in the parent display group corresponding + * to this index. The tree renderer asks JTrees to + * call this method to retrieve a value for display. + */ + public String toString() + { + Object result = getUserObject(); + if ( result == null ) result = "[null]"; + return result.toString(); + } + + // parts of interface TreeNode + + public int getChildCount() + { + if ( ! isFetched() ) fetch(); +//if ( toString().indexOf("154.16406")!=-1){ +//System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this ); +//new RuntimeException().printStackTrace(); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//} + return displayedObjects.count(); + } + + public int getIndex(DisplayGroupNode node) + { + if ( ! isFetched() ) fetch(); + return displayedObjects.indexOfObject( + ((DisplayGroupNode)node).target ); + } + + public boolean getAllowsChildren() + { + return true; + } + + public boolean isLeaf() + { + // if not root node and isLeaf aspect is bound + if ( ( target != null ) + && ( parentGroup != null ) + && ( parentAssociation.leafKey != null ) ) + { + Object value; + if ( parentAssociation.leafDisplayGroup != null ) + { + value = parentGroup.valueForObject( + target, parentAssociation.leafKey ); + } + else + { + value = parentAssociation.leafKey; + } + + // getBoolean returns true for zero, among other things + Object result = ValueConverter.getBoolean( value ); + if ( result != null ) + { + return ((Boolean)result).booleanValue(); + } + } + + // otherwise, we have to fetch and return count + return ( getChildCount() == 0 ); + } + + public Enumeration children() + { + int count = getChildCount(); + Vector v = new Vector(); + for ( int i = 0; i < count; i++ ) + { + v.add( getChildNodeAt( i ) ); + } + return v.elements(); + } + + // parts of interface MutableTreeNode + + public void insert(DisplayGroupNode aChild, int anIndex) + { + insertObjectAtIndex( + ((DisplayGroupNode)aChild).object(), anIndex ); + } + + public void remove(int index) + { + deleteObjectAtIndex( index ); + } + + /** + * Removes the node at the index corresponding + * to the index of the object. + */ + public void remove(DisplayGroupNode node) + { + remove( getIndex( node ) ); + } + + /** + * Removes our object from the parent display group. + */ + public void removeFromParent() + { + int index = parentGroup.displayedObjects().indexOfIdenticalObject( target ); + if ( index != NSArray.NotFound ) + { + parentGroup.deleteObjectAtIndex( index ); + } + else + { + throw new WotonomyException( + "Object not found in parent group: " + target ); + } + } + + /** + * Removes our object from the parent display group + * and adds it to the end of the specified node's children. + */ + public void setParent(DisplayGroupNode newParent) + { + removeFromParent(); + newParent.insertObjectAtIndex( + object(), newParent.displayedObjects.size() ); + } + + /** + * Returns the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public Object getUserObject() + { + return valueForKey( parentAssociation.titlesKey ); + } + + /** + * Sets the value of the displayed property in the parent display group + * at the index that corresponds to the index of this node. + */ + public void setUserObject( Object aValue ) + { + setValueForKey( aValue, parentAssociation.titlesKey ); + } + + /** + * Returns a value from the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, if the titles key is specified, the root + * label is returned, otherwise null is returned. + */ + public Object valueForKey( String aKey ) + { + // if root node + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + return parentAssociation.rootLabel(); + } + return null; + } + return parentGroup.valueForObject( target, aKey ); + } + + /** + * Sets a value on the object in the parent display group + * at the index that corresponds to the index of this node. + * For the root node, this method only works if aKey is the + * titlesAspect's key, otherwise does nothing. + */ + public void setValueForKey(Object aValue, String aKey) + { + // if root node, return. + if ( target == null ) + { + // compare by ref is okay for strings + if ( aKey == parentAssociation.titlesKey ) + { + parentAssociation.setRootLabel( aValue ); + + // how to handle root node? tree event docs don't say. + fireNodesChanged ( treePath().getPath(), + new int[] { 0 }, + new Object[] { this } ); + } + return; + } + + parentGroup.setValueForObject( + aValue, target, aKey ); + } + + /** + * Perform any clean up in this method. + * The node will not be reused after this method is called. + * This implementation removes itself from the parent's + * set of child nodes, sets target and datasource to null, + * and then calls disposeChildNodes(). + */ + protected void dispose() + { //System.out.println( "dispose: " + this.getClass().getName() + " : " + this ); + if ( parentGroup != null ) + { + ((DisplayGroupNode)parentGroup).childNodes.remove( + new ReferenceKey( target ) ); + } + setTarget( (Object) null ); + setDataSource( null ); + disposeChildNodes(); + } + + /** + * Calls dispose() on all child nodes. + */ + protected void disposeChildNodes() + { + Iterator i = new LinkedList(childNodes.values()).iterator(); + while ( i.hasNext() ) + { + ((DisplayGroupNode) i.next()).dispose(); + } + } + + /** + * Called after the target object posts a change notification. + * This implementation re-fetches which triggers + * updateDisplayedObjects to broadcast any tree events. + * This method marks the parent object as changed if: + * (1) this object is not registered in the editing context + * of the titles display group's data source (if any), AND + * (2) the children key is not in the list of attributes + * of the parent object's EOClassDescription. + */ + public void targetChanged() + { + // if not root node + if ( target != null ) + { + // if we're not root and not fetched, stop here. + //FIXME: with this, some nodes have old values when moved. + //FIXME: without this, nodes are unnecessarily fetched. + //FIXME: might have parent modify isFetched of certain child nodes. + if ( isFetched() ) + { + fetch(); + } + else // not fetched - just update the display + { + updateDisplayedObjects(); + } +/* +//disabling this for performance reasons: +//might reenable later or find an alternate approach + // check to see if we need to mark the parent object as changed + EOEditingContext context = dataSource().editingContext(); + if ( ( context == null ) + || ( context.globalIDForObject( target ) == null ) ) + { + DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup; + if ( parentNode.target != null ) + { + // only notify if childrenKey is an attribute of parentDesc + // (and therefore not a toOne or toMany relationship) + EOClassDescription parentDesc = + EOClassDescription.classDescriptionForClass( + parentNode.target.getClass() ); + if ( parentDesc.attributeKeys().contains( parentAssociation.childrenKey ) ) + { + // only notify if no context is already observing the object + // and we are an attribute key + EOObserverCenter.notifyObserversObjectWillChange( parentNode.target ); + } + } + } +*/ + } + else // root node + { + setObjectArray( parentAssociation.titlesDisplayGroup.displayedObjects() ); + } + + // finally, broadcast change event for this node + // even though we're not sure if the displayed value changed. + fireNodeChanged(); + } + + /** + * Fires a change event for this node. + */ + public void fireNodeChanged() + { + // if not root node + if ( target != null ) + { + int index = ((DisplayGroupNode)parentGroup).getIndex( this ); + if ( ( index != -1 ) + && ( treePath().getParentPath() != null ) ) + { + fireNodesChanged ( + treePath().getParentPath().getPath(), + new int[] { index }, + new Object[] { this } ); + } + } + } + +Object[] previouslyDisplayedObjects = new Object[0]; + /** + * Overridden to call to super, fire any tree events, and then + * call updateDisplayedObjects on all fetched child nodes. + * This method compares this node's displayed objects against + * the list of child nodes, synchronizes them, and then broadcasts + * only the necessary events to bring the view component up to date. + */ + public void updateDisplayedObjects() + { +//System.out.println( "updateDisplayedObjects: " + " : " + this ); +//net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); +//new RuntimeException().printStackTrace(); + super.updateDisplayedObjects(); + + // diff lists + boolean proceed = true; + Object[] oldObjects = previouslyDisplayedObjects; + Object[] newObjects = displayedObjects.toArray(); + if ( oldObjects.length == newObjects.length ) + { + proceed = false; + for ( int i = 0; i < newObjects.length; i++ ) + { + if ( oldObjects[i] != newObjects[i] ) + { + proceed = true; + break; + } + } + } + + // this should be set before firing the change events + // in case some clients end up calling this again. + previouslyDisplayedObjects = newObjects; + + DisplayGroupNode node; + Iterator i = childNodes.values().iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + if ( !node.isFetchNeeded() ) + { + node.updateDisplayedObjects(); + } + } + + if ( proceed ) + { +//System.out.println( "DisplayGroupNode.firingEventsForChanges: " ); +//new RuntimeException().printStackTrace(); + fireEventsForChanges( oldObjects, newObjects ); + } + + } + + /** + * Called by processRecentChanges to analyze the + * differences between the lists and broadcast the + * appropriate events. + */ + protected void fireEventsForChanges( + Object[] oldObjects, Object[] newObjects ) + { + // structure changed causes havoc while + // establishing connection in some cases + //if ( oldObjects.length == 0 || newObjects.length == 0 ) + //{ + // fireStructureChanged( treePath().getPath(), null, null ); + // return; + //} + + int insertCount = 0; + int deleteCount = 0; + Object[] inserts = new Object[ newObjects.length ]; + Object[] deletes = new Object[ oldObjects.length ]; + + int i; + int n = -1, o = -1; // last match + int n1 = 0, o1 = 0; // current match test + int n2 = 0, o2 = 0; // scan ahead + + while ( o1 < oldObjects.length && n1 < newObjects.length ) + { + if ( newObjects[n1] == oldObjects[o1] ) + { + // mark as match and continue + o = o1; + n = n1; + } + else + { + // scan ahead for the next match, if any + o2 = o1; + n2 = n1; + + while ( o2 < oldObjects.length || n2 < newObjects.length ) + { + if ( o2 < oldObjects.length && newObjects[n1] == oldObjects[o2] ) + { + // run o1 to o2: mark as deletes + for ( i = o1; i < o2; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + o1 = o2; // reset test + o = o1; // set match + n = n1; // set match + break; + } + if ( n2 < newObjects.length && newObjects[n2] == oldObjects[o1] ) + { + // run n1 to n2: mark as inserts + for ( i = n1; i < n2; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + n1 = n2; // reset test + n = n1; // set match + o = o1; // set match + break; + } + o2++; + n2++; + } + } + if (n != n1) + { + inserts[n1] = newObjects[n1]; + insertCount++; + deletes[o1] = oldObjects[o1]; + deleteCount++; + //increment even though no match: + //the new object was marked as inserted and + //the old object was marked as deleted. + n = n1; + o = o1; + } + o1++; + n1++; + } + + // run o to end of oldObjects: mark as deletes + for ( i = o+1; i < oldObjects.length; i++ ) + { // System.out.println( "delete : " + i ); + deletes[i] = oldObjects[i]; + deleteCount++; + } + + // run n to end of newObjects: mark as inserts + for ( i = n+1; i < newObjects.length; i++ ) + { // System.out.println( "insert : " + i ); + inserts[i] = newObjects[i]; + insertCount++; + } + +//System.out.println( "done : " +//+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 ); +//System.out.println( new NSArray( newObjects ) ); +//System.out.println( new NSArray( inserts ) ); +//System.out.println( new NSArray( deletes ) ); +//System.out.println( new NSArray( oldObjects ) ); + + int c; + Object[] nodes; + int[] indices; + + // broadcast delete event + c = 0; + nodes = new Object[ deleteCount ]; + indices = new int[ deleteCount ]; + for ( i = 0; i < deletes.length; i++ ) + { + if ( deletes[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( deletes[i] ); + c++; + } + } + if ( c > 0 ) + { +// fireNodeChanged(); // force the jtree to get the correct child count + fireNodesRemoved( treePath().getPath(), indices, nodes ); + } + deletes = nodes; // retain for dispose check + + // broadcast insert event + c = 0; + nodes = new Object[ insertCount ]; + indices = new int[ insertCount ]; + for ( i = 0; i < inserts.length; i++ ) + { + if ( inserts[i] != null ) + { + indices[c] = i; + nodes[c] = getChildNodeForObject( inserts[i] ); + if ( nodes[c] == null ) + { + nodes[c] = createChildNodeForObject( newObjects[i] ); + } + c++; + } + } + if ( c > 0 ) + { + fireNodesInserted( treePath().getPath(), indices, nodes ); + } + + // dispose any delete nodes not on insert list + int j; + boolean found; + for ( i = 0; i < deletes.length; i++ ) + { + for ( j = 0; j < nodes.length; j++ ) + { + if ( deletes[i] == nodes[j] ) break; + } + + // did not break early, so not found, so dispose + if ( j == nodes.length ) + { + ((DisplayGroupNode)deletes[i]).dispose(); + } + } + } + + /** + * Sets the target object and creates an registers a target observer. + * If target was not previously null, the existing observer is unregistered. + * Protected access so subclasses and TreeModelAssociation can update our target. + */ + public void setTarget( Object aTarget ) + { + if ( target != null ) + { + EOObserverCenter.removeObserver( targetObserver, target ); + targetObserver.discardPendingNotification(); + } + + if ( aTarget != null ) + { + target = aTarget; + targetObserver = new TargetObserver( this ); + EOObserverCenter.addObserver( targetObserver, target ); + } + } + + /** + * Returns the parent display group, or null if parent is root. + */ + public DisplayGroupNode getParentGroup() + { + if ( parentGroup instanceof DisplayGroupNode ) + { + return (DisplayGroupNode)parentGroup; + } + // presumably the root node + return null; + } + + /** + * Gets all descendants of the this node. + */ + public List getDescendants() + { + return getDescendants( this, true ); + } + + /** + * Gets only the descendants of the this node + * whose children has been loaded - no fetching + * will occur. Useful for load-on-demand trees. + */ + public List getLoadedDescendants() + { + return getDescendants( this, false ); + } + + // breadth first traversal implementation + + /** + * Returns a list of all descendants of the + * specified node. Unfetched nodes are traversed + * only if forceLoad is true. + * This implementation is a breadth-first traversal + * of the nodes starting at the specified node. + */ + static private List getDescendants( DisplayGroupNode aNode, boolean forceLoad ) + { + if ( !forceLoad && !aNode.isFetched ) return NSArray.EmptyArray; + + LinkedList result = new LinkedList(); + LinkedList queue = new LinkedList(); + + queue.add( aNode ); + while ( ! queue.isEmpty() ) + { + checkNode( (DisplayGroupNode) queue.removeFirst(), + queue, result, forceLoad ); + } + + return result; + } + + /** + * Adds each fetched child node of the specified node to + * the result set (optionally forcing the child node to load) + * and adding child node to the end of the queue. + */ + static private void checkNode( DisplayGroupNode aNode, + LinkedList aQueue, LinkedList aResult, boolean forceLoad ) + { + DisplayGroupNode child; + int count = aNode.getChildCount(); + + for ( int i = 0; i < count; i++ ) + { + child = aNode.getChildNodeAt( i ); + + // add to queue if node has fetched children + if ( ( !child.isFetched ) && ( forceLoad ) ) + { + child.fetch(); + } + if ( child.isFetched ) + { + aQueue.addLast( child ); + } + + aResult.add( child ); + } + } + + /** + * Overridden to not fetch on InvalidateAllObjectsInStoreNotification + * unless we've already been fetched, preserving the load-on-demand + * functionality. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + else + if ( ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + || ( EOEditingContext.EditingContextDidSaveChangesNotification + .equals( aNotification.name() ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // if our target object was deleted + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + if ( deletes.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was invalidated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + if ( invalidates != null && + invalidates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { +//System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() ); + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + + // if our target object was updated, set fetchNeeded plus fire changed event + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + if ( updates.indexOfIdenticalObject( target ) != NSArray.NotFound ) + { + if ( parentAssociation.isVisible( this ) && targetObserver != null ) + { + targetObserver.objectWillChange( target ); // force ui to update + fireNodeChanged(); + if ( object() instanceof Component ) ((Component)object()).repaint(); + } + else // make sure we fetch children when we do become visible + setFetchNeeded( true ); + return; + } + } + + super.objectsInvalidatedInEditingContext( aNotification ); + + } + + // inner classes + + /** + * Private class used to force a hashmap to + * perform key comparisons by reference. + */ + private class ReferenceKey + { + private int hashCode; + private Object referent; + + public ReferenceKey( Object anObject ) + { + referent = anObject; + hashCode = anObject.hashCode(); + } + + /** + * Returns the actual key's hash code. + */ + public int hashCode() + { + return hashCode; + } + + /** + * Compares by reference. + */ + public boolean equals( Object anObject ) + { + if ( anObject instanceof ReferenceKey ) + { + return ((ReferenceKey)anObject).referent == referent; + } + return false; + } + } + + /** + * A private class to observe the target object of this node. + */ + private class TargetObserver extends EODelayedObserver + { + Reference ref; + + /** + * Pass in the display group node that will be updated + * when the target changes. + */ + public TargetObserver( DisplayGroupNode aDisplayGroup ) + { + ref = new WeakReference( aDisplayGroup ); + } + + /** + * Repopulate our display group, and calculate the deltas + * so we can broadcast appropriate events. + */ + public void subjectChanged () + { + DisplayGroupNode node = (DisplayGroupNode) ref.get(); + if ( node == null ) return; // node is null if gc'd. + //FIXME: should un-register self from observer center?? + + node.targetChanged(); + } + } + +} +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.64 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.63 2003/06/06 14:20:07 mpowers + * getLoadedDescendants was forcing a fetch of the node it was called on. + * + * Revision 1.62 2003/06/03 14:48:33 mpowers + * Clean-up of notification handling for updates/invalidation/etc. + * Now fetching immediately on notification if the node is visible. + * This averts the infamous IndexOutOfBoundsException that occurs + * if fetching happens during repaint, because the BasicTreeUI is + * caching the number of child nodes before painting begins. + * + * Revision 1.61 2003/01/18 23:33:29 mpowers + * Fixing the build. + * + * Revision 1.60 2002/05/31 15:03:10 mpowers + * Fixes for the previous fix. Fat props to yjcheung. + * + * Revision 1.59 2002/05/28 15:31:36 mpowers + * Fix for updateDisplayedObjects for a subtle case where a node appears in + * the position that another node was moved from. + * + * Revision 1.58 2002/05/24 14:42:02 mpowers + * Prevent repeat events from firing if firing events loops back. + * + * Revision 1.57 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.56 2002/04/19 21:18:45 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.55 2002/04/19 20:53:22 mpowers + * Now firing event fewer events in fireEventsForChanges. + * + * Revision 1.54 2002/04/15 21:52:50 mpowers + * Tightening up TreeModelAssociation and DisplayGroupNode. + * Now only firing root structure changed once. + * Now disposing of root's children. + * Better event coalescing. + * + * Revision 1.53 2002/04/12 20:35:20 mpowers + * Now correctly setting parent display group and data source on creation. + * + * Revision 1.52 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.51 2002/04/03 20:13:36 mpowers + * Now differentiating between node instantiation caused by model expansion + * (user initiated) and by modifications to the model. + * Dispose now disposes all children. + * + * Revision 1.50 2002/03/23 16:20:27 mpowers + * Optimized processRecentChanges, minimized tree events. + * + * Revision 1.49 2002/03/11 03:15:06 mpowers + * Optimized processRecentChanges, minimize event firing, coalescing changes. + * Still need a better diff algorithm to avoid removing nodes. + * + * Revision 1.48 2002/03/10 00:59:39 mpowers + * Interim version: coalesces calls to process recent changes. + * Still does not handle rearranged nodes. + * + * Revision 1.47 2002/03/09 17:33:45 mpowers + * Nodes now track their child nodes by reference, not index. + * + * Revision 1.46 2002/03/08 23:19:07 mpowers + * Added getParentGroup to DisplayGroupNode. + * + * Revision 1.45 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.44 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.42 2002/02/19 22:28:46 mpowers + * DisplayGroupNodes immediately unregister themselves as editors. + * + * Revision 1.41 2002/02/13 16:27:38 mpowers + * Exposing setTarget. + * + * Revision 1.40 2001/11/02 20:55:46 mpowers + * Now using fixed index to send node removed events. This preserves the + * expanded state of the nodes in the corresponding jtree. + * + * Revision 1.39 2001/09/21 21:09:25 mpowers + * Exposed more fields as protected. + * + * Revision 1.38 2001/09/19 15:36:08 mpowers + * Refined behavior for isFetched after notification handling. + * + * Revision 1.37 2001/09/13 14:51:18 mpowers + * DisplayGroupNodes now dispose themselves and mark their parent for update + * when they receive notification that their target has been deleted. + * + * Revision 1.36 2001/09/10 14:10:24 mpowers + * Fix for notification handling. + * + * Revision 1.35 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.34 2001/07/18 22:13:39 mpowers + * getLoadedDescendants now works as advertised. + * Now correctly handling invalidateAllObjects notification. + * + * Revision 1.33 2001/07/18 13:03:32 mpowers + * TreeNodes now refetch only on demand. Previously, once a node had + * been fetched, it was always refetched after an invalidate, even if + * the node was not being displayed. + * + * Revision 1.32 2001/06/18 14:10:28 mpowers + * Cleaned up event firing: no longer firing insert or remove events twice. + * + * Revision 1.31 2001/06/09 16:15:39 mpowers + * Revised the targetChanged scheme because oldObjects and newObjects were + * identical after the target object is invalidated. + * + * Revision 1.30 2001/05/21 22:17:19 mpowers + * Fix for tree out-of-synch problems when nodes are inserted. + * + * Revision 1.29 2001/05/18 21:07:46 mpowers + * Playing with refresh options. + * + * Revision 1.28 2001/05/14 15:25:43 mpowers + * DisplayGroupNodes now only respond to InvalidateAllObjectsInStore + * if they are already fetched. + * + * Revision 1.27 2001/05/08 19:55:58 mpowers + * Fix for node children not refreshing after sibling was inserted. + * + * Revision 1.26 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.25 2001/05/06 22:22:55 mpowers + * Debugging. + * + * Revision 1.24 2001/05/04 14:42:58 mpowers + * Now getting stored values in KeyValueCoding. + * MasterDetail now marks dirty based on whether it's an attribute + * or relation. + * Implemented editing context marker. + * + * Revision 1.23 2001/05/02 18:00:43 mpowers + * Removed debug code. + * + * Revision 1.22 2001/05/02 17:31:20 mpowers + * DisplayGroupNode now does a better job determining when to mark its + * parent dirty. + * + * Revision 1.21 2001/05/01 00:52:32 mpowers + * Implemented breadth-first traversal of tree for node. + * + * Revision 1.20 2001/04/26 01:15:19 mpowers + * Major clean-up of DisplayGroupNode: fitter, happier, more productive. + * + * Revision 1.19 2001/04/22 23:13:35 mpowers + * Minor bug. + * + * Revision 1.18 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.17 2001/04/21 23:05:12 mpowers + * A fairly major revisiting. I've decided to scrap the pass-thru approach + * where every node simply represents an index and not an object. + * The next update will have each node correspond to a specific object. + * + * Revision 1.16 2001/04/13 16:37:37 mpowers + * Handling bounds checking. + * + * Revision 1.15 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.14 2001/03/27 17:45:51 mpowers + * More index bounds checking. + * + * Revision 1.13 2001/03/22 21:25:42 mpowers + * Fixed some nasty issues with jtree's internal state and array bounds. + * + * Revision 1.12 2001/03/19 22:18:58 mpowers + * Root node now mirrors contents of titles display group. + * + * Revision 1.11 2001/03/19 21:38:36 mpowers + * Improved redisplay after edit. Editing nodes off root now works. + * + * Revision 1.10 2001/03/09 22:08:38 mpowers + * Removed unused line. + * + * Revision 1.9 2001/03/07 16:41:04 mpowers + * Now checking size of parent displayed objects array so that we don't + * get array out of bounds execeptions from isLeaf() or object() when + * those messages are called after the TreeAssociation fires a + * nodesDeleted event. I believe that JTree is mistakenly rendering + * those nodes one last time before erasing them. + * + * Revision 1.8 2001/03/06 23:21:27 mpowers + * Now only notifying parent if the object is not registered in the + * editing context, if any. + * + * Revision 1.7 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.6 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.5 2001/01/31 17:59:52 mpowers + * Fixed isLeaf aspect of TreeAssociation. + * + * Revision 1.4 2001/01/25 02:16:25 mpowers + * TreeAssociation now returns DisplayGroupNode.getUserObject. + * + * Revision 1.3 2001/01/24 18:14:40 mpowers + * Fixed problem with leaving children aspect unspecified. + * + * Revision 1.2 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.1 2001/01/24 14:17:12 mpowers + * Major revision to TreeAssociation. Can now add and remove nodes. + * DisplayGroupNode is now it's own class. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java new file mode 100644 index 0000000..ec22bfd --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ListAssociation.java @@ -0,0 +1,368 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.util.Enumeration; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.SwingUtilities; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TextAssociation binds a JList to a display group's +* list of displayable objects. Bindings are: +*
    +*
  • titles: a property convertable to a string for +* display in the cells of the list
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ListAssociation extends EOAssociation + implements ListSelectionListener, + ListDataListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "items" + } ); + + protected DefaultListModel model; + + /** + * Constructor expecting a JList. Throws an + * exception if it doesn't receive one. + * Note: This sets the JList's model to a DefaultListModel. + */ + public ListAssociation ( Object anObject ) + { + super( anObject ); + model = new DefaultListModel(); + ((JList)anObject).setModel( model ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JList ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return TitlesAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + addAsListener(); + super.establishConnection(); + populateFromDisplayGroup(); + selectFromDisplayGroup(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { // System.out.println( "ListAssociation.addAsListener: " + ++count ); + component().getModel().addListDataListener( this ); + component().addListSelectionListener( this ); + } + + protected void removeAsListener() + { // System.out.println( "ListAssociation.removeAsListener: " + --count ); + component().getModel().removeListDataListener( this ); + component().removeListSelectionListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + EODisplayGroup displayGroup; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { +//System.out.println( "subjectChanged: " + +//displayGroup.contentsChanged() + " : " + displayGroup.selectionChanged() +//+ " : " + displayGroup.updatedObjectIndex() ); +//new net.wotonomy.ui.swing.util.StackTraceInspector(); + if ( displayGroup.contentsChanged() ) + { + populateFromDisplayGroup(); + } + + if ( displayGroup.selectionChanged() ) + { + selectFromDisplayGroup(); + } + } + + } + + private void populateFromDisplayGroup() + { + JList component = component(); + EODisplayGroup displayGroup = displayGroupForAspect( TitlesAspect ); + String key = displayGroupKeyForAspect( TitlesAspect ); + + removeAsListener(); + + // remember selection + int[] selectedIndices = component().getSelectedIndices(); + + // clear the model + model.removeAllElements(); + + // populate the model + Object value; + int size = displayGroup.displayedObjects().count(); +//System.out.println( "populateFromDisplayGroup: " + size ); + for ( int i = 0; i < size; i++ ) + { + value = displayGroup.valueForObjectAtIndex( i, key ); + if ( value == null ) value = "[null]"; + model.addElement( value ); + } + + // select the same indexes + for ( int i = 0; i < selectedIndices.length; i++ ) + { + component.addSelectionInterval( + selectedIndices[i], selectedIndices[i] ); // adds one row + } + + addAsListener(); + } + + private void selectFromDisplayGroup() + { + JList component = component(); + EODisplayGroup displayGroup = displayGroupForAspect( TitlesAspect ); + + removeAsListener(); + + int index; + component.clearSelection(); + Enumeration e = + displayGroup.selectionIndexes().objectEnumerator(); + + while ( e.hasMoreElements() ) + { // add selections one-by-one to support non-contiguous + index = ((Number)e.nextElement()).intValue(); + component.addSelectionInterval( + index, index ); // adds one row + } + + addAsListener(); + } + + // interface ListSelectionListener + + public void valueChanged(ListSelectionEvent e) + { + final EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( ( displayGroup != null ) && ( ! e.getValueIsAdjusting() ) ) + { + int[] selectedIndices = component().getSelectedIndices(); + final NSMutableArray indexList = new NSMutableArray(); + for ( int i = 0; i < selectedIndices.length; i++ ) + { + indexList.addObject( new Integer( selectedIndices[i] ) ); + } + + // invoke later so the component is repainted before + // any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + SwingUtilities.invokeLater( new Runnable() { + public void run() + { + displayGroup.setSelectionIndexes( indexList ); + } + }); + } + } + + // interface ListDataListener + + public void intervalAdded(ListDataEvent e) + { + // System.out.println( "intervalAdded" ); + contentsChanged(e); + } + public void intervalRemoved(ListDataEvent e) + { + // System.out.println( "intervalRemoved" ); + contentsChanged(e); + } + public void contentsChanged(ListDataEvent e) + { + // System.out.println( "contentsChanged" ); + + // if we were editing a property, + // we'd notify our display group now. + } + + // convenience + + private JList component() + { + return (JList) object(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/05/15 14:05:55 mpowers + * Now appropriately selectingFromDisplayGroup on establishConnection. + * + * Revision 1.3 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:48:49 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java new file mode 100644 index 0000000..f1568ec --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/MutableDisplayGroupNode.java @@ -0,0 +1,216 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import javax.swing.JTree; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EODisplayGroup; + +/** +* A DisplayGroupNode that exposes the MutableTreeNode interface. +* This was required so that other subclasses of DisplayGroupNode +* could opt out of supporting MutableTreeNode (so that they can +* implement IlvActivity, for example). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + public class MutableDisplayGroupNode + extends DisplayGroupNode implements MutableTreeNode + { + /** + * Constructor for all nodes. + * Root node must have a null delegate. + */ + public MutableDisplayGroupNode( + TreeModelAssociation aParentAssociation, + EODisplayGroup aParentGroup, + Object anObject ) + { + super( aParentAssociation, aParentGroup, anObject ); + } + + public int getIndex(TreeNode node) + { + return getIndex( (DisplayGroupNode) node ); + } + + public TreeNode getChildAt(int childIndex) + { + return (TreeNode) getChildNodeAt( childIndex ); + } + + public TreeNode getParent() + { + Object parent = getParentGroup(); + if ( parent instanceof TreeNode ) + { + return (TreeNode) parent; + } + return null; + } + + public void insert(MutableTreeNode aChild, int anIndex) + { + if ( aChild instanceof DisplayGroupNode ) + { + insertObjectAtIndex( + ((DisplayGroupNode)aChild).object(), anIndex ); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot insert nodes of type: " + aChild ); + } + } + + /** + * Removes the node at the index corresponding + * to the index of the object. + */ + public void remove(MutableTreeNode node) + { + if ( node instanceof DisplayGroupNode ) + { + remove((DisplayGroupNode)node); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot insert nodes of type: " + node ); + } + } + + /** + * Removes the value in the parent display group + * at the index that corresponds to the index of this node + * and add it to the end of the display group that corresponds + * to the user value of the specified node. + */ + public void setParent(MutableTreeNode newParent) + { + if ( newParent instanceof DisplayGroupNode ) + { + setParent((DisplayGroupNode)newParent); + } + else // not a display group node + { + throw new WotonomyException( + "Cannot set parent to nodes of type: " + newParent ); + } + } + + /** + * Overridden to remember expanded state for nodes + * after nodes have been rearranged. + */ + protected void fireEventsForChanges( + Object[] oldObjects, Object[] newObjects ) + { + if ( !( parentAssociation.object() instanceof JTree ) ) + { + super.fireEventsForChanges( oldObjects, newObjects ); + return; + } + + JTree tree = (JTree) parentAssociation.object(); + Map expansionMap = new HashMap(); + DisplayGroupNode node; + TreePath path; + for ( int i = 0; i < oldObjects.length; i++ ) + { + node = (DisplayGroupNode) + getChildNodeForObject( oldObjects[i] ); + if ( node != null && ! node.isLeaf() ) + { + expansionMap.put( node, new Boolean( + tree.isExpanded( node.treePath() ) ) ); + } + } + + super.fireEventsForChanges( oldObjects, newObjects ); + + Object value; + Iterator iterator = new LinkedList( childNodes.values() ).iterator(); + while ( iterator.hasNext() ) + { + node = (DisplayGroupNode) iterator.next(); + value = expansionMap.get( node ); + if ( value != null ) + { + if ( Boolean.TRUE.equals( value ) ) + { + tree.expandPath( node.treePath() ); + } + else + { + tree.collapsePath( node.treePath() ); + } + } + } + } + } +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.7 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.6 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.5 2002/04/03 20:01:47 mpowers + * Now remembers expanded state. + * + * Revision 1.4 2002/03/08 23:19:07 mpowers + * Added getParentGroup to DisplayGroupNode. + * + * Revision 1.3 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.2 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.1 2001/04/21 23:05:56 mpowers + * Contributing the tree-specific concrete subclass of DisplayGroupNode. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java new file mode 100644 index 0000000..d105d89 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/NotificationInspector.java @@ -0,0 +1,333 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.BorderLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.FormattedCellRenderer; +import net.wotonomy.ui.swing.util.ObjectInspector; +import net.wotonomy.ui.swing.util.StackTraceInspector; +import net.wotonomy.ui.swing.util.WindowUtilities; +import net.wotonomy.control.internal.Surrogate; + +/** +* The NotificationInspector displays a JFrame that +* displays notifications as they occur.

+* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class NotificationInspector + implements MouseListener, ActionListener +{ + protected JTable table; + protected JPopupMenu popupMenu; + protected EODisplayGroup displayGroup; + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + protected static final String CLEAR_ALL = "Clear All"; + protected static final String CLEAR_SELECTED = "Clear Selected"; + + +/** +* Displays all notifications on the default notification center. +*/ + public NotificationInspector() + { + this( NSNotificationCenter.defaultCenter() ); + } + +/** +* Displays all notifications from the specified notification center. +*/ + public NotificationInspector( NSNotificationCenter aCenter ) + { + this( aCenter, null, null ); + } + +/** +* Displays notifications from the default notification center +* using the specified name and object filters. +*/ + public NotificationInspector( String notificationName, Object anObject ) + { + this( NSNotificationCenter.defaultCenter(), + notificationName, anObject ); + } + +/** +* Displays notifications on the specified notification center +* using the specified name and object filters. +*/ + public NotificationInspector( NSNotificationCenter aCenter, + String notificationName, Object anObject ) + { + // show stack traces + NSNotification.showStack = true; + + // register for notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + aCenter.addObserver( + this, handleNotification, notificationName, anObject ); + + table = new JTable(); + + popupMenu = new JPopupMenu(); + JMenuItem menuItem = popupMenu.add( CLEAR_SELECTED ); + menuItem.addActionListener( this ); + menuItem = popupMenu.add( CLEAR_ALL ); + menuItem.addActionListener( this ); + + displayGroup = new EODisplayGroup(); + + TableColumn column; + TableColumnAssociation assoc; + + column = new TableColumn(); + column.setHeaderValue( "Time" ); + column.setCellRenderer( new FormattedCellRenderer( new SimpleDateFormat( "hh:mm:ss:SS" ) ) ); + column.setPreferredWidth( 90 ); + column.setMaxWidth( 90 ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "time" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Type" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "name" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Object" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "objectString" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Info" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "userInfoString" ); + assoc.setTable( table ); + assoc.establishConnection(); + + initLayout(); + } + + protected void initLayout() + { + table.addMouseListener( this ); // listen for double-clicks + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + //scrollPane.setPreferredSize( new Dimension( 500, 250 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Notification Inspector" ); + window.getContentPane().add( panel ); + + //window.pack(); + window.setSize( 800, 400 ); + WindowUtilities.cascade( window ); + + // size the columns. +// table.getColumnModel().getColumn( 0 ).setPreferredWidth( 100 ); + + window.show(); + } + +/** +* Handles the notification. +*/ + public void handleNotification( NSNotification aNotification ) + { + Surrogate s = new Surrogate( new Object[] { aNotification } ); + s.directPut( "time", new Date() ); + s.directPut( "objectString", ""+aNotification.object() ); // snapshot of state + s.directPut( "userInfoString", ""+aNotification.userInfo() ); // snapshot of info + displayGroup.insertObjectAtIndex( s, 0 ); + } + + // interface ActionListener + + /** + * Method used to listen for the action event from the popup menu items. + */ + public void actionPerformed( ActionEvent e ) + { + if ( CLEAR_SELECTED.equals( e.getActionCommand() ) ) + { + NSMutableArray objects = new NSMutableArray( displayGroup.allObjects() ); + objects.removeAll( displayGroup.selectedObjects() ); + displayGroup.setObjectArray( objects ); + displayGroup.updateDisplayedObjects(); + } + else if ( CLEAR_ALL.equals( e.getActionCommand() ) ) + { + displayGroup.setObjectArray( null ); + displayGroup.updateDisplayedObjects(); + } + } + + // interface MouseListener + + /** + * Double click to launch object inspector. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + col = table.convertColumnIndexToModel( col ); + + if ( row == -1 ) return; + + if ( col == 0 ) // time + { + new StackTraceInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).stackTrace() ); + } + else + if ( col == 2 ) // object + { + new ObjectInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).object() ); + } + else + if ( col == 3 ) // info + { + new ObjectInspector( ( (NSNotification) ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ).userInfo() ); + } + else + { + new ObjectInspector( ( (Surrogate) + displayGroup.displayedObjects().objectAtIndex( row ) ).getDelegate() ); + } + } + else + { + // Click count is 1 then, check for popup trigger. + if ( e.isPopupTrigger() ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + } + + public void mouseReleased(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.isPopupTrigger() && ( e.getClickCount() == 1 ) ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + + public void mousePressed(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.isPopupTrigger() && ( e.getClickCount() == 1 ) ) + { + popupMenu.show( table, e.getX(), e.getY() ); + } + } + } + + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:23 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.5 2002/11/18 22:11:51 mpowers + * rglista's long-overdue enhancements: can now clear the display! + * + * Revision 1.4 2002/10/24 18:19:03 mpowers + * Now telling NSNotification to generate stack traces. + * + * Revision 1.3 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.2 2001/04/09 21:40:25 mpowers + * Numerous usability enhancements. + * + * Revision 1.1 2001/04/08 20:58:45 mpowers + * Contributing notification inspector. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java new file mode 100644 index 0000000..d2e51ed --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/RadioPanelAssociation.java @@ -0,0 +1,457 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.RadioButtonPanel; + +/** +* RadioPanelAssociation binds RadioButtonPanels to +* display groups. It works exactly like a +* ComboBoxAssociation. Bindings are: +*
    +* +*
  • value: a property of the selected object in the +* display group that will be bind to the item the user +* selects or the text that the user enters in the field.
  • +* +*
  • titles: a property of the objects in the bound +* display group that will appear in the list. If the +* objects aspect is not bound, this property is also +* used to populate the value binding.
  • +* +*
  • objects: optional - if specified, when the user +* selects an title in the list, the property of the +* object at the corresponding index of the bound display +* group will be used to populate the value binding.
  • +* +*
  • enabled: a boolean property of the selected object in the +* display group that determines whether +* the user can edit the field.
  • +* +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class RadioPanelAssociation extends EOAssociation + implements ActionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ValueAspect, + ObjectsAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public RadioPanelAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof RadioButtonPanel ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + super.establishConnection(); + + // prepopulate titles + EODisplayGroup displayGroup = + displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + populateValue(); + addAsListener(); + } + + protected void addAsListener() + { + component().addActionListener( this ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void removeAsListener() + { + component().removeActionListener( this ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + removeAsListener(); + + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // titles aspect + displayGroup = displayGroupForAspect( TitlesAspect ); + if ( displayGroup != null ) + { + // if backing group has changed + if ( displayGroup.contentsChanged() ) + { + key = displayGroupKeyForAspect( TitlesAspect ); + populateTitles( displayGroup, key ); + } + } + + // value aspect + populateValue(); + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( EnabledAspect ); + Object value = + displayGroup.selectedObjectValueForKey( key ); + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != component.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + addAsListener(); + } + + /** + * Called to repopulate the title list from the + * specified display group. + */ + protected void populateTitles( + EODisplayGroup displayGroup, String key ) + { + Object value; + int count = displayGroup.displayedObjects().count(); + String[] titles = new String[ count ]; + for ( int i = 0; i < count; i++ ) + { + value = displayGroup.valueForObjectAtIndex( i, key ); + if ( value != null ) + { + titles[i] = value.toString(); + } + else + { + titles[i] = ""; + } + } + component().setLabels( titles ); + } + + /** + * Called to populate the value from the display group. + */ + protected void populateValue() + { + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + component.setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + + Object value = displayGroup.selectedObjectValueForKey( key ); + + // objects aspect + EODisplayGroup objectsDisplayGroup = + displayGroupForAspect( ObjectsAspect ); + if ( ( objectsDisplayGroup != null ) && ( value != null ) ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + Object match; + int index = NSArray.NotFound; + int count = objectsDisplayGroup.displayedObjects().count(); + for ( int i = 0; i < count; i++ ) + { + match = objectsDisplayGroup.valueForObjectAtIndex( i, objectKey ); + if ( value.equals( match ) ) + { + index = i; + } + } + if ( index == NSArray.NotFound ) + { + if ( component.getValue() != null ) + { + component.setValue( null ); + } + } + else + { + String[] titles = component().getLabels(); + component.setValue( titles[ index ] ); + } + } + else + { + if ( value != null ) value = value.toString(); + component.setValue( (String) value ); + } + } + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + RadioButtonPanel component = component(); + EODisplayGroup displayGroup; + String key; + + // selected title aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + Object value = null; + + // selected object aspect, if any + EODisplayGroup objectsGroup = + displayGroupForAspect( ObjectsAspect ); + if ( objectsGroup != null ) + { + String objectKey = displayGroupKeyForAspect( ObjectsAspect ); + String selectedValue = component.getValue(); + if ( selectedValue != null ) + { + String[] titles = component.getLabels(); + int index = -1; + for ( int i = 0; i < titles.length; i++ ) + { + if ( selectedValue.equals( titles[i] ) ) + { + index = i; + } + } + if ( index != -1 ) + { + value = objectsGroup + .valueForObjectAtIndex( index, objectKey ); + } + } + } + else // just use the selected item + { + value = component.getValue(); + } + + return displayGroup.setSelectedObjectValue( value, key ); + } + + return false; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + writeValueToDisplayGroup(); + } + + // convenience + + private RadioButtonPanel component() + { + return (RadioButtonPanel) object(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/02/16 17:48:07 mpowers + * Populating titles or data not longer marks target object as changed. + * + * Revision 1.1.1.1 2000/12/21 15:48:52 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java new file mode 100644 index 0000000..7194f23 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/ReferenceInspector.java @@ -0,0 +1,283 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.ButtonPanel; +import net.wotonomy.ui.swing.util.WindowUtilities; + +/** +* ReferenceInspector tracks objects until they are garbage collected. +* Use it to track objects that you suspect are not being GCed. +* ReferenceInspector retains only weak references to the objects that +* it tracks, so when those weak references cannot be resolved, the +* object has been garbage collected. Note that under some GC +* implementations, adding a weak reference to an object will delay +* garbage collection for that object. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class ReferenceInspector + implements MouseListener, ActionListener +{ + protected JTable table; + protected JLabel memoryLabel; + + static protected EODisplayGroup displayGroup; + static protected JFrame window; + + /* Reference queue for cleared WeakKeys */ + private static ReferenceQueue queue = new ReferenceQueue(); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + +/** +* Launches a new ReferenceInspector if one does not already exist. +*/ + public ReferenceInspector() + { + if ( window == null ) + { + table = new JTable(); + memoryLabel = new JLabel(); + + displayGroup = new EODisplayGroup(); + + TableColumn column; + TableColumnAssociation assoc; + + column = new TableColumn(); + column.setHeaderValue( "Object" ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "" ); + assoc.setTable( table ); + assoc.establishConnection(); + + column = new TableColumn(); + column.setHeaderValue( "Address" ); + column.setMaxWidth( 100 ); + + assoc = new TableColumnAssociation( column ); + assoc.bindAspect( EOAssociation.ValueAspect, displayGroup, "identityHashCode" ); + assoc.setTable( table ); + assoc.establishConnection(); + + initLayout(); + } + window.show(); + } + +/** +* Adds the specified object to the ReferenceInspector, launching +* a new ReferenceInspector if one does not already exist. +*/ + public ReferenceInspector( Object anObject ) + { + this(); + displayGroup.insertObjectAtIndex( new ExtendedWeakReference( anObject, queue ), 0 ); + window.show(); + } + + protected void initLayout() + { + table.addMouseListener( this ); // listen for double-clicks + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 500, 250 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + ButtonPanel buttonPanel = new ButtonPanel( new String[] { "Update" } ); + buttonPanel.addActionListener( this ); + + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout( new BorderLayout() ); + bottomPanel.add( buttonPanel, BorderLayout.EAST ); + bottomPanel.add( memoryLabel, BorderLayout.CENTER ); + panel.add( bottomPanel, BorderLayout.SOUTH ); + + window = new JFrame(); + window.setTitle( "Reference Inspector" ); + window.getContentPane().add( panel ); + +// javax.swing.Timer timer = new javax.swing.Timer( 10000, this ); +// timer.restart(); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + /* Remove all invalidated entries from the map, that is, remove all entries + whose keys have been discarded. This method should be invoked once by + each public mutator in this class. We don't invoke this method in + public accessors because that can lead to surprising + ConcurrentModificationExceptions. */ + private static void processQueue() + { +// System.out.println( "ReferenceInspector.processQueue:"); + synchronized ( displayGroup ) + { + int idx; + WeakReference rk; + while ((rk = (WeakReference)queue.poll()) != null) + { +// System.out.println( "ReferenceInspector.processQueue: removing object: " + rk ); + if ( rk != null ) + { + idx = displayGroup.displayedObjects().indexOfIdenticalObject( rk ); + if ( idx != NSArray.NotFound ) + { + displayGroup.deleteObjectAtIndex( idx ); + } + } + } + displayGroup.updateDisplayedObjects(); + } + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + Runtime runtime = Runtime.getRuntime(); + runtime.gc(); + processQueue(); + + long totalMemory = runtime.totalMemory() / 1024; + long freeMemory = runtime.freeMemory() / 1024; + memoryLabel.setText( + Long.toString( totalMemory - freeMemory ) + "K / " + + Long.toString( totalMemory ) + "K" ); + } + + // interface MouseListener + + /** + * Double click to launch object inspector. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + col = table.convertColumnIndexToModel( col ); + + if ( row == -1 ) return; + + if ( col == 0 ) // time + { + } + else + { + } + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + public class ExtendedWeakReference extends WeakReference + { + public ExtendedWeakReference(Object referent, ReferenceQueue q) + { + super( referent, q ); + } + + public String toString() + { + if ( get() != null ) + { + return get().toString(); + } + return null; + } + + public String identityHashCode() + { + if ( get() != null ) + { + return Integer.toHexString( System.identityHashCode( get() ) ); + } + return null; + } + + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/03/22 22:39:59 mpowers + * Now shows the window even if previously hidden. + * + * Revision 1.3 2001/10/02 14:22:39 mpowers + * Now shows used and heap memory usage. + * + * Revision 1.2 2001/07/10 16:39:32 mpowers + * Removed printlns. + * + * Revision 1.1 2001/07/10 16:32:50 mpowers + * Adding the reference inspector. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java new file mode 100644 index 0000000..b836dcc --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/SliderAssociation.java @@ -0,0 +1,419 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.util.Iterator; + +import javax.swing.JSlider; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* SliderAssociation binds a JSlider component to +* a display group. Bindings are: +*
    +*
  • value: a property convertable to/from a string
  • +*
  • enabled: a boolean property that determines whether +* the user can select the text in the field
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class SliderAssociation extends EOAssociation + implements ChangeListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, VisibleAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "value" + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public SliderAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JSlider ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener to the specified object. + */ + public void establishConnection () + { + component().addChangeListener( this ); + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + component().removeChangeListener( this ); + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + JSlider component = component(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + key = displayGroupKeyForAspect( ValueAspect ); + component.setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( key ) ); + + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } else { + + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking size of displayGroup + + // convert value to int + value = ValueConverter.convertObjectToClass( value, Integer.class ); + + int intValue; + if ( value == null ) + { + intValue = 0; + } + else + { + intValue = ((Integer)value).intValue(); + } + + if ( component.getValue() != intValue ) + { + component().removeChangeListener( this ); + component.setValue( intValue ); + component().addChangeListener( this ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( displayGroup != null ) || ( key != null ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( converted.booleanValue() != component.isEnabled() ) + { + component.setEnabled( converted.booleanValue() ); + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + key = displayGroupKeyForAspect( VisibleAspect ); + if ( ( displayGroup != null ) || ( key != null ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() != component.isVisible() ) + { + component.setVisible( converted.booleanValue() ); + } + } + } + + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + EODisplayGroup displayGroup = + displayGroupForAspect( ValueAspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( ValueAspect ); + Object value = new Integer( component().getValue() ); + + boolean returnValue = true; + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + } + return false; + } + + // interface ChangeListener + + /** + * Updates object on change. + */ + public void stateChanged(ChangeEvent e) + { + writeValueToDisplayGroup(); + } + + private JSlider component() + { + return (JSlider) object(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.7 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2002/10/11 20:12:58 mpowers + * Updated aspect signature. + * + * Revision 1.5 2002/10/11 20:08:14 mpowers + * Added visible aspect to slider association. + * + * Revision 1.4 2001/11/01 15:54:37 mpowers + * Minor update to aspect signature. + * + * Revision 1.3 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.2 2001/02/16 15:03:34 mpowers + * Fixed: slider sets value in display group after selection changed. + * + * Revision 1.1.1.1 2000/12/21 15:48:55 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java new file mode 100644 index 0000000..bc02f7d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableAssociation.java @@ -0,0 +1,927 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +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.MouseEvent; +import java.util.Enumeration; + +import javax.swing.CellEditor; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumn; +import javax.swing.table.TableModel; + +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TableAssociation binds one or more TableColumnAssociations +* to a display group. You should not instantiate this class +* directly; use TableColumnAssociation.setTable() instead. +* Note that TableAssociation inserts itself as the controlled +* JTable's TableModel. +* +* Bindings are: +*
    +*
  • source: a property convertable to a string for +* display in the cells of the table column
  • +*
  • enabled: a property convertable to a string for +* display in the cells of the table column. +* Note that you can bind this aspect to a key equal to +* "true" or "false" and leave the display group null.
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TableAssociation extends EOAssociation + implements ActionListener, ListSelectionListener, FocusListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + SourceAspect, EnabledAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "tableModel", "tableHeader" + } ); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + EODisplayGroup source; + EODisplayGroup sortTarget; // used by TreeColumnAssociation + NSMutableArray columns; + JTableHeader tableHeader; + + boolean pleaseIgnore; + boolean selectionPaintedImmediately; + boolean selectionTracking; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. setTable() must be called before + * establishing the connection. + */ + public TableAssociation ( Object anObject ) + { + super( anObject ); + source = null; + columns = new NSMutableArray(); + JTable aTable = ((JTable)anObject); + aTable.addFocusListener( this ); + aTable.setModel( + new TableAssociationModel( this ) ); + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + + // why did sun make this harder to use? + //aTable.getInputMap( JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ).put( + // KeyStroke.getKeyStroke( KeyEvent.VK_C, java.awt.Event.CTRL_MASK ), COPY); + //aTable.getActionMap().put(COPY, this); + + aTable.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + aTable.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + tableHeader = new SortedTableHeader(); + tableHeader.setColumnModel( aTable.getColumnModel() ); + aTable.setTableHeader( tableHeader ); + tableHeader.addMouseListener( new TableHeaderListener() ); + pleaseIgnore = false; + selectionPaintedImmediately = false; + selectionTracking = false; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JTable ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( SourceAspect.equals( anAspect ) ) + { + source = aDisplayGroup; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + if ( source == null ) + { + throw new WotonomyException( "No display group: " + + "please ensure that the TableColumnAssociation " + + "has a display group bound to the ValueAspect" ); + } + super.establishConnection(); + selectFromDisplayGroup(); + addAsListener(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { + component().getSelectionModel() + .addListSelectionListener( this ); + } + + protected void removeAsListener() + { + component().getSelectionModel() + .removeListSelectionListener( this ); + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + // stop any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + return editor.stopCellEditing(); + } + return true; + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + if ( source != null ) + { + if ( source.contentsChanged() ) + { + removeAsListener(); + ((TableAssociationModel)component().getModel()). + fireTableDataChanged(); + selectFromDisplayGroup(); + addAsListener(); + + // if we caused this change, do nothing + if ( pleaseIgnore ) + { + pleaseIgnore = false; + } + else // otherwise, update the sort indicator + { + tableHeader.repaint(); + + // cancel any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + editor.cancelCellEditing(); + } + } + } + else + if ( source.selectionChanged() ) + { + removeAsListener(); + selectFromDisplayGroup(); + addAsListener(); + } + } + + } + + private void selectFromDisplayGroup() + { + JTable component = component(); + + int index; + component.getSelectionModel().clearSelection(); + Enumeration e = + source.selectionIndexes().objectEnumerator(); + + while ( e.hasMoreElements() ) + { // add selections one-by-one to support non-contiguous + index = ((Number)e.nextElement()).intValue(); + component.getSelectionModel().addSelectionInterval( + index, index ); // adds one row + } + } + + // interface ListSelectionListener + + public void valueChanged(ListSelectionEvent e) + { + if ( source != null ) + { + if ( selectionTracking || !e.getValueIsAdjusting() ) + { + int[] selectedIndices = component().getSelectedRows(); + final NSMutableArray indexList = new NSMutableArray(); + for ( int i = 0; i < selectedIndices.length; i++ ) + { + indexList.addObject( new Integer( selectedIndices[i] ) ); + } + + // invoke later so the component is repainted before + // any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + Runnable select = new Runnable() + { + public void run() + { + pleaseIgnore = true; + source.setSelectionIndexes( indexList ); + } + }; + if ( selectionPaintedImmediately ) + { + EventQueue.invokeLater( select ); + } + else + { + select.run(); + } + } + } + } + + /** + * Determines whether the selection should be painted + * immediately after the user clicks and therefore + * before the children display group is updated. + * When the children group is bound to many associations + * or is bound to a master-detail association, updating + * the display group can take a perceptibly long time. + * This property defaults to false. + * @see #setSelectionPaintedImmediately + */ + public boolean isSelectionPaintedImmediately() + { + return selectionPaintedImmediately; + } + + /** + * Sets whether the selection should be painted immediately. + * Setting this property to true will let the table paint + * first before the display group is updated. + */ + public void setSelectionPaintedImmediately( boolean isImmediate ) + { + selectionPaintedImmediately = isImmediate; + } + + /** + * Determines whether the selection is actively tracking + * the selection as the user moves the mouse. + * If true, selection will not be updated while the + * list selection event returns true for isValueAdjusting(). + * This property defaults to false. + * @see #setSelectionTracking + */ + public boolean isSelectionTracking() + { + return selectionTracking; + } + + /** + * Sets whether the selection is actively tracking + * the selection as the user moves the mouse. + * Setting this property to true will update the display + * group with each change to the selection. + * This means that any tree selection listers will + * also be notified before the display group is updated + * and will have to invokeLater if they want to see the + * updated display group. + */ + public void setSelectionTracking( boolean isTracking ) + { + selectionTracking = isTracking; + } + + // convenience + private JTable component() + { + return (JTable) object(); + } + + /** + * Copies the contents of the table to the clipboard as a tab-delimited string. + */ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getTabDelimitedString() ); + clipboard.setContents( selection, selection ); + } + + /** + * Converts the contents of the table to a tab-delimited string. + * @return A String containing the text contents of the table. + */ + public String getTabDelimitedString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = component().getModel(); + int cols = model.getColumnCount(); + int rows = model.getRowCount(); + + Object o = null; + for ( int y = 0; y < rows; y++ ) + { + for ( int x = 0; x < cols; x++ ) + { + o = model.getValueAt( y, x ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\t' ); + } + result.append( '\n' ); + } + + return result.toString(); + } + + // interface ActionEventListener + + public void actionPerformed(ActionEvent evt) + { + String cmd = evt.getActionCommand(); + + if ( COPY.equals( cmd ) ) + { + copyToClipboard(); + return; + } + } + + /** + * Used to render the little triangle over the sorted column(s). + */ + class SortedTableHeader extends JTableHeader + { + public void paint(Graphics g) + { + super.paint( g ); + Rectangle r; + TableColumnAssociation association; + int size = columns.size(); + NSArray orderings; + if ( sortTarget != null ) + { + orderings = sortTarget.sortOrderings(); + } + else + { + orderings = source.sortOrderings(); + } + for ( int i = 0; i < size; i++ ) + { + r = getHeaderRect( component().convertColumnIndexToView( i ) ); + association = (TableColumnAssociation) columns.objectAtIndex( i ); + association.drawSortIndicator( r, g, orderings ); + } + } + } + + /** + * Used to listen for clicks on the table header. + */ + class TableHeaderListener extends MouseAdapter + { + public void mouseClicked( MouseEvent evt ) + { + EODisplayGroup displayGroup = sortTarget; + if ( displayGroup == null ) displayGroup = source; + + if ( evt.getClickCount() > 0 ) + { + int columnClicked = tableHeader.columnAtPoint( evt.getPoint() ); + if ( columnClicked != -1 ) + { + columnClicked = component().convertColumnIndexToModel( columnClicked ); + TableColumnAssociation association = (TableColumnAssociation) + columns.objectAtIndex( columnClicked ); + if ( association.isSortable() ) + { + NSMutableArray newOrder = + new NSMutableArray( displayGroup.sortOrderings() ); + + // click once = asc, twice = desc, thrice = clear + EOSortOrdering sortOrdering; + int index = association.getIndexOfMatchingOrdering( newOrder ); + if ( index == -1 ) sortOrdering = null; + else if ( index == 1 ) sortOrdering = association.getSortOrdering( false ); + else sortOrdering = association.getSortOrdering( true ); + + pleaseIgnore = true; + tableHeader.repaint(); + + // stop any cell editing + CellEditor editor = component().getCellEditor(); + if ( editor != null ) + { + editor.stopCellEditing(); + } + + // remove existing key + if ( index != 0 ) + { + newOrder.removeObjectAtIndex( Math.abs( index ) - 1 ); + } + + // put new key at front of list + if ( sortOrdering != null ) + { + newOrder.insertObjectAtIndex( sortOrdering, 0 ); + } + + displayGroup.setSortOrderings( newOrder ); + displayGroup.updateDisplayedObjects(); + } + } + } + } + } + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( ! component().isEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + + /** + * Used as the model for the controlled table. + * Package access so TableColumnAssociation can recognize + * it and use the addColumnAssociation() method. + * Extends AbstractTableModel just so we get event + * broadcasting for free. + */ + static class TableAssociationModel extends AbstractTableModel + { + private TableAssociation parent; + + private TableAssociationModel( TableAssociation aParent ) + { + parent = aParent; + } + + public TableAssociation getAssociation() + { + return parent; + } + + /** + * Adds the column to the list of ColumnAssociations, + * and adds the corresponding column to the table + * at the next available index, setting the value of + * the column's model index accordingly. + * Establishes connection if no columns are currently + * associated. + */ + public void addColumnAssociation( + TableColumnAssociation aColumnAssociation ) + { + // establish connection if necessary + if ( parent.columns.size() == 0 ) + { + parent.establishConnection(); + } + + int newIndex = parent.columns.count(); + parent.columns.add( aColumnAssociation ); + + // add column to table + TableColumn column = (TableColumn) aColumnAssociation.object(); + column.setModelIndex( newIndex ); + parent.component().addColumn( column ); + + } + + /** + * Removes the column from the list of ColumnAssociations, + * and removes the corresponding column from the table. + * Breaks connection if no more columns are associated. + */ + public void removeColumnAssociation( + TableColumnAssociation aColumnAssociation ) + { + int index = parent.columns.indexOfIdenticalObject( aColumnAssociation ); + if ( index == NSArray.NotFound ) return; + + parent.columns.removeObjectAtIndex( index ); + + // remove column from table + TableColumn column = (TableColumn) aColumnAssociation.object(); + parent.component().removeColumn( column ); + + // break connection if necessary + if ( parent.columns.size() == 0 ) + { + parent.breakConnection(); + } + } + + public int getRowCount() + { + if ( parent.source == null ) return 0; + return parent.source.displayedObjects().count(); + } + + public int getColumnCount() + { + return parent.columns.count(); + } + + /** + * Attempts to retrieve the header value from the specified column, + * or returns " " if the value is null. + */ + public String getColumnName(int columnIndex) + { + TableColumnAssociation association = (TableColumnAssociation) + parent.columns.objectAtIndex( columnIndex ); + Object value = ((TableColumn)association.object()).getHeaderValue(); + if ( value != null ) return value.toString(); + return " "; + } + + /** + * Returns the class of the first item in the + * display group bound to the column. + */ + public Class getColumnClass(int columnIndex) + { + Object value; + int count = getRowCount(); + for( int i = 0; i < count; i++ ) + { //First row in column is null find one that is not. + value = ((TableColumnAssociation)parent.columns. + objectAtIndex( columnIndex ) ).valueAtIndex( i ); + if ( value != null ) return value.getClass(); + } + return Object.class; + } + + /** + * Calls the column association's isEditableAtRow method. + */ + public boolean isCellEditable(int rowIndex, + int columnIndex) + { + return + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).isEditableAtRow( rowIndex ); + } + + /** + * Calls the column association's valueAtIndex method. + */ + public Object getValueAt(int rowIndex, + int columnIndex) + { + return + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).valueAtIndex( rowIndex ); + } + + /** + * Calls the column association's setValueAtIndex method. + */ + public void setValueAt(Object aValue, + int rowIndex, + int columnIndex) + { + Object existingValue = getValueAt( rowIndex, columnIndex ); + + // don't update display group if value is the same as before + if ( aValue == existingValue ) return; // handles null case + if ( existingValue != null ) // both are not null + { + Object newValue = aValue; + + // if different types + if ( newValue.getClass() != existingValue.getClass() ) + { + // convert to string since most editors do anyway + newValue = newValue.toString(); + existingValue = existingValue.toString(); + } + if ( newValue.equals( existingValue ) ) + { + // same value - do nothing + return; + } + } + + ((TableColumnAssociation)parent.columns.objectAtIndex( + columnIndex ) ).setValueAtIndex( aValue, rowIndex ); + } + + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.31 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.30 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.29 2002/05/24 14:41:29 mpowers + * Throwing an exception for clarity. + * + * Revision 1.28 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.27 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.25 2002/03/04 22:49:53 mpowers + * Now working with TreeColumnAssociation to delegate sorting behavior. + * + * Revision 1.23 2002/03/04 03:58:17 mpowers + * Refined table header click behavior. + * + * Revision 1.22 2002/03/01 23:41:39 mpowers + * Added facility to get table association from model. + * + * Revision 1.21 2002/03/01 20:41:22 mpowers + * Cleaned up unused code. + * + * Revision 1.20 2002/03/01 15:42:00 mpowers + * Table column headers now always show their sort indicator. + * A third table-column click clears the sort for that column. + * + * Revision 1.19 2002/02/28 23:01:39 mpowers + * TableColumnAssociations add and remove themselves from the TableAssociation + * when their connection is established and broken respectively. + * TableAssociations now break connection if they have no column associations. + * + * Revision 1.18 2002/02/28 22:45:27 mpowers + * Now registers as editing association when table gets focus, so that + * endEditing can appropriate commit any table cell editors. + * + * Revision 1.17 2001/10/26 20:01:50 mpowers + * We're again cancelling instead of stopping editing on subjectChanged. + * I believe that the introduction of NSRunLoop has allowed us to return + * to the correct behavior. + * + * Revision 1.16 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.15 2001/07/25 15:03:01 mpowers + * getColumnName now checks for a column header value before defaulting " ". + * + * Revision 1.14 2001/06/05 19:09:25 mpowers + * Fixed broken build. + * + * Revision 1.13 2001/06/05 19:08:12 mpowers + * Better determination of column class. Previously, if the first object + * was null, Object.class was used. Now we get the first non-null object. + * + * Revision 1.12 2001/05/02 17:39:01 mpowers + * Now selects from display group after initial population. + * + * Revision 1.11 2001/03/29 23:05:22 mpowers + * Fixes for editing table cells. + * + * Revision 1.10 2001/03/09 22:09:22 mpowers + * Now better handling jdk1.1 for rendering the column header. + * + * Revision 1.9 2001/02/27 02:10:38 mpowers + * No longer updating values to the display group if the value + * has not changed. + * + * Revision 1.8 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.7 2001/02/16 17:47:17 mpowers + * Now cancelling any cell editing on subjectChanged(). + * + * Revision 1.6 2001/01/12 19:11:56 mpowers + * Fixed table column click sorting. + * + * Revision 1.5 2001/01/12 17:20:30 mpowers + * Moved EOSortOrdering creation to ColumnAssociation. + * + * Revision 1.4 2001/01/11 21:55:57 mpowers + * Implemented sort indicator for table column headers. + * + * Revision 1.3 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.2 2001/01/08 20:40:51 mpowers + * JTables in 1.3 clear their selection when the data model changes, + * which sends a list selection event. We now reset the selection again + * after the data changes so we're compatible across 1.2 and 1.3. + * + * Revision 1.1.1.1 2000/12/21 15:49:00 mpowers + * Contributing wotonomy. + * + * Revision 1.7 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java new file mode 100644 index 0000000..e1c32f3 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TableColumnAssociation.java @@ -0,0 +1,708 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.util.Iterator; +import java.util.List; + +import javax.swing.JTable; +import javax.swing.table.TableColumn; + +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.TableAssociation.TableAssociationModel; + + +/** +* TableColumnAssociation binds a column of a JTable +* to a property of the elements of a display group. +* Bindings are: +*
    +*
  • value: a property convertable to a string for +* display in the cells of the table column
  • +*
  • editable: a property convertable to a boolean +* that determines the editability of the corresponding +* cells in the column.
  • +*
+ +* Because TableColumns do not have a handle to their +* containing JTable, setTable() must be called before +* calling establishConnection(). This will add the +* controlled TableColumn to the specified JTable. +* +* Columns appear in the table in the order in which +* setTable is called on the corresponding association. +* The original table model index is ignored. +* +* Column names appear in the table based on the value +* of TableColumn.getHeaderValue(). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TableColumnAssociation extends EOAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EditableAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "table" + } ); + + static Color[] sortIndicatorColorList; + + EODisplayGroup valueDisplayGroup, editableDisplayGroup; + String valueKey, editableKey; + boolean sortable; + boolean sortCaseSensitive; + JTable table; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. setTable() must be called before + * establishing the connection. + */ + public TableColumnAssociation ( Object anObject ) + { + super( anObject ); + valueDisplayGroup = null; + valueKey = null; + editableDisplayGroup = null; + editableKey = null; + sortable = true; + sortCaseSensitive = true; + table = null; + } + + /** + * Sets the table to be used for this column association. + * If no TableAssociation exists for the specified table, + * one will be created automatically. The controlled + * table column will be added to the table. Note that + * the table column's model index is ignored: table columns + * appear in the table in the order in which setTable is + * called on their corresponding associations. + */ + public void setTable( JTable aTable ) + { + table = aTable; + if ( table == null ) return; + + // creates table association if not existing + getTableAssociation(); + } + + /** + * Returns the table association for this table column, + * or null if no table has been set. This method will + * create the association if none exists for the table. + */ + public TableAssociation getTableAssociation() + { + if ( table == null ) return null; + + TableAssociation result; + if ( ! ( table.getModel() instanceof TableAssociationModel ) ) + { + result = new TableAssociation( table ); + result.bindAspect( + SourceAspect, displayGroupForAspect( ValueAspect ), "" ); + } + else + { + result = ((TableAssociationModel)table.getModel()).getAssociation(); + } + return result; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof TableColumn ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + if ( EditableAspect.equals( anAspect ) ) + { + editableDisplayGroup = aDisplayGroup; + editableKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + addAsListener(); + + if ( table == null ) throw new WotonomyException( + "A table must be specified by calling setTable()" ); + + // add association to model + TableAssociationModel model = + (TableAssociationModel) table.getModel(); + model.addColumnAssociation( this ); + + super.establishConnection(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + removeAsListener(); + + if ( table == null ) throw new WotonomyException( + "TableColumnAssociation's table may not be null" ); + + // remove association from model + TableAssociationModel model = + (TableAssociationModel) table.getModel(); + model.removeColumnAssociation( this ); + + super.breakConnection(); + } + + protected void addAsListener() + { + } + + protected void removeAsListener() + { + } + + /** + * Returns the value to be displayed at the specified index. + * This method is called by the TableAssocation to populate + * the table model. + * This implementation simply retrieves the value from the + * display group bound to the value aspect. + */ + public Object valueAtIndex( int aRowIndex ) + { + if ( valueDisplayGroup != null ) + { + return valueDisplayGroup.valueForObjectAtIndex( + aRowIndex, valueKey ); + } + return null; + } + + /** + * Sets a value for the specified index. This method is + * called by the TableAssocation after a cell has been + * edited. + * This implementation simply sets the value in the + * display group bound to the value aspect. + */ + public void setValueAtIndex( Object aValue, int aRowIndex ) + { + if ( valueDisplayGroup != null ) + { + valueDisplayGroup.setValueForObjectAtIndex( + aValue, aRowIndex, valueKey ); + } + } + + /** + * Returns whether this column should be sorted when the + * user clicks on the column header. Defaults to true. + */ + public boolean isSortable() + { + return sortable; + } + + /** + * Sets whether this column should be sorted when the + * user clicks on the column header. + */ + public void setSortable( boolean isSortable ) + { + sortable = isSortable; + } + + /** + * Returns whether this column should be sorted + * in a case sensitive manner. Defaults to true. + */ + public boolean isSortCaseSensitive() + { + return sortCaseSensitive; + } + + /** + * Sets whether this column should be sorted when + * in a case sensitive manner. + * If false, the column contents should be string values. + */ + public void setSortCaseSensitive( boolean isCaseSensitive ) + { + sortCaseSensitive = isCaseSensitive; + } + + /** + * Called by the TableAssociation to determine whether + * the value at the specified row is editable. + * This is determined by the binding of the Editable aspect, + * looking at the value of the corresponding index in that + * display group. Note: because the display group may + * not have the same number if items, the selected index is + * used if the editable display group is not the same as the + * the value display group. + */ + public boolean isEditableAtRow( int aRowIndex ) + { + if ( editableKey == null ) return false; + Object value = null; + if ( editableDisplayGroup != null ) + { + // if using the same group for both, return the value for the index + if ( editableDisplayGroup.equals( valueDisplayGroup ) ) + { + value = + editableDisplayGroup.valueForObjectAtIndex( aRowIndex, editableKey ); + } + else // using an external display group to determine editability + { + // ignore index and use the selected object value from display group + value = + editableDisplayGroup.selectedObjectValueForKey( editableKey ); + } + } + else + { + // treat bound key without display group as a value + value = editableKey; + } + if ( value == null ) return false; // null defaults to false + Boolean result = (Boolean) + ValueConverter.convertObjectToClass( value, Boolean.class ); + if ( result == null ) return true; // non-null defaults to true + return result.booleanValue(); + } + + // convenience + + private TableColumn component() + { + return (TableColumn) object(); + } + + /** + * Called by TableAssociation to get a EOSortOrdering suitable + * for the information in this column. + * This implementation returns a EOSortOrdering with the key + * equal to the value aspect's key and the appropriate selector + * for the specified ascending value and the case sensitivity + * of this column. + * Override to customize the sort for your column. + */ + public EOSortOrdering getSortOrdering( boolean isAscending ) + { + if ( isAscending ) + { + if ( isSortCaseSensitive() ) + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareAscending ) ; + } + else + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareCaseInsensitiveAscending ) ; + } + } + else + { + if ( isSortCaseSensitive() ) + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareDescending ) ; + } + else + { + return new EOSortOrdering( + valueKey, + EOSortOrdering.CompareCaseInsensitiveDescending ) ; + } + } + } + + /** + * Returns the one-based index of this assocation's sort ordering + * in the specified list of orderings. If the sign of the returned + * value is negative, the ordering is descending. If the return + * value is zero, no matching ordering was found. + */ + protected int getIndexOfMatchingOrdering( List orderings ) + { + // find index of matching ordering + int index = 0; + EOSortOrdering ordering = null; + Iterator i = orderings.iterator(); + while ( i.hasNext() ) + { + index++; + ordering = (EOSortOrdering) i.next(); + if ( ordering.key().equals( valueKey ) ) + { + // determine ascending or descending + if ( getSortOrdering( true ).equals( ordering ) ) + { + return index; + } + else + if ( getSortOrdering( false ).equals( ordering ) ) + { + return -index; + } + } + } + return 0; + + } + + /** + * Called by TableAssociation to draw some indicator in the + * specified rectangle using the specified graphics to indicate + * the specified sort state. The rectangle corresponds to the + * bounds of the column header. + * This implementation draws a small transparent gray triangle at + * the right edge of the bounding rectangle. + * Override to do something different or to do nothing at all. + */ + protected void drawSortIndicator( Rectangle aBoundingRectangle, + Graphics aGraphicsContext, List orderings ) + { + int index = getIndexOfMatchingOrdering( orderings ); + if ( index == 0 ) return; + + boolean isAscending = ( index > 0 ); + index = Math.abs( index ); + + // turn on anti-aliasing + if ( aGraphicsContext instanceof Graphics2D ) + { + ((Graphics2D)aGraphicsContext).setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON ); + } + + Rectangle r = new Rectangle( aBoundingRectangle ); + + // resize to a right-justified square, sides equal to height + r.setBounds( r.x + r.width - r.height, r.y, r.height, r.height ); + + // resize to about a third smaller + int portion = r.height / 3; + r.grow( -portion, -portion ); + + // transparencies cause java2d printing to rasterize, + // resulting in excessive memory usage and print time. + // aGraphicsContext.setColor( new Color( 0, 0, 0, 255 / (index*2) ) ); + aGraphicsContext.setColor( getSortIndicatorColor( index ) ); + + Polygon triangle; + if ( !isAscending ) + { + triangle = new Polygon( + new int[] { r.x, r.x+r.width/2, r.x+r.width }, + new int[] { r.y, r.y+r.height, r.y }, 3 ); + } + else + { + triangle = new Polygon( + new int[] { r.x, r.x+r.width/2, r.x+r.width }, + new int[] { r.y+r.height, r.y, r.y+r.height }, 3 ); + } + aGraphicsContext.fillPolygon( triangle ); + } + + /** + * Returns a color to be used by the sort indicator based on the index + * of the sorting column. The goal of this method is to make the color + * appear lighter and lighter, the "less" primary the sort order for this + * column is. This can be acheives simply though a "transparent" color, + * however, during printing of the corresponding table, java print + * kicks into "raster" based printing when printing a component with + * a transparent color instead of "vector" based printing. Raster + * based printing can take up to 20-30 times longer to print than + * vector printing and consume several times the amount of memory. + * Raster-based printing should be avoided at all costs if the a component + * is to be printed (as of Java 1.3.1). + * @param index The "sort" index of the associated table column. The higher + * the index, the lighter the color will be. An index of 0 will + * return null. + * @return The color to use when rendering the sort indicator. + */ + protected static Color getSortIndicatorColor( int index ) + { + if ( index == 0 ) return null; + + // Create the color list if not already created. + if ( sortIndicatorColorList == null ) + { + // Default size to 13 elements, it would be extremely rare that a + // user sorts more than 12 columns at a time (although possible). + // (Index 0 is not used.) + sortIndicatorColorList = new Color[ 13 ]; + } + + // Get the color out of the color list. Use the index directly as + // an index into an ordered list. If the color has already been + // created for that index, then return it, otherwise create the color. + if ( ( index < sortIndicatorColorList.length ) && + ( sortIndicatorColorList[ index ] != null ) ) + { + return sortIndicatorColorList[ index ]; + } + + // The following logic performs the same affect as the above + // transparent color, without actually using a transparent color. + // Start with the table header's background color and derive a color + // that is "darker" than that color. Any color this logic creates will + // be between those two colors. + Color lightColor = java.awt.SystemColor.control; + Color darkColor = lightColor.darker().darker(); + + // Make the light color (the upper bound) a little darker, so that even + // the lightest triangle will still be slightly visible. + lightColor = new Color( + Math.max( ( int )( lightColor.getRed() * 0.9), 0 ), + Math.max( ( int )( lightColor.getGreen() * 0.9), 0 ), + Math.max( ( int )( lightColor.getBlue() * 0.9), 0) ); + + // Subtract the light color from the dark color. This is the range + // between the two colors. + Color difference = new Color( lightColor.getRed() - darkColor.getRed(), + lightColor.getGreen() - darkColor.getGreen(), + lightColor.getBlue() - darkColor.getBlue() ); + + // If the index is 1, user the dark color as is. Otherwise scale the + // color closer and closer to the lighter color as the index gets + // biggger and bigger. + if ( index > 1 ) + { + float factor = ( float )Math.pow( 0.5, ( index - 1 ) ); + darkColor = new Color( + Math.max( lightColor.getRed() - ( int )( difference.getRed() * factor ), 0 ), + Math.max( lightColor.getGreen() - ( int )( difference.getGreen() * factor ), 0 ), + Math.max( lightColor.getBlue() - ( int )( difference.getBlue() * factor ), 0 ) ); + } + + // Cache the created color in the color list for this index. + if ( index >= sortIndicatorColorList.length ) + { + // The color list is too small, create a new larger list with + // some padding for even larger indicies. + Color[] oldList = sortIndicatorColorList; + sortIndicatorColorList = new Color[ index + 5 ]; + System.arraycopy( oldList, 0, sortIndicatorColorList, 0, oldList.length ); + } + sortIndicatorColorList[ index ] = darkColor; + + return darkColor; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.16 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.15 2002/08/22 15:42:49 mpowers + * No longer using transparency to render sort indicator (see comments). + * + * Revision 1.14 2002/04/12 21:05:57 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.13 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.12 2002/03/04 22:11:43 mpowers + * Darkened the sort indicator to better differentiate the first sort. + * + * Revision 1.11 2002/03/04 03:58:17 mpowers + * Refined table header click behavior. + * + * Revision 1.10 2002/03/01 15:42:00 mpowers + * Table column headers now always show their sort indicator. + * A third table-column click clears the sort for that column. + * + * Revision 1.9 2002/02/28 23:01:39 mpowers + * TableColumnAssociations add and remove themselves from the TableAssociation + * when their connection is established and broken respectively. + * TableAssociations now break connection if they have no column associations. + * + * Revision 1.8 2001/06/05 16:03:56 mpowers + * Flipped the triangle to be consistent with Aqua. + * + * Revision 1.7 2001/03/09 22:09:22 mpowers + * Now better handling jdk1.1 for rendering the column header. + * + * Revision 1.6 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.5 2001/01/12 19:11:56 mpowers + * Fixed table column click sorting. + * + * Revision 1.4 2001/01/12 17:20:30 mpowers + * Moved EOSortOrdering creation to ColumnAssociation. + * + * Revision 1.3 2001/01/11 21:55:57 mpowers + * Implemented sort indicator for table column headers. + * + * Revision 1.2 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.1.1.1 2000/12/21 15:49:03 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java new file mode 100644 index 0000000..6aa27c3 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TextAssociation.java @@ -0,0 +1,1212 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.text.Format; +import java.text.ParseException; +import java.util.Enumeration; +import java.util.Iterator; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.AbstractDocument; +import javax.swing.text.DefaultStyledDocument; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSNotificationQueue; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TextAssociation binds JTextComponents and other objects +* with getText() and setText() methods to a display group. +* Note that JLabels are supported with both the Text and +* Icon aspects. +* Bindings are: +*
    +*
  • value: a property convertable to/from a string
  • +*
  • editable: a boolean property that determines whether +* the user can edit the text in the field
  • +*
  • enabled: a boolean property that determines whether +* the user can select the text in the field
  • +*
  • visible: a boolean property that determines whether +* the field is visible
  • +*
  • label: a boolean property that determines whether +* field should appear as a read-only, selectable label
  • +*
  • icon: a property that returns a Swing icon, for use +* with JLabels and other components with setIcon() methods. +* If bound to a static string, the string will be used to +* load an image resource from the selected object's class.
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TextAssociation extends EOAssociation + implements FocusListener, ActionListener, DocumentListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, EditableAspect, VisibleAspect, LabelAspect, IconAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text", "enabled", "editable", "visible" + } ); + + private final static NSSelector getText = + new NSSelector( "getText" ); + private final static NSSelector setText = + new NSSelector( "setText", + new Class[] { String.class } ); + private final static NSSelector getDocument = + new NSSelector( "getDocument" ); + private final static NSSelector setIcon = + new NSSelector( "setIcon", + new Class[] { Icon.class } ); + private final static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector addFocusListener = + new NSSelector( "addFocusListener", + new Class[] { FocusListener.class } ); + private final static NSSelector removeFocusListener = + new NSSelector( "removeFocusListener", + new Class[] { FocusListener.class } ); + + // null handling + protected boolean wasNull; + protected static final String EMPTY_STRING = ""; + + // dirty handling + protected boolean needsUpdate; + protected boolean hasDocument; + protected boolean isListening; + + // formatting + protected Format format; + + // on-the-fly validation + protected boolean activeUpdate; + + // type conversion + protected Class lastKnownType; + + // cache the value aspect + private EODisplayGroup valueDisplayGroup; + private String valueKey; + + // hacky flags needed for no activeUpdate + private boolean pleaseIgnoreNextChange = false; + private boolean pleaseAcceptNextChange = false; + private boolean externallyChanged = true; + + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public TextAssociation ( Object anObject ) + { + super( anObject ); + wasNull = false; + needsUpdate = false; + activeUpdate = true; + hasDocument = false; + isListening = true; + valueDisplayGroup = null; + valueKey = null; + format = null; + lastKnownType = null; + + // register for idle notifications + NSSelector handleNotification = + new NSSelector( "handleNotification", + new Class[] { NSNotification.class } ); + NSNotificationCenter.defaultCenter().addObserver( + this, handleNotification, null, this ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return setText.implementedByObject( anObject ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener to the specified object. + */ + public void establishConnection () + { + Object component = object(); + try + { + if ( addActionListener.implementedByObject( component ) ) + { + addActionListener.invoke( component, this ); + } + if ( addFocusListener.implementedByObject( component ) ) + { + addFocusListener.invoke( component, this ); + } + hasDocument = false; + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).addDocumentListener( this ); + hasDocument = true; + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while establishing connection", exc ); + } + + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + Object component = object(); + try + { + if ( removeActionListener.implementedByObject( component ) ) + { + removeActionListener.invoke( component, this ); + } + if ( removeFocusListener.implementedByObject( component ) ) + { + removeFocusListener.invoke( component, this ); + } + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).removeDocumentListener( this ); + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while breaking connection", exc ); + } + super.breakConnection(); + } + + public void objectWillChange( Object anObject ) + { + super.objectWillChange( anObject ); + externallyChanged = true; + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged() + { + if ( pleaseIgnoreNextChange ) + { + pleaseIgnoreNextChange = false; + externallyChanged = false; + return; + } + + externallyChanged = true; + + Object component = object(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) ); + } + + // if activeUpdate or we are not the editing association + if ( activeUpdate || displayGroup.editingAssociation() != this || pleaseAcceptNextChange ) + { + pleaseAcceptNextChange = false; + key = valueKey; + + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = previousValue; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && previousValue != null + && !currentValue.toString().equals( previousValue.toString() ) ) { + value = null; + break; + } + + } // end while + + } else { + + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + // null handling + if ( value == null ) + { + wasNull = true; + value = EMPTY_STRING; + lastKnownType = null; + } + else + { + wasNull = false; + lastKnownType = value.getClass(); + if ( format() != null ) + { + try + { + value = format().format( value ); + } + catch ( IllegalArgumentException exc ) + { + value = value.toString(); + } + } + } + + + try + { + if ( needToReadValueFromDisplayGroup( value.toString(), getText ) ) + { + // No need to listen for any events that might get fired + // while setting the text since we are the one setting it. + boolean wasListening = isListening; + isListening = false; + + // setText is an expensive operation + setText.invoke( component, value.toString() ); + + isListening = wasListening; + needsUpdate = false; + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + } + + // icon aspect + displayGroup = displayGroupForAspect( IconAspect ); + key = displayGroupKeyForAspect( IconAspect ); + if ( key != null ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group + // as a resource to be loaded from the selected class. + value = null; + Object o = displayGroup.selectedObject(); + if ( o != null ) + { + URL url = o.getClass().getResource( key ); + if ( url != null ) + { + value = new ImageIcon( url ); + } + } + } + + try + { + setIcon.invoke( component, value ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( key != null ) + && ( component instanceof Component ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( ((Component)component).isEnabled() != converted.booleanValue() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + // editable aspect + displayGroup = displayGroupForAspect( EditableAspect ); + key = displayGroupKeyForAspect( EditableAspect ); + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() != ((JTextComponent)component).isEditable() ) + { + ((JTextComponent)component).setEditable( converted.booleanValue() ); + } + } + } + + // visible aspect + displayGroup = displayGroupForAspect( VisibleAspect ); + key = displayGroupKeyForAspect( VisibleAspect ); + if ( ( key != null ) + && ( component instanceof Component ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() != ((Component)component).isVisible() ) + { + ((Component)component).setVisible( converted.booleanValue() ); + } + } + } + + // label aspect + displayGroup = displayGroupForAspect( LabelAspect ); + key = displayGroupKeyForAspect( LabelAspect ); + + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() ) + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + areaToLabel( (JTextArea) component ); + } + else + { + fieldToLabel( (JTextComponent) component ); + } + } + } + else + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + labelToArea( (JTextArea) component ); + } + else + { + labelToField( (JTextComponent ) component ); + } + } + } + } + } + } + + private void fieldToLabel( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(false); + aTextField.setOpaque(false); + + // Set the border, colors and font to that of a label + + //LookAndFeel.installBorder(aTextField, "Label.border"); + aTextField.setBorder( null ); + + LookAndFeel.installColorsAndFont(aTextField, + "Label.background", + "Label.foreground", + "Label.font"); + } + + private void labelToField( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(true); + aTextField.setOpaque(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "TextField.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "TextField.background", + "TextField.foreground", + "TextField.font"); + } + + private void areaToLabel( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setLineWrap(true); + aTextArea.setWrapStyleWord(true); + aTextArea.setEditable(false); + + // Set the text area's border, colors and font to + // that of a label + + //LookAndFeel.installBorder(aTextArea, "Label.border"); + aTextArea.setBorder( null ); + + LookAndFeel.installColorsAndFont(aTextArea, + "Label.background", + "Label.foreground", + "Label.font"); + + } + + private void labelToArea( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setEditable(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextArea, "TextArea.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "TextArea.background", + "TextArea.foreground", + "TextArea.font"); + } + + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing() + { + pleaseAcceptNextChange = true; + pleaseIgnoreNextChange = false; + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + boolean returnValue = true; + if ( hasDocument && !needsUpdate ) return true; + + EODisplayGroup displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + String key = valueKey; + Object component = object(); + Object value = null; + try + { + //if ( getText.implementedByObject( component ) ) + //{ + value = getText.invoke( component ); + //} + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) ) + { + value = null; + } + else + if ( format() != null ) + { + try + { + value = format().parseObject( value.toString() ); + } + catch ( ParseException exc ) + { + String message = exc.getMessage(); + //"That format was not recognized."; + if ( displayGroup.associationFailedToValidateValue( + this, value.toString(), key, exc, message ) ) + { + boolean wasListening = isListening; + isListening = false; + JOptionPane.showMessageDialog( + (Component)component, message ); + isListening = wasListening; + } + needsUpdate = false; + return false; + } + } + + if ( ( lastKnownType != null ) && ( value != null ) ) + { + // convert back to last known type, if necessary/possible + Class type = value.getClass(); + if ( ( type != null ) && ( type != lastKnownType ) ) + { + Object converted = + ValueConverter.convertObjectToClass( + value, lastKnownType ); + if ( converted != null ) + { + value = converted; + } + // else: not possible, ignore + } + } + + needsUpdate = false; + + // only update if the value is different from the one in the display group + if ( ! needToWriteValueToDisplayGroup( value, displayGroup ) ) return true; + + // we might lose focus if display group displays a validation message + boolean wasListening = isListening; + isListening = false; + + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + needsUpdate = false; + } + else + { + needsUpdate = false; + returnValue = false; + } + } + isListening = wasListening; + + } + return returnValue; + } + + /** + * Called to determine whether the display group needs to be + * updated. This implementation reads the value from the display + * group and only returns true if the specified value is different. + * This is done as an optimization since writes are more expensive + * than reads. Override to customize this behavior. + */ + protected boolean needToWriteValueToDisplayGroup( + Object aValue, EODisplayGroup aDisplayGroup ) + { + Object existingValue = aDisplayGroup.selectedObjectValueForKey( valueKey ); + if ( aDisplayGroup.selectedObjects().size() == 1 ) + { + if ( existingValue == aValue ) return false; + if ( ( existingValue != null ) && ( existingValue.equals( aValue ) ) ) return false; + if ( ( aValue != null ) && ( aValue.equals( existingValue ) ) ) return false; + } + return true; + } + + /** + * Called to determine whether the controlled component needs to be + * updated. This implementation reads the value from the selector + * and only returns true if the specified value is different. + * This is done as an optimization since updating the component + * can be an expensive operation. Override to customize this behavior. + */ + protected boolean needToReadValueFromDisplayGroup( + Object aValue, NSSelector aSelector ) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException + { + return !aValue.toString().equals( aSelector.invoke( object() ) ); + } + + /** + * Sets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public void setFormat( Format aFormat ) + { + format = aFormat; + } + + /** + * Gets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public Format format() + { + return format; + } + + /** + * Returns whether the text association is configured to actively + * update the model in response to changes in the component. + */ + public boolean isActiveUpdate() + { + return activeUpdate; + } + + /** + * Sets whether the text association should actively + * update the model in response to changes in the component. + * Default is true. False indicates that the model will be updated + * only when the component loses focus or fires an action event. + */ + public void setActiveUpdate( boolean isActiveUpdate ) + { + activeUpdate = isActiveUpdate; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + if ( ! isListening ) return; + if ( needsUpdate ) + { + pleaseAcceptNextChange = true; // needed if activeUpdate = false + writeValueToDisplayGroup(); + } + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + if ( ! isListening ) return; + + pleaseAcceptNextChange = true; + externallyChanged = true; + + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( ! isListening ) return; + if ( endEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + else + { + // probably should notify of a validation error here, + } + } + + /** + * Queues a notification to PostWhenIdle. + */ + protected void queueUpdate(DocumentEvent e) + { + if ( e.getDocument() instanceof DefaultStyledDocument ) + { + if ( e instanceof AbstractDocument.DefaultDocumentEvent ) + { + int docLength = e.getDocument().getLength(); + + if ( ( e.getType().equals( DocumentEvent.EventType.CHANGE ) ) ) + { + if ( e.getOffset() == 0 && e.getLength() == docLength ) + { + // ignore document events for the whole document + // since default styled document broadcasts these + // using invokeLater, and we've already received + // notification about the actual style change. + // see: DefaultStyledDocument.ChangeUpdateRunnable + return; + } + } + } + } + + NSNotificationQueue.defaultQueue().enqueueNotification( + new NSNotification( "TextAssociation.DocumentChanged", this, + new NSDictionary( new Object[] { "event" }, new Object[] { e } ) ), + NSNotificationQueue.PostWhenIdle ); + } + + /** + * Handles idle notification. + */ + public void handleNotification( NSNotification aNotification ) + { + if ( activeUpdate ) + { + writeValueToDisplayGroup(); + } + } + + // interface DocumentListener + + public void insertUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + + public void removeUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + + public void changedUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate( e ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.41 2004/02/05 02:18:18 mpowers + * Now setting border to null to new Aqua LAF behaves. + * + * Revision 1.40 2004/01/28 22:47:56 mpowers + * un-activeUpdate was brokne. + * + * Revision 1.39 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.38 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.37 2003/02/06 16:21:34 mpowers + * Fix for activeUpdate: no longer bothering with editing context's changes. + * + * Revision 1.36 2002/10/24 18:19:24 mpowers + * Bug fix - thanks to dwang. + * + * Revision 1.35 2002/08/02 19:19:30 mpowers + * Added control points for when to read or write from the display group. + * Added flags needed to fix problems with non-activeUpdate and commit key. + * + * Revision 1.33 2002/03/08 23:18:01 mpowers + * Added visible aspect. + * + * Revision 1.32 2002/03/06 16:13:53 mpowers + * Yet another fix for style document changes: swing's DefaultStyledDocument + * using an invoke later to launch a final StyleChanged event, which occurs + * after the TextAssociation has reestablished itself as a document listener, + * causing the item to be marked dirty. We're now handling this case. + * + * Revision 1.31 2002/03/04 22:10:37 mpowers + * Supressing active update only marks dirty when contents have changed. + * + * Revision 1.30 2002/02/23 16:19:12 mpowers + * Now only marking an editing context as dirty if it's not already dirty. + * + * Revision 1.29 2002/02/19 18:38:29 mpowers + * Minor optimization: activeUpdate now checked in handleNotification. + * + * Revision 1.28 2002/02/19 16:36:47 mpowers + * Better support for active update: objects are now marked as changed + * even though the model itself is not updated -- this allows editing + * context itself to be marked as having changes to be saved. + * + * Revision 1.27 2002/01/23 19:50:11 mpowers + * Fix for a null pointer when value is null and last known type is not. + * (from dwang) + * + * Revision 1.26 2002/01/14 19:37:22 mpowers + * Fix for NPE when value is null and auto update is false. + * + * Revision 1.25 2001/12/10 03:16:11 mpowers + * Fixed bug with isListening when no items are in display group. + * + * Revision 1.24 2001/11/16 19:14:51 mpowers + * Brought back the idea of configuring whether updates occur on each change. + * + * Revision 1.23 2001/11/08 20:06:06 mpowers + * Now performing type-conversion as a convenience. + * + * Revision 1.22 2001/11/04 18:24:20 mpowers + * Better handling for non-string values when bulk-editing. + * + * Revision 1.21 2001/11/01 15:53:34 mpowers + * Now that NSNotificationQueue correctly implements PostWhenIdle, we can + * finally discard our use of Swing's Timer in favor of using the queue + * to coalesce document changed events. + * + * Revision 1.20 2001/10/26 19:58:06 mpowers + * Better handling for non-string types. We were testing with equals with the + * new value against the existing value in the component. Now we convert + * the new value to a string before comparing. Fixes case for properties + * of non-String types, like StringBuffer. + * + * Revision 1.19 2001/09/30 21:57:14 mpowers + * Timers were not getting cleaned up if breakConnection was called + * before the timer got a chance to fire. + * + * Revision 1.18 2001/08/22 15:42:26 mpowers + * Added support for JTextComponent label-izing. + * + * Revision 1.17 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.16 2001/07/17 19:53:37 mpowers + * Made some private fields protected for benefit of subclassers. + * + * Revision 1.15 2001/06/30 14:59:36 mpowers + * LabelAspect now sets the text field's opaque setting. + * + * Revision 1.14 2001/06/29 14:54:08 mpowers + * Another fix for timers - timers were definitely causing a memory leak. + * + * Revision 1.13 2001/06/26 21:37:19 mpowers + * Fixed a null pointer in the new key timer scheme. + * + * Revision 1.12 2001/06/25 14:46:03 mpowers + * Fixed a memory leak involving the use of timers. + * + * Revision 1.11 2001/06/01 19:14:59 mpowers + * Text association's enabled aspect is now more discriminating. + * + * Revision 1.10 2001/05/18 21:07:24 mpowers + * Changed the way we handle failure to update object value. + * + * Revision 1.9 2001/03/13 21:39:58 mpowers + * Improved validation handling. + * + * Revision 1.8 2001/03/12 12:49:10 mpowers + * Improved validation handling. + * Having a formatter disables auto-updating. + * + * Revision 1.7 2001/03/09 22:08:13 mpowers + * Now handling any objects that have a valid Document. + * No longer checking enabled before updating the enabled state. + * + * Revision 1.6 2001/03/07 19:57:32 mpowers + * Fixed paste error in IconAspect. + * + * Revision 1.4 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.3 2001/01/31 19:12:33 mpowers + * Implemented auto-updating in TextComponent. + * + * Revision 1.2 2001/01/10 15:53:58 mpowers + * Preventing a null pointer exception if getText were to return null, + * which doesn't happen for JTextFields but might happen for other objects. + * + * Revision 1.1.1.1 2000/12/21 15:49:08 mpowers + * Contributing wotonomy. + * + * Revision 1.13 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java new file mode 100644 index 0000000..49879e9 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TimedTextAssociation.java @@ -0,0 +1,1029 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.net.URL; +import java.text.Format; +import java.text.ParseException; +import java.util.Enumeration; +import java.util.Iterator; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JOptionPane; +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TimedTextAssociation works like TextAssociation, +* but instead of using a delayed event to update the +* model, it uses a timer so that the model is only +* updated if the user pauses typing for some short interval. +* This is useful when the update and/or re-read of the model +* is a costly operation. +* Bindings are: +*
    +*
  • value: a property convertable to/from a string
  • +*
  • editable: a boolean property that determines whether +* the user can edit the text in the field
  • +*
  • enabled: a boolean property that determines whether +* the user can select the text in the field
  • +*
  • label: a boolean property that determines whether +* field should appear as a read-only, selectable label
  • +*
  • icon: a property that returns a Swing icon, for use +* with JLabels and other components with setIcon() methods. +* If bound to a static string, the string will be used to +* load an image resource from the selected object's class.
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TimedTextAssociation extends EOAssociation + implements FocusListener, ActionListener, DocumentListener +{ + //TODO: need to refactor this so that it can subclass text association. + //This implementation is basically a branch from the v1.20 TextAssociation. + + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EnabledAspect, EditableAspect, LabelAspect, IconAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature, + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "text", "enabled", "editable" + } ); + + private final static NSSelector getText = + new NSSelector( "getText" ); + private final static NSSelector setText = + new NSSelector( "setText", + new Class[] { String.class } ); + private final static NSSelector getDocument = + new NSSelector( "getDocument" ); + private final static NSSelector setIcon = + new NSSelector( "setIcon", + new Class[] { Icon.class } ); + private final static NSSelector addActionListener = + new NSSelector( "addActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector removeActionListener = + new NSSelector( "removeActionListener", + new Class[] { ActionListener.class } ); + private final static NSSelector addFocusListener = + new NSSelector( "addFocusListener", + new Class[] { FocusListener.class } ); + private final static NSSelector removeFocusListener = + new NSSelector( "removeFocusListener", + new Class[] { FocusListener.class } ); + + // null handling + protected boolean wasNull; + protected static final String EMPTY_STRING = ""; + + // dirty handling + protected boolean needsUpdate; + protected boolean hasDocument; + protected boolean isListening; + + // formatting + protected Format format; + + // cache the value aspect + private EODisplayGroup valueDisplayGroup; + private String valueKey; + + // coalescing document events + protected boolean autoUpdating; + protected int interval = 400; // adjust as needed + protected Timer keyTimer; + + // NOTE: a new key timer is created for each use and + // is disposed when the timer is stopped. + // Swing's Timer class is kept in a static list of timers + // and each retains a strong reference to their listeners. + // This caused a memory leak as associations typically + // refer to their controlled component which is referred + // to by its parents and so on until no application window + // will ever get garbage collected. yikes. + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public TimedTextAssociation ( Object anObject ) + { + super( anObject ); + wasNull = false; + needsUpdate = false; + hasDocument = false; + isListening = true; + valueDisplayGroup = null; + valueKey = null; + + autoUpdating = true; + keyTimer = null; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return setText.implementedByObject( anObject ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ValueAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ValueAspect.equals( anAspect ) ) + { + valueDisplayGroup = aDisplayGroup; + valueKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * attempts to add this class as an ActionListener + * and as a FocusListener to the specified object. + */ + public void establishConnection () + { + Object component = object(); + try + { + if ( addActionListener.implementedByObject( component ) ) + { + addActionListener.invoke( component, this ); + } + if ( addFocusListener.implementedByObject( component ) ) + { + addFocusListener.invoke( component, this ); + } + hasDocument = false; + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).addDocumentListener( this ); + hasDocument = true; + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while establishing connection", exc ); + } + + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + Object component = object(); + try + { + if ( removeActionListener.implementedByObject( component ) ) + { + removeActionListener.invoke( component, this ); + } + if ( removeFocusListener.implementedByObject( component ) ) + { + removeFocusListener.invoke( component, this ); + } + if ( getDocument.implementedByObject( component ) ) + { + Object document = getDocument.invoke( component ); + if ( document instanceof Document ) + { + ((Document)document).removeDocumentListener( this ); + } + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while breaking connection", exc ); + } + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + Object component = object(); + EODisplayGroup displayGroup; + String key; + Object value; + + // value aspect + displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + if ( component instanceof Component ) + { + ((Component)component).setEnabled( + displayGroup.enabledToSetSelectedObjectValueForKey( valueKey ) ); + } + + key = valueKey; + + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } else { + + // if there's only one object selected. + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking the size of selected objects in displayGroup + + // convert value to string + if ( value == null ) + { + wasNull = true; + value = EMPTY_STRING; + } + else + { + wasNull = false; + if ( format() != null ) + { + try + { + value = format().format( value ); + } + catch ( IllegalArgumentException exc ) + { + value = value.toString(); + } + } + } + + + try + { + if ( ! value.toString().equals( getText.invoke( component ) ) ) + { + // No need to listen for any events that might get fired + // while setting the text since we are the one setting it. + boolean wasListening = isListening; + isListening = false; + + // setText is an expensive operation + setText.invoke( component, value.toString() ); + + isListening = wasListening; + needsUpdate = false; + } + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // icon aspect + displayGroup = displayGroupForAspect( IconAspect ); + key = displayGroupKeyForAspect( IconAspect ); + if ( key != null ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group + // as a resource to be loaded from the selected class. + value = null; + Object o = displayGroup.selectedObject(); + if ( o != null ) + { + URL url = o.getClass().getResource( key ); + if ( url != null ) + { + value = new ImageIcon( url ); + } + } + } + + try + { + setIcon.invoke( component, value ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error while updating component connection", exc ); + } + } + + // enabled aspect + displayGroup = displayGroupForAspect( EnabledAspect ); + key = displayGroupKeyForAspect( EnabledAspect ); + if ( ( key != null ) + && ( component instanceof Component ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = null; + if ( value != null ) + { + converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + } + if ( converted == null ) converted = Boolean.FALSE; + if ( ((Component)component).isEnabled() != converted.booleanValue() ) + { + ((Component)component).setEnabled( converted.booleanValue() ); + } + } + + // editable aspect + displayGroup = displayGroupForAspect( EditableAspect ); + key = displayGroupKeyForAspect( EditableAspect ); + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() != ((JTextComponent)component).isEditable() ) + { + ((JTextComponent)component).setEditable( converted.booleanValue() ); + } + } + } + + // label aspect + displayGroup = displayGroupForAspect( LabelAspect ); + key = displayGroupKeyForAspect( LabelAspect ); + + if ( ( key != null ) + && ( component instanceof JTextComponent ) ) + { + if ( displayGroup != null ) + { + value = + displayGroup.selectedObjectValueForKey( key ); + } + else + { + // treat bound key without display group as a value + value = key; + } + Boolean converted = (Boolean) + ValueConverter.convertObjectToClass( + value, Boolean.class ); + + if ( converted != null ) + { + if ( converted.booleanValue() ) + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + areaToLabel( (JTextArea) component ); + } + else + { + fieldToLabel( (JTextComponent) component ); + } + } + } + else + { + if ( component instanceof JTextComponent ) + { + if ( component instanceof JTextArea ) + { + labelToArea( (JTextArea) component ); + } + else + { + labelToField( (JTextComponent ) component ); + } + } + } + } + } + + } + + private void fieldToLabel( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(false); + aTextField.setOpaque(false); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "Label.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "Label.background", + "Label.foreground", + "Label.font"); + } + + private void labelToField( JTextComponent aTextField ) + { + // turn on wrapping and disable editing and highlighting + + aTextField.setEditable(true); + aTextField.setOpaque(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextField, "TextField.border"); + + LookAndFeel.installColorsAndFont(aTextField, + "TextField.background", + "TextField.foreground", + "TextField.font"); + } + + private void areaToLabel( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setLineWrap(true); + aTextArea.setWrapStyleWord(true); + aTextArea.setEditable(false); + + // Set the text area's border, colors and font to + // that of a label + + LookAndFeel.installBorder(aTextArea, "Label.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "Label.background", + "Label.foreground", + "Label.font"); + + } + + private void labelToArea( JTextArea aTextArea ) + { + // turn on wrapping and disable editing and highlighting + + aTextArea.setEditable(true); + + // Set the border, colors and font to that of a label + + LookAndFeel.installBorder(aTextArea, "TextArea.border"); + + LookAndFeel.installColorsAndFont(aTextArea, + "TextArea.background", + "TextArea.foreground", + "TextArea.font"); + } + + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + if ( keyTimer != null ) + { + keyTimer.stop(); + keyTimer.removeActionListener( this ); + keyTimer = null; + } + return writeValueToDisplayGroup(); + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueToDisplayGroup() + { + boolean returnValue = true; + if ( hasDocument && !needsUpdate ) return true; + + EODisplayGroup displayGroup = valueDisplayGroup; + if ( displayGroup != null ) + { + String key = valueKey; + Object component = object(); + Object value = null; + try + { + //if ( getText.implementedByObject( component ) ) + //{ + value = getText.invoke( component ); + //} + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Error updating display group", exc ); + } + + if ( ( wasNull ) && ( EMPTY_STRING.equals( value ) ) ) + { + value = null; + } + else + if ( format() != null ) + { + try + { + value = format().parseObject( value.toString() ); + } + catch ( ParseException exc ) + { + String message = exc.getMessage(); + //"That format was not recognized."; + if ( displayGroup.associationFailedToValidateValue( + this, value.toString(), key, exc, message ) ) + { + boolean wasListening = isListening; + isListening = false; + JOptionPane.showMessageDialog( + (Component)component, message ); + isListening = wasListening; + } + needsUpdate = false; + return false; + } + } + + needsUpdate = false; + + // only update if the value is different from the one in the display group + Object existingValue = displayGroup.selectedObjectValueForKey( key ); + if ( displayGroup.selectedObjects().size() == 1 ) + { + if ( existingValue == value ) return true; + if ( ( existingValue != null ) && ( existingValue.equals( value ) ) ) return true; + if ( ( value != null ) && ( value.equals( existingValue ) ) ) return true; + } + + // we might lose focus if display group displays a validation message + boolean wasListening = isListening; + isListening = false; + + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + isListening = wasListening; + needsUpdate = false; + } + else + { + isListening = wasListening; + needsUpdate = false; + returnValue = false; + } + } + + } + return returnValue; + } + + /** + * Sets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + * Having a formatter disables auto-updating. + */ + public void setFormat( Format aFormat ) + { + format = aFormat; + } + + /** + * Gets the Format that is used to convert values from the display + * group to and from text that is displayed in the component. + */ + public Format format() + { + return format; + } + + // interface ActionListener + + /** + * Updates object on action performed. + */ + public void actionPerformed( ActionEvent evt ) + { + if ( keyTimer != null ) + { + keyTimer.stop(); + keyTimer.removeActionListener( this ); + keyTimer = null; + } + if ( ! isListening ) return; + if ( needsUpdate ) + { + writeValueToDisplayGroup(); + } + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + if ( ! isListening ) return; + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( ! isListening ) return; + if ( endEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + else + { + // probably should notify of a validation error here, + } + } + + /** + * Returns whether the data model is updated for every change + * in the controlled component. If false, the data is only + * updated on focus lost or the enter key. Default is true. + */ + public boolean isAutoUpdating() + { + if ( format() != null ) return false; + return autoUpdating; + } + + /** + * Sets whether the data model is updated for every change + * in the controlled component. + */ + public void setAutoUpdating( boolean isAutoUpdating ) + { + autoUpdating = isAutoUpdating; + } + + /** + * Triggers the key timer to start. + */ + protected void queueUpdate() + { + if ( isAutoUpdating() ) + { + if ( keyTimer == null ) + { + keyTimer = new Timer( interval, this ); + } + keyTimer.restart(); + } + } + + // interface DocumentListener + + public void insertUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + + public void removeUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + + public void changedUpdate(DocumentEvent e) + { + if ( ! isListening ) return; + needsUpdate = true; + queueUpdate(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2004/01/28 18:34:57 mpowers + * Better handling for enabling. + * Now respecting enabledToSetSelectedObjectValueForKey from display group. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/12/20 18:57:24 mpowers + * (Re-)Contributing TimedTextAssociation. Just like TA, except uses timers. + * + * Revision 1.20 2001/10/26 19:58:06 mpowers + * Better handling for non-string types. We were testing with equals with the + * new value against the existing value in the component. Now we convert + * the new value to a string before comparing. Fixes case for properties + * of non-String types, like StringBuffer. + * + * Revision 1.19 2001/09/30 21:57:14 mpowers + * Timers were not getting cleaned up if breakConnection was called + * before the timer got a chance to fire. + * + * Revision 1.18 2001/08/22 15:42:26 mpowers + * Added support for JTextComponent label-izing. + * + * Revision 1.17 2001/07/30 16:32:55 mpowers + * Implemented support for bulk-editing. Detail associations will now + * apply changes to all selected objects. + * + * Revision 1.16 2001/07/17 19:53:37 mpowers + * Made some private fields protected for benefit of subclassers. + * + * Revision 1.15 2001/06/30 14:59:36 mpowers + * LabelAspect now sets the text field's opaque setting. + * + * Revision 1.14 2001/06/29 14:54:08 mpowers + * Another fix for timers - timers were definitely causing a memory leak. + * + * Revision 1.13 2001/06/26 21:37:19 mpowers + * Fixed a null pointer in the new key timer scheme. + * + * Revision 1.12 2001/06/25 14:46:03 mpowers + * Fixed a memory leak involving the use of timers. + * + * Revision 1.11 2001/06/01 19:14:59 mpowers + * Text association's enabled aspect is now more discriminating. + * + * Revision 1.10 2001/05/18 21:07:24 mpowers + * Changed the way we handle failure to update object value. + * + * Revision 1.9 2001/03/13 21:39:58 mpowers + * Improved validation handling. + * + * Revision 1.8 2001/03/12 12:49:10 mpowers + * Improved validation handling. + * Having a formatter disables auto-updating. + * + * Revision 1.7 2001/03/09 22:08:13 mpowers + * Now handling any objects that have a valid Document. + * No longer checking enabled before updating the enabled state. + * + * Revision 1.6 2001/03/07 19:57:32 mpowers + * Fixed paste error in IconAspect. + * + * Revision 1.4 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.3 2001/01/31 19:12:33 mpowers + * Implemented auto-updating in TextComponent. + * + * Revision 1.2 2001/01/10 15:53:58 mpowers + * Preventing a null pointer exception if getText were to return null, + * which doesn't happen for JTextFields but might happen for other objects. + * + * Revision 1.1.1.1 2000/12/21 15:49:08 mpowers + * Contributing wotonomy. + * + * Revision 1.13 2000/12/20 16:25:41 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java new file mode 100644 index 0000000..728643b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeAssociation.java @@ -0,0 +1,582 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.awt.EventQueue; +import java.awt.Rectangle; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Vector; + +import javax.swing.JTree; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreePath; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TreeAssociation is a TreeModelAssociation further +* customized for JTrees. It binds a JTree to a display group's +* list of displayable objects, each of which may have +* a list of child objects managed by another display +* group, and so on. TreeAssociation works exactly +* like a ListAssociation, with the additional capability +* to specify a "Children" aspect, that will allow child +* objects to be retrieved from a parent display group. +* Note that the children aspect requires the bound +* display group to have a DataSource that can vend a +* DataSource appropriate for the bound key That data +* source is then used to create data sources for +* child nodes, and so on. +* +*
    +* +*
  • titles: a property convertable to a string for +* display in the nodes of the tree. The objects in +* the bound display group will be used to populate the +* initial root nodes of the tree (more accurately, +* children of the offscreen root node in the tree).
  • +* +*
  • children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* The data source of the bound display group is replaced +* a data source that populates the display group with +* the visible nodes in the tree component as determined by +* calling fetchObjectsIntoChildrenGroup. +* If this aspect is not bound, the tree behaves like a list. +*

    +* Binding this aspect with a null display group is the same +* as binding it with the titles display group. +* In this configuration the contents of the titles +* display group will be replaced with the visible nodes in the +* tree component, as specified above, replacing the existing +* data source. +*

    +* In that case, the display groups for the nodes in +* the tree will still use the original data source +* for resolving their children key, and programmatically +* setting the contents of the display group will still +* repopulate the root nodes of the tree. +*
  • +* +*
  • isLeaf: a property of a node value that returns +* a value convertable to a boolean value (aside from +* an actual boolean value, zeroes evaluate to true, +* as does any String containing "yes" or "true" or that +* is convertable to a number equal to zero; other values +* evaluate to false). +*

    +* If the isLeaf aspect is not bound, +* the tree must force nodes to load their children to +* determine whether they are leaf nodes (in effect +* loading the grandchildren for any expanded node). +* If bound, child loading is deferred until the node +* is actually expanded. +*

    +* For example, binding this value to a null +* display group and the key "false" will result in a +* deferred-loading tree that works much like Windows +* Explorer's network volume browser - all nodes appear +* with "pluses" until they are expanded. +*

    +* Note that the display group is ignored: the property +* will be applied directly to the object corresponding +* to the node.
  • +* +*
+* +* All other usage is as TreeModelAssociation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeAssociation extends TreeModelAssociation + implements FocusListener, TreeExpansionListener, TreeWillExpandListener, Runnable +{ + private boolean isExpanding; + private Vector nodeQueue; + private Vector structureQueue; + private boolean isRunning; + + /** + * Constructor expecting a JTree. + */ + public TreeAssociation ( Object anObject ) + { + super( anObject ); + init(); + } + + /** + * Constructor expecting a JTree or similar component + * and specifying a label for the root node. + */ + public TreeAssociation( Object anObject, Object aRootLabel ) + { + super( anObject ); + init(); + rootLabel = aRootLabel; + rootNode.setUserObject( aRootLabel ); + } + + // convenience + private JTree component() + { + return (JTree) object(); + } + + /** + * Called by both constructors. + */ + protected void init() + { + isExpanding = false; + isRunning = false; + nodeQueue = new Vector(); + structureQueue = new Vector(); + component().addFocusListener( this ); + component().addTreeExpansionListener( this ); + component().addTreeWillExpandListener( this ); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof JTree ); + } + + /** + * Overridden to not fire events during initial population. + */ + public void establishConnection () + { + isExpanding = true; + super.establishConnection(); + isExpanding = false; + } + + // interface TreeSelectionListener + + public void valueChanged(TreeSelectionEvent e) + { + if ( ! isListening ) return; +// NOTE: This approach causes focus rectangle to perceptibly trail +// the selection rectangle, presumably because we're called after the +// new selection has been processed but before the focus is processed. +// Users don't like it, so we're going with a preference to use +// invokeLater. +/* + // paint immediately before updating the display group and + // before any potentially lengthy second-order effects happen: + // this improves user-perceived responsiveness of big apps + if ( object() instanceof javax.swing.JComponent ) + { + javax.swing.JComponent component = (javax.swing.JComponent)object(); + component.paintImmediately( component.getBounds() ); + } + selectFromSelectionModel(); +*/ + +// NOTE: This approach uses invoke later to cause the update of +// the display group (which could be lengthly if that in turn +// causes other things to update) to happen after the tree repaints. +// Users like this because it "feels faster", but developers have +// to remember that if they listen to tree selection events they +// will have to do a similar invoke later if they check the display +// group. + + Runnable select = new Runnable() + { + public void run() + { + selectFromSelectionModel(); + } + }; + + if ( selectionPaintedImmediately ) + { + if ( object() instanceof java.awt.Component ) + { + ((java.awt.Component)object()).repaint(); + } + EventQueue.invokeLater( select ); + } + else + { + select.run(); + } + } + + /** + * Overridden to check whether the node is visible + * in the tree on screen. Offscreen in a scrollpane + * does not count. + */ + public boolean isVisible(Object node) + { + JTree tree = (JTree) object(); + TreePath path = ((DisplayGroupNode)node).treePath(); + if ( tree.isVisible( path ) ) + { + Rectangle rowRect = tree.getPathBounds( path ); + if ( rowRect != null ) + { + Rectangle visible = tree.getVisibleRect(); + if ( visible != null ) + { +//System.out.println( "isVisible: intersects: " + visible.intersects( rowRect ) ); + return visible.intersects( rowRect ); + } + } + } +//System.out.println( "isVisible: false" ); + return false; + } + + /** + * Fires a tree nodes changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesChanged(final Object source, + final Object[] path, + final int[] childIndices, + final Object[] children) + { + if ( !isExpanding ) + { + for ( int i = 0; i < children.length; i++ ) + { + nodeQueue.add( children[i] ); + } + if ( !isRunning ) + { + isRunning = true; + EventQueue.invokeLater( this ); + } + } + } + + /** + * Fires a tree nodes inserted event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesInserted(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + super.fireTreeNodesInserted( source, path, childIndices, children ); + EventQueue.invokeLater( this ); + } + } + + /** + * Fires a tree nodes removed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesRemoved(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + super.fireTreeNodesRemoved( source, path, childIndices, children ); + EventQueue.invokeLater( this ); + } + } + + /** + * Fires a tree structure changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeStructureChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + if ( !isExpanding ) + { + structureQueue.add( path[path.length-1] ); + if ( !isRunning ) + { + isRunning = true; + EventQueue.invokeLater( this ); + } + } + } + + /** + * Overridden to return all visible rows in the tree. + */ + public NSArray objectsFetchedIntoChildrenGroup() + { + JTree tree = (JTree) object(); + NSMutableArray objectList = new NSMutableArray(); + + int count = tree.getRowCount(); + for ( int i = 0; i < count; i++ ) + { + objectList.add( + ((DisplayGroupNode) tree.getPathForRow( i ).getLastPathComponent()).object() ); + } + +//new net.wotonomy.ui.swing.util.StackTraceInspector( Integer.toString( objectList.size() ) ); + return objectList; + } + + // interface FocusListener + + /** + * Notifies of beginning of edit. + */ + public void focusGained(FocusEvent evt) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidBeginEditing( this ); + } + } + } + + /** + * Updates object on focus lost and notifies of end of edit. + */ + public void focusLost(FocusEvent evt) + { + if ( ! component().isEditing() ) + { + Object o; + EODisplayGroup displayGroup; + Enumeration e = aspects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + displayGroup = + displayGroupForAspect( e.nextElement().toString() ); + if ( displayGroup != null ) + { + displayGroup.associationDidEndEditing( this ); + } + } + } + } + + // interface TreeWillExpandListener + + public void treeWillExpand(TreeExpansionEvent event) + throws ExpandVetoException + { + isExpanding = true; + } + + public void treeWillCollapse(TreeExpansionEvent event) + throws ExpandVetoException + { + // do nothing + } + + // interface TreeExpansionListener + + /** + * Updates the children display group, if any. + */ + public void treeExpanded(TreeExpansionEvent event) + { //System.out.println( "treeExpanded: " + event.getPath().getLastPathComponent() ); + isExpanding = false; + if ( childrenDisplayGroup != null ) + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + } + } + + /** + * Updates the children display group, if any. + */ + public void treeCollapsed(TreeExpansionEvent event) + { + if ( childrenDisplayGroup != null ) + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + } + } + + // interface Runnable + + /** + * Fires any queued node changed and structure changed events. + * Typically invoked on a delayed event loop. + */ + public void run() + { + DisplayGroupNode node; + int index; + Iterator i; + + i = nodeQueue.iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + index = ((DisplayGroupNode)node.parentGroup).getIndex( node ); + if ( ( index != -1 ) + && ( node.treePath().getParentPath() != null ) ) + { + super.fireTreeNodesChanged( + node, + node.treePath().getParentPath().getPath(), + new int[] { index }, + new Object[] { node } ); + } + } + nodeQueue.clear(); + + i = structureQueue.iterator(); + while ( i.hasNext() ) + { + node = (DisplayGroupNode) i.next(); + super.fireTreeStructureChanged( + node, + node.treePath().getPath(), + null, + null ); + } + structureQueue.clear(); + + isRunning = false; +/* + EventQueue.invokeLater( new Runnable() { public void run() { + ((JTree)object()).treeDidChange(); + ((JTree)object()).getParent().invalidate(); + ((JTree)object()).getParent().validate(); + ((JTree)object()).getParent().update( ((JTree)object()).getGraphics() ); + +// ((JTree)object()).getParent().doLayout(); +// ((JTree)object()).getParent().repaint(); +// ((JTree)object()).repaint(); +// ((JTree)object()).updateUI(); + } } ); +*/ + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.55 2004/02/05 02:18:50 mpowers + * Super was calling back into this class before init() was called. + * + * Revision 1.54 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.53 2003/06/03 14:49:48 mpowers + * Now correctly calculating isVisible based on the component visible rect. + * Now deferring node changed events to a later event queue to allow repaints + * to happen after all changes have taken effect. + * + * Revision 1.52 2002/05/03 21:41:18 mpowers + * No longer clearing the selection model when updating from display group: + * we now only modify if a change needs to be made. + * No longer listening for selection change during firing of delete events: + * delete events cause JTree's to update their selection model. + * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges() + * must happen after the screen is painted, or the selection is not displayed. + * + * Revision 1.51 2002/04/19 21:18:45 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.49 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.48 2002/04/12 20:36:31 mpowers + * Now distinguishing between changes made on titles group by tree expansion + * versus external changes which should cause us to repopulate root nodes. + * + * Revision 1.47 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.46 2002/03/27 20:44:53 mpowers + * Added isVisible test for node. + * + * Revision 1.45 2002/03/08 23:19:57 mpowers + * Refactoring of DelegatingTreeDataSource to facilitate binding of titles + * and children aspects to the same display group. + * + * Revision 1.44 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.43 2002/03/06 13:04:15 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.42 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.41 2002/03/01 23:42:08 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * Revision 1.40 2002/03/01 20:41:39 mpowers + * Now a focus listener and an expansion listener. + * + * Revision 1.39 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java new file mode 100644 index 0000000..f6c90d0 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeColumnAssociation.java @@ -0,0 +1,331 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import javax.swing.JTree; +import javax.swing.table.TableColumn; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.ui.EODisplayGroup; +import net.wotonomy.ui.swing.components.TreeTableCellRenderer; + +/** +* TreeColumnAssociation is a TableColumnAssocation +* that works like a TreeAssociation, allowing any +* table to display hierarchical data in a tabular format. +* This class is mainly a convenience for connecting a +* TreeAssociation to a JTree to a TreeTableCellRenderer +* to a TableColumn.

+* +* Like TableColumnAssociation, you must call setTable() +* to specify the JTable to be used. (The corresponding +* table association will direct all column header sorting +* to the root node of the tree association.) +* +* You may also optionally call setTree() to specify a +* customized JTree to be used. If not specified, a +* slightly customized JTree will be used (see createTree() +* and configureColumn()). +* +* TreeColumnAssociation supports the following bindings, +* just as TableColumnAssociation does: +*
    +*
  • value: a property convertable to a string for +* display in the cells of the table column. This +* binding is equivalent to the titles binding of +* TreeAssociation.
  • +* +*
  • editable: a property convertable to a boolean +* that determines the editability of the corresponding +* cells in the column.
  • +*
+* +* TreeColumnAssociation additionally supports the following +* bindings, just as TreeAssociation does, except that the +* value binding is used instead of the titles binding. +*
    +*
  • children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* If this aspect is not bound, the tree behaves like a list.
  • +* +*
  • isLeaf: a property of a node value that returns +* a value convertable to a boolean value. +* If the isLeaf aspect is not bound, the tree will force +* nodes to load their children to determine whether they +* are leaf nodes (in effect loading the grandchildren for +* any expanded node). +*
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeColumnAssociation extends TableColumnAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ValueAspect, EditableAspect, ChildrenAspect, IsLeafAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "table" + } ); + + EODisplayGroup childrenDisplayGroup, leafDisplayGroup; + String childrenKey, leafKey; + + TreeModelAssociation treeAssociation; + JTree tree; + + /** + * Constructor specifying the object to be controlled by this + * association. Throws an exception if the object is not + * a TableColumn. + */ + public TreeColumnAssociation ( Object anObject ) + { + super( anObject ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( ChildrenAspect.equals( anAspect ) ) + { + childrenDisplayGroup = aDisplayGroup; + childrenKey = aKey; + } + if ( IsLeafAspect.equals( anAspect ) ) + { + leafDisplayGroup = aDisplayGroup; + leafKey = aKey; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Overridden to call createTree if necessary, + * call configureColumn, call createTreeAssociation + * if necessary, and then call to super. + */ + public void establishConnection () + { + if ( tree == null ) + { + tree = createTree(); + } + + configureColumn( (TableColumn) object() ); + + if ( treeAssociation == null ) + { + treeAssociation = createTreeAssociation( tree ); + } + + treeAssociation.bindAspect( TitlesAspect, valueDisplayGroup, valueKey ); + if ( childrenKey != null ) + { + treeAssociation.bindAspect( ChildrenAspect, childrenDisplayGroup, childrenKey ); + } + if ( leafKey != null ) + { + treeAssociation.bindAspect( IsLeafAspect, leafDisplayGroup, leafKey ); + } + + // ensure table association's source is tree asssociation's child group + getTableAssociation().bindAspect( + SourceAspect, treeAssociation.childrenDisplayGroup, "" ); + + treeAssociation.establishConnection(); + + table.setRowHeight( tree.getRowHeight() ); + + super.establishConnection(); + + // cause sort ordering to apply to root node of tree + if ( childrenKey != null ) + { + getTableAssociation().sortTarget = + (EODisplayGroup) treeAssociation.getRoot(); + } + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + super.breakConnection(); + treeAssociation.breakConnection(); + + // restore original source display group + getTableAssociation().sortTarget = null; + } + + /** + * Called by establishConnection if setTree was not called. + * This implementation returns a stock JTree. Override + * to provide your own customized JTree. Note that + * TreeTableCellRenderer will further customize this tree + * when configureColumn is called. + */ + protected JTree createTree() + { + return new JTree(); + } + + /** + * Called by establishConnection to create a tree association + * only if no tree association has already been created. + * This implementation returns a stock TreeAssociation. + * Override to return your own customized TreeAssociation. + */ + protected TreeModelAssociation createTreeAssociation( JTree aTree ) + { + return new TreeAssociation( aTree ); + } + + /** + * Called by establishConnection to configure the column + * with a TreeTableCellRenderer using the current JTree. + * Override to further customize the column, or customize + * your column yourself after the call to establishConnection. + */ + protected void configureColumn( TableColumn aColumn ) + { + aColumn.setCellRenderer( new TreeTableCellRenderer( tree ) ); + } + + /** + * Gets the JTree currently used for the column renderer. + * If not specified, returns null. + */ + public JTree getTree() + { + return tree; + } + + /** + * Gets the TreeModelAssociation currently used for the tree. + * If not tree is not specified, returns null. + */ + public TreeModelAssociation getTreeModelAssociation() + { + if ( tree == null ) return null; + if (!( tree.getModel() instanceof TreeModelAssociation )) return null; + return (TreeModelAssociation) tree.getModel(); + } + + /** + * Sets the JTree to be used for the column renderer. + * If not specified, createTree() will be called to create a JTree. + */ + public void setTree( JTree aTree ) + { + tree = aTree; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.9 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.8 2002/05/03 21:31:50 mpowers + * Actually works better without selectionPaintedImmediately. + * + * Revision 1.7 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.6 2002/03/08 23:18:48 mpowers + * Added accessor for tree association. + * + * Revision 1.5 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.4 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.3 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.2 2002/03/04 22:48:22 mpowers + * Now working with table association to sort the root node. + * + * Revision 1.1 2002/03/01 23:42:09 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java new file mode 100644 index 0000000..86bfa69 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/TreeModelAssociation.java @@ -0,0 +1,1751 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing; + +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Vector; + +import javax.swing.SwingUtilities; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserverProxy; +import net.wotonomy.control.OrderedDataSource; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.ui.EOAssociation; +import net.wotonomy.ui.EODisplayGroup; + +/** +* TreeModelAssociation binds a JTree or similar component +* that uses a TreeModel to a display group's +* list of displayable objects, each of which may have +* a list of child objects managed by another display +* group, and so on. TreeModelAssociation works exactly +* like a ListAssociation, with the additional capability +* to specify a "Children" aspect, that will allow child +* objects to be retrieved from a parent display group. +* +*
    +* +*
  • titles: a property convertable to a string for +* display in the nodes of the tree. The objects in +* the bound display group will be used to populate the +* initial root nodes of the tree (more accurately, +* children of the offscreen root node in the tree).
  • +* +*
  • children: a property of a node value that returns +* zero, one or many objects, each of which will correspond +* to a child node for the corresponding node in the tree. +* The data source of the bound display group is replaced +* a data source that populates the display group with +* the selection in the tree component as determined by +* calling fetchObjectsIntoChildrenGroup. +* If this aspect is not bound, the tree behaves like a list. +*

    +* Binding this aspect with a null display group is the same +* as binding it with the titles display group. +* In this configuration the contents of the titles +* display group will be replaced with the selection in the +* tree component, as specified above, replacing the existing +* data source. +*

    +* In that case, the display groups for the nodes in +* the tree will still use the original data source +* for resolving their children key, and programmatically +* setting the contents of the display group will still +* repopulate the root nodes of the tree. +*
  • +* +*
  • isLeaf: a property of a node value that returns +* a value convertable to a boolean value (aside from +* an actual boolean value, zeroes evaluate to true, +* as does any String containing "yes" or "true" or that +* is convertable to a number equal to zero; other values +* evaluate to false). +*

    +* If the isLeaf aspect is not bound, +* the tree must force nodes to load their children to +* determine whether they are leaf nodes (in effect +* loading the grandchildren for any expanded node). +* If bound, child loading is deferred until the node +* is actually expanded. +*

    +* For example, binding this value to a null +* display group and the key "false" will result in a +* deferred-loading tree that works much like Windows +* Explorer's network volume browser - all nodes appear +* with "pluses" until they are expanded. +*

    +* Note that the display group is ignored: the property +* will be applied directly to the object corresponding +* to the node.
  • +* +*
+* +* This class acts as the TreeModel for the controlled +* component: calling yourcomponent.getModel() will +* return this association. The tree model methods on +* this class are public and may be used to affect changes +* on the controlled components.

+* +* The titles display group's contents are inserted +* into a new display group that acts as the root node. +* After that point, changes in the titles display group +* will cause the tree model to reset itself, creating +* a new display group for the root node. +*

+* +* If a separate display group is bound to the children +* aspect, it will +* be used to hold the selected objects and their siblings +* and selection will be maintained there, and the titles +* display group selection will not be updated. +* Any editing or detail associations should in that case +* be attached to the children display group, not the titles +* group.

+* +* Each node in the tree is an EODisplayGroup that +* contains the child objects of the object it represents +* in the tree. These objects can be programmatically +* inserted, updated, or removed using DisplayGroup +* methods. Each node's takes its parent group's +* sortOrderings until a sort ordering is explicitly +* specified - setting a sort ordering to null will resume +* using the parent group's sort ordering.

+* +* Each node in the tree also implements MutableTreeNode. +* The value that a node represents is the titles property +* value of the object in the parent's displayed objects +* list at the index corresponding to the index of the node. +* Calling toString on a node returns the string representation +* of the titles property value, and setUserObject will update +* that value directly in the corresponding object. +* Moving a node from one parent to another will remove the +* actual object in the parent display group and insert it +* into the destination display group.

+* +* In short, any nodes obtained from this class' +* implementation of TreeModel may be cast as either +* EODisplayGroup or MutableTreeNode and maybe be +* programmatically manipulated in either manner. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeModelAssociation extends EOAssociation + implements TreeModel, TreeSelectionListener +{ + static final NSArray aspects = + new NSArray( new Object[] { + TitlesAspect, ChildrenAspect, IsLeafAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "model" + } ); + + private final static NSSelector getSelectionModel = + new NSSelector( "getSelectionModel" ); + private final static NSSelector setModel = + new NSSelector( "setModel", + new Class[] { TreeModel.class } ); + + EODisplayGroup titlesDisplayGroup, childrenDisplayGroup, leafDisplayGroup; + String titlesKey, childrenKey, leafKey; + DisplayGroupNode rootNode; + Vector listeners; + Object rootLabel; + + TreeSelectionModel selectionModel; + boolean selectionPaintedImmediately; + + boolean insertingChild; + boolean insertingAfter; + + EOObserverProxy recentChangesObserver; + + private boolean pleaseSelectRootNode; + + /** + * Constructor expecting a JTree or any other object + * that has void setModel(TreeModel) and TreeModel getSelectionModel() + * methods. This tree association will be used for the TreeModel. + * The root node will be labeled "Root".

+ * + * As an alternate way to use a TreeModelAssociation, you may pass a + * TreeSelectionModel to the constructor and then manually set your + * component to use this class as its TreeModel. + */ + public TreeModelAssociation ( Object anObject ) + { + super( anObject ); + + titlesDisplayGroup = null; + titlesKey = null; + childrenDisplayGroup = null; + childrenKey = null; + leafDisplayGroup = null; + leafKey = null; + listeners = new Vector(); + + selectionPaintedImmediately = false; + + // after display group nodes process recent changes + recentChangesObserver = new EOObserverProxy( + this, new NSSelector( "processRecentChanges" ), + EODelayedObserver.ObserverPrioritySixth ); + EOObserverCenter.addObserver( recentChangesObserver, this ); + + insertingChild = true; + insertingAfter = true; + + pleaseSelectRootNode = false; + + rootLabel = "Root"; + rootNode = createNode( null, null ); + } + + /** + * Constructor expecting a JTree or similar component + * and specifying a label for the root node. + */ + public TreeModelAssociation( Object anObject, Object aRootLabel ) + { + this( anObject ); + rootLabel = aRootLabel; + rootNode.setUserObject( aRootLabel ); + } + + /** + * Gets the current root label. + */ + public Object rootLabel() + { + return rootLabel; + } + + /** + * Gets the current root label. + */ + public Object getRootLabel() + { + return rootLabel(); + } + + /** + * Sets the root label. + */ + public void setRootLabel( Object aLabel ) + { + rootLabel = aLabel; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return setModel.implementedByObject( anObject ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return TitlesAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + if ( TitlesAspect.equals( anAspect ) ) + { + titlesDisplayGroup = aDisplayGroup; + titlesKey = aKey; + } + if ( ChildrenAspect.equals( anAspect ) ) + { + childrenDisplayGroup = aDisplayGroup; + childrenKey = aKey; + } + if ( IsLeafAspect.equals( anAspect ) ) + { + leafDisplayGroup = aDisplayGroup; + leafKey = aKey; + } + if ( childrenDisplayGroup == null ) + { + childrenDisplayGroup = titlesDisplayGroup; + } + super.bindAspect( anAspect, aDisplayGroup, aKey ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + if ( titlesDisplayGroup == null ) + { + throw new WotonomyException( + "TreeModelAssociation: Titles aspect must be bound" ); + } + + // populate the root node + rootNode = createNode( titlesDisplayGroup, null ); + rootNode.setObjectArray( titlesDisplayGroup.displayedObjects() ); + rootNode.setSortOrderings( titlesDisplayGroup.sortOrderings() ); + + EODataSource dataSource = childrenDisplayGroup.dataSource(); + if ( dataSource == null ) dataSource = titlesDisplayGroup.dataSource(); + while ( dataSource instanceof DelegatingTreeDataSource ) + { // unwrap any existing delegating data sources + dataSource = ((DelegatingTreeDataSource)dataSource).delegateDataSource; + } + // create a new delegating data source + childrenDisplayGroup.setDataSource( + new DelegatingTreeDataSource( this, dataSource ) ); + + //TODO: find out why omitting this line causes weird selection behavior + childrenDisplayGroup.setSortOrderings( new NSArray() ); + + // check for alternate usage + if ( object() instanceof TreeSelectionModel ) + { + selectionModel = (TreeSelectionModel) object(); + } + else // use specified object + { + try + { + setModel.invoke( object(), new Object[] { this } ); + selectionModel = (TreeSelectionModel) + getSelectionModel.invoke( object(), new Object[] {} ); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + addAsListener(); + super.establishConnection(); +/* + fireRootStructureChanged(); + +// titlesGroupChanged = true; +// subjectChanged(); + + // update the children group + removeAsListener(); + childrenDisplayGroup.fetch(); + addAsListener(); + + // update selection + selectFromDisplayGroup( titlesDisplayGroup ); +*/ + } + + protected void fireRootStructureChanged() + { + int count = rootNode.displayedObjects().count(); + int[] childIndices = new int[ count ]; + Object[] children = new Object[ count ]; + for ( int i = 0; i < count; i++ ) + { + childIndices[i] = i; + children[i] = rootNode.getChildNodeAt( i ); + } + + // must fire a tree structure changed with children, + // otherwise the tree gets weird selection behavior + fireTreeStructureChanged( this, new Object[] { rootNode }, + childIndices, children ); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + if ( childrenDisplayGroup != null ) + { + if ( childrenDisplayGroup.dataSource() instanceof DelegatingTreeDataSource ) + { + if ( titlesDisplayGroup == childrenDisplayGroup ) + { + titlesDisplayGroup.setDataSource( ((DelegatingTreeDataSource) + childrenDisplayGroup.dataSource()).delegateDataSource ); + } + else + { + childrenDisplayGroup.setDataSource( null ); + } + } + } + + removeAsListener(); + super.breakConnection(); + } + + protected void addAsListener() + { + isListening = true; + selectionModel.addTreeSelectionListener( this ); + } + + protected void removeAsListener() + { + isListening = false; + selectionModel.removeTreeSelectionListener( this ); + } + + protected boolean isListening = false; + private boolean pleaseIgnore = false; + protected boolean titlesGroupChanged = false; + protected boolean childrenGroupChanged = false; + + /** + * Overridden to better discriminate what is changed. + */ + public void objectWillChange( Object anObject ) + { + if ( ! isListening ) return; + + if ( anObject == titlesDisplayGroup ) + { + titlesGroupChanged = true; + } + if ( anObject == childrenDisplayGroup ) + { + childrenGroupChanged = true; + if ( childrenDisplayGroup.qualifier() != null ) + { + if ( ( rootNode.qualifier() == null ) || + ! childrenDisplayGroup.qualifier().equals( rootNode.qualifier() ) ) + { + // quietly move qualifier from children group to root node + rootNode.setQualifier( childrenDisplayGroup.qualifier() ); + childrenDisplayGroup.setQualifier( null ); + rootNode.updateDisplayedObjects(); + } + } + } + super.objectWillChange( anObject ); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + // titles aspect + if ( titlesGroupChanged ) + { + if ( titlesDisplayGroup.contentsChanged() ) + { + NSArray displayedObjects = titlesDisplayGroup.displayedObjects(); + NSArray childrenObjects; + if ( titlesDisplayGroup != childrenDisplayGroup + || displayedObjects.count() + != (childrenObjects = objectsFetchedIntoChildrenGroup()).count() + || ! displayedObjects.containsAll( childrenObjects ) ) + { + populateFromDisplayGroup( displayedObjects ); + } + } + } + + if ( childrenDisplayGroup.selectionChanged() && !childrenDisplayGroup.contentsChanged() ) + { + selectFromDisplayGroup( childrenDisplayGroup ); + } + + titlesGroupChanged = false; + childrenGroupChanged = false; + } + + /** + * Called by subjectChanged() in response to an external change in the titles display group. + */ + void populateFromDisplayGroup( List displayedObjects ) + { + // trigger processRecentChanges + willChange(); + + // workaround: see below + int previousCount = rootNode.previouslyDisplayedObjects.length; + + // update the root node + rootNode.setObjectArray( displayedObjects ); + + //FIXME: workaround for what appears to be a bug in JTree: + // if root node is not visible and has no children, insert events are ignored + if ( previousCount == 0 ) + { + fireRootStructureChanged(); + } + } + + /** + * Package access so DisplayGroupNode can replace the selection after an update. + */ + void selectFromDisplayGroup( EODisplayGroup aDisplayGroup ) + { // System.out.println( "selectFromDisplayGroup: " + aDisplayGroup.selectedObjects() ); + + removeAsListener(); + + TreePath[] paths = selectionModel.getSelectionPaths(); + NSArray selectedObjects = aDisplayGroup.selectedObjects(); + + // assemble current selection list + List treeSelection = new LinkedList(); + if ( paths != null ) + { + for ( int i = 0; i < paths.length; i ++ ) + { + treeSelection.add( + ((DisplayGroupNode)paths[i].getLastPathComponent()).getUserObject() ); + } + } + + if ( ! ( selectedObjects.size() == treeSelection.size() + && treeSelection.containsAll( selectedObjects ) ) ) + { + selectionModel.clearSelection(); + + // workaround to select root node from valueChanged() + if ( pleaseSelectRootNode ) + { + selectionModel.addSelectionPath( new TreePath( this.getRoot() ) ); + pleaseSelectRootNode = false; + } + + //FIXME: display group is assumed to have only one instance of each object + for ( int i = 0; i < selectedObjects.count(); i++ ) + { + //FIXME: selects only the first instance for now + //selectionModel.addSelectionPaths( + // getPathsForObject( + // selectedObjects.objectAtIndex( i ) ) ); + selectionModel.addSelectionPath( + getPathForObject( + selectedObjects.objectAtIndex( i ) ) ); + } + } + + addAsListener(); + } + + /** + * Returns the first node found that represents the + * specified object, or null if not found. + * This implementation simply calls getPathForObject. + */ + public Object getNodeForObject( Object anObject ) + { + TreePath result = getPathForObject( anObject ); + if ( result != null ) + { + return result.getLastPathComponent(); + } + return null; + } + + /** + * Returns the object represented by the specified node + * which must be a display group node from this tree. + */ + public Object getObjectForNode( Object aNode ) + { + if ( aNode instanceof DisplayGroupNode ) + { + return ((DisplayGroupNode)aNode).getUserObject(); + } + + // not a display group node + throw new WotonomyException( + "Not a display group node: " + aNode ); + } + + /** + * Returns the tree path for the specified node, + * which must be a display group node from this tree. + */ + public TreePath getPathForNode( Object aNode ) + { + if ( aNode instanceof DisplayGroupNode ) + { + return ((DisplayGroupNode)aNode).treePath(); + } + + // not a display group node + throw new WotonomyException( + "Not a display group node: " + aNode ); + } + + /** + * Returns the first tree path for the node that represents + * the specified object, or null if the object does not exist in this tree. + * This implementation does a breadth-first search of the tree + * for the object, looking only at nodes that have been loaded. + * This means that if the object does not exist in the tree, + * the entire tree must be traversed. + */ + public TreePath getPathForObject( Object anObject ) + { + return getPathForObjectInPath( anObject, new TreePath( this.getRoot() ) ); + } + + /** + * Returns the tree path for the node that represents + * the specified object, + * or null if the object does not exist in this tree. + * This implementation does a breadth-first search of the tree + * for the object, looking only at nodes that have been loaded. + * This means that the entire tree is traversed. + */ + public TreePath[] getPathsForObject( Object anObject ) + { + return getPathsForObjectInPath( anObject, new TreePath( this.getRoot() ) ); + } + + /** + * A breadth-first search of the tree starting + * at the specified tree path, comparing by reference. + * Returns immediately with the first match. + */ + private TreePath getPathForObjectInPath( Object anObject, TreePath aPath ) + { + LinkedList queue = new LinkedList(); + + // add the specified path + queue.addLast( aPath ); + + return processQueue( anObject, queue, null ); + } + + /** + * A breadth-first search of the tree starting + * at the specified tree path, comparing by reference. + * The entire branch is searched before returning + * an array of all matches. + */ + private TreePath[] getPathsForObjectInPath( Object anObject, TreePath aPath ) + { + LinkedList queue = new LinkedList(); + + // add the specified path + queue.addLast( aPath ); + + List result = new LinkedList(); + processQueue( anObject, queue, result ); + TreePath[] paths = new TreePath[ result.size() ]; + for ( int i = 0; i < paths.length; i++ ) + { + paths[i] = (TreePath) result.get(i); + } + return paths; + } + + /** + * Processes the specified queue, appending results to aResult if it exists, + * or returning immediately with a TreePath is aResult is null. + */ + private TreePath processQueue( Object anObject, LinkedList aQueue, List aResult ) + { + TreePath path; + while ( ! aQueue.isEmpty() ) + { + path = (TreePath) aQueue.removeFirst(); + path = checkNode( anObject, path, aQueue ); + if ( path != null ) + { + if ( aResult != null ) + { + aResult.add( path ); + } + else + { + return path; + } + } + } + return null; + } + + /** + * Compares the specified object by reference each of the children of + * the node at the end of the specified tree path, adding nodes that + * do not match but have fetched object to the end of the specified queue. + * Returns the path of the first child node that matches the specified object, + * or null if no match was found. + */ + private TreePath checkNode( Object anObject, TreePath aPath, LinkedList aQueue ) + { + TreePath result = null; + Object child; + Object parent = aPath.getLastPathComponent(); + int count = getChildCount( parent ); + + for ( int i = 0; i < count; i++ ) + { + child = getChild( parent, i ); + + // add to queue if node has fetched children + if ( ((DisplayGroupNode)child).isFetched ) + { + aQueue.addLast( aPath.pathByAddingChild( child ) ); + } + + // compares by reference + if ( ((DisplayGroupNode)child).object() == anObject ) + { + // assumes same object cannot be in display group twice + result = aPath.pathByAddingChild( child ); +//System.out.println( "TRUE: " + ((DisplayGroupNode)child).object() + " == " + anObject ); + } +// else +// { +//System.out.println( ((DisplayGroupNode)child).object() + " != " + anObject ); +// } + } + return result; + } + + // interface TreeSelectionListener + + public void valueChanged(TreeSelectionEvent e) + { + if ( ! isListening ) return; + selectFromSelectionModel(); + } + + /** + * Determines whether the selection should be painted + * immediately after the user clicks and therefore + * before the children display group is updated. + * When the children group is bound to many associations + * or is bound to a master-detail association, updating + * the display group can take a perceptibly long time. + * This property defaults to false. + * @see #setSelectionPaintedImmediately + */ + public boolean isSelectionPaintedImmediately() + { + return selectionPaintedImmediately; + } + + /** + * Sets whether the selection should be painted immediately. + * Setting this property to true will let the tree paint + * first before the display group is updated. + * This means that any tree selection listers will + * also be notified before the display group is updated + * and will have to invokeLater if they want to see the + * updated display group. + */ + public void setSelectionPaintedImmediately( boolean isImmediate ) + { + selectionPaintedImmediately = isImmediate; + } + + /** + * Package access so DisplayGroupNode can replace the selection. + * Returns the display group containing the current selection: titles or children. + */ + EODisplayGroup selectFromSelectionModel() + { // System.out.print( "selectFromSelectionModel: " ); + removeAsListener(); + DisplayGroupNode node; + TreePath parentPath; + TreePath[] selectedPaths = selectionModel.getSelectionPaths(); + NSMutableArray selectionList = new NSMutableArray(); + if ( selectedPaths != null ) + { + for ( int i = 0; i < selectedPaths.length; i++ ) + { + // root node is zero - ignore root node + if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) ) + { + // select root in selectFromDisplayGroup() + pleaseSelectRootNode = true; + } + else + { + node = (DisplayGroupNode) + selectedPaths[i].getLastPathComponent(); + Object o = node.object(); + + if ( selectionList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + selectionList.addObject( o ); + } + } + } + } + childrenDisplayGroup.fetch(); //note that we're not currently listening for changes + if ( ! childrenDisplayGroup.selectObjectsIdenticalTo( selectionList ) ) + { + addAsListener(); // because we don't have a listener stack + selectFromDisplayGroup( childrenDisplayGroup ); + removeAsListener(); + } + addAsListener(); + return childrenDisplayGroup; // titles is now children if children not explicitly set + } + + // interface TreeModel + + public Object getRoot() + { + return rootNode; + } + + public Object getChild(Object parent, int index) + { + // interestingly, this gets called by + // BasicTreeUI.paintVerticalPartOfLeg for + // the last child of each expanded tree node. + Object result = ((DisplayGroupNode)parent).getChildNodeAt( index ); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getChildNodeAt( index ); + } + + public int getChildCount(Object parent) + { + int result = ((DisplayGroupNode)parent).getChildCount(); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getChildCount(); + } + + public boolean isLeaf(Object node) + { + boolean result = ((DisplayGroupNode)node).isLeaf(); +//((DisplayGroupNode)node).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)node).isLeaf(); + } + + /** + * Returns whether this node is visible in the UI. + * This implementation returns true. + *

+ * Subclasses should return false if they can + * determine that the node is not displayed or + * expanded or otherwise visible. Non-visible + * nodes will fetch only when they are shown. + */ + public boolean isVisible(Object node) + { + return true; + } + + public void valueForPathChanged(TreePath path, Object newValue) + { + ((DisplayGroupNode)path.getLastPathComponent()).setUserObject( newValue ); + } + + public int getIndexOfChild(Object parent, Object child) + { + int result = ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child ); +//((DisplayGroupNode)parent).suppressRecentChangeProcessing(); +return result; +// return ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child ); + } + + public void addTreeModelListener(TreeModelListener aListener) + { + listeners.add( aListener ); + } + public void removeTreeModelListener(TreeModelListener aListener) + { + listeners.remove( aListener ); + } + + /** + * Fires a tree nodes changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesChanged: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + ((TreeModelListener)it.nextElement()).treeNodesChanged( event ); + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesChanged: caught: " + exc ); + System.out.println( "Source:" + source ); + System.out.println( "Path:" ); + for ( int i = 0; i < path.length; i++ ) + { + System.out.print( path[i] + "-" ); + } + System.out.println(); + System.out.println( "Indices:" ); + for ( int i = 0; i < childIndices.length; i++ ) + { + System.out.print( childIndices[i] + "-" ); + } + System.out.println(); + System.out.println( "Children:" ); + for ( int i = 0; i < children.length; i++ ) + { + System.out.print( children[i] + "-" ); + } + System.out.println(); + exc.printStackTrace(); + } + } + } + + /** + * Fires a tree nodes inserted event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesInserted(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesInserted: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + ((TreeModelListener)it.nextElement()).treeNodesInserted( event ); + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesInserted: caught: " + exc ); + } + } + } + + /** + * Fires a tree nodes removed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeNodesRemoved(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireTreeNodesRemoved: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + try + { + //NOTE: removing nodes causes tree to fire selection change event + // which confuses us if we're rearranging nodes (when sorting, for example). + boolean wasListening = isListening; + if ( wasListening ) isListening = false; + ((TreeModelListener)it.nextElement()).treeNodesRemoved( event ); + if ( wasListening ) isListening = true; + } + catch ( Exception exc ) + { + System.out.println( "TreeModelAssociation.fireTreeNodesRemoved: caught: " + exc ); + } + } + } + + /** + * Fires a tree structure changed event to all listeners. + * Provided as a convenience if you need to make manual + * changes to the tree model. + */ + public void fireTreeStructureChanged(Object source, + Object[] path, + int[] childIndices, + Object[] children) + { + + willChange(); // queue processRecentChanges + TreeModelEvent event = new TreeModelEvent( + source, path, childIndices, children ); +//System.out.println( "fireStructureChanged: " + event ); + Enumeration it = listeners.elements(); + while ( it.hasMoreElements() ) + { + ((TreeModelListener)it.nextElement()).treeStructureChanged( event ); + } + } + + /** + * Creates and returns a new display group node. + */ + public DisplayGroupNode createNode( EODisplayGroup aParentGroup, Object anObject ) + { + return new MutableDisplayGroupNode( this, aParentGroup, anObject ); + } + + /** + * Gets whether new objects programmatically inserted into the children + * display group should be inserted as a child of the first selected node. + * If false, new objects are inserted as siblings of the first selected node. + * Default value is true. + */ + public boolean isInsertingChild() + { + return insertingChild; + } + + /** + * Sets whether new objects programmatically inserted into the children + * display group should be inserted as a child of the first selected node. + * If false, new objects are inserted as siblings of the first selected node. + * Default value is true. + */ + public void setInsertingChild( boolean asChild ) + { + insertingChild = asChild; + } + + /** + * Determines where new objects programmatically inserted into the children + * display group should be inserted, based on the value of insertingChild. + * If insertingChild, isInsertingAfter causes objects to be inserted at + * the end of the selected node's child list; otherwise, objects are inserted + * at the beginning of the list. + * If inserting as a sibling, isInsertingAfter causes objects to be inserted + * before the selected node in the selected node's parent's child list; + * otherwise, objects are inserted after the selected node in the child list. + * Default value is true. + */ + public boolean isInsertingAfter() + { + return insertingAfter; + } + + /** + * Determines where new objects programmatically inserted into the children + * display group should be inserted, based on the value of insertingChild. + * If insertingChild, isInsertingAfter causes objects to be inserted at + * the end of the selected node's child list; otherwise, objects are inserted + * at the beginning of the list. + * If inserting as a sibling, isInsertingAfter causes objects to be inserted + * before the selected node in the selected node's parent's child list; + * otherwise, objects are inserted after the selected node in the child list. + * Default value is true. + */ + public void setInsertingAfter( boolean after ) + { + insertingAfter = after; + } + + /** + * Called to by the children group's data source when it receives + * an insertObject message, usually after an object has been inserted + * into the children display group. + * Return the object that should be passed to the titles display + * group's data source's implementation of insertObject, or return + * null to prevent that method from being called.

+ * This implementation inserts the specified object into the tree + * as determined by calling isInsertingChild and isInsertingAfter, + * then returns the unmodified object. If there's no selection, or + * no selection model, the root node is assumed to be selected. + * And if the root node is selected, the new node will obviously be + * inserted as a child. Override to customize. + */ + protected Object objectInsertedIntoChildrenGroup( Object anObject ) + { + // determine selection + DisplayGroupNode selectedNode = (DisplayGroupNode) getRoot(); + if ( selectionModel != null ) + { + // get selected path + TreePath path = selectionModel.getSelectionPath(); + + // get selected node + if ( path != null ) + { + selectedNode = (DisplayGroupNode) path.getLastPathComponent(); + } + } + // determine location of insertion + int index = 0; + if ( ( isInsertingChild() ) || ( selectedNode == getRoot() ) ) + { + if ( isInsertingAfter() ) + { + index = selectedNode.getChildCount(); + } + } + else // inserting as sibling + { + DisplayGroupNode parentNode = selectedNode.getParentGroup(); + index = parentNode.getIndex( selectedNode ); + if ( isInsertingAfter() ) + { + index++; + } + selectedNode = parentNode; + } + + // insert and return + selectedNode.insertObjectAtIndex( anObject, index ); + return anObject; + } + + /** + * Called to by the children group's data source when it receives + * a deleteObject message, usually after an object has been deleted + * from the children display group. + * Return the object that should be passed to the titles display + * group's data source's implementation of deleteObject, or return + * null to prevent that method from being called.

+ * This implementation deletes all instances of the selected object + * from the tree nodes that are currently loaded, and returns the + * unmodified object. Override to customize. + */ + protected Object objectDeletedFromChildrenGroup( Object anObject ) + { + TreePath[] paths = getPathsForObject( anObject ); + if ( paths != null ) + { + for ( int i = 0; i < paths.length; i++ ) + { + ((DisplayGroupNode)paths[i].getLastPathComponent()).removeFromParent(); + } + } + return anObject; + } + + /** + * Called to by the children group's data source to populate it + * with all selected nodes and their siblings. To customize, + * override this method, or specify a different data source for + * the children display group. + */ + protected NSArray objectsFetchedIntoChildrenGroup() + { + DisplayGroupNode node; + TreePath parentPath; + TreePath[] selectedPaths = selectionModel.getSelectionPaths(); + NSMutableArray objectList = new NSMutableArray(); + if ( selectedPaths != null ) + { + for ( int i = 0; i < selectedPaths.length; i++ ) + { + // root node is zero - ignore root node + if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) ) + { + // select root in selectFromDisplayGroup() + pleaseSelectRootNode = true; + } + else + { + node = (DisplayGroupNode) + selectedPaths[i].getLastPathComponent(); + Object o = node.object(); + + // add all children of parent to object list - includes self + if ( node.parentGroup != null ) + { + Enumeration e = + node.parentGroup.displayedObjects().objectEnumerator(); + while ( e.hasMoreElements() ) + { + // add only if not already in list + o = e.nextElement(); + if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + objectList.addObject( o ); + } + } + } + else // no parent node - add the node by itself + { + // add only if not already in list + if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound ) + { + objectList.addObject( o ); + } + } + } + } + } + + // if no selection + if ( objectList.size() == 0 ) + { + // populate with children of root + objectList.addAll( rootNode.displayedObjects() ); + } + return objectList; + } + + /** + * Queues processRecentChanges to be run in the event queue. + */ + private void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + /** + * Tells the children display group to refetch, so that it reflects + * any changes that were made in the node tree, + * and then updates the selection in the selection model. + * Triggered in response to willChange(). + */ + public void processRecentChanges() + { + Runnable update = new Runnable() + { + public void run() + { + removeAsListener(); // prevent data source refetch: see fetchObjects() + childrenDisplayGroup.fetch(); + addAsListener(); + selectFromDisplayGroup( childrenDisplayGroup ); + } + }; + if ( isListening ) + { + if ( selectionPaintedImmediately ) + { + // if painting selection immediately, run even later + // so that AWT's repaint event fires before we do. + SwingUtilities.invokeLater( update ); + } + else + { + // otherwise run now + update.run(); + } + } + } + + /** + * Delegates most behaviors to the specified data source, + * except fetchObjects, which calls fetchObjectsIntoChildrenGroup + * on the tree model association. If delegate is null, + * calls are passed to the superclass which is a PropertyDataSource. + */ + static class DelegatingTreeDataSource extends PropertyDataSource + { + TreeModelAssociation parentAssociation; + EODataSource delegateDataSource; + + public DelegatingTreeDataSource( + TreeModelAssociation aTreeModelAssociation, EODataSource aDataSource ) + { + parentAssociation = aTreeModelAssociation; + delegateDataSource = aDataSource; + } + + /** + * Calls to delegateDataSource if it exists, otherwise + * calls to super. + */ + public Object createObject() + { + if ( delegateDataSource != null ) + { + return delegateDataSource.createObject(); + } + return super.createObject(); + } + + /** + * Calls objectInsertedIntoChildrenGroup, and if not null + * calls to delegateDataSource.insertObject if it exists, + * and super.insertObjectAtIndex if not. + */ + public void insertObjectAtIndex( Object anObject, int anIndex ) + { + anObject = + parentAssociation.objectInsertedIntoChildrenGroup( + anObject ); + if ( anObject != null ) + { + if ( delegateDataSource != null ) + { + if ( delegateDataSource instanceof OrderedDataSource ) + { + ((OrderedDataSource)delegateDataSource).insertObjectAtIndex( anObject, anIndex ); + } + else + { + delegateDataSource.insertObject( anObject ); + } + } + else + { + super.insertObjectAtIndex( anObject, anIndex ); + } + } + } + + /** + * Calls objectDeletedIntoChildrenGroup, and if not null + * calls to delegateDataSource if it exists. + */ + public void deleteObject( Object anObject ) + { + anObject = + parentAssociation.objectDeletedFromChildrenGroup( + anObject ); + if ( anObject != null ) + { + if ( delegateDataSource != null ) + { + delegateDataSource.deleteObject( anObject ); + } + super.deleteObject( anObject ); + } + } + + /** + * Overridden to return the delegate's editing context, + * the titles display group's editing context, + * and failing that calling to super. + */ + public EOEditingContext editingContext () + { + EOEditingContext result = null; + if ( delegateDataSource != null ) + { + result = delegateDataSource.editingContext(); + } + if ( result == null ) + { + EODataSource parentDataSource = + parentAssociation.titlesDisplayGroup.dataSource(); + if ( parentDataSource != this && parentDataSource != null ) + { + result = parentAssociation.titlesDisplayGroup. + dataSource().editingContext(); + } + } + if ( result == null ) + { + result = super.editingContext(); + } + return result; + } + + /** + * Returns a List containing the objects in this + * data source. + */ + public NSArray fetchObjects () + { + // if titles group is doing double-duty as children group + if ( parentAssociation.titlesDisplayGroup == parentAssociation.childrenDisplayGroup ) + { + // if we're not initiating this fetch + if ( parentAssociation.isListening ) + { + // need to call to delegate to see if we should update values + if ( delegateDataSource != null ) + { +// System.out.println( "fetching from delegate (slow!)" ); + NSArray result = delegateDataSource.fetchObjects(); + NSArray rootObjects = parentAssociation.rootNode.displayedObjects(); + // if titles data source has different objects, return them + if ( rootObjects.count() != result.count() + || ! rootObjects.containsAll( result ) ) + { + // this will force the root node to repopulate in subjectChanged() +//System.out.println( "fetchObjects: data source" ); + return result; + } + } + } + } + // otherwise: just repopulate the titles group +//System.out.println( "fetchObjects: objectsFetchedIntoChildrenGroup" ); + return parentAssociation.objectsFetchedIntoChildrenGroup(); + } + + /** + * Returns a data source that is capable of + * manipulating objects of the type returned by + * applying the specified key to objects + * vended by this data source. + * @see #qualifyWithRelationshipKey + */ + public EODataSource + dataSourceQualifiedByKey ( String aKey ) + { + if ( delegateDataSource != null ) + { + return delegateDataSource.dataSourceQualifiedByKey( aKey ); + } + return null; + } + + /** + * Restricts this data source to vend those + * objects that are associated with the specified + * key on the specified object. + */ + public void + qualifyWithRelationshipKey ( + String aKey, Object anObject ) + { + if ( delegateDataSource != null ) + { + delegateDataSource.qualifyWithRelationshipKey( aKey, anObject ); + } + } + + /** + * Returns the value from the delegateDataSource, if it exists. + * Otherwise calls super. + */ + public EOClassDescription classDescriptionForObjects() + { + if ( delegateDataSource != null ) + { + return delegateDataSource.classDescriptionForObjects(); + } + return super.classDescriptionForObjects(); + } + + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.20 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.19 2002/05/03 21:41:18 mpowers + * No longer clearing the selection model when updating from display group: + * we now only modify if a change needs to be made. + * No longer listening for selection change during firing of delete events: + * delete events cause JTree's to update their selection model. + * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges() + * must happen after the screen is painted, or the selection is not displayed. + * + * Revision 1.18 2002/04/23 19:12:28 mpowers + * Reimplemented fireEventsForChanges. Fitter and happier. + * + * Revision 1.17 2002/04/19 21:18:46 mpowers + * Removed tree event coalescing, which was causing way too many problems. + * The fireChangeEvent algorithm is way faster than before, so we should + * still be better off than before. At least now, we don't have to track + * whether the view component has encountered a particular node. + * + * Revision 1.16 2002/04/18 20:36:11 mpowers + * TreeModelAssociation now populates children group before selected objects. + * Got rid of the forceOnSync workaround for cancelled selection change. + * + * Revision 1.15 2002/04/15 21:52:50 mpowers + * Tightening up TreeModelAssociation and DisplayGroupNode. + * Now only firing root structure changed once. + * Now disposing of root's children. + * Better event coalescing. + * + * Revision 1.14 2002/04/12 21:05:58 mpowers + * Now distinguishing changes in titles group even better. + * + * Revision 1.11 2002/04/10 21:20:04 mpowers + * Better handling for tree nodes when working with editing contexts. + * Better handling for invalidation. No longer broadcasting events + * when nodes have not been "registered" in the tree. + * + * Revision 1.10 2002/04/03 20:01:24 mpowers + * Removed printlns. + * + * Revision 1.8 2002/03/11 03:16:28 mpowers + * Better handling of change events; coalescing changes to children group. + * + * Revision 1.7 2002/03/08 23:19:57 mpowers + * Refactoring of DelegatingTreeDataSource to facilitate binding of titles + * and children aspects to the same display group. + * + * Revision 1.6 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.5 2002/03/06 13:04:16 mpowers + * Implemented cascading qualifiers in tree nodes. + * + * Revision 1.4 2002/03/04 22:47:48 mpowers + * Fixed sort ordering for titles group. Optimization for delegate selection. + * + * Revision 1.3 2002/03/04 12:28:47 mpowers + * Revised case where children and titles are bound to same display group. + * + * Revision 1.2 2002/03/01 23:42:09 mpowers + * Implemented TreeColumnAssociation, and updated documentation. + * + * Revision 1.1 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.38 2002/02/18 03:46:08 mpowers + * Implemented TreeTableCellRenderer. + * + * Revision 1.37 2002/02/13 21:20:15 mpowers + * Updated comments. + * + * Revision 1.36 2001/11/21 15:13:25 mpowers + * Better repainting for selectionPaintedImmediately. + * Better handling for selection with multiple instances of the same + * object in the tree (from yjcheung). + * + * Revision 1.35 2001/11/20 19:13:51 mpowers + * Finished implementation of children group's specialized data source. + * + * Revision 1.34 2001/11/19 16:30:37 mpowers + * Tree repaint strategy is now a preference: selectionPaintedImmediately. + * + * Revision 1.33 2001/11/15 17:56:41 mpowers + * Initial implementation of data source for the children display group. + * + * Revision 1.32 2001/11/14 00:05:54 mpowers + * Eliminated the run later in favor of repainting the component immediately. + * This makes things more predictable for users of the association that + * want to listen to mouse or selection events on the tree. + * + * Revision 1.31 2001/11/02 20:43:15 mpowers + * Fixes for delegate's shouldChangeSelection veto (from yjcheung). + * + * Revision 1.30 2001/10/29 20:42:56 mpowers + * On selection change, repainting tree before notifying display group; + * using NSRunLoop instead of SwingUtilities. + * + * Revision 1.29 2001/10/12 20:12:53 mpowers + * Better handling of selection change vetoing when changing selection + * to a node that is not the sibling of the originally selected node. + * + * Revision 1.28 2001/09/14 13:40:26 mpowers + * User-initiated selection changes are now handled on the next event loop + * so that the component repaints the new selection before any potentially + * lengthy logic is triggered by the selection change. + * + * Revision 1.27 2001/09/10 14:10:03 mpowers + * Tree now handles multiple instances of the same object. + * + * Revision 1.26 2001/07/18 13:03:32 mpowers + * TreeNodes now refetch only on demand. Previously, once a node had + * been fetched, it was always refetched after an invalidate, even if + * the node was not being displayed. + * + * Revision 1.25 2001/05/14 15:25:35 mpowers + * No longer copying titles group's data source to children group. + * + * Revision 1.24 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.23 2001/05/01 00:52:32 mpowers + * Implemented breadth-first traversal of tree for node. + * + * Revision 1.22 2001/04/26 01:15:19 mpowers + * Major clean-up of DisplayGroupNode: fitter, happier, more productive. + * + * Revision 1.21 2001/04/22 23:13:35 mpowers + * Minor bug. + * + * Revision 1.20 2001/04/22 23:05:33 mpowers + * Totally revised DisplayGroupNode so each object gets its own node + * (so the nodes are no longer fixed by index). + * + * Revision 1.19 2001/04/21 23:06:33 mpowers + * A major revisiting to support the revising of DisplayGroupNode. + * + * Revision 1.18 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.17 2001/03/29 21:35:08 mpowers + * Now handling circular references in the graph. + * + * Revision 1.16 2001/03/22 21:25:42 mpowers + * Fixed some nasty issues with jtree's internal state and array bounds. + * + * Revision 1.15 2001/03/19 21:37:58 mpowers + * Improved refresh of titles display group. + * Fixed dangling selection problem after refresh. + * + * Revision 1.14 2001/03/09 22:08:57 mpowers + * Trying to handle the dangling reference problem after an update. + * + * Revision 1.13 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.12 2001/01/25 02:16:25 mpowers + * TreeModelAssociation now returns DisplayGroupNode.getUserObject. + * + * Revision 1.11 2001/01/24 18:14:40 mpowers + * Fixed problem with leaving children aspect unspecified. + * + * Revision 1.10 2001/01/24 17:49:15 mpowers + * Added getObjectForNode and getNodeForObject convenience methods. + * + * Revision 1.9 2001/01/24 17:44:11 mpowers + * Renamed getPathForNode to getPathForObject to be more precise. + * And created a new getPathForNode method. + * + * Revision 1.8 2001/01/24 17:20:29 mpowers + * Children display group now holds siblings of selected objects + * in addition to the selected objects. + * + * Revision 1.5 2001/01/19 23:21:15 mpowers + * Fine tuning events broadcast from TreeModelAssociation. + * + * Revision 1.4 2001/01/18 21:27:29 mpowers + * Major rework of TreeModelAssociation. + * + * Revision 1.2 2001/01/11 20:29:19 mpowers + * Expanded access to tree event firing methods. + * + * Revision 1.1.1.1 2000/12/21 15:49:18 mpowers + * Contributing wotonomy. + * + * Revision 1.20 2000/12/20 16:25:42 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java new file mode 100644 index 0000000..1fef587 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AbsoluteLayout.java @@ -0,0 +1,74 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.LayoutManager; +import java.io.Serializable; + +/** + * AbsoluteLayout specifies that all components in the + * container will be placed according to their size + * and their location relative to the container's origin.

+ * + * You can achieve the same effect by setting a container's + * layout manager to null, but this class allows you to subclass + * it if you need specific control or functionality. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public class AbsoluteLayout implements LayoutManager, Serializable +{ + public void addLayoutComponent(String name, + Component comp) + { + } + + public void removeLayoutComponent(Component comp) + { + } + + public Dimension preferredLayoutSize(Container parent) + { + return minimumLayoutSize( parent ); + } + + public Dimension minimumLayoutSize(Container parent) + { + int width = 0; + int height = 0; + + Component[] c = parent.getComponents(); + for ( int i = 0; i < c.length; i++ ) + { + width = Math.max( width, c[i].getLocation().x + c[i].getBounds().width ); + height = Math.max( height, c[i].getLocation().y + c[i].getBounds().height ); + } + + return new Dimension( width, height ); + } + + public void layoutContainer(Container parent) + { + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java new file mode 100644 index 0000000..c36f5e2 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlphaTextField.java @@ -0,0 +1,335 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +/** +* AlphaTextField is a "smart" text field that restricts the user's input. The +* input can be restricted to alphabetic, alphanumeric, or all characters. The +* maximum number of characters can also be limited. +* The defaults for this component is alphabetic only string of unlimited length. +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class AlphaTextField extends SmartTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Sets the input to alphabetic characters only. The characters "a-z" and "A-Z" +* are the only valid characters. All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHABETIC = 0; + +/** +* Sets the input to alphanumeric characters only. The characters "a-z", "A-Z" +* and "0-9" are the only valid characters. All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHANUMERIC = 1; + +/** +* Sets the input to alphanumeric characters and a few special characters only. +* The valid characters are "a-z", "A-Z", "0-9", space, "-", "_", "\", and ":". +* This is helpful for file names (with paths) as input strings. +* All other characters will be ignored. +* @see #getAlphaType() +*/ + public static final int ALPHANUMERIC_PLUS = 2; + +/** +* Sets the input to all characters without restriction. +* @see #getAlphaType() +*/ + public static final int ALL = 3; + + +/******************************* +* DATA MEMBERS +*******************************/ + + // The level of input restrictions, defaults to ALPHABETIC + private int alphaType; + + // The maximum length of the input string, defaults to 0, no maximum + private int stringLength; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* The default constructor of this class. The default string of this text +* field is set to the empty string (""). The maximum length is set to 0, +* which specifies no limit. +*/ + public AlphaTextField() + { + this("", 0); + } + +/** +* Constructor of this class with the initial text of the text field specified. +* The maximum length is set to 0, which specifies no limit. +* @param text Initial text of the text field. +*/ + public AlphaTextField(String text) + { + this(text, 0); + } + +/** +* Constructor of this class with width (in columns) of the text field specified. +* The initial text is set to the empty string (""). The maximum length is set +* to 0, which specifies no limit. +* @param columns The width of the text field in characters. +*/ + public AlphaTextField(int columns) + { + this("", columns); + } + +/** +* Constructor of this class with width (in columns) and initial text of the +* text field specified. The maximum length is set to 0, which specifies no limit. +* @param text Initial text of the text field. +* @param columns The width of the text field in characters. +*/ + public AlphaTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Constructor that allows the user to set the Alpha type of the text field +* and the maximum string length. +* @param anAlphaType The character restriction type. +* @param aLength The maximum number of characters allowed in the string. +*/ + public AlphaTextField(int anAlphaType, int aLength) + { + super( "", 0 ); + setAlphaType( anAlphaType ); + setStringLength( aLength ); + } + +/** +* Gets the current restriction type of this text field. +* @see #ALPHABETIC +* @see #ALPHANUMERIC +* @see #ALPHANUMERIC_PLUS +* @see #ALL +* @return The current restriction type as defined by the constansts of this class. +*/ + public int getAlphaType() + { + return alphaType; + } + +/** +* Sets the restriction type of this text field. +* @see #ALPHABETIC +* @see #ALPHANUMERIC +* @see #ALPHANUMERIC_PLUS +* @see #ALL +* @param newAlphaType The restriction of this text field. +*/ + public void setAlphaType(int newAlphaType) + { + switch (newAlphaType) + { + case ALPHABETIC: + case ALPHANUMERIC: + case ALPHANUMERIC_PLUS: + case ALL: + { + alphaType = newAlphaType; + break; + } + default: + { + alphaType = ALPHABETIC; + break; + } + } + } + +/** +* Sets the maximum string length of this text field. If the length is set to +* zero, then there is no limit. The default string length is zero. Negative +* sizes will set the length to zero. +* @param newStringLength The maximum length of the string that the user can input. +*/ + public void setStringLength(int newStringLength) + { + if (newStringLength < 0) + { + stringLength = 0; + } + else + { + stringLength = newStringLength; + } + } + +/** +* Gets the current length of the maximum string size the user can enter. +* @return The maximum length the string of the text field can be. +*/ + public int getStringLength() + { + return stringLength; + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + + protected boolean isValidCharacter(char aChar) + { + // if its a non-printable character, then its ok + if ((aChar < ' ') || (aChar > '~')) + { + return true; + } + + // can only be a printable character now, check it for validation + return isValidCharacterType(aChar); + } + + protected boolean isValidString(String aString) + { + if (aString.length() > stringLength) + { + return false; + } + + for (int i = 0; i < aString.length(); ++i) + { + if (!(isValidCharacterType(aString.charAt(i)))) + { + return false; + } + } + + return true; + } + + protected void postProcessing() + { + // No need to do anything. + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + + private boolean isValidCharacterType(char aChar) + { + switch (alphaType) + { + case ALPHABETIC: + { + if (!(isValidAlphabeticCharacter(aChar))) + { + return false; + } + break; + } + case ALPHANUMERIC: + { + if (!(isValidAlphanumericCharacter(aChar))) + { + return false; + } + break; + } + case ALPHANUMERIC_PLUS: + { + if (!(isValidAlphanumericPlusCharacter(aChar))) + { + return false; + } + break; + } + case ALL: + { + if (!(isValidAllCharacter(aChar))) + { + return false; + } + break; + } + default: + { + return false; + } + } + + return true; + } + + private boolean isValidAlphabeticCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z'))) + { + return false; + } + return true; + } + + private boolean isValidAlphanumericCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z')) && ((aChar < '0') || (aChar > '9'))) + { + return false; + } + return true; + } + + private boolean isValidAlphanumericPlusCharacter(char aChar) + { + if (((aChar < 'A') || (aChar > 'Z')) && ((aChar < 'a') || (aChar > 'z')) && ((aChar < '0') || (aChar > '9'))) + { + if ((aChar != ' ') && (aChar != '_') && (aChar != '-') && (aChar != ':') && (aChar != '\\')) + { + return false; + } + } + return true; + } + + private boolean isValidAllCharacter(char aChar) + { + if ((aChar < ' ') || (aChar > '~')) + { + return false; + } + return true; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java new file mode 100644 index 0000000..46d2693 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/AlternatingRowCellRenderer.java @@ -0,0 +1,129 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.UIManager; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that wraps another TableCellRenderer +* and sets the background to the specified color for odd-numbered rows. +* This makes every other row appear to be a different color, +* which helps users distinguish rows of data in densely-packed +* tables. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class AlternatingRowCellRenderer implements TableCellRenderer { + + protected TableCellRenderer wrappedRenderer; + protected Color alternateColor; + + /** + * Default constructor uses a lighter shade of the system control color + * and wraps a DefaultTableCellRenderer. + */ + public AlternatingRowCellRenderer() + { + this( new DefaultTableCellRenderer() ); + } + + /** + * Uses the specified color for the background of the alternating rows, + * and wraps a DefaultTableCellRenderer. + */ + public AlternatingRowCellRenderer( + Color aColor ) + { + this( aColor, new DefaultTableCellRenderer() ); + } + + /** + * Uses the uses a lighter shade of the system control color + * for the background of the alternating rows, + * and wraps the specified TableCellRenderer. + */ + public AlternatingRowCellRenderer( + TableCellRenderer aRenderer ) + { + Color c = UIManager.getColor( "control" ); + c = new Color( // lighten this color just slightly + (int) ( c.getRed() + ( ( 255 - c.getRed() ) / 1.5 ) ), + (int) ( c.getGreen() + ( ( 255 - c.getGreen() ) / 1.5 ) ), + (int) ( c.getBlue() + ( ( 255 - c.getBlue() ) / 1.5 ) ) ); + + alternateColor = c; + wrappedRenderer = aRenderer; + } + + /** + * Uses the specified color for the background of the alternating rows, + * and wraps the specified TableCellRenderer. + */ + public AlternatingRowCellRenderer( + Color aColor, TableCellRenderer aRenderer ) + { + alternateColor = aColor; + wrappedRenderer = aRenderer; + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + Component result = wrappedRenderer.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column ); + if ( ! isSelected ) + { + if ( row % 2 == 0 ) + { + if ( ! result.getBackground().equals( table.getBackground() ) ) + { + result.setBackground( table.getBackground() ); + } + } + else + { + if ( ! result.getBackground().equals( alternateColor ) ) + { + // jdk1.3's default renderer is opaque + if ( result instanceof JComponent ) + { + ((JComponent)result).setOpaque( true ); + } + + result.setBackground( alternateColor ); + } + } + } + return result; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java new file mode 100644 index 0000000..1c438b6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterFlowLayout.java @@ -0,0 +1,515 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +/** + * BetterFlowLayout works just like FlowLayout, except that + * you can specify a vertical orientation in addition to the + * usual horizontal orientations. You can also specify that + * all the components be sized to the same height and/or width. + * By default, the behavior is identical to FlowLayout. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class BetterFlowLayout extends FlowLayout { + + /** + * This value indicates vertical orientation and + * that each column of components should be top-justified. + */ + public static final int TOP = 32; + + /** + * This value indicates vertical orientation and + * that each column of components should be centered. + */ + public static final int CENTER_VERTICAL = 16; + + /** + * This value indicates vertical orientation and + * that each column of components should be bottom-justified. + */ + public static final int BOTTOM = 8; + + /** + * Tracks orientation. + */ + protected boolean isHorizontal = true; + + /** + * Tracks component sizing of width. + */ + protected boolean isWidthUniform = false; + /** + * Tracks component sizing of height. + */ + protected boolean isHeightUniform = false; + + /** + * Constructs a new Flow Layout with a centered alignment and a + * default 5-unit horizontal and vertical gap. + */ + public BetterFlowLayout() { + this(CENTER, 5, 5); + } + + /** + * Constructs a new Flow Layout with the specified alignment and a + * default 5-unit horizontal and vertical gap. + * The value of the alignment argument must be one of + * BetterFlowLayout.LEFT, BetterFlowLayout.RIGHT, + * or BetterFlowLayout.CENTER. + * @param align the alignment value + */ + public BetterFlowLayout(int align) { + this(align, 5, 5); + } + + /** + * Creates a new flow layout manager with the indicated alignment + * and the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * BetterFlowLayout.LEFT, BetterFlowLayout.RIGHT, + * or BetterFlowLayout.CENTER. + * @param align the alignment value. + * @param hgap the horizontal gap between components. + * @param vgap the vertical gap between components. + */ + public BetterFlowLayout(int align, int hgap, int vgap) { + setHgap(hgap); + setVgap(vgap); + setAlignment(align); + } + + /** + * Sets whether all components should have the same height. + * @param isUniform the new value. + * @see #isHeightUniform + */ + public void setHeightUniform(boolean isUniform) { + isHeightUniform = isUniform; + } + + /** + * Sets whether all components should have the same width. + * @param isUniform the new value. + * @see #isWidthUniform + */ + public void setWidthUniform(boolean isUniform) { + isWidthUniform = isUniform; + } + + /** + * Determines whether all components will have the same height. + * The uniform height will be the maximum of the preferred heights + * of all the components in the container. + * This value defaults to false. + * @return whether components will have the same height. + */ + public boolean isHeightUniform() { + return isHeightUniform; + } + + /** + * Determines whether all components will have the same width. + * The uniform height will be the maximum of the preferred widths + * of all the components in the container. + * This value defaults to false. + * @return whether components will have the same width. + */ + public boolean isWidthUniform() { + return isWidthUniform; + } + + /** + * Sets the alignment for this layout. + * Possible values for horizontal orientation are LEFT, + * RIGHT, and CENTER. + * Possible values for vertical orientation are TOP, + * BOTTOM, and CENTER_VERTICAL. + * @param align the alignment value. + * @see java.awt.FlowLayout#getAlignment + */ + public void setAlignment(int align) { + if ( ( align == TOP ) || ( align == BOTTOM ) || ( align == CENTER_VERTICAL ) ) + { + isHorizontal = false; + } + else + { + isHorizontal = true; + } + + super.setAlignment( align ); + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSize(Container target) { + if ( isHorizontal ) { + return preferredLayoutSizeHorizontal( target ); + } else { + return preferredLayoutSizeVertical( target ); + } + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSizeHorizontal(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + int nmembers = target.getComponentCount(); + int maxWidth = 0; + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + dim.height = Math.max(dim.height, d.height); + maxWidth = Math.max(maxWidth, d.width); + if (i > 0) { + dim.width += getHgap(); + } + dim.width += d.width; + } + } + if ( isWidthUniform ) + dim.width = ( maxWidth + getHgap() ) * nmembers - getHgap(); + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + getHgap()*2; + dim.height += insets.top + insets.bottom + getVgap()*2; + return dim; + } + } + + /** + * Returns the preferred dimensions for this layout given the components + * in the specified target container. + * @param target the component which needs to be laid out + * @return the preferred dimensions to lay out the + * subcomponents of the specified container. + * @see Container + * @see #minimumLayoutSize + * @see java.awt.Container#getPreferredSize + */ + public Dimension preferredLayoutSizeVertical(Container target) { + synchronized (target.getTreeLock()) { + Dimension dim = new Dimension(0, 0); + int nmembers = target.getComponentCount(); + int maxHeight = 0; + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + dim.width = Math.max(dim.width, d.width); + maxHeight = Math.max(maxHeight, d.height); + if (i > 0) { + dim.height += getVgap(); + } + dim.height += d.height; + } + } + if ( isHeightUniform ) + dim.height = ( maxHeight + getVgap() ) * nmembers - getVgap(); + Insets insets = target.getInsets(); + dim.width += insets.left + insets.right + getHgap()*2; + dim.height += insets.top + insets.bottom + getVgap()*2; + return dim; + } + } + + /** + * Returns the minimum dimensions needed to layout the components + * contained in the specified target container. + * @param target the component which needs to be laid out + * @return the minimum dimensions to lay out the + * subcomponents of the specified container. + * @see #preferredLayoutSize + * @see java.awt.Container + * @see java.awt.Container#doLayout + */ + public Dimension minimumLayoutSize(Container target) { + // preferred size is also the minimum size + if ( isHorizontal ) { + return preferredLayoutSizeHorizontal( target ); + } else { + return preferredLayoutSizeVertical( target ); + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this BetterFlowLayout object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + public void layoutContainer(Container target) { + if ( isHorizontal ) { + layoutContainerHorizontal( target ); + } else { + layoutContainerVertical( target ); + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this BetterFlowLayout object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + protected void layoutContainerHorizontal(Container target) { + synchronized (target.getTreeLock()) { + Insets insets = target.getInsets(); + int maxwidth = target.getSize().width - (insets.left + insets.right + getHgap()*2); + int nmembers = target.getComponentCount(); + int x = 0, y = insets.top + getVgap(); + int rowh = 0, start = 0; + + boolean ltr = true; // target.getComponentOrientation().isLeftToRight(); + Dimension uniform = getUniformDimension( target ); + + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if ( isWidthUniform ) + d.width = uniform.width; + if ( isHeightUniform ) + d.height = uniform.height; + m.setSize(d.width, d.height); + + if ((x == 0) || ((x + d.width) <= maxwidth)) { + if (x > 0) { + x += getHgap(); + } + x += d.width; + rowh = Math.max(rowh, d.height); + } else { + moveComponentsHorizontal(target, insets.left + getHgap(), y, maxwidth - x, rowh, start, i, ltr); + x = d.width; + y += getVgap() + rowh; + rowh = d.height; + start = i; + } + } + } + moveComponentsHorizontal(target, insets.left + getHgap(), y, maxwidth - x, rowh, start, nmembers, ltr); + } + } + + /** + * Centers the elements in the specified row, if there is any slack. + * @param target the component which needs to be moved + * @param x the x coordinate + * @param y the y coordinate + * @param width the width dimensions + * @param height the height dimensions + * @param rowStart the beginning of the row + * @param rowEnd the the ending of the row + */ + private void moveComponentsHorizontal(Container target, int x, int y, int width, int height, + int rowStart, int rowEnd, boolean ltr) { + synchronized (target.getTreeLock()) { + switch (getAlignment()) { + case LEFT: + x += ltr ? 0 : width; + break; + case CENTER: + x += width / 2; + break; + case RIGHT: + x += ltr ? width : 0; + break; +//1.2 case LEADING: +//1.2 break; +//1.2 case TRAILING: +//1.2 x += width; +//1.2 break; + } + for (int i = rowStart ; i < rowEnd ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + if (ltr) { + m.setLocation(x, y + (height - m.getBounds().height) / 2); + } else { + m.setLocation(target.getBounds().width - x - m.getBounds().width, y + (height - m.getBounds().height) / 2); + } + x += m.getBounds().width + getHgap(); + } + } + } + } + + /** + * Lays out the container. This method lets each component take + * its preferred size by reshaping the components in the + * target container in order to satisfy the constraints of + * this BetterFlowLayout object. + * @param target the specified component being laid out. + * @see Container + * @see java.awt.Container#doLayout + */ + protected void layoutContainerVertical(Container target) { + synchronized (target.getTreeLock()) { + + Insets insets = target.getInsets(); + int maxheight = target.getBounds().height - (insets.top + insets.bottom + getVgap()*2); + int nmembers = target.getComponentCount(); + int y = 0, x = insets.left + getHgap(); + int colw = 0, start = 0; + + Dimension uniform = getUniformDimension( target ); + for (int i = 0 ; i < nmembers ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + Dimension d = m.getPreferredSize(); + if ( isWidthUniform ) + d.width = uniform.width; + if ( isHeightUniform ) + d.height = uniform.height; + m.setSize(d.width, d.height); + + if ((y == 0) || ((y + d.height) <= maxheight)) { + if (y > 0) { + y += getVgap(); + } + y += d.height; + colw = Math.max(colw, d.width); + } else { + moveComponentsVertical(target, x, insets.top + getVgap(), colw, maxheight - y, start, i ); + y = d.height; + x += getHgap() + colw; + colw = d.width; + start = i; + } + } + } + moveComponentsVertical(target, x, insets.top + getVgap(), colw, maxheight - y, start, nmembers ); + } + } + + /** + * Centers the elements in the specified row, if there is any slack. + * @param target the component which needs to be moved + * @param x the x coordinate + * @param y the y coordinate + * @param width the width dimensions + * @param height the height dimensions + * @param colStart the beginning of the column + * @param colEnd the the ending of the column + */ + private void moveComponentsVertical(Container target, int x, int y, int width, int height, + int colStart, int colEnd) { + synchronized (target.getTreeLock()) { + switch (getAlignment()) { + case TOP: + y += 0; + break; + case CENTER_VERTICAL: + y += ( height / 2 ); // - preferredLayoutSize( target ).height ) / 2 ); + break; + case BOTTOM: + y += height; + break; + } + for (int i = colStart ; i < colEnd ; i++) { + Component m = target.getComponent(i); + if (m.isVisible()) { + m.setLocation(x + (width - m.getBounds().width) / 2, y ); +// m.setLocation(x, y ); +// m.setSize( width, m.getBounds().height ); //! + y += m.getBounds().height + getVgap(); + } + } + } + } + + /** + * Returns a dimension representing the maximum preferred + * height and width of all the components in the container. + * @param target the container to scan. + * @return a dimension containing the maximum values. + */ + protected Dimension getUniformDimension(Container target) { + Component m = null; + Dimension preferred = null; + int maxWidth = 0, maxHeight = 0; + int nmembers = target.getComponentCount(); + for ( int i = 0; i < nmembers; i++ ) { + m = target.getComponent( i ); + if ( m.isVisible() ) { + preferred = m.getPreferredSize(); + maxWidth = Math.max( maxWidth, preferred.width ); + maxHeight = Math.max( maxHeight, preferred.height ); + } + } + return new Dimension( maxWidth, maxHeight ); + } + + /** + * Returns a string representation of this BetterFlowLayout + * object and its values. + * @return a string representation of this layout. + */ + public String toString() { + String str = ""; + switch (getAlignment()) { + case TOP: str = ",align=top"; break; + case CENTER_VERTICAL: str = ",align=vertical"; break; + case BOTTOM: str = ",align=bottom"; break; + default: return super.toString(); + } + return getClass().getName() + "[hgap=" + getHgap() + ",vgap=" + getVgap() + str + "]"; + } + + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java new file mode 100644 index 0000000..6e23ca1 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterRootLayout.java @@ -0,0 +1,274 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Rectangle; + +import javax.swing.JPanel; +import javax.swing.JRootPane; + +/** +* A custom layout for a JRootPane that handles the the layout of a +* JRootPane's layeredPane, glassPane, and menuBar, and in addition +* handles four decorative components arranged in a border layout. +* Add the decorative components to the JRootPane using the directional +* constants; CENTER is reserved for the content pane and menu bar. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class BetterRootLayout extends BorderLayout +{ + /** + * Returns the amount of space the layout would like to have. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's preferred size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension preferredLayoutSize(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.preferredLayoutSize( parent ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Returns the minimum amount of space the layout needs. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's minimum size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension minimumLayoutSize(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.minimumLayoutSize( parent ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Returns the maximum amount of space the layout can use. + * + * @param the Container for which this layout manager is being used + * @return a Dimension object containing the layout's maximum size + * @throws ClassCastException if parent is not a JRootPane + */ + public Dimension maximumLayoutSize(Container target) + { + JRootPane rootPane = (JRootPane) target; + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + JPanel contentProxy = null; + if(rootPane.getContentPane() != null) { + contentProxy = new JPanel(); + contentProxy.setMinimumSize( + rootPane.getContentPane().getMinimumSize() ); + contentProxy.setMaximumSize( + rootPane.getContentPane().getMaximumSize() ); + contentProxy.setPreferredSize( + rootPane.getContentPane().getPreferredSize() ); + proxyPanel.add( contentProxy, CENTER ); + } + JPanel menuProxy = null; + if(rootPane.getJMenuBar() != null) { + menuProxy = new JPanel(); + menuProxy.setMinimumSize( + rootPane.getJMenuBar().getMinimumSize() ); + menuProxy.setMaximumSize( + rootPane.getJMenuBar().getMaximumSize() ); + menuProxy.setPreferredSize( + rootPane.getJMenuBar().getPreferredSize() ); + proxyPanel.add( menuProxy, NORTH ); + } + + this.addLayoutComponent( proxyPanel, CENTER ); + + Dimension result = super.maximumLayoutSize( target ); + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + + return result; + } + + /** + * Instructs the layout manager to perform the layout for the specified + * container. + * + * @param the Container for which this layout manager is being used + * @throws ClassCastException if parent is not a JRootPane + */ + public void layoutContainer(Container parent) + { + JRootPane rootPane = (JRootPane) parent; + + Rectangle b = parent.getBounds(); + Insets i = rootPane.getInsets(); + int w = b.width - i.right - i.left; + int h = b.height - i.top - i.bottom; + + // layout panes + + if(rootPane.getLayeredPane() != null) { + rootPane.getLayeredPane().setBounds(i.left, i.top, w, h); + } + if(rootPane.getGlassPane() != null) { + rootPane.getGlassPane().setBounds(i.left, i.top, w, h); + } + + // handle proxy panel + + JPanel proxyPanel = new JPanel(); + proxyPanel.setLayout( new BorderLayout() ); + + this.addLayoutComponent( proxyPanel, CENTER ); + + super.layoutContainer( parent ); + + // use proxy sizes to set sizes of layeredPane's children + + Rectangle proxyRect = proxyPanel.getBounds(); + if(rootPane.getJMenuBar() != null) { + Rectangle menuRect = proxyPanel.getBounds(); + menuRect.height = rootPane.getJMenuBar().getPreferredSize().height; + rootPane.getJMenuBar().setBounds( menuRect ); + proxyRect.y += menuRect.height; + proxyRect.height -= menuRect.height; + } + if(rootPane.getContentPane() != null) { + rootPane.getContentPane().setBounds( proxyRect ); + } + + this.removeLayoutComponent( proxyPanel ); + + proxyPanel.removeAll(); + } + + /** + * Passes NORTH, SOUTH, EAST, WEST and CENTER to super implementation, + * and ignores all others. + */ + public void addLayoutComponent(Component comp, Object constraints) + { + if ( NORTH.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( SOUTH.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( EAST.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( WEST.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + else + if ( CENTER.equals( constraints ) ) + { + super.addLayoutComponent( comp, constraints ); + } + + // otherwise, ignore + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java new file mode 100644 index 0000000..deb0eb6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/BetterTableUI.java @@ -0,0 +1,123 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.event.MouseEvent; + +import javax.swing.event.MouseInputListener; +import javax.swing.plaf.basic.BasicTableUI; + +/** +* BetterTableUI allows a JTable to be disabled by +* listening for MouseEvents and then forwarding them +* to the usual MouseInputHandler only if the table +* is enabled.

+* +* This class also works around a bug where an editable +* table's selection is changed when clicking in an edit +* cell while the control key is down. This typically +* happened while users were copying/pasting data from +* cell to cell.

+* +* To use, call JTable.setUI() on any +* JTable with a BetterTableUI as the parameter. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class BetterTableUI extends BasicTableUI implements MouseInputListener +{ +/** +* The listener to get all mouse events when the table is enabled. +*/ + protected MouseInputListener delegateHandler; + +/** +* Overridden to set self as mouse listener and create delegate. +*/ + protected MouseInputListener createMouseInputListener() + { + // normal handler is a protected inner class of parent + delegateHandler = new MouseInputHandler(); + + return this; + } + + // interface MouseInputListener + + public void mouseClicked(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseClicked(event); + } + } + + public void mouseDragged(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseDragged(event); + } + } + + public void mouseEntered(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseEntered(event); + } + } + + public void mouseExited(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseExited(event); + } + } + + public void mouseMoved(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseMoved(event); + } + } + + public void mousePressed(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + // workaround bug - control key removes an existing selection + if ( table.isEditing() && event.isControlDown() ) return; + + delegateHandler.mousePressed(event); + } + } + + public void mouseReleased(MouseEvent event) + { + if ( (table!=null) && (table.isEnabled()) ) + { + delegateHandler.mouseReleased(event); + } + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java new file mode 100644 index 0000000..769e866 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ButtonPanel.java @@ -0,0 +1,610 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.AWTEventMulticaster; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionListener; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Enumeration; +import java.util.Vector; + +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.UIManager; + +/** +* ButtonPanel handles display and event broadcasting of standard buttons like +* OK/Cancel/Save/etc. The constructor takes a list or array of strings, each +* representing a button to appear on the panel from left to right. +* Any button click will send an action event to all listeners with the action +* command containing the corresponding string. Note action events are simply +* forwarded from the buttons themselves, so the source of the event will be +* the button, not the button panel. The button panel is the source of the +* STATE_CHANGED events that notify about changes to the panel itself.

+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ButtonPanel extends JPanel implements ActionListener, MouseMotionListener +{ + // TODO: Button text should be read from resources. +/** +* Specifies a "OK" button. +* This is also the action command sent by the OK button. +*/ + public static final String OK = "OK"; +/** +* Specifies a "Save" button. +* This is also the action command sent by the Save button. +*/ + public static final String SAVE = "Save"; +/** +* Specifies a "Refresh" button. +* This is also the action command sent by the Refresh button. +*/ + public static final String REFRESH = "Refresh"; +/** +* Specifies a "Clear All" button. +* This is also the action command sent by the Clear All button. +*/ + public static final String CLEAR_ALL = "Clear All"; +/** +* Specifies a "Refresh" button. +* This is also the action command sent by the Cancel button. +*/ + public static final String CANCEL = "Cancel"; +/** +* Specifies a "Yes" button. +* This is also the action command sent by the Yes button. +*/ + public static final String YES = "Yes"; +/** +* Specifies a "No" button. +* This is also the action command sent by the No button. +*/ + public static final String NO = "No"; +/** +* Specifies an "Add" button. +* This is also the action command sent by the Add button. +*/ + public static final String ADD = "Add"; +/** +* Specifies a "Remove" button. +* This is also the action command sent by the Remove button. +*/ + public static final String REMOVE = "Remove"; +/** +* This is the action command to all listeners when the button state is changed. +*/ + public static final String STATE_CHANGED = "STATE_CHANGED"; + +/** +* This is the container to which buttons are added. +*/ + protected Container buttonContainer = null; // useful for subclasses +/** +* This is the list of all buttons on the panel. +*/ + protected Vector buttonList = null; +/** +* The insets for this panel, so they can be modified. +*/ + protected Insets insets = new Insets( 5, 5, 5, 5 ); + +/** +* This is the layout manager - which must be a FlowLayout or subclass. +*/ + protected FlowLayout buttonPanelLayout = null; + + // for action multicasting + protected ActionListener actionListener = null; + + +/** +* Constructs a ButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public ButtonPanel() + { + buttonList = new Vector(); + initLayout(); + + // default labels for bean layout + setLabels( new String[] { "One", "Two", "Three" } ); + } + +/** +* This method is responsible for the initial layout of the panel. +* Subclasses can implement different layouts, but this method +* is responsible for initializing buttonContainer and buttonPanelLayout +* and setting the container to use the layout. +*/ + protected void initLayout() + { + this.setInsets( super.getInsets() ); + buttonContainer = this; + buttonPanelLayout = new BetterFlowLayout( BetterFlowLayout.RIGHT ); + buttonContainer.setLayout(buttonPanelLayout); + ((BetterFlowLayout)buttonPanelLayout).setWidthUniform( true ); + + // setBackground( Color.blue ); // useful for debugging + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public ButtonPanel( String[] buttonList ) + { + this(); + setLabels( buttonList ); + } + +/** +* Constructs a ButtonPane using specified actions. For each action, a button +* is created, that when pressed the corresponding action is activated. The +* "name" of the action is used as the title of the button. +* @param actionList An array of actions to be used to create buttons with. +*/ + public ButtonPanel( Action[] actionList ) + { + this(); + setLabels( actionList ); + } + +/** +* Creates the buttons to appear on the panel. Any existing buttons +* are replaced. The labels are used as names and action commands +* in addition to labels. +* @param labels An array of strings to be used in labeling the buttons. +* If null, all buttons will be removed. +*/ + public void setLabels( String[] labels ) + { + if ( labels == null ) + { + labels = new String[] {}; + } + + buttonContainer.removeAll(); + this.buttonList = new Vector( labels.length ); + + String item = null; + Component button; + for ( int i = 0; i < labels.length; i++ ) + { + item = labels[i]; + if ( item != null ) + { + button = createComponentWithLabel( item.toString() ); + this.buttonList.addElement( item ); + addComponentToPanel( button ); + button.setEnabled( this.isEnabled() ); +/* + if ( i == 0 ) + { + JRootPane root = SwingUtilities.getRootPane( button ); + if ( root != null ) + root.setDefaultButton( button ); + } +*/ + } + else + { + throw new IllegalArgumentException( "ButtonPanel.setButtons: nulls are not allowed." ); + } + } + + this.revalidate(); + this.repaint(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED ) ); + } + +/** +* +*/ + public void setLabels( Action[] actions ) + { + if ( actions == null ) + { + actions = new Action[] {}; + } + + buttonContainer.removeAll(); + this.buttonList = new Vector( actions.length ); + + Action action = null; + Component button; + for ( int i = 0; i < actions.length; i++ ) + { + action = actions[i]; + if ( action != null ) + { + String name = ( String )action.getValue( Action.NAME ); + button = createComponentWithLabel( name ); + this.buttonList.addElement( name ); + addComponentToPanel( button ); + button.setEnabled( this.isEnabled() ? action.isEnabled() : false ); + + // Add the action to the "button" if it knows about action listeners. + try + { + Method addActionListenerMethod = + button.getClass().getMethod( "addActionListener", new Class[] { ActionListener.class } ); + addActionListenerMethod.invoke( button, new Object[] { action } ); + } + catch ( NoSuchMethodException e ) { /* Do Nothing */ } + catch ( IllegalAccessException e ) { e.printStackTrace(); /* TODO: Do Something? */ } + catch ( InvocationTargetException e ) { e.printStackTrace(); /* TODO: Do Something? */ } + + // Create a new listener for property change events and have + // the action broadcast to that listener. + PropertyChangeListener pcListener = new ActionChangeListener( button ); + action.addPropertyChangeListener( pcListener ); + } + else + { + throw new IllegalArgumentException( "ButtonPanel.setButtons: nulls are not allowed." ); + } + } + + this.revalidate(); + this.repaint(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATE_CHANGED ) ); + } + + +/** +* Gets the labels of the buttons that appear on the panel, ordered from left to right. +* @return A new list containing strings used in labeling the buttons. +*/ + public String[] getLabels() + { + String[] labels = new String[ buttonList.size() ]; + int i = 0; + for ( Enumeration it = buttonList.elements(); it.hasMoreElements(); ) + { + labels[i++] = it.nextElement().toString(); + } + return labels; + } + +/** +* Gets the first component having the specified name. +* @return A component with the specified name, or null if none match. +*/ + public Component getButton( String aLabel ) + { + if ( aLabel == null ) return null; + + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( aLabel.equals( c.getName() ) ) + { + return c; + } + } + return null; + } + +/** +* Creates a new component with the specified label. +* The label is also used for the component's name +* and action command, if any. +* (This implementation returns a JButton.) +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; // TODO: get string from resource + JButton newButton = new JButton(); // might allow other types in future + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + return newButton; + } + +/** +* Adds a component to the right-most side of the layout. +* @param aComponent The component to be added to the layout. +*/ + protected void addComponentToPanel( Component aComponent ) + { + buttonContainer.add( aComponent ); + } + + +/** +* Changes the alignment of the buttons in the panel. Defaults to right-justified. +* @param alignment A valid alignment code, per BetterFlowLayout implementation. +* @see BetterFlowLayout +*/ + public void setAlignment( int alignment ) + { + buttonPanelLayout.setAlignment(alignment); + buttonContainer.doLayout(); + } +/** +* Gets the alignment of the buttons in the panel. +* @return An alignment code, per FlowLayout implementation. +* @see FlowLayout +*/ + public int getAlignment() + { + return buttonPanelLayout.getAlignment(); + } + +/** +* Changes the horizontal spacing between components in the panel. +* @param newHgap the new spacing, in pixels. May not be negative. +*/ + public void setHgap( int newHgap ) + { + if ( newHgap < 0 ) return; // may not be negative + buttonPanelLayout.setHgap( newHgap ); + } + +/** +* Gets the current horizontal spacing between components. +* @return the current horizontal spacing, in pixels. +*/ + public int getHgap() + { + return buttonPanelLayout.getHgap(); + } + +/** +* Changes the vertical spacing between components in the panel. +* @param newVgap the new spacing, in pixels. May not be negative. +*/ + public void setVgap( int newVgap ) + { + if ( newVgap < 0 ) return; // may not be negative + buttonPanelLayout.setVgap( newVgap ); + } + +/** +* Gets the current vertical spacing between components. +* @return the current vertical spacing, in pixels. +*/ + public int getVgap() + { + return buttonPanelLayout.getVgap(); + } + +/** +* Changes the insets for this panel. +* @param newInsets the new insets. +*/ + public void setInsets( Insets newInsets ) + { + insets = newInsets; + } + +/** +* Overridden to return the user-specified insets for this panel. +* @return the current insets for this panel. +*/ + public Insets getInsets() + { + return insets; + } + +/** +* Overridden to call setEnabled on all components on panel. +* @param isEnabled whether to enable the panel and all components on it. +*/ + public void setEnabled( boolean isEnabled ) + { + super.setEnabled( isEnabled ); + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + buttonContainer.getComponent( i ).setEnabled( isEnabled ); + } + } + + // Action Multicast methods + +/** +* Adds an action listener to the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + + // interface ActionListener + +/** +* Called by buttons on panel and by other components that +* might be set to broadcast events to this listener. +* @param e An action event to be received. +*/ + public void actionPerformed(ActionEvent e) + { + broadcastEvent(e); + } + +/** +* A property change listener that listens specifically for property changes +* from action objects. This is the class that ties in the action to the +* button. This class is added to an action as a property change listener. +* The corresponding component is referenced by this class toe easily handle +* updates to the component caused by changes to the action. +*/ + public class ActionChangeListener implements PropertyChangeListener + { + /** The UI component that is affected by the action's changes. */ + Component theComponent; + + /** + * Constructs an ActionChangeListener with the given component being + * the recipient of the action's changes. + * @param The component to bind with the action. + */ + public ActionChangeListener( Component aComponent ) + { + super(); + theComponent = aComponent; + } + + /** + * Called whenever a property changes on the action object. + * @pram e The property change event generated by the action. + */ + public void propertyChange( PropertyChangeEvent e ) + { + String propertyName = e.getPropertyName(); + if ( propertyName.equals( Action.NAME ) ) + { + String name = ( String )e.getNewValue(); + if ( theComponent instanceof AbstractButton ) + { + AbstractButton button = ( AbstractButton )theComponent; + String oldName = button.getName(); + button.setText( name ); + button.setName( name ); + button.setActionCommand( name ); + + // Replace the old name of the component with the new name + // in the ButtonPanel's list of components. + buttonList.setElementAt( name, buttonList.indexOf( oldName ) ); + } + + // TODO: If component is not a button (or doesn't define the getText() + // then what should be done. + } + else if ( propertyName.equals( "enabled" ) ) + { + Boolean enabled = ( Boolean )e.getNewValue(); + theComponent.setEnabled( ButtonPanel.this.isEnabled() ? enabled.booleanValue() : false ); + } + + // TODO: Icon? + } + } + + + // for testing + + public static void main( String[] argv ) + { + try + { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + JFrame dialog = new JFrame(); + BorderLayout bl = new BorderLayout( 20, 20 ); + + ButtonPanel panel = new ButtonPanel(); +// ButtonPanel panel = new ButtonPanel( new String[] { "OkayOkay", "CancelCancel" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, BorderLayout.CENTER ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + + panel.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + panel.getButton( "One" ).setEnabled( false ); + + dialog.pack(); + dialog.setVisible( true ); + + try + { + BeanInfo info = Introspector.getBeanInfo( ButtonPanel.class ); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for ( int i = 0; i < props.length; i++ ) + { + System.out.println( props[i].getName() ); + } + } + catch (Exception exc) + { + System.out.println( exc ); + } + + + + + } + + public void mouseDragged(MouseEvent e) + { + } + + public void mouseMoved(MouseEvent e) + { + } + + + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java new file mode 100644 index 0000000..5e847ae --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/CheckButtonPanel.java @@ -0,0 +1,272 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; +import java.util.List; +import java.util.Vector; + +import javax.swing.AbstractButton; +import javax.swing.JCheckBox; +import javax.swing.border.EmptyBorder; + +/** +* CheckButtonPanel is a simple extension of ButtonPanel. +* Differences are that it uses JCheckBoxes and the +* default alignment is vertical. The panel defaults to having +* no buttons selected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class CheckButtonPanel extends ButtonPanel +{ +/** +* Constructs a CheckButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public CheckButtonPanel() + { + super(); + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public CheckButtonPanel( String[] buttonList ) + { + super( buttonList ); + } + +/** +* Overridden to set vertical-center alignment and zero vgap. +*/ + protected void initLayout() + { + super.initLayout(); + buttonPanelLayout.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + buttonPanelLayout.setVgap( 0 ); + } + +/** +* Overridden to return a JRadioButton. +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; + JCheckBox newButton = new JCheckBox(); + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + + // reduce insets per java l&f guidelines (was 4 on each side) + newButton.setBorder( new EmptyBorder( 1, 4, 1, 4 ) ); + + return newButton; + } + +/** +* Sets the value of the button whose name matches the given text value. +* @param aName A String matching the name of one of the buttons. +* If null, empty, or not matching, nothing happens. +* @param aValue A value to set the button. +*/ + public void setValue(String aName, boolean aValue) + { + if ( aName != null ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( c.getName().equals( aName ) ) + { + ((AbstractButton)c).setSelected( aValue ); + c.repaint(); + return; + } + } + } + } + // null, empty, or not matching - exit. + System.out.println( "CheckButtonPanel.setValue: not found: " + aName ); + } + +/** +* Sets the state of the specified buttons to the specified value. +* @param aLabelArray An Array of Strings listing the buttons to be set. +* @param aValue The value to which the specified buttons will be set. +*/ + public void setValues(String[] aLabelArray, boolean aValue) + { + if ( aLabelArray != null ) + { + for ( int i = 0; i < aLabelArray.length; i++ ) + { + setValue( aLabelArray[i], aValue ); + } + } + } + +/** +* Convenience method to set all checkboxes on the panel. +* @param aValue The value to which all checkboxes on the panel will be set. +*/ + public void setAllValues(boolean aValue) + { + setValues( getLabels(), aValue ); + } + +/** +* Convenience method to check all boxes on the panel. +*/ + public void checkAll() + { + setAllValues( true ); + } + +/** +* Convenience method to clear all boxes on the panel. +*/ + public void clearAll() + { + setAllValues( false ); + } + +/** +* A convenience method to set only those buttons on the entire +* panel that should be checked. Buttons not in the list are unchecked. +* @param aLabelArray An Array of Strings listing the buttons to be set. +*/ + public void setCheckedValues(String[] aLabelArray) + { + setAllValues( false ); + setValues( aLabelArray, true ); + } + +/** +* Gets the labels of all checkboxes that are checked. +* @return A List of Strings containing the labels of the boxes that are checked. +*/ + public List getCheckedValueList() + { + Vector v = new Vector(); + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( ((AbstractButton)c).isSelected() ) + { + v.addElement(c.getName()); + } + } + } + return v; + } + +/** +* Gets the labels of all checkboxes that are checked. +* @return A String Array containing the labels of the boxes that are checked. +*/ + public String[] getCheckedValues() + { + List v = getCheckedValueList(); + String[] result = new String[ v.size() ]; + for ( int i = 0; i < v.size(); i++ ) + { + result[i] = (String) v.get(i); + } + return result; + } + +/** +* Gets the value of the specified button. +* @param aName A String matching the name of one of the buttons. +* @return True if the button is checked, False if it is not checked. +* NOTE: If the button is not found in the list, False is returned. +*/ + public boolean getValue( String aName ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( ( c instanceof AbstractButton ) && ( ((AbstractButton)c).isSelected() ) ) + { + if ( ((AbstractButton)c).getText().equals( aName ) ) + { + return ((AbstractButton)c).isSelected(); + } + } + } + return false; + } + + // for testing + + public static void main( String[] argv ) + { + try + { + javax.swing.UIManager.setLookAndFeel( javax.swing.UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + javax.swing.JFrame dialog = new javax.swing.JFrame(); + java.awt.BorderLayout bl = new java.awt.BorderLayout( 20, 20 ); + + CheckButtonPanel panel = new CheckButtonPanel( new String[] { "One", "Two", "Three" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, java.awt.BorderLayout.CENTER ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + + panel.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + panel.getButton( "One" ).setEnabled( false ); + panel.setValues( new String[] { "One" }, true ); + panel.setValue( "Three", true ); +// panel.setCheckedValues( new String[] { "Two" } ); + String[] values = panel.getCheckedValues(); + for ( int i = 0; i < values.length; i++ ) + { + System.out.println( values[i] ); + } + + dialog.pack(); + dialog.setVisible( true ); + + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java new file mode 100644 index 0000000..a0a14ac --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellEditor.java @@ -0,0 +1,84 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.DefaultCellEditor; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JTable; + +/** +* A TableCellEditor that edits colors - it launches a color dialog when clicked. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +class ColorCellEditor extends DefaultCellEditor { + Color currentColor = null; + + public ColorCellEditor(JButton b) + { + super(new JCheckBox()); // unfortunately, the constructor + // expects a check box, combo box, + // or text field. + editorComponent = b; + setClickCountToStart(1); // this is usually 1 or 2. + + // must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + fireEditingStopped(); + } + } + ); + } + + protected void fireEditingStopped() + { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() + { + return currentColor; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) + { + ((JButton)editorComponent).setText(value.toString()); + currentColor = (Color)value; + return editorComponent; + } +} + + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java new file mode 100644 index 0000000..0552183 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ColorCellRenderer.java @@ -0,0 +1,81 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.border.Border; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that renders colors. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ColorCellRenderer extends JLabel implements TableCellRenderer { + Border unselectedBorder = null; + Border selectedBorder = null; + boolean isBordered = true; + + public ColorCellRenderer(boolean isBordered) + { + super(); + this.isBordered = isBordered; + setOpaque(true); // must do this for background to show up. + } + + public Component getTableCellRendererComponent( + JTable table, Object color, + boolean isSelected, boolean hasFocus, + int row, int column) + { + setBackground((Color)color); + if (isBordered) + { + if (isSelected) + { + if (selectedBorder == null) + { + selectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getSelectionBackground()); + } + setBorder(selectedBorder); + } + else + { + if (unselectedBorder == null) + { + unselectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getBackground()); + } + setBorder(unselectedBorder); + } + } + return this; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java new file mode 100644 index 0000000..2bf8dd6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ComboBoxCellRenderer.java @@ -0,0 +1,57 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; + +import javax.swing.JComboBox; +import javax.swing.JTable; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that paints a JComboBox. Useful if +* you want to visibly display the JComboBox before the +* user clicks on the cell. +* +* @author bsafa@intersectsoft.com +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class ComboBoxCellRenderer extends JComboBox implements TableCellRenderer { + + public ComboBoxCellRenderer() + { + super(); + setOpaque(true); + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + setBackground(Color.white); + setSelectedItem(value); + return this; + } +} + + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java new file mode 100644 index 0000000..18ed035 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/DateTextField.java @@ -0,0 +1,630 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import java.awt.event.KeyEvent; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Vector; + +import javax.swing.JOptionPane; +import javax.swing.JTextField; + + +/** +* DateTextField is a "smart" text field that restricts the user's input. The +* input is restructed to a string representing a date format. +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class DateTextField extends JTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Use the current date for this text field. +*/ + public static final int CURRENT_DATE = 0; + +/** +* Use blanks for this text field. +*/ + public static final int BLANKS = 1; + +/** +* Use underscores for this text field. +*/ + public static final int UNDERSCORES = 2; + +/** +* Use just a 4-digit year for this text field. +*/ + public static final int YEAR = 3; + + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int PASTE = 22; // Ctl-V + private static final int CUT = 24; // Ctl-X + + +/******************************* +* DATA MEMEBERS +*******************************/ + private int defaultType = CURRENT_DATE; + + private boolean warningMessageActive = false; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default Constructor. +*/ + public DateTextField() + { + this(1, 1, 1999, 0); + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + +/** +* Constructor. +* @param month Number of the month, January being 1. +* @param data The day of the month. +* @param year The year. +*/ + public DateTextField(int month, int date, int year) + { + this(month, date, year, 0); + } + +/** +* Constructor. +* @param columns Width of the text field (in characters). +*/ + public DateTextField(int columns) + { + this(1, 1, 1998, columns); + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + +/** +* Constructor. +* @param month Number of the month, January being 1. +* @param data The day of the month. +* @param year The year. +* @param columns Width of the text field (in characters). +*/ + public DateTextField(int month, int date, int year, int columns) + { + super("", columns); + + super.setText(createDateString(month, date, year)); + + this.addFocusListener(new FocusAdapter() + { + public void focusLost(FocusEvent e) + { + if (!(e.isTemporary())) + { + validateDateString(e); + } + } + }); + } + +/** +* Sets the date type to display when the user has not entered any date yet. +* Default is the current date. +* @see #CURRENT_DATE +* @see #BLANKS +* @see #UNDERSCORES +* @param newDefaultType The type of date to display when there is no date data. +*/ + public void setDefaultType(int newDefaultType) + { + if (newDefaultType == BLANKS) + { + defaultType = BLANKS; + super.setText(" / / "); + } + else if (newDefaultType == UNDERSCORES) + { + defaultType = UNDERSCORES; + super.setText("__/__/____"); + } + else if (newDefaultType == YEAR) + { + defaultType = YEAR; + super.setText("0000"); + } + else + { + defaultType = CURRENT_DATE; + + Calendar rightNow = Calendar.getInstance(); + + super.setText(createDateString(rightNow.get(Calendar.MONTH) + 1, + rightNow.get(Calendar.DATE), + rightNow.get(Calendar.YEAR))); + } + } + +/** +* Returns the type of date to display when there is no user input. +* @see #CURRENT_DATE +* @see #BLANKS +* @see #UNDERSCORES +* @return The type of date to display when there is no date to display. +*/ + public int getDefaultType() + { + return defaultType; + } + +/** +* Sets the text field to the string representation of the specified date. +* @param aDate The date to set the text field to. +*/ + public void setDate(Date aDate) + { + Calendar aCalendar = Calendar.getInstance(); + + aCalendar.setTime(aDate); + + super.setText(createDateString(aCalendar.get(Calendar.MONTH) + 1, + aCalendar.get(Calendar.DATE), + aCalendar.get(Calendar.YEAR))); + } + +/** +* Sets the text field directly from a Date object. +* @param aDate The date to set the text field to. +*/ + public void setText( Date aDate ) + { + setDate( aDate ); + } + +/** +* Sets the text field to the date specified in the string. This is overridden +* from the parent class to insure a valid date is inputted. The format of the +* date expected is the type of date format this text field is currently set to. +* @param aString A string representing a date in this text field current format. +*/ + public void setText( String aString ) + { + Date testDate = null; + + if ( aString != null ) + { + ParsePosition position = new ParsePosition( 0 ); + + if ( defaultType == YEAR ) + { + SimpleDateFormat yearFormatter = new SimpleDateFormat( "yyyy" ); + testDate = yearFormatter.parse( aString, position ); + } + else + { + SimpleDateFormat fullDateFormatter = new SimpleDateFormat( "MM/dd/yyyy" ); + testDate = fullDateFormatter.parse( aString, position ); + } + } + + // The string is not a valid date, use default value for date then. + if ( testDate == null ) + { + Calendar aCalendar = Calendar.getInstance(); + + testDate = aCalendar.getTime(); + } + + setDate( testDate ); + } + +/** +* Returns the date as represented by the date string in the text field. +* @return The date in the text field. +*/ + public Date getDate() throws NumberFormatException + { + Calendar aCalendar = Calendar.getInstance(); + int year = 1980; + int month = 0; + int date = 1; + int[] tempArray = {1,3,5,7,8,10,12}; + Vector monthsWith31Days = new Vector(7); + + for (int i = 0; i < tempArray.length; ++i) + { + monthsWith31Days.addElement(new Integer(tempArray[i])); + } + + aCalendar.set(year, month, date, 12, 0, 0); + + try + { + String dateString = getText(); + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + dateString)); + + if (defaultType == YEAR) + { + year = Integer.parseInt(dateString); + + aCalendar.set(year, 0, 1, 12, 0, 0); + + return aCalendar.getTime(); + } + + month = Integer.parseInt(dateString.substring(0, 2).trim()); + date = Integer.parseInt(dateString.substring(3, 5).trim()); + year = Integer.parseInt(dateString.substring(6).trim()); + + if ((month < 1) || (month > 12)) + { + throw nfException; + } + + if ((date < 1) || (date > 31)) + { + throw nfException; + } + + if ((date == 31) && (!(monthsWith31Days.contains(new Integer(month))))) + { + throw nfException; + } + + if ((date == 30) && (month == 2)) + { + throw nfException; + } + + if ((date == 29) && (month == 2)) + { + if ((year % 100) == 0) + { + if ((year % 400) != 0) + { + throw nfException; + } + } + else + { + if ((year % 4) != 0) + { + throw nfException; + } + } + } + } + catch (IndexOutOfBoundsException ioobe) + { + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + getText())); + throw nfException; + } + catch (NumberFormatException nfe) + { + NumberFormatException nfException = new NumberFormatException(new String("Invalid Date String: " + getText())); + throw nfException; + } + + aCalendar.set(year, (month - 1), date, 12, 0, 0); + + return aCalendar.getTime(); + } + + public void processKeyEvent(KeyEvent e) + { + String currentString = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int currentCaretPosition = 0; + int selectionStart = 0; + int selectionEnd = 0; + int modifierPosition = 0; + int modifierDirection = 1; + char modifierCharacter; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean cut = false; + boolean keyPressed = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + cut = (newChar == CUT); + + keyPressed = (e.paramString().startsWith("KEY_PRESSED")); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste) || (cut))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + if ((isPrintableCharacter(newChar)) || (backspace) || (delete)) + { + // Both the key "pressed" and key "released" events get passed + // in here for the delete and backspace key. Only processes + // these keys if the event is key "pressed". + if (((backspace) || (delete)) && (!(keyPressed))) + { + // Don't do anything, pass through to consumption. + } + else + { + // Analyze the current contents of the field + currentString = getText(); + currentLength = currentString.length(); + + char[] tempText = new char[currentLength]; + + currentCaretPosition = getCaretPosition(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + // if a range is selected, then get rid of it and place the caret + // at the begginning of the range and continue processing. + if (selectionStart != selectionEnd) + { + selectionEnd = selectionStart; + setSelectionEnd(selectionEnd); + + currentCaretPosition = selectionStart; + setCaretPosition(currentCaretPosition); + } + + if (currentCaretPosition <= currentLength) + { + // a number of delete or backspace was pressed, delete and + // backspace deletes a number and places a "space" there + + // if caret at start of string and the backspace pressed OR + // caret at end of string and delete or number pressed THEN + // don't do anything, otherwise process key stroke + if (((currentCaretPosition == 0) && (backspace)) || + ((currentCaretPosition == currentLength) && (!(backspace)))) + { + // Don't do any processing. + } + else + { + modifierPosition = currentCaretPosition; + if (backspace) + { + modifierDirection = -1; + modifierPosition += modifierDirection; + } + + // Overwrite the current position with the new character + // inputted or overwrite using a space or underscore if + // the backspace or delete key was pressed. + if (defaultType != YEAR) + { + modifierCharacter = + ((delete)||(backspace)) ? + ((defaultType == UNDERSCORES) ? '_' : ' ') : + newChar; + } + else + { + // We are dealing with a 4-digit year. Overwrite + // with new character or "0" if delete or backspace + // was pressed. + modifierCharacter = ((delete)||(backspace)) ? ('0') : newChar; + } + + if (currentString.charAt(modifierPosition) == '/') + { + modifierPosition += modifierDirection; + } + + for (int i = 0; i < currentLength; ++i) + { + if (i == modifierPosition) + { + tempText[i] = modifierCharacter; + } + else + { + tempText[i] = currentString.charAt(i); + } + } + + testString = new String(tempText); + if (isValidString(testString)) + { + super.setText(testString); + if (backspace) + { + setCaretPosition(modifierPosition); + } + else + { + setCaretPosition(modifierPosition + 1); + } + } + } + } + } + + e.consume(); + } + else if ((cut) || (paste)) + { + e.consume(); + } + // else its a non-printable character, let it pass through + } + else + { + e.consume(); + } + } + + super.processKeyEvent(e); + } + + private boolean isValidCharacter(char aChar) + { + if (((aChar >= '!') && (aChar <= '/')) || ((aChar >= ':') && (aChar <= '~'))) + { + return false; + } + return true; + } + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + + private boolean isValidDate(int month, int date, int year) + { + if ((month < 1) || (month > 12)) + { + return false; + } + + if ((date < 1) || (date > 31)) + { + return false; + } + + if ((year < 0) || (year > 9999)) + { + return false; + } + + return true; + } + + private boolean isValidString(String aString) + { + return true; + } + + private String createDateString(int month, int date, int year) + { + String dateString = ""; + + if (isValidDate(month, date, year)) + { + if (defaultType != YEAR) + { + if (month < 10) + { + dateString = "0"; + } + + dateString += String.valueOf(month); + dateString += "/"; + + if (date < 10) + { + dateString += "0"; + } + + dateString += String.valueOf(date); + dateString += "/"; + } + + if (year < 1000) + { + dateString += "0"; + if (year < 100) + { + dateString += "0"; + if (year < 10) + { + dateString += "0"; + } + } + } + + dateString += String.valueOf(year); + } + else + { + if (defaultType == YEAR) + { + dateString = "1999"; + } + else + { + dateString = "01/01/1999"; + } + } + + return dateString; + } + + private void validateDateString(FocusEvent e) + { + if (!(warningMessageActive)) + { + try + { + getDate(); + } + catch (NumberFormatException nfe) + { + System.out.println("Invalid Date String!!!"); + warningMessageActive = true; + JOptionPane.showMessageDialog(this, "Invald Date: " + getText(), "Warning", JOptionPane.WARNING_MESSAGE); + warningMessageActive = false; + if (defaultType == YEAR) + { + super.setText("1999"); + } + else + { + super.setText("01/01/1999"); + } + } + } + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java new file mode 100644 index 0000000..b3e2a76 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/FormattedCellRenderer.java @@ -0,0 +1,284 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.text.Format; + +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +/** +* A cell renderer for dealing with formatted content. +* Subclasses can specify formats or colors or styles for specific values +* or locations in the table by overridding getFormatForContext(), +* getForegroundForContext() and/or getBackgroundForContext(). +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class FormattedCellRenderer extends DefaultTableCellRenderer +{ + protected Format currentFormat, defaultFormat; + protected Color defaultForeground, defaultBackground; + protected Font defaultFont; + +/** +* Default constructor with no specified format. +*/ + public FormattedCellRenderer() + { + this( (Format) null ); + } + +/** +* Constructor specifying a format for renderered content. +*/ + public FormattedCellRenderer( Format aFormat ) + { + currentFormat = null; + defaultFormat = aFormat; + defaultForeground = super.getForeground(); + defaultBackground = super.getForeground(); + } + +/** +* Returns the format currently in use to format cell content. +* @return The Format that is currently being used. +*/ + public Format getFormat() + { + return defaultFormat; + } + +/** +* Sets the format to be used to format cell content. +*/ + public void setFormat( Format aFormat ) + { + defaultFormat = aFormat; + } + +/** +* Overrides to retain the default foreground color, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setForeground(Color c) { + super.setForeground(c); + defaultForeground = c; + } + +/** +* Overrides to retain the default background color, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setBackground(Color c) { + super.setBackground(c); + defaultBackground = c; + } + +/** +* Overrides to retain the default font, +* much the same as the DefaultCellRenderer does. +* We have to do this because DefaultCellRenderer's +* ivars are private. +*/ + public void setFont(Font f) { + super.setFont(f); + defaultFont = f; + } + +/** +* Overridden to format the value with the appropriate Format. If the +* value cannot be formatted with the Format, the superclass method is called. +* @param value An Object to be formatted. +*/ + protected void setValue(Object value) + { + if ( currentFormat != null ) + { + try + { + +// if ( ( value instanceof Number ) && ( value.toString().indexOf( "E" ) != -1 ) ) +// { +// System.out.println( "FormattedCellRenderer.setValue: format = '" + currentFormat.getClass() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: value = '" + value + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: double value = '" + ((Number)value).doubleValue() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: float value = '" + ((Number)value).floatValue() + "'" ); +// System.out.println( "FormattedCellRenderer.setValue: converted = '" + currentFormat.format( value ) + "'" ); +// } + + // WORKAROUND: This works around what may be a rounding bug in DecimalFormat. (PR 256/297) + currentFormat.format( ZERO ); + + // DEBUG: code to test for weird one/zero problem (PR 256/297) + String result = currentFormat.format( value ); +/* above workaround seems to be working + if ( result.equals( "1" ) ) + { + System.out.println( "FormattedCellRenderer.setValue: Could be the ONE/ZERO problem!" ); + System.out.println( "FormattedCellRenderer.setValue: format = '" + currentFormat.getClass() + "'" ); + System.out.println( "FormattedCellRenderer.setValue: original value = '" + value + "'" ); + System.out.println( "FormattedCellRenderer.setValue: result = '" + result + "'" ); + } +*/ + setText( result ); + + +// setText( currentFormat.format( value ) ); + return; + } + catch ( IllegalArgumentException exc ) + { + // fall back on superclass implementation + } + } + super.setValue( value ); + } + + // FIXME: remove this when possible + private static Double ZERO = new Double( 0.0 ); + +/** +* Overridden to call context delegate methods. +*/ + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + Format format; + + // allow for context-sensitve formatting + format = getFormatForContext( table, value, isSelected, hasFocus, row, column ); + if ( format != null ) + { + currentFormat = format; + } + else + { + currentFormat = defaultFormat; + } + + Color color; + + // allow for context-sensitve foreground color + color = getForegroundForContext( table, value, isSelected, hasFocus, row, column ); + if ( color != null ) + { + super.setForeground( color ); + } + else + { + super.setForeground( defaultForeground ); + } + + // allow for context-sensitve background color + color = getBackgroundForContext( table, value, isSelected, hasFocus, row, column ); + if ( color != null ) + { + super.setBackground( color ); + } + else + { + super.setBackground( defaultBackground ); + } + + // have to call this here because super defaults to table's font + Component result = + super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column ); + // NOTE: DefaultTableCellRenderer returns itself. + + // allow for context-sensitve font + Font font = getFontForContext( table, value, isSelected, hasFocus, row, column ); + if ( font != null ) + { + result.setFont( font ); + } + else + { + result.setFont( defaultFont ); + } + + return result; + + } + +/** +* Override this method to provide a specific format for the +* specific cell to be rendered by this component. Any format +* returned by this method will take precedence of the format +* specified by setFormat().

+* This default implementation returns null. +* @return A Format for this cell, or null to rely on the the +* format specified by setFormat(). +*/ + public Format getFormatForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a foreground color for the renderer. +* Because the table specifies colors for selected cells, +* these colors will only be used when renderering unselected cells.

+* This default implementation returns null. +* @return A Color for the foreground of the cell, or null to rely on +* the table's default color scheme. +*/ + public Color getForegroundForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a background color for the renderer. +* Because the table specifies colors for selected cells, +* these colors will only be used when renderering unselected cells.

+* This default implementation returns null. +* @return A Color for the background of the cell, or null to rely on +* the table's default color scheme. +*/ + public Color getBackgroundForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +/** +* Override this method to provide a font for the renderer.

+* This default implementation returns null. +* @return A Font for the cell, or null to rely on the table's default font. +*/ + public Font getFontForContext(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + return null; + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java new file mode 100644 index 0000000..8320d08 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/IconCellRenderer.java @@ -0,0 +1,845 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +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.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.Enumeration; +import java.util.EventObject; +import java.util.Vector; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.tree.TreeCellEditor; +import javax.swing.tree.TreeCellRenderer; + +/** +* A cell renderer that displays icons in addition to text, +* and additionally is an editor in case you want to click +* the icon to trigger some kind of action. +* You probably should override both getStringForContext and +* getIconForContext to achieve your desired results. +* To receive mouse clicks, set the same instance of the +* renderer as the editor for the same component.

+* +* One notable addition is that this class is an action event +* broadcaster. ActionEvents are broadcast when the mouse is +* clicked on the button with an action event containing a +* user-configurable string that defaults to CLICKED.

+* +* The renderer itself can be used as a JComponent if +* you need something like a JLabel that allows you to click +* on the icon. You will want to call setIcon and setText +* to configure the component since the renderer method would +* not be called. (If you add an instance of the renderer +* to a container, you cannnot use the same instance as an +* editor in a table, tree, or list.) +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class IconCellRenderer extends JPanel + implements TableCellRenderer, TableCellEditor, + TreeCellRenderer, TreeCellEditor, ListCellRenderer, + Runnable, ActionListener, MouseListener +{ + public static final String CLICKED = "CLICKED"; + + /** + * The panel that is re-used to render everything. + * This is returned by getRendererForContext. + */ + protected JPanel rendererPanel; + protected JLabel rendererLabel; + protected JButton rendererButton; + + /** + * The panel that is used to receive mouse clicks. + * It must be a different component from rendererPanel. + * This is returned by getEditorForContext. + */ + protected JPanel editorPanel; + protected JLabel editorLabel; + protected JButton editorButton; + + private Object lastKnownValue; + private JComponent lastKnownComponent; + + // do as DefaultTableCellRenderer does + private Border noFocusBorder; + private Border treeFocusBorder; + private Color unselectedForeground; + private Color unselectedBackground; + + private Vector actionListeners; + private String actionCommand; + private Vector cellEditorListeners; + + private boolean editable; + private boolean clickable; + + /** + * Default constructor. + */ + public IconCellRenderer() + { + editable = true; + clickable = true; + + noFocusBorder = new EmptyBorder(1, 1, 1, 1); + treeFocusBorder = new LineBorder( + UIManager.getColor("Tree.selectionBorderColor") ); + setActionCommand( CLICKED ); + + rendererPanel = new JPanel(); + rendererPanel.setLayout( new GridBagLayout() ); + + editorPanel = this; + editorPanel.setLayout( new GridBagLayout() ); + + // set up constraints + GridBagConstraints imageConstraints = new GridBagConstraints(); + imageConstraints.gridx = 0; + GridBagConstraints labelConstraints = new GridBagConstraints(); + labelConstraints.fill = GridBagConstraints.HORIZONTAL; + labelConstraints.gridx = 1; + labelConstraints.weightx = 1.0; + labelConstraints.ipadx = 1; + labelConstraints.insets = new Insets( 0, 1, 0, 0 ); // sweat the pixel + + // make the editor panel go away when not in use + // and pass through all mouse events to container + + //this is not very useful since editorLabel and editorButton + //get all of the events + editorPanel.addMouseListener( this ); + + rendererLabel = new JLabel(); + rendererLabel.setOpaque( false ); + rendererPanel.add( rendererLabel, labelConstraints ); + + editorLabel = new JLabel(); + editorLabel.setText( "" ); // default state + editorLabel.setOpaque( false ); + editorPanel.add( editorLabel, labelConstraints ); + + unselectedForeground = rendererLabel.getForeground(); + unselectedBackground = rendererLabel.getBackground(); + + rendererButton = new JButton(); + rendererButton.setBorder( null ); + rendererButton.setBorderPainted( false ); + rendererButton.setContentAreaFilled( false ); + rendererButton.setFocusPainted( false ); + rendererButton.setMargin( new Insets( 0, 0, 0, 0 ) ); + rendererPanel.add( rendererButton, imageConstraints ); + + editorButton = new JButton(); + editorButton.setEnabled( clickable ); // default state + editorButton.setIcon( null ); // default state + editorButton.setBorder( null ); + editorButton.setBorderPainted( false ); + editorButton.setContentAreaFilled( false ); + editorButton.setFocusPainted( false ); + editorButton.setMargin( new Insets( 0, 0, 0, 0 ) ); + editorPanel.add( editorButton, imageConstraints ); + + editorButton.addActionListener( this ); + + //add these in order to dispatch the MouseEvents + //to the lastKnownComponent, and proper management of + //DnD operations + editorLabel.addMouseListener( this ); + editorButton.addMouseListener( this ); + } + +/** +* Returns the text string currently displayed in the editor component. +*/ + public String getText() + { + return editorLabel.getText(); + } + +/** +* Sets the text string displayed in the editor component. +* Default is an empty string. +*/ + public void setText( String aString ) + { + editorLabel.setText( aString ); + } + +/** +* Returns the icon currently displayed in the editor component. +*/ + public Icon getIcon() + { + return editorButton.getIcon(); + } + +/** +* Sets the icon currently displayed in the editor component. +* Default is null. +*/ + public void setIcon( Icon anIcon ) + { + editorButton.setIcon( anIcon ); + if ( !isClickable() ) + { + editorButton.setDisabledIcon( anIcon ); + } + } + +/** +* Returns whether the editor component's label text is editable. +*/ + public boolean isEditable() + { + return editable; + } + +/** +* Sets whether the editor component's label text is editable. +* Default is true. Editable text is not yet implemented. +*/ + public void setEditable( boolean isEditable ) + { + editable = isEditable; + } + +/** +* Returns whether the editor component's icon is clickable. +*/ + public boolean isClickable() + { + return clickable; + } + +/** +* Sets whether the editor component's icon is clickable. +* Default is true. +*/ + public void setClickable( boolean isClickable ) + { + clickable = isClickable; + editorButton.setEnabled( clickable ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getListCellRendererComponent(JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) + { + lastKnownComponent = list; + return getRendererForContext( + list, value, index, 0, isSelected, cellHasFocus, false, true ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) + { + lastKnownComponent = table; + return getRendererForContext( + table, value, row, column, isSelected, hasFocus, false, true ); + } + +/** +* Returns the component from getRendererForContext. +*/ + public Component getTreeCellRendererComponent(JTree tree, + Object value, + boolean selected, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) + { + lastKnownComponent = tree; + return getRendererForContext( + tree, value, row, 0, selected, hasFocus, expanded, leaf ); + } + +/** +* Returns getEditorForContext with the same parameters with hasFocus true. +*/ + public Component getTableCellEditorComponent(JTable table, + Object value, boolean isSelected, int row, int column) + { + lastKnownValue = value; + lastKnownComponent = table; + return getEditorForContext( + table, value, row, column, isSelected, true, false, true ); + } + +/** +* Returns the component from getEditorForContext with hasFocus true. +*/ + public Component getTreeCellEditorComponent(JTree tree, + Object value, + boolean isSelected, + boolean expanded, + boolean leaf, + int row) + { + + + lastKnownValue = value; + lastKnownComponent = tree; + + return getEditorForContext( + tree, value, row, 0, isSelected, true, expanded, leaf ); + } + +/** +* This default implementation returns a JPanel that is configured by +* calling configureComponentForContext. +* @return An component that is used to render content. +*/ + public Component getRendererForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + + + configureComponentForContext( rendererPanel, rendererButton, rendererLabel, + container, value, row, column, + isSelected, hasFocus, isExpanded, isLeaf ); + return rendererPanel; + } + +/** +* This method returns a separate component that should be visually +* identical to the renderer component. We can't simply reuse the +* renderer component because the renderer is still used to paint +* the table while the editor component is displayed. Clicks are +* received on this component. +* This default implementation returns a JPanel that is configured by +* calling configureComponentForContext. +* @return A component used to receive clicks on the cell. +*/ + public Component getEditorForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + configureComponentForContext( editorPanel, editorButton, editorLabel, + container, value, row, column, + true, hasFocus, isExpanded, isLeaf ); // editor should always be selected + + return editorPanel; + } + +/** +* Called to configure components +*/ + protected void configureComponentForContext( + JPanel component, JButton iconButton, JLabel label, + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + if (hasFocus) + { + if ( container instanceof JTable ) + { + component.setBorder( + UIManager.getBorder("Table.focusCellHighlightBorder") ); + } + else + { + component.setBorder( noFocusBorder ); + } + + if ( container instanceof JTree ) // was: (false) + { + label.setBorder( treeFocusBorder ); + } + else + { + label.setBorder( noFocusBorder ); + } + } + else + { + label.setBorder(noFocusBorder); + component.setBorder(noFocusBorder); + } + + if (isSelected) + { + if ( container instanceof JTree ) + { + label.setOpaque( true ); + label.setForeground(UIManager.getColor("Tree.selectionForeground")); + label.setBackground(UIManager.getColor("Tree.selectionBackground")); + component.setBackground(container.getBackground()); + } + else if ( container instanceof JTable ) + { + label.setOpaque( false ); + label.setForeground( ((JTable)container).getSelectionForeground() ); + component.setBackground(((JTable)container).getSelectionBackground()); + } + else + { + label.setOpaque( false ); + label.setForeground(UIManager.getColor("Table.selectionForeground")); + component.setBackground(UIManager.getColor("Table.selectionBackground")); + } + } + else + { + label.setOpaque( false ); + label.setForeground(container.getForeground()); + component.setBackground(container.getBackground()); + } + + label.setFont(container.getFont()); + + Icon icon = getIconForContext( + container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf ); + iconButton.setIcon( icon ); + if ( !isClickable() ) + { + iconButton.setDisabledIcon( icon ); + } + + String text = getStringForContext( + container, value, row, column, isSelected, hasFocus, isExpanded, isLeaf ); + + if ( ( text == null ) || ( "".equals( text ) ) ) + { + if ( ! label.getText().equals( "" ) ) + label.setText( "" ); + } + else + { + if ( ! label.getText().equals( text ) ) + label.setText( text ); + } + } + +/** +* Override this method to provide an icon for the renderer. +* This default implementation returns null. +* @return An icon to be displayed in the cell, or null to omit the +* icon from the cell. +*/ + public Icon getIconForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + return null; + } + +/** +* Override this method to provide a string for the renderer. +* This default implementation returns toString on the value parameter, +* or null if the value is null. +* @return A string to be displayed in the cell. +*/ + public String getStringForContext( + JComponent container, Object value, + int row, int column, + boolean isSelected, boolean hasFocus, + boolean isExpanded, boolean isLeaf ) + { + if ( value == null ) return null; + return value.toString(); + } + + /** + * Adds the specified listener to the list of listeners + * to be notified when the button receives a click. + */ + public void addActionListener( ActionListener aListener ) + { + if ( actionListeners == null ) + { + actionListeners = new Vector( 2 ); + } + actionListeners.add( aListener ); + } + + /** + * Removes the specified listener from the list of listeners + * to be notified when the button receives a click. + */ + public void removeActionListener( ActionListener aListener ) + { + actionListeners.remove( aListener ); + } + + /** + * Broadcasts the specified action event to all listeners. + */ + protected void fireActionEvent( ActionEvent anActionEvent ) + { + if ( actionListeners == null ) return; + // vector's enumeration is not fail-fast + Enumeration e = actionListeners.elements(); + while ( e.hasMoreElements() ) + { + ((ActionListener)e.nextElement()).actionPerformed( anActionEvent ); + } + } + + /** + * Returns the action command broadcast when this icon + * receives a click. Defaults to CLICKED. + */ + public String getActionCommand() + { + return actionCommand; + } + + /** + * Sets the action command broadcast when this table + * receives a double click. + */ + public void setActionCommand( String anActionCommand ) + { + actionCommand = anActionCommand; + } + +// interface CellEditor + + /** + * Returns lastKnownValue, although this should not be called. + */ + public Object getCellEditorValue() + { + return lastKnownValue; + } + + /** + * Returns true. + */ + public boolean isCellEditable(EventObject anEvent) + { + return true; + } + + /** + * Returns true. + */ + public boolean shouldSelectCell(EventObject anEvent) + { + return true; + } + + /** + * Fires an editing stopped event and returns true. + */ + public boolean stopCellEditing() + { + ChangeEvent event = new ChangeEvent( this ); + if ( cellEditorListeners != null ) + { + // vector's enumeration is not fail-fast + Enumeration e = cellEditorListeners.elements(); + while ( e.hasMoreElements() ) + { + // broadcast editing cancelled since no value is edited + ((CellEditorListener)e.nextElement()).editingCanceled( event ); + } + } + lastKnownComponent = null; + return true; + } + + /** + * Fires an editing cancelled event and returns true. + */ + public void cancelCellEditing() + { + //HACK: cancelCellEditing() causes for the dragGesture + //to be NOT recognized AT ALL since on the next MOUSE_PRESSED + //the cell editor first needs to startEditing() [if in the tree + //the CellEditorListener is a BasicTreeUI class] + //(before the drag gesture event can be recognized). + //Also the lastKnownComponent should not be set to null, + //none of the mouse events won't dispathced to the lastKnownComponent + //in that case. + + //Not calling it at all does seem to fix it, but what are the + //consequences??? + //Trying to workaround this might solve it, but it introduces having + //an extra listener (a MouseMotionListnener), which might be wasteful + //(i.e. only if a Mouse_dragged event has been initiated, but DragGesture + //hasn't been recognized, postpone calling this till finish the DnD event) + //But what if do DnD and not exited ??? The mouseExited() is not called + //anyway until the DnD event is done. + + ChangeEvent event = new ChangeEvent( this ); + if ( cellEditorListeners == null ) return; + // vector's enumeration is not fail-fast + Enumeration e = cellEditorListeners.elements(); + + while ( e.hasMoreElements() ) + { + ((CellEditorListener)e.nextElement()).editingCanceled( event ); + } + + //DO not nullify this + lastKnownComponent = null; + } + + /** + * Adds the specified listener to the list of listeners + * to be notified when the table receives a double click. + */ + public void addCellEditorListener( CellEditorListener aListener ) + { + if ( cellEditorListeners == null ) + { + cellEditorListeners = new Vector( 2 ); + } + cellEditorListeners.add( aListener ); + } + + /** + * Removes the specified listener from the list of listeners + * to be notified when the table receives a double click. + */ + public void removeCellEditorListener( CellEditorListener aListener ) + { + cellEditorListeners.remove( aListener ); + } + +// interface ActionListener + + /** + * Puts ourself on the end of the event queue for + * firing our action event to all listeners. + */ + public void actionPerformed( ActionEvent evt ) + { + //commented out in order NOT to set lastKnownComponent to null, since + //if this object is inside a table or tree, relying on getCellEditorValue() + //to return the currently edited object + //cancelCellEditing(); + + SwingUtilities.invokeLater( this ); + } + +// interface Runnable + + /** + * Fires the action event to all listeners. + * This is triggered by a click on the icon. + */ + public void run() + { + fireActionEvent( new ActionEvent( this, 0, getActionCommand() ) ); + } + +// interface MouseListener + + /** + * Passes through editor mouse clicks to last known component. + * (left click only) + */ + public void mouseClicked(MouseEvent e) + { + if(lastKnownComponent != null){ + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + + } + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + + /** + * Passes through editor right-mouse (popup trigger) mouse events to last known component. + * Needed for possible displaying of popup menus on right click + */ + public void mousePressed(MouseEvent e) + { + if ( e.isPopupTrigger() ) + { + if(lastKnownComponent != null) + { + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + } + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + } + + /** + * Does nothing. + */ + public void mouseReleased(MouseEvent e) + { + if ( e.isPopupTrigger() ) + { + if(lastKnownComponent != null){ + + Object source = e.getSource(); + if(source != null) + { + if(source == editorPanel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorPanel, e, lastKnownComponent ) ); + } + + else if(source == editorLabel) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorLabel, e, lastKnownComponent ) ); + } + + else if(source == editorButton) + { + lastKnownComponent.dispatchEvent( + SwingUtilities.convertMouseEvent( + editorButton, e, lastKnownComponent ) ); + } + } + } + } + } + + /** + * Does nothing. + */ + public void mouseEntered(MouseEvent e) + { + } + + /** + * Cancels cell editing. + */ + public void mouseExited(MouseEvent e) + { + Object source = e.getSource(); + if(source != null && source instanceof JComponent){ + //need to convert the Point from the source's coordinate system to editorPanel's coordinate system. + //(note that simple editorPanel.contains(e.getPoint()) fails if source is editorButton) + + Point convertedPoint = SwingUtilities.convertPoint((JComponent) source, e.getPoint(), editorPanel); + + //check if exited from editorButton, but still inside the editorPanel (works for editorLabel as well) + if(!editorPanel.contains(convertedPoint)){ + + //This was getting called before, but it interfers with the DnD operation + cancelCellEditing(); + } + } + } + + /* This might be redundant + public void cleanUp(){ + + //since cancelCellEditing() was never called call it now + cancelCellEditing(); + stopCellEditing(); + + editorButton.removeActionListener( this ); + editorPanel.removeMouseListener( this ); + editorLabel.removeMouseListener( this ); + editorButton.removeMouseListener( this ); + lastKnownComponent = null; + lastKnownValue = null; + } + */ +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java new file mode 100644 index 0000000..cdaa218 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/ImagePanel.java @@ -0,0 +1,104 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.ImageObserver; + +import javax.swing.JPanel; + +/** +* A JPanel that renders an image, tiling as necessary to +* fill the panel. +* The preferred size of the panel is the size of the image +* and will change until the image is fully loaded, so using +* a media tracker is recommended. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ +public class ImagePanel extends JPanel implements ImageObserver +{ + protected Image image; + protected int imageWidth, imageHeight; + + public ImagePanel() + { + this( null ); + } + + public ImagePanel( Image anImage ) + { + image = anImage; + if ( anImage != null ) + { + prepareImage( image, this ); + // these may return -1 + imageWidth = image.getWidth( this ); + imageHeight = image.getHeight( this ); + } + else + { + imageWidth = 0; + imageHeight = 0; + } + } + + protected void paintComponent(Graphics g) + { + if ( ( image != null ) && ( imageWidth > 0 ) && ( imageHeight > 0 ) ) + { + int width = getWidth(); + int height = getHeight(); + + for ( int x = 0; x < width; x += imageWidth ) + { + for ( int y = 0; y < height; y += imageHeight ) + { + g.drawImage( image, x, y, + imageWidth, imageHeight, + getBackground(), this ); + } + } + } + } + + public boolean imageUpdate(Image img, + int infoflags, + int x, + int y, + int width, + int height) + { + imageWidth = width; + imageHeight = height; + setPreferredSize( new Dimension( width, height ) ); + revalidate(); + repaint(); + + if ( ( infoflags & ImageObserver.ALLBITS ) == ImageObserver.ALLBITS ) + { + return false; + } + return true; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java new file mode 100644 index 0000000..55c1e36 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/InfoPanel.java @@ -0,0 +1,1693 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.AWTEventMulticaster; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.BeanInfo; +import java.beans.Introspector; +import java.beans.MethodDescriptor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.Box; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +/** +* InfoPanel uses labels and textfields (or any other component - see below) +* to display a list of keys and values in a well-aligned and consistent manner, +* conforming to alignment and pixel spacing in the java look and feel +* design guidelines. +*

+* +* Each key is displayed in a label to the left of the component that contains +* the corresponding value. Each row is displayed starting at the top of the +* component's available area. Each row's height is the maximum preferred +* height of its components and the field itself gets as much of the width as +* it can, dependent on the length of the longest label.

+* +* The values in the fields can be editable, and the +* current value can be retrieved using the key - for this reason, unique keys +* are recommended.

+* +* As a convenience, push buttons may be placed across the +* bottom of the panel in a manner similar to ButtonPanel.

+* +* The panel forwards any ActionEvents generated by the components and +* buttons on it to all registered listeners.

+* +* Optionally, any component can be used instead of a textfield. +* However, get/setValueForKey() and get/setEditable() +* may not work for those components. Use getComponentForKey() to +* access them instead. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class InfoPanel extends JPanel implements ActionListener +{ +/** +* Special label for an empty pair - a label and component +* that take up space but are hidden from view. This might +* be useful for achieving certain layouts. +*/ + public static final String HIDDEN = "(hidden)"; + + /** Cache for the introspectComponent method */ + private static Map _method_cache = + Collections.synchronizedMap( new HashMap(30) ); + + protected Container listContainer = null; + protected int hgap; // set in constructor + protected int vgap; // set in constructor + protected int margin; // set in constructor + protected int columns; // set in constructor + protected List fields = null; + protected List labels = null; + protected List fieldSpacers = null; + protected ButtonPanel buttonPanel = null; + protected boolean isEditable = true; + protected String prefix; + protected String postfix; + protected int labelAnchor; + protected int labelAlign; +// protected Component marginStrut = null; + + // for action multicasting + protected ActionListener actionListener = null; + +/** +* Constructs an empty InfoPanel. +*/ + public InfoPanel() + { + hgap = 12; // per java l&f guidelines + vgap = 6; // java l&f says 11 + columns = 1; // default columns + margin = 0; // default margin: none + prefix = ""; // default prefix: none + postfix = ":"; // per java l&f guidelines + fields = new ArrayList(); + labels = new ArrayList(); + labelAnchor = GridBagConstraints.NORTHWEST; + // per java l&f guidelines (CENTER is nicer) + labelAlign = SwingConstants.LEFT; + // per java l&f guidelines + + doInitialLayout(); + } + +/** +* Constructs an InfoPanel with the specified labels +* each paired with a blank textfield. +* @param labelArray An Array containing the labels in the +* order in which they should appear from top to bottom. +* A null value produces an empty panel. +*/ + public InfoPanel( String[] labelArray ) + { + this(); + setLabels( labelArray ); + } + +/** +* Creates a set of labels and empty textfields after first +* clearing all existing components on the panel. +* @param labelArray An Array containing the labels in the order +* in which they should appear from top to bottom. A null +* value will clear the panel. +*/ + public void setLabels( String[] labelArray ) + { + removeAll(); + if ( labelArray == null ) return; // null clears panel + for ( int i = 0; i < labelArray.length; i++ ) + { + addPair( labelArray[i], new JTextField() ); + } + } + +/** +* Retrieves the labls for the components on the panel +* in the order in which they are displayed from top WIDTH bottom. +* These are the keys used to reference values or to reference +* the components directly. +* @return An Array of Strings containing the labels. +*/ + public String[] getLabels() + { + int length = fields.size(); + String[] labelArray = new String[ length ]; + for ( int i = 0; i < length; i++ ) + { + labelArray[i] = ((Component)fields.get(i)).getName(); + } + return labelArray; + } + +/** +* Retrieves the constant used to anchor the labels in place. +* The default value is GridBagConstraints.NORTHWEST. +*/ + public int getLabelAnchor() + { + return labelAnchor; + } + +/** +* Sets the constant used to anchor the labels in place +* and reflows the layout. +* @param anAnchorConstant An anchor constant from +* GridBagConstraints. +*/ + public void setLabelAnchor( int anAnchorConstant ) + { + labelAnchor = anAnchorConstant; + updateLabels(); + } + +/** +* Retrieves the constant used to align the labels in place. +* The default value is GridBagConstraints.CENTER. +*/ + public int getLabelAlignment() + { + return labelAlign; + } + +/** +* Sets the constant used to align the labels in place +* and reflows the layout. +* @param anAlignmentConstant LEFT, CENTER, or RIGHT constants +* from SwingUtilities. +*/ + public void setLabelAlignment( int anAlignmentConstant ) + { + labelAlign = anAlignmentConstant; + updateLabels(); + } + +/** +* Factory method for creating panel spacers. +* This implementation returns a JPanel with +* opaque set to false. Override to customize. +*/ + public JPanel createPanel() + { + JPanel result = new JPanel(); + result.setOpaque( false ); + return result; + } + +/** +* This method is responsible for the initial layout of the panel. +* All labels and textfields will later added to listContainer. +* This method is responsible for initializing listContainer. +*/ + protected void doInitialLayout() + { + listContainer = createPanel(); + listContainer.setLayout( new BetterGridBagLayout() ); + this.setLayout( new BorderLayout() ); + this.add( listContainer, BorderLayout.NORTH ); + + //listContainer.setBackground( Color.blue ); // useful for testing + //this.setBackground( Color.red ); + } + +/** +* Changes the horizontal spacing between the label and the components in the panel. +* Note: Assumes listContainer uses a GridBagLayout. +* @param newHgap the new spacing, in pixels. May not be negative. +*/ + public void setHgap( int newHgap ) + { + if ( newHgap < 0 ) return; // may not be negative + this.hgap = newHgap; + updateGaps(); + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current horizontal spacing between components. +* @return the current horizontal spacing, in pixels. +*/ + public int getHgap() + { + return this.hgap; + } + +/** +* Changes the vertical spacing between components in the panel. +* Note: Assumes listContainer uses a GridBagLayout. +* @param newVgap the new spacing, in pixels. May not be negative. +*/ + public void setVgap( int newVgap ) + { + if ( newVgap < 0 ) return; // may not be negative + this.vgap = newVgap; + updateGaps(); + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current vertical spacing between components. +* @return the current vertical spacing, in pixels. +*/ + public int getVgap() + { + return this.vgap; + } + +/** +* Sets the minimum width for the labels column. +* This left margin will grow if one of the labels +* is wider than this value. +* Note: assumes GridBagLayout. +* @param newMargin the new minimum margin in pixels. May not be negative. +*/ + public void setMargin( int newMargin ) + { + if ( newMargin < 0 ) return; // may not be negative + this.margin = newMargin; + + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints constraints = null; + Component c = null; + int count = listContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = listContainer.getComponent( i ); + constraints = gridBag.getConstraints( c ); + if ( constraints.gridy == 0 && constraints.gridx % 2 == 0 ) + { // if this is a label spacer + // replace it with an appropriately sized box + listContainer.remove( c ); + listContainer.add( Box.createHorizontalStrut( this.margin ), constraints ); + } + } + } + + this.revalidate(); + this.repaint(); + + } + +/** +* Gets the current minimum margin for the labels column. +* @return the current minimum margin in pixels. +*/ + public int getMargin() + { + return this.margin; + } + +/** +* Sets the number of columns for the panel. +* Label/Component pairs will start from the top left +* and fill in to the right before wrapping to the +* next row. The default number of columns is one. +* Note: assumes GridBagLayout. +* @param newColumns the new number of columns. May not be less than one. +*/ + public void setColumns( int newColumns ) + { + if ( newColumns < 1 ) return; // may not be less than one. + int oldColumns = this.columns; + this.columns = newColumns; + + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + int count = listContainer.getComponentCount(); + Component[] components = listContainer.getComponents(); + GridBagConstraints[] constraints = new GridBagConstraints[ components.length ]; + for ( int i = 0; i < components.length; i++ ) + { + constraints[i] = gridBag.getConstraints( components[i] ); + } + listContainer.removeAll(); + for ( int i = 0; i < components.length; i++ ) + { + if ( constraints[i].gridy != 0 ) + { // ignore first row which is reserved for spacers. + + // translate component to new position + // (columns*2 accounts for two grid columns for one "actual" column) + int index = ( constraints[i].gridy - 1 ) * oldColumns*2 + constraints[i].gridx; + constraints[i].gridy = ( index / (newColumns*2) ) + 1; + constraints[i].gridx = index % (newColumns*2) ; + listContainer.add( components[i], constraints[i] ); + } + } + createSpacers(); // replace the spacers + updateGaps(); + } + + this.revalidate(); + this.repaint(); + + } + +/** +* Sets the vertical weight used for determining how to distribute additional +* vertical space in the component. +* @param aComponent Key that exists in the layout. +* @return weighty The weight of the component, or -1.0 if not found. +*/ + public double getVerticalWeightForKey( String key ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return -1.0; + if ( ! ( listContainer.getLayout() instanceof GridBagLayout ) ) return -1.0; + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints gbc = layout.getConstraints( c ); + return gbc.weighty; + } + +/** +* Sets the vertical weight used for determining how to distribute additional +* vertical space in the component. By default, all weights are zero, so each +* component gets its preferred height. If any weights are specified, then +* additional space is allocated to those components proportionately. +* @param aComponent Key that exists in the layout. +* @param weighty The new weight. +*/ + public void setVerticalWeightForKey( String key, double weighty ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return; + if ( ! ( listContainer.getLayout() instanceof GridBagLayout ) ) return; + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints gbc = layout.getConstraints( c ); + gbc.weighty = weighty; + layout.setConstraints( c, gbc ); + // handle adding on-the-fly + updateGaps(); + this.revalidate(); + this.repaint(); + } + +/** +* Gets the current number of columns. +* @return the current number of columns. +*/ + public int getColumns() + { + return this.columns; + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a blank JPanel will be used. +*/ + public void addPair( String key, Component component ) + { + addRow( key, new Component[] { component } ); + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a blank JPanel will appear. +*/ + public void addRow( String key, Component component ) + { + addRow( key, new Component[] { component } ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param components An array of components that will be placed next to the label. +* Any nulls in the list will be replaced with blank JPanels. +*/ + public void addRow( + String key, Component[] components ) + { + addCompositeComponent( key, makeCompositeComponent( key, components ) ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the components - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param west A component that will appear to the left of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +* @param center A component that will appear between the other components, +* taking up available space. +* A null will be replaced with a blank JPanel. +* @param east A component that will appear to the right of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +*/ + public void addRow( + String key, Component west, Component center, Component east ) + { + addCompositeComponent( key, + makeCompositeComponent( key, + west, center, east ) ); + } + +/** +* Appends a label containing a key and the specified components +* to the bottom of the panel. Any registered action listeners +* will receive action events from the components - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param west A component that will appear to the left of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +* @param north A component that will appear above all the other components, +* as tall as its preferred height and as wide as the info panel itself. +* @param center A component that will appear between the other components, +* taking up available space. A null will be replaced with a blank JPanel. +* @param south A component that will appear below all the other components, +* as tall as its preferred height and as wide as the info panel itself. +* @param east A component that will appear to the right of the other components, +* as wide as its preferred width and as tall as the tallest of the other components. +* A null will be replaced with a blank JPanel. +*/ + public void addRow( + String key, Component west, Component north, + Component center, Component south, Component east ) + { + addCompositeComponent( key, + makeCompositeComponent( key, + west, north, center, south, east ) ); + } + +/** +* Produces a container that contains the specified components, +* using GridLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component[] components ) + { + JPanel panel = createPanel(); + if ( components.length != 0 ) + { + panel.setLayout( new GridLayout( 1, components.length, hgap, vgap ) ); + + Component c; + for ( int i = 0; i < components.length; i++ ) + { + c = components[i]; + if ( c != null ) + { + introspectComponent( c, key ); + panel.add( c ); + } + } + } + return panel; + } + +/** +* Produces a container that contains the specified components, +* using BorderLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component west, Component center, Component east ) + { + JPanel panel = createPanel(); + panel.setLayout( new BorderLayout( hgap, vgap ) ); + + if ( west != null ) + { + introspectComponent( west, key ); + panel.add( west, BorderLayout.WEST ); + } + + if ( center != null ) + { + introspectComponent( center, key ); + panel.add( center, BorderLayout.CENTER ); + } + + if ( east != null ) + { + introspectComponent( east, key ); + panel.add( east, BorderLayout.EAST ); + } + + return panel; + } + +/** +* Produces a container that contains the specified components, +* using BorderLayout. Nulls are ignored. +* This implementation returns a JPanel. +*/ + protected Container makeCompositeComponent( + String key, Component west, Component north, + Component center, Component south, Component east ) + { + JPanel panel = createPanel(); + panel.setLayout( new BorderLayout( hgap, vgap ) ); + + if ( west != null ) + { + introspectComponent( west, key ); + panel.add( west, BorderLayout.WEST ); + } + + if ( north != null ) + { + introspectComponent( north, key ); + panel.add( north, BorderLayout.WEST ); + } + + if ( center != null ) + { + introspectComponent( center, key ); + panel.add( center, BorderLayout.CENTER ); + } + + if ( south != null ) + { + introspectComponent( south, key ); + panel.add( south, BorderLayout.CENTER ); + } + + if ( east != null ) + { + introspectComponent( east, key ); + panel.add( east, BorderLayout.EAST ); + } + + return panel; + } + +/** +* Override to return a specific component to be used +* as a label. This implementation calls createLabel(). +*/ + protected Component createLabelForKey( String aKey ) + { + return createLabel(); + } + +/** +* Provided for backwards compatibility, and called by +* the default implementation of createLabelForKey. +* This implementation returns a JLabel. +*/ + protected JLabel createLabel() + { + return new JLabel(); + } + +/** +* Appends a label containing a key and the specified component +* to the bottom of the panel. Any registered action listeners +* will receive action events from the component - the key corresponding +* to the component will be used as the action command. +* @param key A string that will be displayed in a label, preferrably unique. +* @param component A component that will be placed next to the label. +* If null, a stock JTextField will be used. +*/ + protected void addCompositeComponent( String key, Component component ) + { + if ( key == null ) + { + key = ""; + } + Component label = createLabelForKey( key ); + Component field = component; + if ( field == null ) + { + field = new JTextField( 15 ); // default to 15 columns + } + field.setName( key ); // for association and reference + label.setName( key ); // ditto + if ( label instanceof JLabel ) + { + ((JLabel)label).setHorizontalAlignment( labelAlign ); + ((JLabel)label).setLabelFor( field ); // for accessibility + } + if ( "".equals( key ) ) + { + setText( label, "" ); + } + else + { + setText( label, prefix + key + postfix ); + } + field.setEnabled( this.isEditable ); // was: setEditable + + GridBagConstraints gbc = new GridBagConstraints(); + + if ( listContainer.getComponentCount() == 0 ) + { // we've just initialized or called removeAll + createSpacers(); + } + + gbc.gridx = ( fields.size() % this.columns ) * 2; + gbc.gridy = ( fields.size() / this.columns ) + 1; // spacer is at index zero + gbc.weightx = 0.0; + gbc.weighty = 0.0; + gbc.anchor = this.labelAnchor; + gbc.fill = GridBagConstraints.HORIZONTAL; + listContainer.add( label, gbc ); + + gbc.fill = GridBagConstraints.BOTH; + gbc.gridx = gbc.gridx + 1; + //FIXME: components default to the labelAnchor - should be different? + gbc.weightx = 1.0; + gbc.weighty = 0.0; + + listContainer.add( field, gbc ); + + if ( key.equals( HIDDEN ) ) + { // these components are not to be shown + setText( label, " " ); + field.setVisible( false ); + } + + fields.add( field ); // using list not map to allow for duplicate keys + labels.add( label ); // ditto + + // handle adding on-the-fly + updateGaps(); + this.revalidate(); + this.repaint(); + } + +/** +* Introspects a component to set the action command and to add the +* InfoPanel to its list of ActionListeners. +* @param aComponent The Component to be introspected. +* @param aKey The action command to be set. +*/ + protected void introspectComponent( Component aComponent, String aKey ) + { + // try to set properties of whatever component this might be + try { + Method [] methods = + (Method []) _method_cache.get( aComponent.getClass() ); + if (methods == null) { + Class componentClass = aComponent.getClass(); + BeanInfo info = + Introspector.getBeanInfo( componentClass ); + + MethodDescriptor[] descriptors = + info.getMethodDescriptors(); + Method setMethod = null; + Method addMethod = null; + for ( int i = 0; + ((setMethod == null || addMethod == null) && + i < descriptors.length); + i++ ) + { + Method m = descriptors[i].getMethod(); + String name = m.getName (); + if ( setMethod == null && + name.equals( "setActionCommand" ) ) + { + setMethod = m; + } + else if ( addMethod == null && + name.equals( "addActionListener" ) ) + { + addMethod = m; + } + } + + methods = new Method [] {setMethod, addMethod}; + _method_cache.put (componentClass, methods); + } + if (methods [0] != null) { + methods [0].invoke( aComponent, new Object[] { aKey } ); + } + if (methods [1] != null) { + methods [1].invoke( aComponent, new Object[] { this } ); + listenedToComponents.add( aComponent ); + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + System.out.println( "InfoPanel.introspectComponent: " + exc ); + } + } + +/** +* Called to populate a label component with the specified text. +* This implementation attempts to call setText(String) on the component. +* Override to customize. +*/ + protected void setText( Component c, String text ) + { + try + { + Method m = c.getClass().getMethod( "setText", new Class[] { String.class } ); + if ( m != null ) + { + m.invoke( c, new Object[] { text } ); + } + } + catch ( Exception exc ) + { + // no such method: ignore + } + } + +/** +* Creates spacer components on the reserved first grid row +* for each column of labels and fields. +* This allows us to set the margin for those label columns, +* and set the preferred width of the field columns. +* A list containing the field spacers should be assigned to +* the fieldSpacers instance variable. +*/ + private void createSpacers() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + // insert spacers for labels column + GridBagLayout gridBag = (GridBagLayout) listContainer.getLayout(); + GridBagConstraints constraints = new GridBagConstraints(); + constraints.gridy = 0; + constraints.fill = GridBagConstraints.HORIZONTAL; + + fieldSpacers = new LinkedList(); + Component fieldSpacer; + for ( int i = 0; i < this.columns; i++ ) + { + constraints.gridx = i * 2; + listContainer.add( Box.createHorizontalStrut( this.margin ), constraints ); + + constraints.gridx = i * 2 + 1; + fieldSpacer = Box.createHorizontalStrut( 0 ); + fieldSpacers.add( fieldSpacer ); + listContainer.add( fieldSpacer, constraints ); + } + } + } + +/** +* Updates the insets for all components. +*/ + protected void updateGaps() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + Component c = null; + GridBagConstraints gbc = null; + double totalWeightY = 0.0; + int count = listContainer.getComponentCount(); + int i; + for ( i = 0; i < count; i++ ) + { + c = listContainer.getComponent( i ); + gbc = layout.getConstraints( c ); + totalWeightY += gbc.weighty; + if ( (gbc.gridx + 1) % ( this.columns * 2 ) == 0 ) + { // if last component in row + gbc.insets = new Insets( 0, 0, this.vgap, 0 ); + } + else + { + if ( gbc.gridx % 2 == 0 ) + { // is a label column - NOTE: uses eleven pixels before component, per l&f guide + gbc.insets = new Insets( 0, 0, this.vgap, 11 ); + } + else + { // is a component column + if ( gbc.gridy != 0 ) + { + if ( c instanceof JPanel ) ((JPanel)c).setPreferredSize( null ); + gbc.insets = new Insets( 0, 0, this.vgap, this.hgap ); + } + } + } + layout.setConstraints( c, gbc ); + } + + //hack: gridbag clumps components in center if weighty is zero + // if sum of weighty is zero, top-justify the list container + this.remove( listContainer ); + if ( totalWeightY == 0.0 ) + { + this.add( listContainer, BorderLayout.NORTH ); + } + else // put list container in center so it will grow + { + this.add( listContainer, BorderLayout.CENTER ); + } + } + } + +/** +* Updates the label alignment. +*/ + protected void updateLabels() + { + if ( listContainer.getLayout() instanceof GridBagLayout ) + { + GridBagLayout layout = (GridBagLayout) listContainer.getLayout(); + Component c = null; + GridBagConstraints gbc = null; + Iterator it = labels.iterator(); + while ( it.hasNext() ) + { + c = (Component) it.next(); + if ( c instanceof JLabel ) + { + ((JLabel)c).setHorizontalAlignment( labelAlign ); + } + gbc = layout.getConstraints( c ); + gbc.anchor = this.labelAnchor; + layout.setConstraints( c, gbc ); + } + } + } + +/** +* Convenience method that uses a stock JTextField. +* @param key A string that will be displayed in a label, preferrably unique. +* @param value A string that will be displayed in a textfield. +*/ + public void addPair( String key, String value ) + { + addPair( key, value, null ); + } + +/** +* Convenience method that uses the specified JTextField or subclass +* and sets it to the specified value. +* @param key A string that will be displayed in a label, preferrably unique. +* @param value A string that will be displayed in a textfield. +* @param textField A JTextField or subclass that will be used to display the value. +* If null, a stock JTextField will be used. +*/ + public void addPair( String key, String value, JTextField textField ) + { + if ( value == null ) + { + value = ""; + } + JTextField field = textField; + if ( field == null ) + { + field = new JTextField( 15 ); // default to 15 columns + } + else + { + field = textField; + } + field.setText( value ); + + addPair( key, (Component) field ); + } + +/** +* Removes all components from the list. Buttons, if any, +* will remain unchanged - use setButtons( null ) to remove +* them. NOTE: does not call super.removeAll(). +*/ + public void removeAll() + { + Object component; + Method method; + Class[] paramClasses = new Class[] { ActionListener.class }; + Object[] paramObjects = new Object[] { this }; + + Iterator iterator = listenedToComponents.iterator(); + while ( iterator.hasNext() ) + { + component = iterator.next(); + try + { + method = component.getClass().getMethod( "removeActionListener", paramClasses ); + if ( method != null ) + { + method.invoke( component, paramObjects ); + } + } + catch ( Exception exception ) + { + // No removeActionListener() method, move along. + } + } + + listenedToComponents.clear(); + + listContainer.removeAll(); + fields.clear(); + labels.clear(); + this.revalidate(); + this.repaint(); + + //FIXME: It is very confusing that this + // implementation does not call super.removeAll(). + } + +/** +* Adds one or buttons to the bottom of the panel with the specified labels +* from left to right. Any action listeners will receive action events +* from clicks on these buttons - the supplied label will be used as the action command. +* @param buttons A string array containing the strings to be used for the button labels +* and action commands. A null value will remove the button panel. +* @see ButtonPanel +*/ + public void setButtons( String[] buttons ) + { + if ( buttonPanel == null ) + { + buttonPanel = new ButtonPanel(); + buttonPanel.setInsets( new Insets( 6, 0, 0, 0 ) ); + // button panel has a 11-pixel top inset + // and java l&f guide says 17-pixels before command buttons + buttonPanel.addActionListener( this ); + this.add( buttonPanel, BorderLayout.SOUTH ); + } + if ( buttons == null ) + { + this.remove( buttonPanel ); + buttonPanel = null; + } + else + { + buttonPanel.setLabels( buttons ); + } + + this.revalidate(); + this.repaint(); + } + protected Collection listenedToComponents = new LinkedList(); + +/** +* Retrieves the names of the buttons that are displayed, if any. +* @return A string array containing the strings used for the button labels +* and action commands, or null if no buttons have been created. +* @see ButtonPanel +*/ + public String[] getButtons() + { + if ( buttonPanel == null ) + { + return null; // none created + } + + return buttonPanel.getLabels(); + } + +/** +* Retrieves the actual button panel, if any. +* @return A button panel, or null if none has been created. +* @see ButtonPanel +*/ + public ButtonPanel getButtonPanel() + { + return buttonPanel; + } + + +/** +* Sets whether the values displayed in the panel should be editable. Defaults to true. +* @param isEditable Whether the values should be editable. +*/ + public void setEditable( boolean isEditable ) + { + this.isEditable = isEditable; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { + ( (Component) enumeration.next() ).setEnabled( isEditable ); + } + } + +/** +* Gets whether the values displayed in the panel are editable. +* @return Whether the values should be editable. +*/ + public boolean isEditable() + { + return this.isEditable; + } + +/** +* Sets the field associated with the key to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param key A string representing the key associated with the field. Nulls are converted to an empty string. +* @param value A object to be displayed in the specified field. Nulls are converted to an empty string. +*/ + public void setValueForKey( String key, Object value ) + { + setValueForKey( key, value, 0 ); + } + +/** +* Sets the field associated with the key to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param key A string representing the key associated with the field. Nulls are converted to an empty string. +* @param value A object to be displayed in the specified field. Nulls are converted to an empty string. +*/ + public void setValueForKey( String key, Object value, int index ) + { + if ( key == null ) + { + key = ""; + } + + Container field = null; + for ( int i = 0; i < fields.size(); i++ ) + { + field = (Container) fields.get(i); + if ( key.equals( field.getName() ) ) + { + setValueForIndex( index, i, value ); + return; + } + } + // else not found - ignore + } + +/** +* Sets the first field at the specified row index to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param row The row index of the component. +* @param value A object to be displayed in the specified field. +* Nulls are converted to an empty string. +*/ + public void setValueForIndex( int row, Object value ) + { + setValueForIndex( row, 0, value ); + } + +/** +* Sets the field at the specified row index and column index to the specified value. +* Note: If the component does not respond to setText() or setString() +* or setValue() the value will not be set. JTextFields and the like will work. +* @param row The row index of the component. +* @param index The column index of the component. +* @param value A object to be displayed in the specified field. +* Nulls are converted to an empty string. +*/ + public void setValueForIndex( int row, int col, Object value ) + { + Container field = (Container) fields.get( row ); + Component c = field.getComponent( col ); + setValueForComponent( c, value ); + } + + + +/** +* Sets the value in the field at the specified index. +* Note: If the component does not respond to setText() or setString() +* or setValue() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @param value A object to be displayed in the specified field. +*/ + protected void setValueForComponent( Component aComponent, Object value ) + { + // try to set a text or string property + try { + BeanInfo info = Introspector.getBeanInfo( aComponent.getClass() ); + MethodDescriptor[] methods = info.getMethodDescriptors(); + for ( int i = 0; i < methods.length; i++ ) + { + Method m = methods[i].getMethod(); + Class[] paramTypes = m.getParameterTypes(); + if ( paramTypes.length == 1 ) + { + if ( m.getName().equals( "setText" ) ) + { + if ( paramTypes[0].getName().equals( String.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + if ( m.getName().equals( "setString" ) ) + { + if ( paramTypes[0].getName().equals( String.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + if ( m.getName().equals( "setValue" ) ) + { + if ( paramTypes[0].getName().equals( Object.class.getName() ) ) + { + m.invoke( aComponent, new Object[] { value } ); + } + } + } + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + // FIXME: should log error in ErrorManager + System.out.println( "InfoPanel.setValueForComponent: " + exc ); + } + } + +/** +* Gets the value in the field at the specified index. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @return An object representing the value in the field at the specified index, +* or null if the component does not have a text property or if the index is out of bounds. +*/ + public Object getValueForIndex( int anIndex ) + { + return getValueForIndex( anIndex, 0 ); + } + +/** +* Gets the value in the field at the specified row and column. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param A valid index. +* @return An object representing the value in the field at the specified index, +* or null if the component does not have a text property or if the index is out of bounds. +*/ + public Object getValueForIndex( int row, int col ) + { + if ( ( row >= fields.size() ) || ( row < 0 ) ) + { // out of bounds + return null; + } + + Container field = (Container) fields.get( row ); + Component c = field.getComponent( col ); + return getValueForComponent( c ); + } + +/** +* Gets the value in the field associated with the key. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param key An string representing the key associated with the field. Nulls are converted to an empty string. +* @return An object representing the value in the field associated with the key, +* or null if the key does not exist or if the component does not have a text property. +*/ + public Object getValueForKey( String key ) + { + return getValueForKey( key, 0 ); + } + +/** +* Gets the value in the field associated with the key. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param key An string representing the key associated with the field. Nulls are converted to an empty string. +* @return An object representing the value in the field associated with the key, +* or null if the key does not exist or if the component does not have a text property. +*/ + public Object getValueForKey( String key, int index ) + { + if ( key == null ) + { + key = ""; + } + + Container field = null; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + field = (Container) enumeration.next(); + if ( key.equals( field.getName() ) ) + { + Component c = field.getComponent( index ); + if ( c != null ) + { + return getValueForComponent( c ); + } + } + } + // else not found + return null; + } + +/** +* Gets the value in the specified component. +* Note: If the component does not respond to getText() or getString() +* or getSelectedItem() this method will return null. JTextFields and the like will work. +* @param aComponent The specified component. +* @return An object representing the value in the component. +* or null if the component does not have a text property. +*/ + protected Object getValueForComponent( Component aComponent ) + { + // try to get a text or string property + try + { + BeanInfo info = Introspector.getBeanInfo( aComponent.getClass() ); + MethodDescriptor[] methods = info.getMethodDescriptors(); + for ( int i = 0; i < methods.length; i++ ) + { + Method m = methods[i].getMethod(); + Class[] paramTypes = m.getParameterTypes(); + if ( m.getName().equals( "getText" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + if ( m.getName().equals( "getString" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + if ( m.getName().equals( "getSelectedItem" ) ) + { + if ( paramTypes.length == 0 ) + { + return m.invoke( aComponent, new Object[] {} ); + } + } + // TODO: should also handle variants of setValue() + } + } + catch ( Exception exc ) + { // error occured while introspecting... move along. + System.out.println( "InfoPanel.getValueFromComponent: " + exc ); + } + + // not found + return null; + } + +/** +* Gets the component associated with the key as a JTextField, for backwards compatibility. +* @param key A string representing the key associated with the component. Nulls are converted to an empty string. +* @return A JTextField that contains the value associated with the key, +* or null if the key does not exist or if the component is not a JTextField. +*/ + public JTextField getFieldForKey( String key ) + { + Component c = getComponentForKey( key ); + if ( c instanceof JTextField ) + { + return (JTextField) c; + } + return null; + } + +/** +* Gets the component associated with the key. If more than one component is associated +* with the key, returns the first such component. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + public Component getComponentForKey( String key ) + { + return getComponentForKey( key, 0 ); + } + +/** +* Gets the component associated with the key and index. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + public Component getComponentForKey( String key, int index ) + { + Container c = getCompositeComponentForKey( key ); + if ( c == null ) return null; + return c.getComponent( index ); + } + +/** +* Gets the component at the specified row. If more than one component exists +* on that row, returns the first such component. +* @return A component or null if the row does not exist. +*/ + public Object getComponentForIndex( int row ) + { + return getComponentForIndex( row, 0 ); + } + +/** +* Gets the component at the specified row and column. +* @return A component or null if the index is out of bounds. +*/ + public Object getComponentForIndex( int row, int col ) + { + if ( ( row > fields.size() ) || ( row < 0 ) ) + { // out of bounds + return null; + } + + Container field = (Container) fields.get( row ); + return field.getComponent( col ); + } + +/** +* Gets the container associated with the key. +* @param key A string representing the key associated with the component. +* Nulls are converted to an empty string. +* @return A component that contains the value associated with the key, +* or null if the key does not exist. +*/ + protected Container getCompositeComponentForKey( String key ) + { + if ( key == null ) + { + key = ""; + } + + JPanel field = null; + Iterator enumeration = fields.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + field = (JPanel) enumeration.next(); + if ( key.equals( field.getName() ) ) + { + return field; + } + } + + // else not found + return null; + } + +/** +* Provided for backwards compatibility: calls getLabelComponentForKey. +* @param key A string representing the key associated with the compoent. +* Nulls are converted to an empty string. +* @return Component label object associated with the key, or null if the key does not exist +* or if the label component is not an instance of JLabel. +*/ + public JLabel getLabelForKey( String key ) + { + Component result = getLabelComponentForKey( key ); + if ( result instanceof JLabel ) return (JLabel) result; + return null; + } + +/** +* Get the label component associated with the key. +* @param key A string representing the key associated with the compoent. +* Nulls are converted to an empty string. +* @return Component label object associated with the key, or null if the key does not exist. +*/ + public Component getLabelComponentForKey( String key ) + { + if ( key == null ) + { + key = ""; + } + + Component label = null; + Iterator enumeration = labels.iterator(); + while ( enumeration.hasNext() ) + { // finds first value in list with specified key + label = (Component) enumeration.next(); + if ( key.equals( label.getName() ) ) + { + return label; + } + } + + // else not found + return null; + } + +/** +* Replaces the first component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param key A string representing the key to be associated with the component. +* Nulls are converted to an empty string. +* @param c A component to be placed next to the label corresponding to the key. +* Nulls are converted to a JTextField. +*/ + public void setComponentForKey( String key, Component c ) + { + setComponentForKey( key, c, 0 ); + } + +/** +* Replaces the component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param key A string representing the key to be associated with the component. +* Nulls are converted to an empty string. +* @param c A component to be placed next to the label corresponding to the key. +* Nulls are converted to a JTextField. +*/ + public void setComponentForKey( String key, Component c, int index ) + { + if ( c == null ) + { + c = new JTextField( 15 ); + } + if ( key == null ) + { + key = ""; + } + + Container container = this.getCompositeComponentForKey( key ); + Component field = container.getComponent( index ); + Object value = this.getValueForKey( key, index ); + if ( field != null ) + { + container.remove( index ); + container.add( c, index ); + c.setEnabled( this.isEditable ); + introspectComponent( c, key ); + setValueForComponent( c, value ); + } + } + +/** +* Replaces the first component in the specified row. Any value in the existing +* component will be copied to the new component. +* @param row A valid index. +* @param c A component to be placed next to the label corresponding to the key. +*/ + public void setComponentForIndex( int row, Component c ) + { + setComponentForIndex( row, 0, c ); + } + +/** +* Replaces the component associated with the key. Any value in the existing +* component will be copied to the new component. +* @param row A valid index. +* @param c A component to be placed next to the label corresponding to the key. +*/ + public void setComponentForIndex( int row, int col, Component c ) + { + setComponentForKey( getLabels()[row], c, col ); + } + +/** +* Sets the string that appears before each label's text on the panel. +* @param aString A String to be used as the label prefix. +*/ + public void setLabelPrefix( String aString ) + { + prefix = aString; + setLabels( getLabels() ); // force refresh + } + +/** +* Gets the string that appears before each label's text on the panel. +* Defaults to "", an empty string. +* @return A String that is currently used as the label prefix. +*/ + public String getLabelPrefix() + { + return prefix; + } + +/** +* Sets the string that appears after each label's text on the panel. +* Defaults to ": ", a colon followed by a space. +* @param aString A String to be used as the label postfix. +*/ + public void setLabelPostfix( String aString ) + { + postfix = aString; + setLabels( getLabels() ); // force refresh + } + +/** +* Gets the string that appears after each label's text on the panel. +* @return A String that is currently used as the label postfix. +*/ + public String getLabelPostfix() + { + return postfix; + } + +/** +* Adds an action listener to the list that will be +* notified by events occurring in the panel. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by events occurring in the panel. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + + // interface ActionListener + +/** +* Called by buttons on panel and by other components that +* might be set to broadcast events to this listener. +* Simply forwards the action event unchanged. +* @param e An action event to be received. +*/ + public void actionPerformed(ActionEvent e) + { +// if ( e.getSource() instanceof AbstractButton ) +// { + broadcastEvent(e); +// } + } + + /** + * GridBagLayout allocates weightx only after considering + * the preferred width of the components in a column. + * We'd prefer that preferred width wasn't considered, + * so that the layout worked more like a html-table. + * GridBagLayout is poorly factored for subclassing, + * so this code is going to get a little bit ugly. + * Really, what good is a protected method that returns + * a private class? Would have liked to just override + * getLayoutInfo and be done with it. + */ + private class BetterGridBagLayout extends GridBagLayout + { + public Dimension preferredLayoutSize(Container parent) + { + preprocess(); + return super.preferredLayoutSize( parent ); + } + + public Dimension minimumLayoutSize(Container parent) + { + preprocess(); + return super.minimumLayoutSize( parent ); + } + + + public void layoutContainer(Container parent) + { + preprocess(); + super.layoutContainer( parent ); + } + + protected void preprocess() + { + if ( fieldSpacers == null ) return; + Iterator i; + + // find the field with the widest preferred size + Component c; + int maxWidth = 0; + i = fields.iterator(); + while ( i.hasNext() ) + { + c = (Component) i.next(); + maxWidth = Math.max( maxWidth, + Math.max( c.getPreferredSize().width, c.getMinimumSize().width ) ); + } + + // set each column's spacers to that preferred size + Dimension min = new Dimension( 0, 0 ); + Dimension pref = new Dimension( maxWidth, 0 ); + i = fieldSpacers.iterator(); + while ( i.hasNext() ) + { + ((Box.Filler)i.next()).changeShape( min, pref, pref ); + } + } + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java new file mode 100644 index 0000000..b73c74d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyDelayTimer.java @@ -0,0 +1,188 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.AWTEventMulticaster; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import javax.swing.Timer; + +/** +* KeyDelayTimer is a utility that listens for KeyEvents from one +* or more components. After receiving a KeyEvents the timer will +* broadcast an action event if a specified time interval passes without +* a subsequent KeyEvent.

+* +* This utility is useful for implementing any kind of auto-complete +* feature in a user interface. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class KeyDelayTimer implements ActionListener, KeyListener +{ + // delay timer for keypress-sensitve events + protected Timer keyTimer = null; + protected Component lastFieldTouched = null; + protected long timeLastFieldTouched = 0; + protected int interval = 400; // adjust as needed + + // for action multicasting + protected ActionListener actionListener = null; + +/** +* Default constructor. +*/ + public KeyDelayTimer() + { + keyTimer = new Timer( interval, this ); + } + +/** +* Convenience constructor. +* @param listener An action listener to be notified of delay events. +*/ + public KeyDelayTimer( ActionListener listener ) + { + this(); + addActionListener( listener ); + } + +/** +* Returns the last component that generated a KeyEvent. +* @return The component that sent the most recent KeyEvent. +*/ + public Component getComponent() + { + return lastFieldTouched; + } + +/** +* Returns the number of milliseconds before an ActionEvent is generated. +* The default is 400. +* @return The current delay interval in milliseconds. +*/ + public int getInterval() + { + return interval; + } + +/** +* Sets the number of milliseconds before an ActionEvent will be generated +* after a KeyEvent is received. +* @param millis The new delay interval in milliseconds. +*/ + public void setInterval( int millis ) + { + interval = millis; + keyTimer.setDelay( interval / 2 ); + } + + // interface KeyListener + + public void keyTyped(KeyEvent e) + { + } + public void keyPressed(KeyEvent e) + { + } + +/** +* Receives key events from one or more components. +* Records the component and the time this event was received, +* then starts the timer. +* @param e The key event in question. +*/ + public void keyReleased(KeyEvent e) + { // handles keystrokes in the textfields (except ENTER and ESCAPE) + if ( ( Character.isLetterOrDigit( e.getKeyChar() ) ) + || ( e.getKeyCode() == KeyEvent.VK_SPACE ) + || ( e.getKeyCode() == KeyEvent.VK_DELETE ) + || ( e.getKeyCode() == KeyEvent.VK_BACK_SPACE ) ) + { + this.lastFieldTouched = e.getComponent(); + this.timeLastFieldTouched = System.currentTimeMillis(); + this.keyTimer.start(); + return; + } + } + + // interface ActionListener + +/** +* Receives ActionEvents from the internal timer. +* If the interval has passed without another KeyEvent, +* an ActionEvent is broadcast, with the name of this class +* as the ActionCommand, and the internal timer is stopped. +* @param e The action event in question. +*/ + public void actionPerformed(ActionEvent e) + { + if ( e.getSource() == keyTimer ) + { + if ( System.currentTimeMillis() - this.timeLastFieldTouched > interval ) + { + this.keyTimer.stop(); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, this.getClass().getName() ) ); + } + return; + } + } + + // Action Multicast methods + +/** +* Adds an action listener to the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be notified. +*/ + public void addActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.add(actionListener, l); + } +/** +* Removes an action listener from the list that will be +* notified by button events and changes in button state. +* @param l An action listener to be removed. +*/ + public void removeActionListener(ActionListener l) + { + actionListener = AWTEventMulticaster.remove(actionListener, l); + } +/** +* Notifies all registered action listeners of a pending Action Event. +* @param e An action event to be broadcast. +*/ + protected void broadcastEvent(ActionEvent e) + { + if (actionListener != null) + { + actionListener.actionPerformed(e); + } + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java new file mode 100644 index 0000000..95b8a19 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/KeyableCellEditor.java @@ -0,0 +1,350 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.io.Serializable; +import java.text.Format; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.border.LineBorder; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; +import javax.swing.table.TableCellEditor; + +/** +* A table cell editor customized for keyboard navigation, much like +* working with a spreadsheet. The default cell editor unfortunately +* does none of these things: +*

    +*
  • Selects text on start of editing. +*
  • Up and down keys move edit cell up and down. +*
  • Right and left keys move cell when selection caret is at end of text. +*
  • Escape cancels editing. +*
  • Enter commits edit. +*
  • Edits are properly committed on lost focus. +*
  • Tab and shift-tab work as expected. +*
  • Cell selection moves with the edit cell. +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class KeyableCellEditor implements TableCellEditor, FocusListener, + KeyListener, Serializable +{ + List listeners; + JTextField textField; + Object lastValue; + Format currentFormat; + + JTable table; + +/** +* Default constructor - a standard JTextField will be used for editing. +*/ + public KeyableCellEditor() + { + this( (JTextField) null ); + } + +/** +* Constructor specifying a type of JTextField to be used for editing. +* The JTextField will have its border replaced with a black line border. +* @param aTextField A JTextField or subclass for editing values. +*/ + public KeyableCellEditor( JTextField aTextField ) + { + listeners = new Vector(); + lastValue = null; + + // default to stock JTextField + textField = aTextField; + if ( textField == null ) + { + textField = new JTextField(); + } + + textField.setBorder(new LineBorder(Color.black)); + + // handle arrow keys while caret is showing + textField.addKeyListener( this ); + + // handle lost focus + textField.addFocusListener( this ); + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) + { + this.table = table; + table.removeKeyListener( this ); // if any + table.addKeyListener( this ); + return getEditorComponent( value ); + } + + protected Component getEditorComponent( Object value ) + { + if ( value != null ) + { + textField.setText( value.toString() ); + } + else + { + textField.setText( "" ); + } + + if ( value instanceof Number ) + { + textField.setHorizontalAlignment(JTextField.RIGHT); + } + else + { + textField.setHorizontalAlignment(JTextField.LEFT); + } + + // remember original value + lastValue = value; + + // select all text and get focus + textField.selectAll(); + textField.requestFocus(); + + return textField; + } + + public Object getCellEditorValue() + { + return lastValue; + } + + public boolean isCellEditable(EventObject anEvent) + { + // key events should replace the selection + // NOTE: For whatever reason, key events trigger result in a null parameter + if ( anEvent == null ) + { + textField.setText(""); + textField.requestFocus(); + return true; + } + + return true; + } + + public boolean shouldSelectCell(EventObject anEvent) + { // System.out.println( "KeyableCellEditor.shouldSelectCell: " + anEvent ); + + // key events should replace the selection + // NOTE: For whatever reason, key events are not generated + if ( anEvent instanceof KeyEvent ) + { + textField.setText(""); + textField.requestFocus(); + return true; + } + + // otherwise, select all text and continue + textField.selectAll(); + textField.requestFocus(); + + return true; + } + + public boolean stopCellEditing() + { + lastValue = textField.getText(); + fireEditingStopped(); + table.removeKeyListener( this ); // if any + return true; + } + + public void cancelCellEditing() + { + fireEditingCanceled(); + table.removeKeyListener( this ); // if any + } + + public void addCellEditorListener(CellEditorListener l) + { + listeners.add( l ); + } + + public void removeCellEditorListener(CellEditorListener l) + { + listeners.remove( l ); + } + + protected void fireEditingCanceled() + { + ChangeEvent event = new ChangeEvent( this ); + Iterator it = new ArrayList( listeners ).iterator(); // copy to prevent modification exception + while ( it.hasNext() ) + { + ((CellEditorListener)it.next()).editingCanceled( event ); + } + } + + protected void fireEditingStopped() + { + ChangeEvent event = new ChangeEvent( this ); + Iterator it = new ArrayList( listeners ).iterator(); // copy to prevent modification exception + while ( it.hasNext() ) + { + ((CellEditorListener)it.next()).editingStopped( event ); + } + } + + protected void onEnterKey() + { + stopCellEditing(); + } + + protected void onEscapeKey() + { + cancelCellEditing(); + } + + protected void moveEditCell( int dRow, int dCol ) + { + if ( table == null ) return; + int row = table.getSelectedRow() + dRow; + int col = table.getSelectedColumn() + dCol; + + row = Math.max( 0, row ); + row = Math.min( row, table.getRowCount() - 1 ); + col = Math.max( 0, col ); + col = Math.min( col, table.getColumnCount() - 1 ); + + stopCellEditing(); + table.setRowSelectionInterval( row, row ); + table.setColumnSelectionInterval( col, col ); + table.editCellAt( row, col ); + textField.selectAll(); + textField.requestFocus(); + } + + // interface KeyListener + + public void keyTyped(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyTyped: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + } + + public void keyPressed(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyPressed: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + + // catch LEFT and RIGHT here before JTextField consumes them + + int keyCode = e.getKeyCode(); + if ( keyCode == KeyEvent.VK_LEFT ) + { + if ( textField.getSelectionStart() == 0 ) + { + moveEditCell( 0, -1 ); + e.consume(); + return; + } + } + if ( keyCode == KeyEvent.VK_RIGHT ) + { + if ( textField.getSelectionEnd() == textField.getText().length() ) + { + moveEditCell( 0, 1 ); + e.consume(); + return; + } + } + if ( keyCode == KeyEvent.VK_UP ) + { + moveEditCell( -1, 0 ); + e.consume(); + return; + } + if ( keyCode == KeyEvent.VK_DOWN ) + { + moveEditCell( 1, 0 ); + e.consume(); + return; + } + } + + public void keyReleased(KeyEvent e) + { // System.out.println( "KeyableCellEditor.keyReleased: " + KeyEvent.getKeyText( e.getKeyCode() ) ); + + // catch ENTER here to allow JTextField to process it as well + + int keyCode = e.getKeyCode(); + if ( keyCode == KeyEvent.VK_ENTER ) + { + onEnterKey(); + return; + } + if ( keyCode == KeyEvent.VK_ESCAPE ) + { + onEscapeKey(); + return; + } + + // tabs are apparently only received on key release + if ( keyCode == KeyEvent.VK_TAB ) + { + if ( e.isShiftDown() ) + { + moveEditCell( 0, -1 ); + } + else + { + moveEditCell( 0, 1 ); + } + e.consume(); + return; + } + + } + + // interface FocusListener + + public void focusGained(FocusEvent e) + { // System.out.println( "focusGained: " ); + } + + public void focusLost(FocusEvent e) + { // System.out.println( "focusLost: " ); + stopCellEditing(); + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java new file mode 100644 index 0000000..4a7f07e --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/LineWrappingRenderer.java @@ -0,0 +1,154 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; +import java.awt.Dimension; + +import javax.swing.JList; +import javax.swing.JViewport; +import javax.swing.ListCellRenderer; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** +* A list cell renderer that wraps its text to subsequent lines +* depending on the length of text string and the width of the +* parent list. +* +* This renderer depends on listening to the parent list's viewport +* and fixing the list's width to match the viewport's size. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @date $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 904 $ +*/ +public class LineWrappingRenderer extends MultiLineLabel + implements ListCellRenderer, ChangeListener +{ + protected static Border noFocusBorder; + + protected JList list; + protected JViewport viewport; + protected int preferredWidth; + +/** +* Required constructor. The renderer keeps a reference to +* the list in which it is used and its viewport. This list +* is the only list that may use this renderer. The renderer +* will use the current size of the list to determine where +* lines will initially break. +* @param containerList The list that will be using this renderer. +*/ + public LineWrappingRenderer( JList containerList ) + { + super(); + setLineWrap(true); + noFocusBorder = new EmptyBorder(1, 1, 1, 1); + + list = containerList; + preferredWidth = 400; + if ( list.getParent() instanceof JViewport ) + { + viewport = (JViewport) list.getParent(); + viewport.addChangeListener( this ); + int newWidth = viewport.getExtentSize().width; + if ( newWidth > 0 ) preferredWidth = newWidth; + } + else + { + // should function adequately in absence of a viewport + // System.err.println( "LineWrappingRenderer.init: list.getParent = " + list.getParent() ); + } + } + +/** +* Returns the preferred size of the label, with width +* constrained to the current width. +* @return the size +*/ + public Dimension getPreferredSize() + { + int width = getWidth(); + if ( width != preferredWidth ) + { + // if component has not yet been placed within the list + if ( width < list.getWidth() / 2 ) width = list.getWidth(); + preferredWidth = width; + } + return new Dimension( preferredWidth, super.getPreferredSize().height ); + } + +/** +* Returns this component with the width set to the +* width of the specified JList. +* @return this component. +*/ + public Component getListCellRendererComponent ( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus ) + { // System.out.println( "LineWrappingRenderer.getListCellRendererComponent:" ); + + if ( list != this.list ) + { + System.err.println( "LineWrappingRenderer.getListCellRendererComponent: " + + "warning: the list using the renderer is not the list specified in the constructor." ); + } + + if (isSelected) + { + setBackground(this.list.getSelectionBackground()); + setForeground(this.list.getSelectionForeground()); + } + else + { + setBackground(this.list.getBackground()); + setForeground(this.list.getForeground()); + } + + setText((value == null) ? "" : value.toString()); + + setEnabled(this.list.isEnabled()); + setFont(this.list.getFont()); + setBorder( (cellHasFocus) ? UIManager.getBorder("List.focusCellHighlightBorder") : noFocusBorder ); + + return this; + } + +/** +* Overridden to respond to viewport changes. +*/ + public void stateChanged(ChangeEvent e) + { + int newWidth = viewport.getExtentSize().width; + if ( newWidth > 0 ) preferredWidth = newWidth; + + // set fixed width on list + list.setFixedCellWidth( preferredWidth ); + setSize( preferredWidth, super.getSize().height ); + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java new file mode 100644 index 0000000..b5f8a9b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/MultiLineLabel.java @@ -0,0 +1,135 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import javax.swing.JTextArea; +import javax.swing.LookAndFeel; +import javax.swing.text.Highlighter; + +/** +* A custom JTextArea that looks and feels like a JLabel, but supports +* line wrapping. This works a lot more like the IFC label component. +* NOTE: doesn't support icons (yet). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @date $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* @revision $Revision: 904 $ +*/ +public class MultiLineLabel extends JTextArea +{ + /** + * Saves a reference to the original highlighter + * to enable/disable text selection. + */ + protected Highlighter originalHighlighter; + +/* +* Creates a MultiLineLabel instance with an empty string for the title. +*/ + public MultiLineLabel() + { + super(); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + } + +/* +* Creates a MultiLineLabel instance with the specified text. +* @param text The specified text. +*/ + public MultiLineLabel( String text ) + { + super( text ); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + } + +/* +* Overridden to look like a label. +* @param text The specified text. +*/ + public void updateUI() + { + // got the implementation idea from usenet + + super.updateUI(); + + // turn on wrapping and disable editing and highlighting + + setLineWrap(true); + setWrapStyleWord(true); + setEditable(false); + setSelectable( false ); + + // Set the text area's border, colors and font to + // that of a label + + LookAndFeel.installBorder(this, "Label.border"); + + LookAndFeel.installColorsAndFont(this, + "Label.background", + "Label.foreground", + "Label.font"); + } + +/** +* Sets whether text is selectable. +* Default is non-selectable text. +*/ + public void setSelectable( boolean selectable ) + { + if ( selectable ) + { + setHighlighter( originalHighlighter ); + } + else + { + originalHighlighter = getHighlighter(); + setHighlighter( null ); + } + } + +/** +* Gets whether text is selectable. +* Default is non-selectable text. +*/ + public boolean isSelectable() + { + return ( getHighlighter() != null ); + } + +/** +* Overridden to return false. +*/ + public boolean isFocusTraversable() + { + return false; + } +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java new file mode 100644 index 0000000..b3d2d03 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/NumericTextField.java @@ -0,0 +1,434 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +/** +* NumericTextField is a "smart" text field that restricts the user's input. The +* input is restructed to numeric only wich can be of two types: integer and real +* numbers. A range can also be placed on the text field. The default type is +* integer, with the being (Integer.MIN_VALUE, Integer.MAX_VALUE). +* +* @author rob@straylight.princeton.com +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class NumericTextField extends SmartTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + +/** +* Restrict the text input to integers (whole numbers) only. +*/ + public final static int INTEGER = 0; + +/** +* Restrict the text input to floating-point numbers only. +*/ + public final static int FLOAT = 1; + + private Number maximumValue = null; + private Number minimumValue = null; + + private boolean sign = false; + private int newCaretPosition = 0; + private int valueType = INTEGER; + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default constructor. +*/ + public NumericTextField() + { + this("", 0); + } + +/** +* Constructor. +* @param text The initial string the text field is set to. +*/ + public NumericTextField(String text) + { + this(text, 0); + } + +/** +* Constructor. +* @param columns Width of the text field (in characters). +*/ + public NumericTextField(int columns) + { + this("", columns); + } + +/** +* Constructor. +* @param text The initial string the text field is set to. +* @param columns Width of the text field (in characters). +*/ + public NumericTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Sets the upper limit of the range of numbers to accept. +* @param newMaximumValue The maximum number accepted by the text field. +*/ + public void setMaximumValue(double newMaximumValue) + { + if (newMaximumValue >= 0) + { + maximumValue = new Double( newMaximumValue ); + } + else + { + maximumValue = null; + } + } + +/** +* Returns the upper limit of the range of numbers to accept. +* @return The maximum number accepted by this text field. +*/ + public double getMaximumValue() + { + if ( valueType == INTEGER ) + { + return (maximumValue == null) ? (double) Integer.MAX_VALUE : maximumValue.doubleValue(); + } + else + { + return (maximumValue == null) ? Double.MAX_VALUE : maximumValue.doubleValue(); + } + } + +/** +* Sets the lower limit of the range of numbers to accept. +* @param newMinimumValue The minimum number accepted by the text field. +*/ + public void setMinimumValue(double newMinimumValue) + { + if (newMinimumValue <= 0) + { + minimumValue = new Double( newMinimumValue ); + } + else + { + minimumValue = null; + } + } + +/** +* Returns the lower limit of the range of numbers to accept. +* @return The minimum number accepted by this text field. +*/ + public double getMinimumValue() + { + if ( valueType == INTEGER ) + { + return (minimumValue == null) ? (double) Integer.MIN_VALUE : minimumValue.doubleValue(); + } + else + { + return (minimumValue == null) ? -1.0*Double.MAX_VALUE : minimumValue.doubleValue(); + // NOTE: Double.MIN_VALUE returns the smallest positive value - oooops. + } + } + +/** +* Sets which type of number this text field can accept. +* @see #INTEGER +* @see #FLOAT +* @param newValueType The type of number to accept. +*/ + public void setValueType(int newValueType) + { + if ((newValueType != INTEGER) && (newValueType != FLOAT)) + { + valueType = INTEGER; + } + else + { + valueType = newValueType; + } + } + +/** +* Returns which type of number this text field accepts. The default is +* integer. +* @see #INTEGER +* @see #FLOAT +* @return The type of number to accept. +*/ + public int getValueType() + { + return valueType; + } + +/** +* Returns the integer numeric value of the string in the text field. The type +* can be either integer of float. +* @return The current value in the text field. +*/ + public int getIntValue() + { + int value = 0; + + try + { + value = Integer.parseInt(getText()); + } + catch (NumberFormatException e) + { + try + { + Double dValue = Double.valueOf(getText()); + value = dValue.intValue(); + } + catch (NumberFormatException ignored) {} + } + + return value; + } + +/** +* Sets the text field to integer value specified. +* @param aValue An integer value to display in the text field. +*/ + public void setIntValue(int aValue) + { + setText(Integer.toString(aValue)); + } + +/** +* Returns the real number numeric value of the string in the text field. The type +* can be either integer of float. +* @return The current value in the text field. +*/ + public double getDoubleValue() + { + Double value = new Double(0); + + try + { + value = Double.valueOf(getText()); + } + catch (NumberFormatException ignored) {} + + return value.doubleValue(); + } + +/** +* Sets the text field to the double value specified. If the text field type is +* FLOAT then the the number is display as a real number. If the text field +* type is INTEGER then the number is converted to a whole number for displaying. +* @param aValue A double value to display in the text field. +*/ + public void setDoubleValue(double aValue) + { + Double temp = new Double(aValue); + + if (valueType == FLOAT) + { + setText(temp.toString()); + } + else + { + setText(Integer.toString(temp.intValue())); + } + } + +/******************************* +* PROTECTED METHODS +*******************************/ + + protected boolean isValidCharacter(char aChar) + { + if (((aChar >= ' ') && (aChar <= '/')) || ((aChar >= ':') && (aChar <= '~'))) + { + if (aChar == '.') + { + if ( valueType == FLOAT ) + { + return true; + } + else + { + return false; + } + } + else if (aChar == '-') + { + if ( getMinimumValue() < 0 ) + { + return true; + } + else + { + return false; + } + } + else if (aChar == '+') + { + if ( getMaximumValue() >= 0 ) + { + return true; + } + else + { + return false; + } + } + return false; + } + return true; + } + + protected boolean isValidString(String aString) + { + int iValue = 0; + double dValue = 0.0; + + String tempString = new String(scanForSignChar(aString)); + + if ( valueType == INTEGER ) + { + try + { + iValue = Integer.parseInt(tempString); + } + catch (NumberFormatException e1) + { + if ((tempString.compareTo("-") == 0) && (getMinimumValue() < 0.0)) + { + iValue = 0; + } + else + { + return false; + } + } + if ((((double)iValue) < getMinimumValue()) || (((double)iValue) > getMaximumValue())) + { + return false; + } + } + else + { + // Double.valueOf requires a zero before the decimal point + if ( tempString.startsWith( "." ) ) + { + tempString = "0" + tempString; + } + try + { + dValue = Double.valueOf(tempString).doubleValue(); + } + catch (NumberFormatException e2) + { + if ((tempString.compareTo("-") == 0) && (getMinimumValue() < 0.0)) + { + dValue = 0.0; + } + else + { + return false; + } + } + + if ((dValue < getMinimumValue()) || (dValue > getMaximumValue())) + { + return false; + } + } + + return true; + } + + protected void postProcessing() + { + if (sign) + { + setText(scanForSignChar(getText())); + setCaretPosition(newCaretPosition); + } + sign = false; + } + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private String scanForSignChar(String aString) + { + String newString = ""; + boolean positive = false; + boolean negative = false; + int oldCaretPosition = getCaretPosition(); + int charactersAdded = 0; + + newCaretPosition = 0; + + if (aString.length() <= 0) + { + return aString; + } + + for (int i = 0; i < aString.length(); ++i) + { + switch (aString.charAt(i)) + { + case '+': positive = true; + break; + case '-': negative = true; + break; + default: newString += aString.charAt(i); + charactersAdded++; + break; + } + + if ((i + 1) == oldCaretPosition) + { + newCaretPosition = charactersAdded; + } + } + + if ((!(positive)) && (negative)) + { + newString = "-" + newString; + newCaretPosition++; + } + + if (positive || negative) + { + sign = true; + } + + return newString; + } +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java new file mode 100644 index 0000000..9db2834 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTable.java @@ -0,0 +1,572 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Vector; + +import javax.swing.BorderFactory; +import javax.swing.DefaultCellEditor; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; + +/** +* PropertyEditorTable is a table designed to display and edit the properties +* of an object. Because JTable assumes all cells in a column display +* the same data type, we have to subclass to determine the class +* based on the cell contents. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PropertyEditorTable extends JTable { + +// +// Constructors +// + + /** + * Constructs a default JTable which is initialized with a default + * data model, a default column model, and a default selection + * model. + * + * @see #createDefaultDataModel + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable() { + super(null, null, null); + } + + /** + * Constructs a JTable which is initialized with dm as the + * data model, a default column model, and a default selection + * model. + * + * @param dm The data model for the table + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm) { + super(dm, null, null); + } + + /** + * Constructs a JTable which is initialized with dm as the + * data model, cm as the column model, and a default selection + * model. + * + * @param dm The data model for the table + * @param cm The column model for the table + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm, TableColumnModel cm) { + super(dm, cm, null); + } + + /** + * Constructs a JTable which is initialized with dm as the + * data model, cm as the column model, and sm as the + * selection model. If any of the parameters are null this + * method will initialize the table with the corresponding + * default model. The autoCreateColumnsFromModel flag is set + * to false if cm is non-null, otherwise it is set to true + * and the column model is populated with suitable TableColumns + * for the columns in dm. + * + * @param dm The data model for the table + * @param cm The column model for the table + * @param sm The row selection model for the table + * @see #createDefaultDataModel + * @see #createDefaultColumnModel + * @see #createDefaultSelectionModel + */ + public PropertyEditorTable(TableModel dm, TableColumnModel cm, ListSelectionModel sm) { + super( dm, cm, sm ); + } + + /** + * Constructs a JTable with numRows and numColumns of + * empty cells using the DefaultTableModel. The columns will have + * names of the form "A", "B", "C", etc. + * + * @param numRows The number of rows the table holds + * @param numColumns The number of columns the table holds + */ + public PropertyEditorTable(int numRows, int numColumns) { + super( numRows, numColumns ); + } + + /** + * Constructs a JTable to display the values in the Vector of Vectors, + * rowData, with column names, columnNames. + * The Vectors contained in rowData should contain the values + * for that row. In other words, the value of the cell at row 1, + * column 5 can be obtained with the following code: + *

+ *

((Vector)rowData.elementAt(1)).elementAt(5);
+ *

+ * All rows must be of the same length as columnNames. + *

+ * @param rowData The data for the new table + * @param columnNames Names of each column + */ + public PropertyEditorTable(final Vector rowData, final Vector columnNames) { + super( rowData, columnNames ); + } + + /** + * Constructs a JTable to display the values in the two dimensional array, + * rowData, with column names, columnNames. + * rowData is an Array of rows, so the value of the cell at row 1, + * column 5 can be obtained with the following code: + *

+ *

 rowData[1][5]; 
+ *

+ * All rows must be of the same length as columnNames. + *

+ * @param rowData The data for the new table + * @param columnNames Names of each column + */ + public PropertyEditorTable(final Object[][] rowData, final Object[] columnNames) { + super( rowData, columnNames ); + } + + /** + * Returns the type of the column at the specified view position. + * + * @return the type of the column at position column in the view + * where the first column is column 0. + * + * Modified mln: now a wrapper for getCellClass() + * + */ + public Class getColumnClass(int column) { + return getCellClass( 0, column ); + } + + /** + * Returns the type of the cell at the specified view position. + * + * @return the type of the cell at position row, column in the view + * where the first column is column 0. + * + * Modified mln: new methods + * + */ + public Class getCellClass(int row, int column) { + TableModel model = getModel(); + if ( model instanceof PropertyEditorTableModel ) + return ((PropertyEditorTableModel)model).getCellClass( row, column ); + else + return model.getColumnClass(convertColumnIndexToModel(column)); + } + + /** + * Return an appropriate renderer for the cell specified by this this row and + * column. If the TableColumn for this column has a non-null renderer, return that. + * If not, find the class of the data in this column (using getColumnClass()) + * and return the default renderer for this type of data. + * + * @param row the row of the cell to render, where 0 is the first + * @param column the column of the cell to render, where 0 is the first + * + * Modified mln: calls getCellClass if there's no column model + * + */ + public TableCellRenderer getCellRenderer(int row, int column) { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellRenderer renderer = tableColumn.getCellRenderer(); + if (renderer == null) { + renderer = getDefaultRenderer(getCellClass(row, column)); + } + return renderer; + } + + + /** + * Return an appropriate editor for the cell specified by this this row and + * column. If the TableColumn for this column has a non-null editor, return that. + * If not, find the class of the data in this column (using getColumnClass()) + * and return the default editor for this type of data. + * + * @param row the row of the cell to edit, where 0 is the first + * @param column the column of the cell to edit, where 0 is the first + * + * Modified mp: calls getCellClass if there's no column model + * + */ + public TableCellEditor getCellEditor(int row, int column) { + TableColumn tableColumn = getColumnModel().getColumn(column); + TableCellEditor editor = tableColumn.getCellEditor(); + if (editor == null) { + editor = getDefaultEditor(getCellClass(row, column)); + } + return editor; + } + + protected void createDefaultRenderers() { + super.createDefaultRenderers(); + +/* // copying this code here as a sample of creating a renderer + // Dates + DefaultTableCellRenderer dateRenderer = new DefaultTableCellRenderer() { + DateFormat formatter = DateFormat.getDateInstance(); + public void setValue(Object value) { + setText((value == null) ? "" : formatter.format(value)); } + }; + dateRenderer.setHorizontalAlignment(JLabel.RIGHT); + setDefaultRenderer(Date.class, dateRenderer); +*/ + + DefaultTableCellRenderer fontRenderer = new DefaultTableCellRenderer() { + public void setValue(Object value) { + setText( getFontDescription( (Font) value ) ); + } + }; + fontRenderer.setHorizontalAlignment(JLabel.RIGHT); + setDefaultRenderer(Font.class, fontRenderer); + + setUpColorRenderer( this ); + setUpMethodRenderer( this ); + } + + protected String getFontDescription( Font f ) { + String s; + if ( f != null ) { + s = f.getName(); + if ( f.isBold() ) s += " Bold"; + if ( f.isItalic() ) s += " Italic"; + s += " " + f.getSize(); + } else { + s = ""; + } + return s; + } + + protected void createDefaultEditors() { + super.createDefaultEditors(); + +/* // copying this code here as a sample of creating an editor + // Numbers + JTextField rightAlignedTextField = new JTextField(); + rightAlignedTextField.setHorizontalAlignment(JTextField.RIGHT); + rightAlignedTextField.setBorder(new LineBorder(Color.black)); + setDefaultEditor(Number.class, new DefaultCellEditor(rightAlignedTextField)); + + // Booleans + JCheckBox centeredCheckBox = new JCheckBox(); + centeredCheckBox.setHorizontalAlignment(JCheckBox.CENTER); + setDefaultEditor(Boolean.class, new DefaultCellEditor(centeredCheckBox)); +*/ + setUpColorEditor( this ); + setUpMethodEditor( this ); + } + + + // following code lifted from: + // http://java.sun.com/docs/books/tutorial/ui/swing/example-swing/TableDialogEditDemo.java + + class ColorRenderer extends JLabel + implements TableCellRenderer { + Border unselectedBorder = null; + Border selectedBorder = null; + boolean isBordered = true; + + public ColorRenderer(boolean isBordered) { + super(); + this.isBordered = isBordered; + this.setOpaque(true); //MUST do this for background to show up. + } + + public Component getTableCellRendererComponent( + JTable table, Object color, + boolean isSelected, boolean hasFocus, + int row, int column) { + this.setBackground((Color)color); + if (isBordered) { + if (isSelected) { + if (selectedBorder == null) { + selectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getSelectionBackground()); + } + this.setBorder(selectedBorder); + } else { + if (unselectedBorder == null) { + unselectedBorder = BorderFactory.createMatteBorder(2,5,2,5, + table.getBackground()); + } + this.setBorder(unselectedBorder); + } + } + return this; + } + } + + private void setUpColorRenderer(JTable table) { + table.setDefaultRenderer(Color.class, + new ColorRenderer(true)); + } + + //Set up the editor for the Color cells. + private void setUpColorEditor(JTable table) { + //First, set up the button that brings up the dialog. + final JButton button = new JButton("") { + public void setText(String s) { + //Button never shows text -- only color. + } + }; + button.setBackground(Color.white); + button.setBorderPainted(false); + button.setMargin(new Insets(0,0,0,0)); + + //Now create an editor to encapsulate the button, and + //set it up as the editor for all Color cells. + final ColorEditor colorEditor = new ColorEditor(button); + table.setDefaultEditor(Color.class, colorEditor); + + //Set up the dialog that the button brings up. + final JColorChooser colorChooser = new JColorChooser(); + //XXX: PENDING: add the following when setPreviewPanel + //XXX: starts working. + //JComponent preview = new ColorRenderer(false); + //preview.setPreferredSize(new Dimension(50, 10)); + //colorChooser.setPreviewPanel(preview); + ActionListener okListener = new ActionListener() { + public void actionPerformed(ActionEvent e) { + colorEditor.currentColor = colorChooser.getColor(); + } + }; + final JDialog dialog = JColorChooser.createDialog(button, + "Pick a Color", + true, + colorChooser, + okListener, + null); //XXXDoublecheck this is OK + + //Here's the code that brings up the dialog. + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + button.setBackground(colorEditor.currentColor); + colorChooser.setColor(colorEditor.currentColor); + //Without the following line, the dialog comes up + //in the middle of the screen. + //dialog.setLocationRelativeTo(button); + dialog.show(); + } + }); + } + + /* + * The editor button that brings up the dialog. + * We extend DefaultCellEditor for convenience, + * even though it mean we have to create a dummy + * check box. Another approach would be to copy + * the implementation of TableCellEditor methods + * from the source code for DefaultCellEditor. + */ + class ColorEditor extends DefaultCellEditor { + Color currentColor = null; + + public ColorEditor(JButton b) { + super(new JCheckBox()); //Unfortunately, the constructor + //expects a check box, combo box, + //or text field. + editorComponent = b; + setClickCountToStart(1); //This is usually 1 or 2. + + //Must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + fireEditingStopped(); + } + }); + } + + protected void fireEditingStopped() { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() { + return currentColor; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) { + ((JButton)editorComponent).setText(value.toString()); + currentColor = (Color)value; + return editorComponent; + } + } + + class MethodRenderer extends JLabel + implements TableCellRenderer { + + Method theMethod = null; + JTable theTable = null; + + public MethodRenderer() { + super(); + } + + public Component getTableCellRendererComponent( + JTable table, Object method, + boolean isSelected, boolean hasFocus, + int row, int column) { + theMethod = (Method) method; + theTable = table; + setText( " " + theMethod.getReturnType().getName() ); + return this; + } + } + + private void setUpMethodRenderer(JTable table) { + table.setDefaultRenderer(Method.class, + new MethodRenderer()); + } + + /* + * We extend DefaultCellEditor for convenience, + * as with ColorEditor. + */ + class MethodEditor extends DefaultCellEditor { + Method theMethod = null; + JTable theTable = null; + + public MethodEditor(JButton b) { + super(new JCheckBox()); //Unfortunately, the constructor + //expects a check box, combo box, + //or text field. + editorComponent = b; + setClickCountToStart(1); //This is usually 1 or 2. + + //Must do this so that editing stops when appropriate. + b.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + fireEditingStopped(); + } + }); + } + + protected void fireEditingStopped() { + super.fireEditingStopped(); + } + + public Object getCellEditorValue() { + return theMethod; + } + + public Component getTableCellEditorComponent(JTable table, + Object value, + boolean isSelected, + int row, + int column) { + ((JButton)editorComponent).setText(value.toString()); + theMethod = (Method)value; + theTable = table; + return editorComponent; + } + } + + //Set up the editor for the Method cells. + private void setUpMethodEditor(PropertyEditorTable table) { + //First, set up the button that brings up the dialog. + final JButton button = new JButton("invoking method") { + public void setText(String s) { + //Button never shows text -- only color. + } + }; + button.setBackground(Color.white); + button.setBorderPainted(false); + button.setMargin(new Insets(0,0,0,0)); + + //Now create an editor to encapsulate the button, and + //set it up as the editor for all Color cells. + final MethodEditor methodEditor = new MethodEditor(button); + table.setDefaultEditor(Method.class, methodEditor); + + // handle the button-click + final PropertyEditorTable theTable = table; + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + Component parent = SwingUtilities.getRoot( theTable ); + if ( parent == null ) parent = theTable; + + Cursor oldCursor = parent.getCursor(); + parent.setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) ); + + Object result = null; + Object inspectedObject = ((PropertyEditorTableModel) + methodEditor.theTable.getModel()).inspectedObject; + try + { + methodEditor.theMethod.setAccessible( true ); + result = methodEditor.theMethod.invoke( + inspectedObject, (Object[])null ); + } + catch ( Exception exc ) + { + System.err.println( "PropertyEditorTable.MethodRenderer.actionPerformed: " + + "Error occurred: " + exc ); + } + theTable.methodInvoked( inspectedObject, methodEditor.theMethod, result ); + + parent.setCursor( oldCursor ); + } + }); + } + +/** +* Called by the method cell editor when a method is invoked. +* @param anObject The object upon which the method was invoked. +* @param aMethod The method that was invoked. +* @param aResult The result of the method invocation; may be null. +*/ + public void methodInvoked( Object anObject, Method aMethod, Object aResult ) + { + System.out.println( aMethod.getName() + ": " + aResult ); + } +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java new file mode 100644 index 0000000..f6a2a8d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/PropertyEditorTableModel.java @@ -0,0 +1,418 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.Vector; + +import javax.swing.Timer; +import javax.swing.table.AbstractTableModel; + +/** +* PropertyEditorTableModel introspects an object to facilitate +* editing it in a PropertyTable. +* +* Because the model always reflects the current state of the +* inspected object, it is useful to have a table update at +* automated intervals. By default, this feature is turned off. +* If you turn it on, you'll want to remember to turn it off +* when you're done with the table model. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PropertyEditorTableModel extends AbstractTableModel implements ActionListener +{ + protected Object inspectedObject = null; + + final String[] columnNames = { "Property", "Value" }; + static final String METHOD_TAG = " "; + + Vector properties = new Vector(); + Hashtable methods = new Hashtable(0); + + public void setObject( Object o ) { + inspectedObject = o; + Class c = o.getClass(); + Method[] m = c.getMethods(); + + properties = new Vector(); + methods = new Hashtable(m.length, 1F); + String name, propertyName; + for ( int i = 0; i < m.length; i++ ) { +// if ( m[i].getName().startsWith( "get" ) ) +// System.out.println( m[i].getName() + ": " + m[i].getReturnType().getName() ); + + // get methods + if ( + ( ( m[i].getName().startsWith( "get" ) ) || ( m[i].getName().startsWith( "is" ) ) ) + && ( m[i].getParameterTypes().length == 0 ) + ) { + name = m[i].getName(); + if ( m[i].getName().startsWith( "is" ) ) { + propertyName = name.substring( 2, name.length() ); + // probably should only add "is" methods if accompanied by "set" method + } else { // "get" + propertyName = name.substring( 3, name.length() ); + } + if ( + ( m[i].getReturnType().getName().equals( String.class.getName() ) ) + || ( m[i].getReturnType().getName().equals( "boolean" ) ) + || ( m[i].getReturnType().getName().equals( "int" ) ) + || ( m[i].getReturnType().getName().equals( "float" ) ) + || ( m[i].getReturnType().getName().equals( "char" ) ) + || ( m[i].getReturnType().getName().equals( Color.class.getName() ) ) + || ( m[i].getReturnType().getName().equals( Font.class.getName() ) ) + ) { + properties.addElement( propertyName ); // put property + methods.put( name, m[i] ); // put method + } + else + { + // handle unknown types as invokable methods: + properties.addElement( propertyName + METHOD_TAG ); + methods.put( propertyName + METHOD_TAG, m[i] ); + } + } + else + // set methods + if ( ( m[i].getName().startsWith( "set" ) ) && + ( m[i].getParameterTypes().length == 1 ) ) { + name = m[i].getName(); + if ( + ( m[i].getParameterTypes()[0].getName().equals( String.class.getName() ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "boolean" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "int" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( "float" ) ) + || ( m[i].getParameterTypes()[0].getName().equals( Color.class.getName() ) ) +// || ( m[i].getParameterTypes()[0].getName().equals( Font.class.getName() ) ) + ) { +// System.out.println( "Accepted: " + name + ": " + m[i].getParameterTypes()[0].getName() ); + methods.put( name, m[i] ); // set method + } else { +// System.out.println( "Rejected: " + name + ": " + m[i].getParameterTypes()[0].getName() ); + } + } + else + // zero-parameter methods to be invoked on click + if ( m[i].getParameterTypes().length == 0 ) + { + properties.addElement( m[i].getName() + METHOD_TAG ); + methods.put( m[i].getName() + METHOD_TAG, m[i] ); + } + + } + + sort(properties); + fireTableDataChanged(); + } + + public int getColumnCount() { + return columnNames.length; + } + + public int getRowCount() { + return properties.size(); + } + + public String getColumnName(int col) { + return columnNames[col]; + } + + public Object getValueAt(int row, int col) { + if ( col == 0 ) + return properties.elementAt( row ); + else + { + Method m = null; + m = (Method) methods.get( "get" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) // try "is" + m = (Method) methods.get( "is" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) { // try entire method name + m = (Method) methods.get( (String) properties.elementAt( row ) ) ; + if ( m != null ) return m; + } + try { + return m.invoke( inspectedObject, (Object[])null ); + } catch ( Exception exc ) { + System.out.println( "InspectorFrame.tableModel.getValueAt: error occured while reflecting: " ); + System.out.println( exc ); + } + return null; + } + } + + public Class getColumnClass( int col ) { +// System.out.println( "getColumnClass" ); +/* try { + throw new Exception(); + } catch ( Exception exc ) { + exc.printStackTrace( System.out ); + } +*/ return new String().getClass(); + } + + public Class getCellClass(int row, int col) { +// System.out.println( "getCellClass" ); +/* + + Class c; + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) + c = new Object().getClass(); + else { + c = m.getParameterTypes()[0]; + + // special case for boolean + if ( c.getName().equals( "boolean" ) ) + c = new Boolean(true).getClass(); + + // special case for int + if ( c.getName().equals( "int" ) ) + c = new Integer(0).getClass(); + } + System.out.println( row + ": " + c.getName() ); + return c; +*/ + +// return new String().getClass(); + + Object o = getValueAt( row, col ); + if ( o == null ) o = "null"; + return o.getClass(); + } + + /* + * Don't need to implement this method unless your table's + * editable. + */ + public boolean isCellEditable(int row, int col) { + //Note that the data/cell address is constant, + //no matter where the cell appears onscreen. + if (col < 1) { + return false; + } else { + // handle method invocation + if ( ((String)properties.elementAt(row)).endsWith(METHOD_TAG) ) return true; + // handle read-only properties + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + if ( m == null ) + return false; + else + return true; + } + } + + /* + * Don't need to implement this method unless your table's + * data can change. + */ + public void setValueAt(Object value, int row, int col) { + // test for inspected object + if ( inspectedObject == null ) return; + // handle method invocation - no need to update values + if ( ((String)properties.elementAt(row)).endsWith( METHOD_TAG ) ) + { + fireTableDataChanged(); + return; + }; + + // handle writable properties + Method m = (Method) methods.get( "set" + ( (String) properties.elementAt( row ) ) ) ; + String parameterType = m.getParameterTypes()[0].getName(); + + // ugly cast code + if ( + ( parameterType.equals( "int" ) ) + || ( parameterType.equals( "java.lang.Integer" ) ) + ) + { + try { + value = new Integer((String)value); + } catch (NumberFormatException e) { + System.out.println("PropertyEditorTableModel.setValueAt: User attempted to enter non-integer" + + " value (" + value + + ") into an integer-only column."); + } + } + Object[] parameters = { value }; + try { + m.invoke( inspectedObject, parameters ); + if ( inspectedObject instanceof Component ) { + Component c = (Component)inspectedObject; + if ( c.getParent() != null ) + c.getParent().repaint(); + } + } catch ( Exception exc ) { + System.out.println( "PropertyEditorTableModel.setValueAt: error occured while reflecting: " ); + System.out.println( exc ); + } + + fireTableDataChanged(); + } + + + protected void sort(Vector v){ + quickSort(v, 0, v.size()-1); + } + + + // Liberated from the BasicDirectoryModel which was... + // Liberated from the 1.1 SortDemo + + // This is a generic version of C.A.R Hoare's Quick Sort + // algorithm. This will handle arrays that are already + // sorted, and arrays with duplicate keys.
+ // + // If you think of a one dimensional array as going from + // the lowest index on the left to the highest index on the right + // then the parameters to this function are lowest index or + // left and highest index or right. The first time you call + // this function it will be with the parameters 0, a.length - 1. + // + // @param a an integer array + // @param lo0 left boundary of array partition + // @param hi0 right boundary of array partition + private void quickSort(Vector v, int lo0, int hi0) { + int lo = lo0; + int hi = hi0; + String mid; + + if (hi0 > lo0) { + // Arbitrarily establishing partition element as the midpoint of + // the array. + mid = (String) v.elementAt((lo0 + hi0) / 2); + + // loop through the array until indices cross + while(lo <= hi) { + // find the first element that is greater than or equal to + // the partition element starting from the left Index. + // + // Nasty to have to cast here. Would it be quicker + // to copy the vectors into arrays and sort the arrays? + while((lo < hi0) && lt((String)v.elementAt(lo), mid)) { + ++lo; + } + + // find an element that is smaller than or equal to + // the partition element starting from the right Index. + while((hi > lo0) && lt(mid, (String)v.elementAt(hi))) { + --hi; + } + + // if the indexes have not crossed, swap + if(lo <= hi) { + swap(v, lo, hi); + ++lo; + --hi; + } + } + + + // If the right index has not reached the left side of array + // must now sort the left partition. + if(lo0 < hi) { + quickSort(v, lo0, hi); + } + + // If the left index has not reached the right side of array + // must now sort the right partition. + if(lo < hi0) { + quickSort(v, lo, hi0); + } + + } + } + + private void swap(Vector a, int i, int j) { + Object T = a.elementAt(i); + a.setElementAt(a.elementAt(j), i); + a.setElementAt(T, j); + } + + protected boolean lt(String a, String b) { + return a.compareTo(b) < 0; + } + + // automated updates + + private boolean autoUpdating = false; + private int updateInterval = 2000; // one-second delay on average + protected Timer timer = null; + + public boolean isAutoUpdating() + { + return autoUpdating; + } + + public void setAutoUpdating( boolean shouldAutoUpdate ) + { + if ( shouldAutoUpdate ) + { + if ( timer == null ) + { + timer = new Timer( updateInterval, this ); + timer.setRepeats( true ); + timer.start(); + } + } + else + { + if ( timer != null ) + { + timer.stop(); + timer = null; + } + } + + autoUpdating = shouldAutoUpdate; + } + + public int getUpdateInterval() + { + return updateInterval; + } + + public void setUpdateInterval( int anInterval ) + { + if ( timer != null ) + { + timer.setDelay( anInterval ); + } + + updateInterval = anInterval; + } + + public void actionPerformed( ActionEvent evt ) + { + if ( evt.getSource() == timer ) + { + fireTableDataChanged(); + } + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java new file mode 100644 index 0000000..2956c71 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/RadioButtonPanel.java @@ -0,0 +1,174 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; + +import javax.swing.AbstractButton; +import javax.swing.ButtonGroup; +import javax.swing.JRadioButton; +import javax.swing.border.EmptyBorder; + +/** +* RadioButtonPanel is a simple extension of ButtonPanel. +* Differences are that it uses radio buttons and the +* default alignment is vertical. The radio buttons are +* placed in a ButtonGroup and the panel defaults to having +* no buttons selected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class RadioButtonPanel extends ButtonPanel +{ +/** +* A ButtonGroup to help manage button state. +*/ + protected ButtonGroup buttonGroup; + +/** +* ButtonGroup does not make it easy to unselect all buttons. +* The preferred way to do it is actually to create a hidden button. +*/ + protected JRadioButton hiddenButton; + +/** +* Constructs a RadioButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public RadioButtonPanel() + { + super(); + } + +/** +* Constructs a ButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public RadioButtonPanel( String[] buttonList ) + { + super( buttonList ); + } + +/** +* Overridden to set vertical-center alignment and 2-pixel vgap. +*/ + protected void initLayout() + { + super.initLayout(); + buttonPanelLayout.setAlignment( BetterFlowLayout.CENTER_VERTICAL ); + buttonPanelLayout.setVgap( 2 ); // looks nicer than java l&f recommendation (imho) + } + +/** +* Overridden to return a JRadioButton. +* @param aLabel The label for the component that will be created. +* @return The newly created component. +*/ + protected Component createComponentWithLabel( String aLabel ) + { + String buttonLabel = aLabel; + JRadioButton newButton = new JRadioButton(); + newButton.setName( aLabel ); + newButton.setText( buttonLabel ); + newButton.setActionCommand( aLabel ); + newButton.addActionListener( this ); + + // reduce insets per java l&f guidelines (was 4 on each side) + newButton.setBorder( new EmptyBorder( 0, 4, 0, 4 ) ); + + if ( buttonGroup == null ) + { + buttonGroup = new ButtonGroup(); + + // cheesy hack to allow a buttongroup to have no items selected. + // note that the button is not added to container or buttonList. + hiddenButton = new JRadioButton( "Hidden Button" ); + buttonGroup.add( hiddenButton ); + } + buttonGroup.add( newButton ); + + return newButton; + } + +/** +* Selects the button whose name matches the given text value. +* @param newText A String matching the name of one of the buttons. +* If null, empty, or not matching, all buttons are deselected. +*/ + public void setValue(String aName) + { + if ( aName != null ) + { + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( c instanceof AbstractButton ) + { + if ( c.getName().equals( aName ) ) + { + ((AbstractButton)c).setSelected( true ); + return; + } + } + } + } + + // null, empty, or not matching - deselect all + hiddenButton.setSelected( true ); + } + +/** +* Gets the name of the currently selected button. +* @return A string matching the name of the currently selected button, +* or null of no button is selected. +*/ + public String getValue() + { + String result = null; + Component c = null; + int count = buttonContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = buttonContainer.getComponent( i ); + if ( ( c instanceof AbstractButton ) && ( ((AbstractButton)c).isSelected() ) ) + { + return c.getName(); + } + } + return result; + } + +/** +* Tests whether the specified value is checked. +* @param aValue A value to be tested. +* @return True if the specified value is checked, otherwise false. +*/ + public boolean getValue( String aValue ) + { + if ( aValue == null ) return false; + return aValue.equals( getValue() ); + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java new file mode 100644 index 0000000..6914cf6 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartPasswordField.java @@ -0,0 +1,274 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.KeyEvent; + +import javax.swing.JPasswordField; + +/** + * SmartPasswordField is an extention of JPasswordField. It does everything + * a JPassword does, as well as limit the number of characters. The user + * of this class can specify that a password can only have a maximum of + * 10 characters for instance. + * + * @author rob@straylight.princeton.com + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public class SmartPasswordField extends JPasswordField +{ + +/******************************* +* CONSTANTS +*******************************/ + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int SPACE = 32; + private static final int DASH = 45; + private static final int UNDERSCORE = 95; + private static final int PERIOD = 46; + private static final int PASTE = 22; // Ctl-V + + private int passwordLength = Integer.MAX_VALUE; + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* Default constructor. +*/ + public SmartPasswordField() + { + super(); + } + +/** +* This constructor allows the user to set the maximum length of the password. +* @param aLength The maximum length of the password. +*/ + public SmartPasswordField( int aLength ) + { + this(); + setPasswordLength( aLength ); + } + +/** +* Sets the maximum lenght of the password. The value must be 0 or greater. +* If the length specified is less than 0, then no action occurs. +* @param aLength The maximum lenght of the password. +*/ + public void setPasswordLength( int aLength ) + { + if ( aLength >= 0 ) + { + passwordLength = aLength; + } + } + +/** +* Returns the current maximum length of the password. +* @return The current maximum length of the password. +*/ + public int getPasswordLength() + { + return passwordLength; + } + +/** +* This method processes a key event. This event is generated by input from the +* keyboard when this text field has the focus. This method is called for every +* key that is pressed and released on the keyboard. This includes modifier +* keys like the shift and alt keys. This class looks at the key and determines +* if the key is valid input given the restrictions of this class.

+* @param e A key event generated by a keyboard action. +*/ + public void processKeyEvent(KeyEvent e) + { + String currentText = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int selectionStart = 0; + int selectionEnd = 0; + int endOfHead = 0; + int startOfTail = 0; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean insertionPoint = false; + boolean selectionAtStart = false; + boolean selectionAtEnd = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + + if ((isPrintableCharacter(newChar)) || (backspace) || (delete) || (paste)) + { + // Analyze the current contents of the field + currentText = new String( getPassword() ); + currentLength = currentText.length(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + insertionPoint = (selectionStart == selectionEnd); + selectionAtStart = (selectionStart == 0); + selectionAtEnd = (selectionEnd >= currentLength); + if (selectionEnd > currentLength) + { + setSelectionEnd(currentLength); + } + + // Generate new string + if (selectionStart > 0) // Create head of test string + { + endOfHead = selectionStart; + if (insertionPoint && backspace) + { + endOfHead -= 1; + } + testString += currentText.substring(0, endOfHead); + } + + if (!(backspace || delete || paste)) // Add the new character + { + testString += newChar; + } + + if (paste) // Add the string from the clipboard + { + Transferable data = getToolkit().getSystemClipboard().getContents(this); + if (data != null) + { + try + { + String clipString = (String)data.getTransferData(DataFlavor.stringFlavor); + testString += clipString; + } + catch (java.io.IOException ioe) + { + // Do nothing + } + catch (UnsupportedFlavorException ufe) + { + // Do nothing + } + } + } + + if (selectionEnd < currentLength) // Add the tail of the string + { + startOfTail = selectionEnd; + if (insertionPoint && delete) + { + startOfTail += 1; + } + testString += currentText.substring(startOfTail); + } + + } + + if (testString.compareTo("") != 0) // Null string is OK + { + if (!(isValidString(testString))) + { + e.consume(); + } + } + } + else + { + e.consume(); + } + } + super.processKeyEvent(e); + + postProcessing(); + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + +/** +* Returns whether the inputted character is valid or not. In this case all +* characters are valid input. +* @param aChar A character to perform the validity test with. +* @return True if the character is valid for this subclassed text field.
+* False is the character is not valid. +*/ + protected boolean isValidCharacter(char aChar) + { + return true; + } + +/** +* Returns whether a string is valid for this text field. As the user types from +* the keyboard, this method is called to determine if the new string in the text +* field is valid based upon the restriction of this class. The length of the +* new string is checked against the maximum password length. +* @param aString The string to perform the validity check with. +* @return True if the length of the string is less than or equal to the maximum length. +* False if the character is not valud. +*/ + protected boolean isValidString(String aString) + { + if ( aString.length() > passwordLength ) + { + return false; + } + + return true; + } + +/** +* This class does not need any post processing. +*/ + protected void postProcessing() + { + /* Do Nothing */ + } + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java new file mode 100644 index 0000000..cee37e1 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/SmartTextField.java @@ -0,0 +1,244 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.event.KeyEvent; + +import javax.swing.JTextField; + +/** + * SmartTextField is an abstract class for that allows the text field to + * intelligently analyze the user's input in real-time. As the user enters + * keystrokes, the generated string is analyzed to determine if the new string + * is valid based on the criteria of the concrete classes that extend this + * class. An invalid keystroke is rejected and not displayed in the text + * field. This class can be extended to to create smart text fields that only + * accept integers or floating points number or alphabetic strings of maximum + * length. These are several examples. + * + * @author rob@straylight.princeton.com + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ +public abstract class SmartTextField extends JTextField +{ + +/******************************* +* CONSTANTS +*******************************/ + private static final int BACKSPACE = 8; + private static final int DELETE = 127; + private static final int SPACE = 32; + private static final int DASH = 45; + private static final int UNDERSCORE = 95; + private static final int PERIOD = 46; + private static final int PASTE = 22; // Ctl-V + + +/******************************* +* PUBLIC METHODS +*******************************/ + +/** +* This method processes a key event. This event is generated by input from the +* keyboard when this text field has the focus. This method is called for every +* key that is pressed and released on the keyboard. This includes modifier +* keys like the shift and alt keys. This class looks at the key and determines +* if the key is valid input given the restrictions of the concrete sub-classes.

+* Example - A smart text field only allows alphabetic characters. If the key +* pressed is a "2" then this method will determine that the key is invalid and +* "consume" the key event.

+* Note - Every printable character has a "TYPED" key event. Currentlt under +* Java 1.2.1 this does not happen. Bug 4186905 relating this bug has been +* fixed and is awaiting release. +* @param e A key event generated by a keyboard action. +*/ + public void processKeyEvent(KeyEvent e) + { + String currentText = ""; + String testString = ""; + char newChar = e.getKeyChar(); + int currentLength = 0; + int selectionStart = 0; + int selectionEnd = 0; + int endOfHead = 0; + int startOfTail = 0; + boolean backspace = false; + boolean delete = false; + boolean paste = false; + boolean insertionPoint = false; + boolean selectionAtStart = false; + boolean selectionAtEnd = false; + + backspace = (newChar == BACKSPACE); + delete = (newChar == DELETE); + paste = (newChar == PASTE); + + if ((e.getKeyCode() == KeyEvent.VK_UNDEFINED) || ((backspace) || (delete) || (paste))) // A "key-typed" event + { + if (isValidCharacter(newChar)) + { + + if ((isPrintableCharacter(newChar)) || (backspace) || (delete) || (paste)) + { + // Analyze the current contents of the field + currentText = getText(); + currentLength = currentText.length(); + + selectionStart = getSelectionStart(); + selectionEnd = getSelectionEnd(); + + insertionPoint = (selectionStart == selectionEnd); + selectionAtStart = (selectionStart == 0); + selectionAtEnd = (selectionEnd >= currentLength); + if (selectionEnd > currentLength) + { + setSelectionEnd(currentLength); + } + + // Generate new string + if (selectionStart > 0) // Create head of test string + { + endOfHead = selectionStart; + if (insertionPoint && backspace) + { + endOfHead -= 1; + } + testString += currentText.substring(0, endOfHead); + } + + if (!(backspace || delete || paste)) // Add the new character + { + testString += newChar; + } + + if (paste) // Add the string from the clipboard + { + Transferable data = getToolkit().getSystemClipboard().getContents(this); + if (data != null) + { + try + { + String clipString = (String)data.getTransferData(DataFlavor.stringFlavor); + testString += clipString; + } + catch (java.io.IOException ioe) + { + // Do nothing + } + catch (UnsupportedFlavorException ufe) + { + // Do nothing + } + } + } + + if (selectionEnd < currentLength) // Add the tail of the string + { + startOfTail = selectionEnd; + if (insertionPoint && delete) + { + startOfTail += 1; + } + testString += currentText.substring(startOfTail); + } + + } + + if (testString.compareTo("") != 0) // Null string is OK + { + if (!(isValidString(testString))) + { + e.consume(); + } + } + } + else + { + e.consume(); + } + } + super.processKeyEvent(e); + + postProcessing(); + } + + +/******************************* +* PROTECTED METHODS +*******************************/ + +/** +* Default constructor for this class. The initial text of the smart text field +* can be specified as well as the size (in characters) of the text field. +* @param text The initial string that is displayed in the text field. +* @param columns THe width of the text field in characters. +*/ + protected SmartTextField(String text, int columns) + { + super(text, columns); + } + +/** +* Returns whether a character is valid for this text field. As the user types +* from the keyboard, this method is called to determine if the character is a +* valid character based in the restrictions of the subclass. +* @param aChar A character to perform the validity test with. +* @return True if the character is valid for this subclassed text field.
+* False is the character is not valid. +*/ + abstract protected boolean isValidCharacter(char aChar); + +/** +* Returns whether a string is valid for this text field. As the user types from +* the keyboard, this method is called to determine if the new string in the text +* field is valid based upon the restriction of the subclass. This is done after +* the character has been determined to be valid since there can be restrictions +* placed on the text string as a whole, such a maximum length or date format. +* @param aString The string to perform the validity check with. +* @return True if the string is valid for this subclassed text field.
+* False if the character is not valud. +*/ + abstract protected boolean isValidString(String aString); + +/** +* This method is used by the any subclass that need to complete any processing +* of the text string in the text field after all the requirement checks have +* been performed. +*/ + abstract protected void postProcessing(); + + +/******************************* +* PRIVATE METHODS +*******************************/ + + private boolean isPrintableCharacter(char inputChar) + { + if ((inputChar >= ' ') && (inputChar <= '~')) + { + return true; + } + return false; + } + +} diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java new file mode 100644 index 0000000..3d9a85b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/StatusButtonPanel.java @@ -0,0 +1,276 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.lang.reflect.Method; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; + +/** +* StatusButtonPanel extends ButtonPanel to provide a space +* to display status messages in a consistent manner.

+* Messages are erased after a certain predefined interval, +* defaulting to 10 seconds. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class StatusButtonPanel extends ButtonPanel +{ +/** +* This is the action command to all listeners when the status text is changed. +*/ + public static final String STATUS_CHANGED = "STATUS_CHANGED"; + + // note: weirdness happens if you initialize + // this variable. Because it is set by initLayout + // and initLayout is called by the superclass constructor, + // this variable would get initialized after initLayout + // is called... + protected Component statusComponent; // = null; + + protected Timer timer = null; + protected int interval = 10000; // adjust as needed + +/** +* Constructs a StatusButtonPanel. Three buttons are created +* so the panel is filled when used in a GUI-builder environment. +*/ + public StatusButtonPanel() + { + super(); + setupTimer(); + } + +/** +* Constructs a StatusButtonPanel using specified buttons. +* @param buttonList An array containing the strings to be used in labeling the buttons. +*/ + public StatusButtonPanel( String[] buttonList ) + { + super( buttonList ); + setupTimer(); + } + +/** +* Initializes the timer instance variable. +*/ + protected void setupTimer() + { + timer = new Timer( interval, this ); + timer.addActionListener( this ); + timer.setRepeats( false ); + timer.start(); + } + +/** +* Returns the number of milliseconds before the status message is cleared. +* The default is 10000. +* @return The current delay interval in milliseconds. +*/ + public int getDelayInterval() + { + return interval; + } + +/** +* Sets the number of milliseconds before the status message is cleared. +* @param millis The new delay interval in milliseconds. +*/ + public void setDelayInterval( int millis ) + { + interval = millis; + timer.setDelay( interval ); + } + +/** +* Returns the visual component used to display the status. +* @return A component used for displaying status. +*/ + public Component getStatusComponent() + { + return statusComponent; + + } +/** +* Receives ActionEvents from the internal timer. +* @param e The action event in question. +*/ + public void actionPerformed(ActionEvent e) + { + if ( e.getSource() == timer ) + { + setText( "" ); + return; + } + + // otherwise continue with superclass implementation + super.actionPerformed( e ); + } + +/** +* This method is responsible for the initial layout of the panel. +* Subclasses can implement different layouts, but this method +* is responsible for initializing buttonPanelLayout to a valid +* layout manager and setting this panel to use it. This method +* must should initialize statusComponent to a component that ideally +* has get/setText methods, although this is not required. +*/ + protected void initLayout() + { + + statusComponent = new JTextField(); + JTextField textField = (JTextField) statusComponent; + textField.setColumns( 20 ); + textField.setBackground( getBackground() ); + textField.setEditable( false ); + +// statusComponent = new PickListPanel(); // for testing + + this.setLayout( new GridBagLayout() ); + + GridBagConstraints gbc = + new GridBagConstraints(); + gbc.gridx = GridBagConstraints.RELATIVE; + gbc.gridy = GridBagConstraints.RELATIVE; + gbc.gridwidth = 1; + gbc.gridheight = 1; + gbc.weightx = 1.0; + gbc.weighty = 0.0; + gbc.anchor = GridBagConstraints.CENTER; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(0, 5, 0, 10); + gbc.ipadx = 0; + gbc.ipady = 0; + +//1.2 new GridBagConstraints(GridBagConstraints.RELATIVE, GridBagConstraints.RELATIVE, 1, 1, 1.0, 0.0, +//1.2 GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(0, 5, 0, 10), 0, 0 ); + + this.add( statusComponent, gbc ); + + buttonContainer = new JPanel(); + buttonPanelLayout = new BetterFlowLayout(); + buttonContainer.setLayout(buttonPanelLayout); + buttonPanelLayout.setAlignment( BetterFlowLayout.RIGHT ); + ((BetterFlowLayout)buttonPanelLayout).setWidthUniform( true ); + gbc.weightx = 0.0; + gbc.insets = new Insets( 0, 0, 0, 0 ); + this.add( buttonContainer, gbc ); + } + +/** +* Sets the text to appear in the status area. +* @param newText A string to appear in the status area. Nulls are allowed. +*/ + public void setText(String newText) + { + // TODO: should use property introspection instead + + // use reflection to call the "setText" method, if any. + try + { + Class c = statusComponent.getClass(); + Method m = c.getMethod( "setText", new Class[] { new String().getClass() } ); + m.invoke( statusComponent, new Object[] { newText } ); + broadcastEvent( new ActionEvent( this, ActionEvent.ACTION_PERFORMED, STATUS_CHANGED ) ); + statusComponent.paint( statusComponent.getGraphics() ); + } + catch ( Exception exc ) + { + // "setText" method does not exist; do nothing. + } + + // if non-empty string, start the timer + if ( ! "".equals( newText ) ) + { + timer.restart(); + } + } + +/** +* Gets the text in the status area. +* @return The string being displayed in the status area. +*/ + public String getText() + { + // TODO: should use property introspection instead + + String value = ""; + // use reflection to call the "setText" method, if any. + try + { + Class c = statusComponent.getClass(); + Method m = c.getMethod( "getText", (Class[])null ); + value = (String) m.invoke( statusComponent, (Object[])null ); + } + catch ( Exception exc ) + { + // "getText" method does not exist; do nothing. + } + return value; + } + + // for testing + + public static void main( String[] argv ) + { + try + { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + } + catch (Exception exc) + { + + } + + JFrame dialog = new JFrame(); + BorderLayout bl = new BorderLayout( 20, 20 ); + +// StatusButtonPanel panel = new StatusButtonPanel(); +// System.out.println( panel.statusComponent ); + StatusButtonPanel panel = new StatusButtonPanel( new String[] { "Okay", "Cancel" } ); + + dialog.getContentPane().setLayout( bl ); + dialog.getContentPane().add( panel, BorderLayout.SOUTH ); + dialog.setLocation( 50, 50 ); + // dialog.setSize( 450, 150 ); + dialog.pack(); + dialog.setVisible( true ); + + panel.setBorder( new EmptyBorder( 5, 5, 5, 5 ) ); + panel.setAlignment( BetterFlowLayout.RIGHT ); +// panel.getButton( "One" ).setEnabled( false ); + panel.setText( "File saved." ); + System.out.println( panel.getText() ); + } + +} + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java new file mode 100644 index 0000000..a51ed16 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TintedImageFilter.java @@ -0,0 +1,100 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Color; +import java.awt.image.RGBImageFilter; + +/** + * TintedImageFilter tints all gray pixels half-way towards + * the value passed into the constructor. This "tints" a + * mostly grayscale image. This has proven useful for tinting + * user interface decorative images towards one of the SystemColor + * constants to better mesh with a platform look and feel. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 893 $ + */ + public class TintedImageFilter extends RGBImageFilter + { + double redOffset, greenOffset, blueOffset; + + public TintedImageFilter( Color aColor ) + { + canFilterIndexColorModel = true; + redOffset = getOffset( aColor.getRed() ); + greenOffset = getOffset( aColor.getGreen() ); + blueOffset = getOffset( aColor.getBlue() ); + } + + /** + * Calculates the offset used to modify color + * values. This method returns half the difference + * between the specified color level and 192. + */ + protected double getOffset( int colorValue ) + { + return ( colorValue - 192 ) / 2; + } + + public int filterRGB(int x, int y, int rgb) + { + + int red = ( rgb & 0xff0000 ) >> 16; + int green = ( rgb & 0x00ff00 ) >> 8; + int blue = ( rgb & 0x0000ff ); + + // if roughly black + if ( red + green + blue < 30 ) return rgb; + + // if roughly gray + if ( ( Math.abs( red - green ) < 10 ) + && ( Math.abs( red - blue ) < 10 ) ) + { + red += redOffset; + if ( red < 0 ) red = 0; + if ( red > 255 ) red = 255; + green += greenOffset; + if ( green < 0 ) green = 0; + if ( green > 255 ) green = 255; + blue += blueOffset; + if ( blue < 0 ) blue = 0; + if ( blue > 255 ) blue = 255; + + return new Color( red, green, blue ).getRGB(); + } + + return rgb; + } + } + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/01/18 21:27:04 mpowers + * Made the tinting a little darker. + * + * Revision 1.1 2001/01/12 17:36:27 mpowers + * Contributing TintedImageFilter. + * + * + */ diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java new file mode 100644 index 0000000..f0bb6c2 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeChooser.java @@ -0,0 +1,727 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Stack; +import java.util.Vector; + +import javax.swing.ComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JToolBar; +import javax.swing.JTree; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; +import javax.swing.UIManager; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* TreeChooser is a FileChooser-like panel that +* uses a TreeModel as a data source. It basically +* provides an alternative to JTree for rendering +* and manipulating tree-like data. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeChooser extends JPanel + implements ActionListener, ListSelectionListener, + TreeSelectionListener, TreeModelListener, ListCellRenderer +{ + /** + * The TreeChooser responds to this action command + * by calling displayPrevious(). + */ + public static final String BACK = "Back"; + + /** + * The TreeChooser responds to this action command + * by calling displayHome(). + */ + public static final String HOME = "Home"; + + /** + * The TreeChooser responds to this action command + * by calling displayParent(). + */ + public static final String UP = "Up"; + + /** + * The TreeChooser responds to this action command + * by attempting to navigate to the first node in + * the current selection and display that node's children. + */ + public static final String SELECT = "Select"; + + protected JList contents; + protected JComboBox pathCombo; + protected JToolBar toolBar; + + protected TreeModel model; + protected TreeSelectionModel selectionModel; + protected TreeCellRenderer renderer; + protected TreePath displayPath; + protected Stack pathStack; + protected int pathIndent; + + private ChooserComboBoxModel comboBoxModel; + private JTree bogusJTree; // needed for tree cell renderer + private Dimension preferredSize; + + public TreeChooser() + { + preferredSize = new Dimension( 300, 200 ); + model = new DefaultTreeModel( new DefaultMutableTreeNode( "Root" ) ); + displayPath = new TreePath( model.getRoot() ); + selectionModel = new DefaultTreeSelectionModel(); + renderer = new DefaultTreeCellRenderer(); + pathStack = new Stack(); + pathIndent = 0; // 16; + comboBoxModel = new ChooserComboBoxModel( this ); + + bogusJTree = new JTree(); + bogusJTree.setModel( model ); + + init(); + displayHome(); + + stopListening(); // clear existing listeners + startListening(); + } + + public Dimension getPreferredSize() + { + return preferredSize; + } + + protected void init() + { + this.setLayout( new BorderLayout( 10, 10 ) ); + + contents = initList(); + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION ); + // synchs with DefaultTreeSelectionModel + + JScrollPane scrollPane = new JScrollPane( contents ); + scrollPane.setPreferredSize( new Dimension( 200, 150 ) ); + this.add( scrollPane, BorderLayout.CENTER ); + + Component previewPane = initPreviewPane(); + if ( previewPane != null ) + { + this.add( previewPane, BorderLayout.EAST ); + } + + JPanel navigationPanel = new JPanel(); + navigationPanel.setLayout( new BorderLayout( 10, 10 ) ); + this.add( navigationPanel, BorderLayout.NORTH ); + + pathCombo = initComboBox(); + if ( pathCombo != null ) + { + pathCombo.setModel( comboBoxModel ); + + // put combo in a grid bag to handle varying + // heights of JToolBars across platforms + JPanel panel = new JPanel(); + panel.setLayout( new GridBagLayout() ); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + panel.add( pathCombo, gbc ); + navigationPanel.add( panel, BorderLayout.CENTER ); + } + + Component toolBar = initToolBar(); + if ( toolBar != null ) + { + navigationPanel.add( toolBar, BorderLayout.EAST ); + } + + } + + /** + * Creates tool bar or return null if no tool bar is desired. + * This implementation returns a JToolBar containing buttons + * for BACK, UP, and HOME. + */ + protected Component initToolBar() + { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable( false ); + JButton button; + button = new JButton( UIManager.getIcon("FileChooser.upFolderIcon") ); + button.setActionCommand( UP ); + button.addActionListener( this ); + toolBar.add( button ); + button = new JButton( UIManager.getIcon("FileChooser.homeFolderIcon") ); + button.setActionCommand( HOME ); + button.addActionListener( this ); + toolBar.add( button ); +/* + button = new JButton( UIManager.getIcon("FileChooser.newFolderIcon") ); + button.setActionCommand( BACK ); + button.addActionListener( this ); + toolBar.add( button ); +*/ + return toolBar; + } + + /** + * Creates the component that is used to display a preview of the + * selected item(s) in the content area. This component would listen + * to the selection model to update itself when the selected items change. + * Return null to omit this component. + * This implementation returns null. + */ + protected Component initPreviewPane() + { + return null; + } + + /** + * Creates the JComboBox that is used to render the path leading to + * the displayed contents. Return null to omit this combo box. + * This implementation returns a stock JComboBox that uses this + * class as its cell renderer. + */ + protected JComboBox initComboBox() + { + JComboBox comboBox = new JComboBox(); + comboBox.setRenderer( this ); + return comboBox; + } + + /** + * Creates the JList that is used to render the path leading to + * the displayed contents. This method may not return null. + * This implementation returns a stock JList that uses this + * class as its cell renderer and fires a SELECT action event + * on double click. + */ + protected JList initList() + { + JList list = new JList(); + list.setCellRenderer( this ); + list.addMouseListener( new MouseAdapter() + { + public void mouseClicked( MouseEvent evt ) + { + if ( evt.getClickCount() > 1 ) + { + actionPerformed( new ActionEvent( this, 0, SELECT ) ); + } + } + }); + return list; + } + + /** + * Begins listening to the specified tree model + * and tree selection model. + */ + protected void startListening() + { + model.addTreeModelListener( this ); + selectionModel.addTreeSelectionListener( this ); + contents.addListSelectionListener( this ); + } + + /** + * Stops listening to the specified tree model + * and tree selection model. + */ + protected void stopListening() + { + model.removeTreeModelListener( this ); + selectionModel.removeTreeSelectionListener( this ); + contents.removeListSelectionListener( this ); + } + + /** + * Returns the TreeModel used by the TreeChooser. + */ + public TreeModel getModel() + { + return model; + } + + /** + * Sets the TreeModel used by the TreeChooser. + */ + public void setModel( TreeModel aTreeModel ) + { + stopListening(); + model = aTreeModel; + bogusJTree.setModel( aTreeModel ); + pathStack.removeAllElements(); + startListening(); + displayHome(); + } + + /** + * Returns the TreeSelectionModel used by the TreeChooser. + */ + public TreeSelectionModel getSelectionModel() + { + return selectionModel; + } + + /** + * Sets the TreeSelectionModel used by the TreeChooser. + */ + public void setSelectionModel( TreeSelectionModel aSelectionModel ) + { + selectionModel = aSelectionModel; + if ( aSelectionModel.getSelectionMode() == + TreeSelectionModel.SINGLE_TREE_SELECTION ) + { + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.SINGLE_SELECTION ); + } + else + { + contents.getSelectionModel().setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION ); + } + updateSelection(); + } + + /** + * Returns the TreeCellRenderer used by the TreeChooser. + */ + public TreeCellRenderer getRenderer() + { + return renderer; + } + + /** + * Sets the TreeCellRenderer used by the TreeChooser. + */ + public void setRenderer( TreeCellRenderer aRenderer ) + { + renderer = aRenderer; + updateContents(); + } + + /** + * Displays the "home" directory. + * This implementation displays the root node's children. + */ + public void displayHome() + { + setDisplayPath( null ); + } + + /** + * Displays the parent path of the currently displayed path. + */ + public void displayParent() + { + setDisplayPath( displayPath.getParentPath() ); + } + + /** + * Displays the last displayed path before the current one, + * emulating the behavior of a "back" button. + */ + public void displayPrevious() + { + if ( pathStack.empty() ) + { + displayHome(); + } + else + { + setDisplayPathDirect( (TreePath) pathStack.pop() ); + updateContents(); + } + } + + /** + * Pushes the previous item onto the stack, sets + * the display path, and then updates the contents. + * If aPath is null, the root node's children are displayed. + */ + public void setDisplayPath( TreePath aPath ) + { + if ( aPath == null ) + { + aPath = new TreePath( getModel().getRoot() ); + } + if ( ! displayPath.equals ( aPath ) ) + { + pathStack.push( displayPath ); + setDisplayPathDirect( aPath ); + } + updateContents(); + } + + /** + * Sets the displayPath field and does not + * update the stack nor update the contents. + */ + protected void setDisplayPathDirect( TreePath aPath ) + { + displayPath = aPath; + } + + /** + * Gets the currently displayed path. + */ + public TreePath getDisplayPath() + { + return displayPath; + } + + /** + * Called when selected path changes or when model indicates + * that the displayed path has changed. + */ + protected void updateContents() + { + stopListening(); + + // update combo box + comboBoxModel.fireContentsChanged(); + + // update list contents + Object displayedObject = displayPath.getLastPathComponent(); +/* +//FIXME: this display group doesn't seem to be getting the sort orderings from parent +if ( displayedObject instanceof net.wotonomy.ui.EODisplayGroup ) +System.out.println( ((net.wotonomy.ui.EODisplayGroup)displayedObject).displayedObjects() ); +*/ + int count = model.getChildCount( displayedObject ); + Object[] children = new Object[ count ]; + for ( int i = 0; i < count; i++ ) + { + children[i] = model.getChild( displayedObject, i ); + } + contents.setListData( children ); + + startListening(); + + // synchronize the selection + updateSelection(); + } + + /** + * Updates the selection in the list to reflect the + * selection in the tree selection model. + */ + public void updateSelection() + { + int index; + Object last = displayPath.getLastPathComponent(); + TreePath[] selectionPaths = selectionModel.getSelectionPaths(); + if ( selectionPaths != null ) + { + List selectedIndices = new LinkedList(); + for ( int i = 0; i < selectionPaths.length; i++ ) + { + if ( displayPath.equals( selectionPaths[i].getParentPath() ) ) + { + index = getModel().getIndexOfChild( + last, selectionPaths[i].getLastPathComponent() ); + if ( index != -1 ) + { + selectedIndices.add( new Integer( index ) ); + } + else // should never happen + { + throw new WotonomyException( + "Could not find child of displayed node." ); + } + } + } + int[] selected = new int[ selectedIndices.size() ]; + for ( int i = 0; i < selected.length; i++ ) + { + selected[i] = ((Integer)selectedIndices.get(i)).intValue(); + } + stopListening(); + contents.setSelectedIndices( selected ); + startListening(); + } + } + + // interface TreeModelListener + + public void treeNodesChanged( TreeModelEvent evt ) + { +/* + if ( displayPath.getLastPathComponent().toString().equals( + evt.getTreePath().getLastPathComponent().toString() ) ) + { +System.out.println( "TreeChooser.treeNodesChanged: " + count++ ); +*/ + updateContents(); +/* + } + else + { + System.out.println( evt.getTreePath() + " != " + displayPath ); + } +*/ + } + + public void treeNodesInserted( TreeModelEvent evt ) + { +// updateContents(); + } + + public void treeNodesRemoved( TreeModelEvent evt ) + { +// updateContents(); + } + + public void treeStructureChanged( TreeModelEvent evt ) + { + if ( ( evt.getTreePath().equals( displayPath ) ) + || ( evt.getTreePath().isDescendant( displayPath ) ) ) + { +// setDisplayPath( evt.getTreePath() ); + } + + displayHome(); + } + + // interface TreeSelectionListener + + /** + * Called when the tree selection model's value changes. + * This is presumably an external change, so this calls + * updateSelection. + */ + public void valueChanged( TreeSelectionEvent evt ) + { + updateSelection(); + } + + // interface ListSelectionListener + + /** + * Called when user changes the selection in the list. + * This implementation updates the tree selection model + * with the corresponding selection. + */ + public void valueChanged( ListSelectionEvent evt ) + { + if ( ! evt.getValueIsAdjusting() ) + { + Object last = displayPath.getLastPathComponent(); + int[] selection = contents.getSelectedIndices(); + TreePath[] selectionPaths = new TreePath[ selection.length ]; + for ( int i = 0; i < selection.length; i++ ) + { + selectionPaths[i] = displayPath.pathByAddingChild( + getModel().getChild( last, selection[i] ) ); + } + selectionModel.setSelectionPaths( selectionPaths ); + } + + } + + // interface ListCellRenderer + + /** + * This method returns the component returned by the tree cell renderer. + */ + public Component getListCellRendererComponent( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus ) + { + boolean isLeaf = ( model.isLeaf( value ) ); + + bogusJTree.setForeground( list.getForeground() ); + bogusJTree.setBackground( list.getBackground() ); + + JComponent result = (JComponent) renderer.getTreeCellRendererComponent( + bogusJTree, value, isSelected, (list != contents), + isLeaf, index, cellHasFocus ); +/* + if ( ( list != contents ) && ( index > -1 ) ) + { + result.setBorder( + BorderFactory.createEmptyBorder( 0, index*pathIndent, 0, 0 ) ); + } + else + { + result.setBorder( + BorderFactory.createEmptyBorder() ); + } +*/ + return result; + } + + // interface ActionListener + + public void actionPerformed( ActionEvent evt ) + { + String command = evt.getActionCommand(); + + if ( HOME.equals( command ) ) + { + displayHome(); + } + else + if ( UP.equals( command ) ) + { + displayParent(); + } + else + if ( BACK.equals( command ) ) + { + displayPrevious(); + } + else + if ( SELECT.equals( command ) ) + { + Cursor oldCursor = getCursor(); + setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) ); + + int index = contents.getSelectedIndex(); + // if selection + if ( index != -1 ) + { + Object parent = displayPath.getLastPathComponent(); + Object child = getModel().getChild( parent, index ); + // if selected item is not a leaf + if ( getModel().getChildCount( child ) > 0 ) + { + // navigate to selected item + setDisplayPath( displayPath.pathByAddingChild( child ) ); + } + } + + setCursor( oldCursor ); + } + + } + + private class ChooserComboBoxModel implements ComboBoxModel + { + TreeChooser treeChooser; + Vector listeners; + + ChooserComboBoxModel( TreeChooser aTreeChooser ) + { + treeChooser = aTreeChooser; + listeners = new Vector(); + } + + public int getSize() + { + return treeChooser.displayPath.getPathCount(); + } + + public Object getElementAt(int index) + { + return treeChooser.displayPath.getPathComponent( index ); + } + + public Object getSelectedItem() + { + return treeChooser.displayPath.getLastPathComponent(); + } + + public void setSelectedItem(Object anItem) + { + if ( ! ( + treeChooser.displayPath.getLastPathComponent().equals( anItem ) ) ) + { + Object[] items = treeChooser.displayPath.getPath(); + TreePath path = new TreePath( getModel().getRoot() ); + for ( int i = 1; i < items.length; i++ ) + { + if ( path.getLastPathComponent() == anItem ) + { + treeChooser.setDisplayPath( path ); + return; + } + path = path.pathByAddingChild( items[i] ); + } + } + } + + public void addListDataListener(ListDataListener l) + { + listeners.add( l ); + } + + public void removeListDataListener(ListDataListener l) + { + listeners.remove( l ); + } + + public void fireContentsChanged() + { + Enumeration e = listeners.elements(); + while ( e.hasMoreElements() ) + { + ((ListDataListener)e.nextElement()).contentsChanged( + new ListDataEvent( + this, ListDataEvent.CONTENTS_CHANGED, 0, getSize() ) ); + } + } + } + +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java new file mode 100644 index 0000000..fbf3791 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/TreeTableCellRenderer.java @@ -0,0 +1,224 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2002 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.components; + +import java.awt.Component; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.JViewport; +import javax.swing.table.TableCellRenderer; + +/** +* A TableCellRenderer that paints a portion of a JTree. +* Extends JViewport to take advantage of buffering and +* fast blitting (avoids repeated clipping and repainting). +* Defaults opaque to false: to see selection background +* painted, call setOpaque( true ). +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TreeTableCellRenderer extends JViewport implements TableCellRenderer, MouseListener { + + JTree tree; + Component emptyComponent; + JTable delegateTable; + int lastKnownColumn; + + /** + * Constructor takes a JTree and modifies it by setting + * rootVisible to false, showsRootHandles to true, + * opaque to false, and border to null. + */ + public TreeTableCellRenderer( JTree aTree ) + { + setView( aTree ); + setBorder( null ); + tree = aTree; + tree.setRootVisible( false ); + tree.setShowsRootHandles( true ); + tree.setBorder( null ); + tree.setOpaque( false ); + + Object renderer = tree.getCellRenderer(); + if ( renderer instanceof JComponent ) + { + ((JComponent)renderer).setOpaque( false ); + } + Object editor = tree.getCellEditor(); + if ( editor instanceof JComponent ) + { + ((JComponent)editor).setOpaque( false ); + } + + this.setOpaque( false ); + emptyComponent = new JLabel(); + } + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) + { + if ( isSelected ) + { + setForeground( table.getSelectionForeground() ); + setBackground( table.getSelectionBackground() ); + } + else + { + setForeground( table.getForeground() ); + setBackground( table.getBackground() ); + } + + lastKnownColumn = column; + if ( delegateTable != table ) + { + if ( delegateTable != null ) + { + delegateTable.removeMouseListener( this ); + } + table.addMouseListener( this ); + delegateTable = table; + } + + Rectangle rect = tree.getRowBounds( row ); + if ( rect != null ) + { + setViewPosition( new Point( 0 /*rect.x*/, rect.y ) ); + + //FIXME: this causes problems for some LAFs (like Metal): + // in particular, the table height seems to get stuck. + //if ( table.getRowHeight( row ) != rect.height ) + //{ + // table.setRowHeight( row, rect.height ); + //} + return this; + } + else + { + return emptyComponent; + } + } + + public void mouseClicked(MouseEvent e) + { + delegateToTree( e ); + } + + public void mousePressed(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseReleased(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseEntered(MouseEvent e) + { + delegateToTree( e ); + } + + public void mouseExited(MouseEvent e) + { + delegateToTree( e ); + } + + protected void delegateToTree(MouseEvent e) + { + int col = delegateTable.getColumnModel().getColumnIndexAtX( e.getX() ); + if ( col == lastKnownColumn ) + { + Rectangle nodeRect = tree.getRowBounds( 0 ); + Rectangle cellRect = delegateTable.getCellRect( -1, col, false ); + if ( nodeRect != null ) + { + e.translatePoint( -cellRect.x, nodeRect.y ); + tree.dispatchEvent( // e ); + new MouseEvent( tree, e.getID(), e.getWhen(), e.getModifiers(), + e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger() ) ); + } + } + } + + public void repaint() + { + //if ( delegateTable != null ) delegateTable.repaint(); + + // not calling super.repaint() does not seem to cause + // any problems so we're not doing it. + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.11 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.10 2002/04/12 20:07:35 mpowers + * Fixed cool/annoying view position. + * + * Revision 1.9 2002/04/09 18:12:21 mpowers + * Fixes for 1.4. + * + * Revision 1.8 2002/03/22 22:39:24 mpowers + * Can now move column to any position in the table. + * + * Revision 1.7 2002/03/11 03:13:22 mpowers + * Adjusting for viewport position; no longer responding to repaint(). + * + * Revision 1.6 2002/03/07 23:04:36 mpowers + * Refining TreeColumnAssociation. + * + * Revision 1.5 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.3 2002/02/27 23:19:17 mpowers + * Refactoring of TreeAssociation to create TreeModelAssociation parent. + * + * Revision 1.2 2002/02/18 23:13:55 mpowers + * Only setting row height when needed. + * + * Revision 1.1 2002/02/18 03:46:08 mpowers + * Implemented TreeTableCellRenderer. + * + * + */ + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html new file mode 100644 index 0000000..618651b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/components/package.html @@ -0,0 +1,26 @@ + +

+Contains various and useful Swing components. +These can be used in conjunction with +the ui framework, however, there are no +dependencies and all of these components can be used +independently of the rest of the framework. +

+

+Of note are the ButtonPanel classes, which +automate the placement and layout of buttons +in a manner consistent with the Java Look and Feel +guidelines. This uses the BetterFlowLayout which +is another useful class. +

+

+Also of note is the InfoPanel, which automates +the placement and layout of labeled fields on +a panel. +

+

+And the +various cell renderer and editor components can +be useful as well. +

+ diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html new file mode 100644 index 0000000..574cc7b --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/package.html @@ -0,0 +1,21 @@ + +

+Contains associations designed for use +with the Swing framework. +

+

+In general, most text components can use the +TextAssociation, and most buttons, including +checkboxes and radio buttons, can use the +ButtonAssociation. +

+

+Tables are handled with the TableColumnAssociation +by dealing with TableColumns that are later added +to a table. +

+

+The TreeAssociation will handle any component that +uses a TreeModel and a TreeSelectionModel. +

+ diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java new file mode 100644 index 0000000..4412dbc --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ClassGrabber.java @@ -0,0 +1,126 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Hashtable; + +/** + * ClassGrabber is a class loader used by WindowGrabber. + * It simply loads classes by filename and nothing more. + * It exists mainly because the java 1.1 class loading + * framework doesn't easily allow the creation of class + * loaders nor the loading of arbitrary classes. + * + * @author michael@mpowers.net + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class ClassGrabber extends ClassLoader +{ + Hashtable classMap = new Hashtable(); + + public ClassGrabber() + { + super(); + } + + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + Class c = (Class) classMap.get( name ); + + if ( c != null ) return c; + + try + { + c = findSystemClass( name ); + } + catch ( Exception exc1 ) + { + // System.err.print( "findSystemClass: " + name + ": " ); + // System.err.println( exc1 ); + } + + if ( c != null ) return c; + + try + { + c = findLoadedClass( name ); + } + catch ( Exception exc1 ) + { + // System.err.print( "findLoadedClass: " + name + ": " ); + // System.err.println( exc1 ); + } + + if ( c != null ) return c; + + try + { + InputStream input = new BufferedInputStream( new FileInputStream( name ) ); + ByteArrayOutputStream output = new ByteArrayOutputStream( 200 ); + int ch; + while ( ( ch = input.read() ) != -1 ) + { + output.write( ch ); + } + byte[] data = output.toByteArray(); + c = defineClass( null, data, 0, data.length ); + } + catch ( Exception exc ) + { + System.err.print( "getResource: " + name + ": " ); + System.err.println( exc ); + c = null; + } + + if ( c != null ) + { + classMap.put( name, c ); + if ( resolve ) resolveClass( c ); + } + + return c; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:51:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java new file mode 100644 index 0000000..c63157d --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ComponentHighlighter.java @@ -0,0 +1,160 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URL; + +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import javax.swing.Timer; + +/** +* Visually highlights a component with the specified image for a brief period. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +*/ +public class ComponentHighlighter implements ActionListener +{ + // lots of state to track + JRootPane rootPane; + JComponent component; + Component oldGlassPane; + JLabel imageLabel; + Timer timer; + JPanel glassPane; + +/** +* Alternate "Fire-and-forget" constructor loads an image from a URL. +* @param aComponent A Component that will be highlighted. +* @param aURL A URL pointing to an image. +*/ + public ComponentHighlighter( JComponent aComponent, URL aURL ) + { + if ( aURL == null ) return; + init( aComponent, Toolkit.getDefaultToolkit().getImage( aURL ) ); + } + +/** +* "Fire-and-forget" constructor. +* @param aComponent A Component that will be highlighted. +* @param anImage An image, preferably an animated GIF with transparency, +* that will slide along the length of the component. +*/ + public ComponentHighlighter( JComponent aComponent, Image anImage ) + { + init( aComponent, anImage ); + } + + protected void init( JComponent aComponent, Image anImage ) + { + if ( ( aComponent == null ) || ( anImage == null ) ) return; + + component = aComponent; + rootPane = SwingUtilities.getRootPane( component ); + oldGlassPane = rootPane.getGlassPane(); + + glassPane = new JPanel(); + rootPane.setGlassPane( glassPane ); + glassPane.setVisible( true ); + glassPane.setOpaque( false ); + glassPane.setLayout( null ); + + ImageIcon icon = new ImageIcon( anImage ); + + imageLabel = new JLabel(); + imageLabel.setIconTextGap( 0 ); + imageLabel.setIcon( icon ); + imageLabel.setSize( icon.getIconWidth(), icon.getIconHeight() ); + glassPane.add( imageLabel ); + + Rectangle bounds = component.getBounds(); + if ( component.getParent() instanceof Component ) + { + bounds = SwingUtilities.convertRectangle( (Container) component.getParent(), + bounds, rootPane.getContentPane() ); + } + imageLabel.setLocation( + bounds.x, bounds.y + bounds.height - imageLabel.getBounds().height ); + + glassPane.revalidate(); + glassPane.repaint(); + + component.transferFocus(); // halts a caret, if necessary + + timer = new Timer( 80, this ); + timer.setRepeats( true ); + timer.start(); + } + + public void actionPerformed( ActionEvent evt ) + { + Rectangle bounds = imageLabel.getBounds(); + Rectangle target = component.getBounds(); + if ( component.getParent() instanceof Component ) + { + target = SwingUtilities.convertRectangle( (Container) component.getParent(), + target, rootPane.getContentPane() ); + } + + if ( bounds.x + bounds.width > target.x + target.width ) + { // clean up and end + timer.stop(); + rootPane.setGlassPane( oldGlassPane ); + component.requestFocus(); + return; + } + + // else, slide to the right and continue + imageLabel.setLocation( + bounds.x + Math.max( bounds.width / 12, 1 ), bounds.y ); + imageLabel.repaint(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:51:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java new file mode 100644 index 0000000..82fd897 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/GIFEncoder.java @@ -0,0 +1,520 @@ +/* + The source code in this file, GIFEncoder.java, + belongs to the public domain. +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.AWTException; +import java.awt.Image; +import java.awt.image.PixelGrabber; +import java.io.IOException; +import java.io.OutputStream; + +/** + * GIFEncoder is a class which takes an image and saves it to a stream + * using the GIF file format. A GIFEncoder is constructed with either + * an AWT Image (which must be fully loaded) or a set of RGB arrays. + * The image can be written out with a call to write.

+ * + * Three caveats: + *
    + *
  • GIFEncoder will convert the image to indexed color upon + * construction, and this is not fast. + * + *
  • The image cannot have more than 256 colors, since GIF is an 8 + * bit format. + * + *
  • Since the image must be completely loaded into memory, + * there may be problems with large images. + *
+ * + * This implementation is heavily based on code made available by + * Adam Doppelt, which was based upon gifsave.c, which was written + * and released by Sverre H. Huseby. + * + * @author amd@brown.edu + * @author sverrehu@ifi.uio.no + * @author michael@mpowers.net + */ +public class GIFEncoder +{ + short width_, height_; + int numColors_; + byte pixels_[], colors_[]; + + ScreenDescriptor sd_; + ImageDescriptor id_; + +/** + * Construct a GIFEncoder. The constructor will convert the image to + * an indexed color array. This may take some time. If more than 256 + * colors are encountered, all subsequent colors are mapped to the first + * color encountered. + * @param image The image to encode. The image must be completely loaded. + * @exception AWTException Will be thrown if the pixel grab fails. This + * can happen if Java runs out of memory. + * */ + public GIFEncoder(Image image) throws AWTException + { + width_ = (short)image.getWidth(null); + height_ = (short)image.getHeight(null); + + int values[] = new int[width_ * height_]; + PixelGrabber grabber = new PixelGrabber( + image, 0, 0, width_, height_, values, 0, width_); + + try + { + if(grabber.grabPixels() != true) + throw new AWTException("Grabber returned false: " + + grabber.status()); + } + catch (InterruptedException e) + { + } + + byte r[][] = new byte[width_][height_]; + byte g[][] = new byte[width_][height_]; + byte b[][] = new byte[width_][height_]; + int index = 0; + for (int y = 0; y < height_; ++y) + { + for (int x = 0; x < width_; ++x) + { + r[x][y] = (byte)((values[index] >> 16) & 0xFF); + g[x][y] = (byte)((values[index] >> 8) & 0xFF); + b[x][y] = (byte)((values[index]) & 0xFF); + ++index; + } + } + toIndexedColor(r, g, b); + } + +/** + * Construct a GIFEncoder. The constructor will convert the image to + * an indexed color array. This may take some time.

+ * Each array stores intensity values for the image. In other words, + * r[x][y] refers to the red intensity of the pixel at column x, row y. + * @param r An array containing the red intensity values. + * @param g An array containing the green intensity values. + * @param b An array containing the blue intensity values. + * + * @exception AWTException Will be thrown if the image contains more than + * 256 colors. + * */ + public GIFEncoder(byte r[][], byte g[][], byte b[][]) throws AWTException + { + width_ = (short)(r.length); + height_ = (short)(r[0].length); + + toIndexedColor(r, g, b); + } + +/** + * Writes the image out to a stream in the GIF file format. This will + * be a single GIF87a image, non-interlaced, with no background color. + * This may take some time.

+ * + * @param output The stream to output to. This should probably be a + * buffered stream. + * + * @exception IOException Will be thrown if a write operation fails. + * */ + public void write(OutputStream output) throws IOException + { + BitUtils.writeString(output, "GIF87a"); + ScreenDescriptor sd = new ScreenDescriptor(width_, height_, + numColors_); + sd.write(output); + + output.write(colors_, 0, colors_.length); + + ImageDescriptor id = new ImageDescriptor(width_, height_, ','); + id.write(output); + + byte codesize = BitUtils.bitsNeeded(numColors_); + if (codesize == 1) + ++codesize; + output.write(codesize); + + LZWCompressor.LZWCompress(output, codesize, pixels_); + output.write(0); + id = new ImageDescriptor((byte)0, (byte)0, ';'); + id.write(output); + output.flush(); + } + + void toIndexedColor(byte r[][], byte g[][], + byte b[][]) throws AWTException + { + pixels_ = new byte[width_ * height_]; + colors_ = new byte[256 * 3]; + int colornum = 0; + for (int x = 0; x < width_; ++x) + { + for (int y = 0; y < height_; ++y) + { + int search; + for (search = 0; search < colornum; ++search) + if (colors_[search * 3] == r[x][y] && + colors_[search * 3 + 1] == g[x][y] && + colors_[search * 3 + 2] == b[x][y]) + break; + + if (search > 255) + search = 0; + //throw new AWTException("Too many colors."); + + pixels_[y * width_ + x] = (byte)search; + + if (search == colornum) { + colors_[search * 3] = r[x][y]; + colors_[search * 3 + 1] = g[x][y]; + colors_[search * 3 + 2] = b[x][y]; + ++colornum; + } + } + } + + numColors_ = 1 << BitUtils.bitsNeeded(colornum); + byte copy[] = new byte[numColors_ * 3]; + System.arraycopy(colors_, 0, copy, 0, numColors_ * 3); + colors_ = copy; + } + +} + +class BitFile +{ + OutputStream output_; + byte buffer_[]; + int index_, bitsLeft_; + + public BitFile(OutputStream output) + { + output_ = output; + buffer_ = new byte[256]; + index_ = 0; + bitsLeft_ = 8; + } + + public void flush() throws IOException + { + int numBytes = index_ + (bitsLeft_ == 8 ? 0 : 1); + if (numBytes > 0) + { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + } + + public void writeBits(int bits, int numbits) throws IOException { + int bitsWritten = 0; + int numBytes = 255; + do + { + if ((index_ == 254 && bitsLeft_ == 0) || index_ > 254) + { + output_.write(numBytes); + output_.write(buffer_, 0, numBytes); + + buffer_[0] = 0; + index_ = 0; + bitsLeft_ = 8; + } + + if (numbits <= bitsLeft_) + { + buffer_[index_] |= (bits & ((1 << numbits) - 1)) << + (8 - bitsLeft_); + bitsWritten += numbits; + bitsLeft_ -= numbits; + numbits = 0; + } + else + { + buffer_[index_] |= (bits & ((1 << bitsLeft_) - 1)) << + (8 - bitsLeft_); + bitsWritten += bitsLeft_; + bits >>= bitsLeft_; + numbits -= bitsLeft_; + buffer_[++index_] = 0; + bitsLeft_ = 8; + } + + } + while (numbits != 0); + } +} + +class LZWStringTable +{ + private final static int RES_CODES = 2; + private final static short HASH_FREE = (short)0xFFFF; + private final static short NEXT_FIRST = (short)0xFFFF; + private final static int MAXBITS = 12; + private final static int MAXSTR = (1 << MAXBITS); + private final static short HASHSIZE = 9973; + private final static short HASHSTEP = 2039; + + byte strChr_[]; + short strNxt_[]; + short strHsh_[]; + short numStrings_; + + public LZWStringTable() + { + strChr_ = new byte[MAXSTR]; + strNxt_ = new short[MAXSTR]; + strHsh_ = new short[HASHSIZE]; + } + + public int addCharString(short index, byte b) + { + int hshidx; + + if (numStrings_ >= MAXSTR) + return 0xFFFF; + + hshidx = Hash(index, b); + while (strHsh_[hshidx] != HASH_FREE) + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + + strHsh_[hshidx] = numStrings_; + strChr_[numStrings_] = b; + strNxt_[numStrings_] = (index != HASH_FREE) ? index : NEXT_FIRST; + + return numStrings_++; + } + + public short findCharString(short index, byte b) + { + int hshidx, nxtidx; + + if (index == HASH_FREE) + return b; + + hshidx = Hash(index, b); + while ((nxtidx = strHsh_[hshidx]) != HASH_FREE) + { + if (strNxt_[nxtidx] == index && strChr_[nxtidx] == b) + return (short)nxtidx; + hshidx = (hshidx + HASHSTEP) % HASHSIZE; + } + + return (short)0xFFFF; + } + + public void clearTable(int codesize) + { + numStrings_ = 0; + + for (int q = 0; q < HASHSIZE; q++) + { + strHsh_[q] = HASH_FREE; + } + + int w = (1 << codesize) + RES_CODES; + for (int q = 0; q < w; q++) + { + addCharString((short)0xFFFF, (byte)q); + } + } + + static public int Hash(short index, byte lastbyte) + { + return ((int)((short)(lastbyte << 8) ^ index) & 0xFFFF) % HASHSIZE; + } +} + +class LZWCompressor { + + public static void LZWCompress(OutputStream output, int codesize, + byte toCompress[]) throws IOException + { + byte c; + short index; + int clearcode, endofinfo, numbits, limit, errcode; + short prefix = (short)0xFFFF; + + BitFile bitFile = new BitFile(output); + LZWStringTable strings = new LZWStringTable(); + + clearcode = 1 << codesize; + endofinfo = clearcode + 1; + + numbits = codesize + 1; + limit = (1 << numbits) - 1; + strings.clearTable(codesize); + bitFile.writeBits(clearcode, numbits); + + for (int loop = 0; loop < toCompress.length; ++loop) + { + c = toCompress[loop]; + if ((index = strings.findCharString(prefix, c)) != -1) + { + prefix = index; + } + else + { + bitFile.writeBits(prefix, numbits); + if (strings.addCharString(prefix, c) > limit) { + if (++numbits > 12) { + bitFile.writeBits(clearcode, numbits - 1); + strings.clearTable(codesize); + numbits = codesize + 1; + } + limit = (1 << numbits) - 1; + } + + prefix = (short)((short)c & 0xFF); + } + } + + if (prefix != -1) + bitFile.writeBits(prefix, numbits); + + bitFile.writeBits(endofinfo, numbits); + bitFile.flush(); + } +} + +class ScreenDescriptor +{ + public short localScreenWidth_, localScreenHeight_; + private byte byte_; + public byte backgroundColorIndex_, pixelAspectRatio_; + + public ScreenDescriptor(short width, short height, int numColors) + { + localScreenWidth_ = width; + localScreenHeight_ = height; + setGlobalColorTableSize((byte)(BitUtils.bitsNeeded(numColors) - 1)); + setGlobalColorTableFlag((byte)1); + setSortFlag((byte)0); + setColorResolution((byte)7); + backgroundColorIndex_ = 0; + pixelAspectRatio_ = 0; + } + + public void write(OutputStream output) throws IOException + { + BitUtils.writeWord(output, localScreenWidth_); + BitUtils.writeWord(output, localScreenHeight_); + output.write(byte_); + output.write(backgroundColorIndex_); + output.write(pixelAspectRatio_); + } + + public void setGlobalColorTableSize(byte num) + { + byte_ |= (num & 7); + } + + public void setSortFlag(byte num) + { + byte_ |= (num & 1) << 3; + } + + public void setColorResolution(byte num) + { + byte_ |= (num & 7) << 4; + } + + public void setGlobalColorTableFlag(byte num) + { + byte_ |= (num & 1) << 7; + } +} + +class ImageDescriptor +{ + public byte separator_; + public short leftPosition_, topPosition_, width_, height_; + private byte byte_; + + public ImageDescriptor(short width, short height, char separator) + { + separator_ = (byte)separator; + leftPosition_ = 0; + topPosition_ = 0; + width_ = width; + height_ = height; + setLocalColorTableSize((byte)0); + setReserved((byte)0); + setSortFlag((byte)0); + setInterlaceFlag((byte)0); + setLocalColorTableFlag((byte)0); + } + + public void write(OutputStream output) throws IOException + { + output.write(separator_); + BitUtils.writeWord(output, leftPosition_); + BitUtils.writeWord(output, topPosition_); + BitUtils.writeWord(output, width_); + BitUtils.writeWord(output, height_); + output.write(byte_); + } + + public void setLocalColorTableSize(byte num) + { + byte_ |= (num & 7); + } + + public void setReserved(byte num) + { + byte_ |= (num & 3) << 3; + } + + public void setSortFlag(byte num) + { + byte_ |= (num & 1) << 5; + } + + public void setInterlaceFlag(byte num) + { + byte_ |= (num & 1) << 6; + } + + public void setLocalColorTableFlag(byte num) + { + byte_ |= (num & 1) << 7; + } +} + +class BitUtils +{ + public static byte bitsNeeded(int n) + { + byte ret = 1; + + if (n-- == 0) + return 0; + + while ((n >>= 1) != 0) + ++ret; + + return ret; + } + + public static void writeWord(OutputStream output, + short w) throws IOException + { + output.write(w & 0xFF); + output.write((w >> 8) & 0xFF); + } + + static void writeString(OutputStream output, + String string) throws IOException + { + for (int loop = 0; loop < string.length(); ++loop) + output.write((byte)(string.charAt(loop))); + } +} + + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java new file mode 100644 index 0000000..6c8d7ee --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/ObjectInspector.java @@ -0,0 +1,226 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.lang.reflect.Method; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; +import javax.swing.table.TableModel; + +import net.wotonomy.ui.swing.components.PropertyEditorTable; +import net.wotonomy.ui.swing.components.PropertyEditorTableModel; + +/** +* The ObjectInspector displays a JFrame containing +* a PropertyEditorTable that displays an object.

+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ + +public class ObjectInspector implements ActionListener, MouseListener +{ + protected JTable table = null; + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + + /** + * Displays the specified object in a frame. + */ + public ObjectInspector( Object anObject ) + { + initLayout( anObject ); + } + + protected void initLayout( Object aTargetObject ) + { + PropertyEditorTableModel model = + new PropertyEditorTableModel(); + model.setObject( aTargetObject ); + table = new PropertyEditorTable() + { + public void methodInvoked( Object anObject, Method aMethod, Object aResult ) + { + if + ( ( aResult == null ) + || ( aResult instanceof Number ) + || ( aResult instanceof Boolean ) + || ( aResult instanceof String ) ) + { + System.out.println( aMethod.getName() + ": " + aResult ); + } + else + { + new ObjectInspector( aResult ); + } + } + }; + table.setModel( model ); + table.addMouseListener( this ); // listen for double-clicks + + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 325, 350 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( aTargetObject.getClass().getName() ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + // interface MouseListener + + /** + * Double click to call invokeFileFromString. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + + if ( ( row == -1 ) || ( col != 0 ) ) return; + + /* do something here */ + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + + // interface ActionEventListener - for listening to key commands + + public void actionPerformed(ActionEvent evt) + { + if ( COPY.equals( evt.getActionCommand() ) ) + { + copyToClipboard(); + return; + } + } + +/** +* Copies the contents of the table to the clipboard as a tab-delimited string. +*/ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getTabDelimitedString() ); + clipboard.setContents( selection, selection ); + } + + /** + * Converts the contents of the table to a tab-delimited string. + * @return A String containing the text contents of the table. + */ + public String getTabDelimitedString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = table.getModel(); + int cols = model.getColumnCount(); + int rows = model.getRowCount(); + + Object o = null; + for ( int y = 0; y < rows; y++ ) + { + for ( int x = 0; x < cols; x++ ) + { + o = model.getValueAt( y, x ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\t' ); + } + result.append( '\n' ); + } + + return result.toString(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.1.1.1 2000/12/21 15:51:27 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java new file mode 100644 index 0000000..f5fe3e4 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/PositionComparator.java @@ -0,0 +1,89 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Point; +import java.io.Serializable; +import java.util.Comparator; + +import javax.swing.SwingUtilities; + +/** +* A Comparator that will sort components in a common container +* based first on their y-coordinate and then on their x-coordinate, +* producing a list sorted from top to bottom and left to right. +* If all components are not in the same container, the resulting +* sort is undefined. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class PositionComparator implements Comparator, Serializable +{ + private Container rootContainer; + private transient Component c1, c2; + private transient Point p1, p2; + +/** +* Standard constructor to configure the comparator. +* @param aContainer The common container for all the objects to be compared. +*/ + public PositionComparator( Container aContainer ) + { + rootContainer = aContainer; + } + + // interface Comparable + + public int compare(Object o1, Object o2) + { + c1 = (Component) o1; + c2 = (Component) o2; + + p1 = SwingUtilities.convertPoint( c1.getParent(), c1.getLocation(), rootContainer ); + p2 = SwingUtilities.convertPoint( c2.getParent(), c2.getLocation(), rootContainer ); + + if ( p1.y != p2.y ) + { + return p1.y - p2.y; + } + return p1.x - p2.x; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:51:27 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java new file mode 100644 index 0000000..7e61411 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/StackTraceInspector.java @@ -0,0 +1,457 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.border.EmptyBorder; +import javax.swing.event.TableModelListener; +import javax.swing.table.TableModel; + +import net.wotonomy.ui.swing.components.MultiLineLabel; + +/** +* The StackTraceInspector displays a JFrame containing +* stack trace information for a Throwable.

+* +* There are also a few static methods for obtaining +* information about the current stack, which is useful +* for determining who's calling you at runtime. +* +* @author michael@mpowers.net +* @version $Revision: 904 $ +*/ + +public class StackTraceInspector + implements TableModel, MouseListener, ActionListener +{ + protected JTable table = null; + protected List tableModelListeners = null; + protected List methodNames = new ArrayList(); + + // key command to copy contents to clipboard + static public final String COPY = "COPY"; + +/** +* Displays the current stack trace at the time +* of instantiation in a table on a frame. +*/ + public StackTraceInspector() + { + initLayout( parseStackTrace( new RuntimeException() ), null ); + } + +/** +* Displays the current stack trace at the time +* of instantiation in a table on a frame +* annotated with the specified message. +*/ + public StackTraceInspector( String aMessage ) + { + initLayout( parseStackTrace( new RuntimeException() ), aMessage ); + } + +/** +* Displays the stack trace for the given throwable +* in a table on a frame. +* @param aThrowable A Throwable whose stack will be examined. +*/ + public StackTraceInspector( Throwable aThrowable ) + { + initLayout( parseStackTrace( aThrowable ), + aThrowable.getClass() + ": " + aThrowable.getMessage() ); + } + +/** +* Simply displays the list items in a dialog. +* Presumably (but not necessarily) called from +* the other constructors. (I guess if you just +* want a frame with strings in table, you can +* call this.) +* @param aStringList A List containing Strings. +*/ + public StackTraceInspector( List aStringList ) + { + initLayout( aStringList, null ); + } + + protected void initLayout( List items, String message ) + { + methodNames = new ArrayList( items ); + table = new JTable( this ); // this class is the table model + table.addMouseListener( this ); // listen for double-clicks + + // set up keyboard events for cut-copy: Ctrl-C, Ctrl-X + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_C, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + table.registerKeyboardAction( this, COPY, + KeyStroke.getKeyStroke( KeyEvent.VK_X, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() ), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder( new Insets( 10, 10, 10, 10 ) ) ); + panel.setLayout( new BorderLayout( 10, 10 ) ); + + if ( message != null ) + { + panel.add( new MultiLineLabel( message ), BorderLayout.NORTH ); + } + + JScrollPane scrollPane = new JScrollPane( table ); + scrollPane.setPreferredSize( new Dimension( 325, 350 ) ); + panel.add( scrollPane, BorderLayout.CENTER ); + + JFrame window = new JFrame(); + window.setTitle( "Stack Trace Inspector" ); + window.getContentPane().add( panel ); + + window.pack(); + WindowUtilities.cascade( window ); + window.show(); + } + + // interface TableModel + + public int getRowCount() + { + return methodNames.size(); + } + + public int getColumnCount() + { + return 1; + } + + public String getColumnName(int columnIndex) + { + switch ( columnIndex ) + { + case 0: + return "Methods"; + case 1: + return "Property"; + } + System.out.println( "StackTraceInspector.getColumnName: unknown column: " + columnIndex ); + return ""; + } + + public Class getColumnClass(int columnIndex) + { + switch ( columnIndex ) + { + case 0: + return String.class; + case 1: + return String.class; + } + System.out.println( "StackTraceInspector.getColumnClass: unknown column: " + columnIndex ); + return Object.class; + } + + public boolean isCellEditable(int rowIndex, + int columnIndex) + { + return false; + } + + public Object getValueAt(int rowIndex, + int columnIndex) + { + return methodNames.get( rowIndex ); + } + + public void setValueAt(Object aValue, + int rowIndex, + int columnIndex) + { + } + + public void addTableModelListener(TableModelListener l) + { + if ( tableModelListeners == null ) + { + tableModelListeners = new ArrayList(); + } + tableModelListeners.add( l ); + } + + public void removeTableModelListener(TableModelListener l) + { + if ( tableModelListeners != null ) + { + tableModelListeners.remove( l ); + } + } + + // interface MouseListener + + /** + * Double click to call invokeFileFromString. + */ + + public void mouseClicked(MouseEvent e) + { + if ( e.getSource() == table ) + { + if ( e.getClickCount() > 1 ) + { + int row = table.rowAtPoint( e.getPoint() ); + int col = table.columnAtPoint( e.getPoint() ); + + if ( ( row == -1 ) || ( col != 0 ) ) return; + + invokeFileFromString( methodNames.get( row ).toString() ); + } + } + } + + public void mouseReleased(MouseEvent e) {} + public void mousePressed(MouseEvent e) {} + public void mouseEntered(MouseEvent e) {} + public void mouseExited(MouseEvent e) {} + + + // interface ActionEventListener - for listening to key commands + + public void actionPerformed(ActionEvent evt) + { + if ( COPY.equals( evt.getActionCommand() ) ) + { + copyToClipboard(); + return; + } + } + +/** +* Copies the contents of the table to the clipboard as a tab-delimited string. +*/ + public void copyToClipboard() + { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Clipboard clipboard = toolkit.getSystemClipboard(); + StringSelection selection = + new StringSelection( getSelectedStackString() ); + clipboard.setContents( selection, selection ); + } + +/** +* Converts the selected contents of the table to a string. +* @return A String containing the text contents of the table. +*/ + public String getSelectedStackString() + { + StringBuffer result = new StringBuffer(64); + + TableModel model = table.getModel(); + + Object o; + int[] selectedRows = table.getSelectedRows(); + for ( int i = 0; i < selectedRows.length; i++ ) + { + o = model.getValueAt( selectedRows[i], 0 ); + if ( o == null ) o = ""; + result.append( o ); + result.append( '\n' ); + } + + return result.toString(); + } + + + // static methods + +/** +* Obtains a list of strings representing the stack trace +* associated with this throwable starting with the most recent call. +* @param aThrowable A Throwable whose stack trace is parsed. +* @return a List containing the method names as Strings. +*/ + static public List parseStackTrace( Throwable aThrowable ) + { + String trace = null; + + // create new stream + ByteArrayOutputStream os = new ByteArrayOutputStream( 256 ); + PrintStream newErr = new PrintStream( os ); + aThrowable.printStackTrace( newErr ); // prints to System.err + + // convert to string + trace = os.toString(); + + List result = new ArrayList(); + + // populate list with parsed trace, starting from top + String token; + StringTokenizer tokens = new StringTokenizer( trace, "\n" ); + tokens.nextToken(); // strip off description of throwable + while ( tokens.hasMoreTokens() ) + { + token = tokens.nextToken(); + if ( token.indexOf( StackTraceInspector.class.getName() ) == -1 ) + { // add only those methods not from this class + + // strip whitespace, "at " from front, and \r from end + token.trim(); + token = token.substring( 4, token.length() - 1 ); + + result.add( token ); + } + } + + return result; + } + +/** +* Convenience method that obtains a String representing +* the caller's caller. +* @return a String representing a method in stack trace format. +*/ + static public String getMyCaller() + { + List trace = parseStackTrace( new RuntimeException() ); + if ( trace.size() > 1 ) + { + return trace.get( 1 ).toString(); + } + + return null; + } + +/** +* Prints a stack trace up to the first method whose fully +* qualified class name begins with "java" to System.out. +*/ + static public void printShortStackTrace() + { + String s; + Iterator i = parseStackTrace( new RuntimeException() ).iterator(); + while ( i.hasNext() ) + { + System.out.println( " " + ( s = i.next().toString() ) ); + if ( s.startsWith( "java" ) ) break; + } + } + + protected void invokeFileFromString( String aString ) + { + // strip off parentheses, if any + int openParam = aString.indexOf( "(" ); + if ( openParam != -1 ) + { + aString = aString.substring( 0, openParam ); + } + + // separate class name from method name + int lastDot = aString.lastIndexOf( "." ); + if ( lastDot == -1 ) return; + String className = aString.substring( 0, lastDot ); + String methodName = aString.substring( lastDot + 1 ); + + // convert "."s to file separator characters + StringBuffer buf = new StringBuffer(); + StringTokenizer tokens = new StringTokenizer( className, "." ); + while ( true ) + { + buf.append( tokens.nextToken() ); + if ( ! tokens.hasMoreTokens() ) break; + buf.append( File.separator ); + } + String path = buf.toString(); + java.net.URL url = ClassLoader.getSystemResource( path + ".java" ); + if ( url == null ) return; // do nothing + + String name = url.getFile(); + + // try to launch the document + try + { + // NOTE: This is Windows-dependent! + String args[] = new String[] { + "cmd", "/c", "\"start \"\" \"" + name.substring(1) + "\"\"" }; + // this translates to: cmd /c "start "" "path"" + // apparently an array is more reliable for calling exec(). + // all the extra quotes are to handle paths with spaces. + // trims off the first "/" before the drive letter. + // needed a dummy title for the console or it wouldn't work. + Runtime.getRuntime().exec( args ); + } + catch ( Exception exc ) + { + System.out.println( "DocumentLinkPanel.invokeDocument: " + exc ); + } + return; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.4 2002/11/16 16:33:31 mpowers + * Now using platform-specific accelerator key for shortcuts. + * + * Revision 1.3 2001/07/18 21:53:33 mpowers + * Added a string argument for display as a message. + * + * Revision 1.2 2001/07/17 14:01:43 mpowers + * Added short stack trace method. + * + * Revision 1.1.1.1 2000/12/21 15:51:34 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:45 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java new file mode 100644 index 0000000..36dbacf --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/TextInputRangeChecker.java @@ -0,0 +1,368 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.text.JTextComponent; + +/** +* This class will actively check the inputs of 2 numbers in seperate text +* components. The number in the text components represent an upper and lower +* bound to some range. This class checks to make sure the user inputs values +* in the lower bound text field that are less than the value of the upper +* bound and vice versa for the upper bound text field. This class will also +* check to make sure the bounds fall within a given range if specified. +* +* The checks are automatically performed when the focus is lost on either +* component. If the inputs are correct then no event occurs. If the inputs +* are not correct, then a dialog message is displayed stating the reason why +* the bounds are invalid, and the original correct value is restored into the +* text components. +* +* @author rglista +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class TextInputRangeChecker implements FocusListener +{ + protected static final int NONE = 0; + protected static final int LOWER = 1; + protected static final int UPPER = 2; + + private JTextComponent lowerComponent; + private JTextComponent upperComponent; + private double maxRange; + private double lowerNumber; + private double upperNumber; + private Collection focusListeners; + + private String invalidLowerMessage; + private String invalidUpperMessage; + private String invalidEitherMessage; + private String invalidRangeMessage; + + +/** +* Constructor with some of the settable parameters. No range checking is used. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent ) + { + this( aLowerTextComponent, anUpperTextComponent, null, null, 0.0 ); + } + +/** +* Constructor with some of the settable parameters. No range checking is +* used. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param lowerTextName The name of the lower bound, eg - start year. +* @param upperTextName The name of the upper bound, eg - end year. +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + String lowerTextName, String upperTextName ) + { + this( aLowerTextComponent, anUpperTextComponent, lowerTextName, upperTextName, 0.0 ); + } + +/** +* Constructor with some of the settable parameters. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param aMaxRange The range the bounds muist fall between, if 0 then no range +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + double aMaxRange ) + { + this( aLowerTextComponent, anUpperTextComponent, null, null, aMaxRange ); + } + +/** +* Constructor with all the settable parameters. +* @param aLowerTextComponent A text component for the lower bound. +* @param anUpperTextComponent A text component for the upper bound. +* @param lowerTextName The name of the lower bound, eg - start year. +* @param upperTextName The name of the upper bound, eg - end year. +* @param aMaxRange The range the bounds muist fall between, if 0 then no range +* is used. +*/ + public TextInputRangeChecker( JTextComponent aLowerTextComponent, + JTextComponent anUpperTextComponent, + String lowerTextName, String upperTextName, + double aMaxRange ) + { + lowerComponent = aLowerTextComponent; + upperComponent = anUpperTextComponent; + maxRange = aMaxRange; + + focusListeners = new ArrayList( 1 ); // For most cases, there will be only 1 listener. + + lowerComponent.addFocusListener( this ); + upperComponent.addFocusListener( this ); + + lowerNumber = getNumber( lowerComponent ); + upperNumber = getNumber( upperComponent ); + + if ( ( lowerTextName != null ) && ( upperTextName != null ) ) + { + invalidLowerMessage = "The " + lowerTextName + " must be less than or equal to the " + upperTextName + "."; + invalidUpperMessage = "The " + upperTextName + " must be greater than or equal to the " + lowerTextName + "."; + invalidEitherMessage = "The " + lowerTextName + " and/or the " + upperTextName + " are not correct."; + invalidRangeMessage = "The maximum range for the " + lowerTextName + " and " + upperTextName + " is " + maxRange + "."; + } + else + { + invalidLowerMessage = "The lower bound must be less than or equal to the upper bound."; + invalidUpperMessage = "The upper bound must be greater than or equal to the lower bound."; + invalidEitherMessage = "The upper and/or lower bounds are not correct."; + invalidRangeMessage = "The maximum range is " + maxRange + "."; + } + } + +/** +* Allows the caller to perform the validation of the bounds programatically. +* The lower bound is compared to the upper bound and range checking is performed. +* If the lower bound is greater than the upper bound, or the range between the +* bounds is greater than the max range, then validation fails. +* @return TRUE is validation is successfull, FALSE if it fails. +*/ + public boolean performCheck() + { + return validate( null ); + } + +/** +* Adds the listener to the lists of focus listener maintened by this object. +* When one of the 2 text components receives a focus event, this object will +* fire that focus event to any of its listeners. This is useful when the +* calling object wants to be notified of the components focus events, but wants +* to ensure that the validation has occured first. +*

+* NOTE: The focus is only fired if the validation was successful. This might +* have to be changed. +* @param aListener A Focus Listener to receive Focus Events. +*/ + public void addFocusListener( FocusListener aListener ) + { + focusListeners.add( aListener ); + } + +/** +* Returns the last valid value of the lower bound. If this is called while +* the user is updating the text component but before the focus is lost, the +* value returned will be the original value before the user started updating +* the bound. +* @return The last valid value of the lower bound. +*/ + public double getLastValidatedLowerNumber() + { + return lowerNumber; + } + +/** +* Returns the last valid value of the upper bound. If this is called while +* the user is updating the text component but before the focus is lost, the +* value returned will be the original value before the user started updating +* the bound. +* @return The last valid value of the upper bound. +*/ + public double getLastValidatedUpperNumber() + { + return upperNumber; + } + +/** +* Method used to be notified when one of the text components has gained its +* focus. +*/ + public void focusGained( FocusEvent e ) + { + lowerNumber = getNumber( lowerComponent ); + upperNumber = getNumber( upperComponent ); + } + +/** +* Method used to be notified when one of the text components has lost its +* focus. Automatic validation occurs here. +*/ + public void focusLost( FocusEvent e ) + { + if ( e.isTemporary() ) + { + return; + } + + if ( validate( e.getSource() ) ) + { + fireFocusEvent( e ); + } + } + +/** +* Fires a focus lost event if the validation was successfull. +*/ + protected void fireFocusEvent( FocusEvent e ) + { + for ( Iterator it = focusListeners.iterator(); it.hasNext(); ) + { + ( ( FocusListener )it.next() ).focusLost( e ); + } + } + +/** +* Validates the bounds inputed by the user. +* @param aComponent The component to use to display a dialog window, if neccessray. +* If null, then the parent window of the text componets will be used. +* @return TRUE if validation was successful, FALSE otherwise. +*/ + protected boolean validate( Object aComponent ) + { + int componentUsed = NONE; + if ( aComponent == lowerComponent ) + { + componentUsed = LOWER; + } + else if ( aComponent == upperComponent ) + { + componentUsed = UPPER; + } + + double lower = getNumber( lowerComponent ); + double upper = getNumber( upperComponent ); + + if ( lower > upper ) + { + if ( componentUsed == LOWER ) + { + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidLowerMessage, lowerComponent ); + } + else if ( componentUsed == UPPER ) + { + upperComponent.setText( Double.toString( upperNumber ) ); + displayMessage( invalidUpperMessage, upperComponent ); + } + else + { + upperComponent.setText( Double.toString( upperNumber ) ); + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidEitherMessage, lowerComponent.getTopLevelAncestor() ); + } + + return false; + } + + if ( maxRange != 0.0 ) + { + if ( ( upper - lower ) > maxRange ) + { + if ( componentUsed == LOWER ) + { + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidRangeMessage, lowerComponent ); + } + else if ( componentUsed == UPPER ) + { + upperComponent.setText( Double.toString( upperNumber ) ); + displayMessage( invalidRangeMessage, upperComponent ); + } + else + { + upperComponent.setText( Double.toString( upperNumber ) ); + lowerComponent.setText( Double.toString( lowerNumber ) ); + displayMessage( invalidRangeMessage, lowerComponent.getTopLevelAncestor() ); + } + + return false; + } + } + + lowerNumber = lower; + upperNumber = upper; + return true; + } + +/** +* Creates a JOptionPane to display the reason why the bounds failed validation. +*/ + protected void displayMessage( final String message, final Component parent ) + { + SwingUtilities.invokeLater( new Runnable() + { + public void run() + { + JOptionPane.showMessageDialog( parent, message, "Data Entry Error", + JOptionPane.ERROR_MESSAGE ); + } + } ); + } + +/** +* Gets the number represented in the text component. If the text does not +* represent a number, then zero is returned. +*/ + protected double getNumber( JTextComponent aComponent ) + { + try + { + return Double.valueOf( aComponent.getText() ).doubleValue(); +//1.2 return Double.parseDouble( aComponent.getText() ); + } + catch ( NumberFormatException e ) + { + System.out.println("[GUI] TextInputRangeChecker.getNumber(): The text is NOT a number: " + aComponent.getText() ); + return 0.0; + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1.1.1 2000/12/21 15:51:49 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java new file mode 100644 index 0000000..c360105 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowGrabber.java @@ -0,0 +1,203 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.Component; +import java.awt.Image; +import java.awt.Window; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.OutputStream; + +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JWindow; + +/** + * WindowGrabber is a collection of static utility methods + * for taking screen shots of lightweight containers. All + * components that are not native peers will be drawn to a + * bitmap that will be run-length compressed (LZW encoding, GIF format) + * and written to the specified file or output stream.

+ * + * Any Swing/JFC or IFC window is a good candidate for use with these + * methods. The component is not expected to contain more than 256 colors + * (the maximum that GIF allows). While there is a color depth limitation, + * the compression is lossless, with no blurs or bleeding color. + * + * @author michael@mpowers.net + * @version $Revision: 904 $ + * $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ + */ +public class WindowGrabber +{ +/** + * Captures the screen contents of the specified component, + * and write it to a file with the specified name. + * + * @param aComponent a lightweight component or container. + * @param aFileName the name of the file to write, optionally preceded by path. + * @return True if the file was successfully written, false if there was an error. + * Errors are written to the standard error stream. + */ + static public boolean grab( Component aComponent, String aFileName ) + { + OutputStream output = null; + try + { + output = new FileOutputStream( aFileName ); + } + catch ( Exception exc ) + { + System.err.println( exc ); + return false; + } + + return grab( aComponent, output ); + } + +/** + * Captures the screen contents of the specified component, + * and write it to a file with the specified name. + * + * @param aComponent a lightweight component or container. + * @param anOutputStream an output stream to which the image will be written. + * @return True if the image was successfully written, false if there was an error. + * Errors are written to the standard error stream. + */ + static public boolean grab( Component aComponent, OutputStream anOutputStream ) + { + Image img = aComponent.createImage( + aComponent.getSize().width, aComponent.getSize().height ); + aComponent.paintAll( img.getGraphics() ); + + try { + GIFEncoder encoder = new GIFEncoder( img ); + encoder.write( anOutputStream ); + anOutputStream.flush(); + } catch ( Exception exc ) { + System.err.println( exc ); + return false; + } + + return true; + } + + protected static void processFilenames( String path, String[] filenames ) + { + ClassLoader loader = new ClassGrabber(); + + File f; + for ( int i = 0; i < filenames.length; i++ ) + { + try + { + f = new File( path + filenames[i] ); + if ( f.isDirectory() ) + { + processFilenames ( + path + filenames[i] + "/", + f.list( new FilenameFilter() + { + public boolean accept(File dir, String name) + { + return name.endsWith( ".class" ); + } + }) + ); + } + else + { + System.out.println( "Loading " + filenames[i] + ": " ); + Class c = loader.loadClass( path + filenames[i] ); + + System.out.println( c ); + + if ( JWindow.class.isAssignableFrom( c ) || + JDialog.class.isAssignableFrom( c ) || + JFrame.class.isAssignableFrom( c ) ) + { + try + { + Window w = (Window) c.newInstance(); + if ( w.getBounds().width * w.getBounds().height == 0 ) + { // if size not specified or set, pack it + w.pack(); + } + String gifName = // replace .class with .gif + filenames[i].substring( + 0, filenames[i].length() - 5 ) + "gif"; + w.addNotify(); + w.repaint(); + grab( w, gifName ); + System.out.println( "wrote: " + gifName ); + } + catch ( Exception exc ) + { + System.err.println( "WindowGrab failed for " + filenames[i] + ": " ); + exc.printStackTrace(); + } + + } + else + { + System.out.println( "not a JWindow, JDialog, or JFrame; ignored." ); + } + } + } + catch ( Exception exc ) + { + System.err.println( filenames[i] + ": " + exc ); + } + } + } + +/** + * Captures images of any Swing window component classes specified + * as parameters. If created, image file will have the same name + * as the corresponding class file, but with a ".gif" extension. + * + * @param argv a list of filenames or directory names. + */ + public static void main( String[] argv ) + { + processFilenames( "", argv ); + System.exit( 0 ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:51:49 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java new file mode 100644 index 0000000..a36ba12 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/WindowUtilities.java @@ -0,0 +1,521 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui.swing.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.Window; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** +* A collection of window-related utilities. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +* $Date: 2006-02-18 18:19:05 -0500 (Sat, 18 Feb 2006) $ +* +*/ +public class WindowUtilities +{ + +/** +* Place frame at center (vertically and horizontally) of screen. +*/ + public static final int CENTER = 0; + +/** +* Center dialog on frame area of parent, if any. +*/ + public static final int CENTER_PARENT = 1; + +/** +* Place lower and to the right of the last window +* placed in this manner. Will wrap to top and left +* of screen as necessary. +*/ + public static final int CASCADE = 10; + + // cascade state + private static int lastX = 0; + private static int lastY = 0; + private static int incrementX = 20; + private static int incrementY = 20; + +/** +* Place the window in the center of the screen. +* Note: don't forget to first set the size of your window. +* This is a convenience method and simply calls place() with +* the CENTER parameter. +* @param aWindow The window to be centered. +* @see #place +*/ + public static void center( Window aWindow ) + { + place( aWindow, CENTER ); + } + +/** +* Place lower and to the right of the last window +* placed in this manner. Will wrap to top and left +* of screen as necessary. +* This is a convenience method and simply calls place() with +* the CASCADE parameter. +* @param aWindow The window to be cascaded. +* @see #place +*/ + public static void cascade( Window aWindow ) + { + place( aWindow, CASCADE ); + } + +/** +* Place the window in the specified location. +* Note: don't forget to first set the size of your window. +* @param aWindow The window to be placed. +* @param location Where on screen to place the frame. +*/ + public static void place( Window aWindow, int location) + { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + Dimension mySize = aWindow.getSize(); + int x = (aWindow.getLocation()).x; + int y = (aWindow.getLocation()).y; + float aspectRatio = (float)screenSize.height/(float)screenSize.width; + + // hack to make windows appear on left monitor if dual monitor + // if aspect ratio is less than 0.6, assume dual monitor + if (aspectRatio < 0.6) + { + screenSize.width = screenSize.width/2; + } + + switch (location) + { + case CENTER_PARENT: + if ( ( ! ( aWindow instanceof Dialog ) ) + || ( ((Dialog)aWindow).getParent() == null ) ) +//1.2 || ( ((Dialog)aWindow).getOwner() == null ) ) + { + place( aWindow, CENTER ); + return; + } + Point parentLocation = (((Dialog)aWindow).getParent()).getLocation(); +//1.2 (((Dialog)aWindow).getOwner()).getLocation(); + Dimension parentSize = (((Dialog)aWindow).getParent()).getSize(); + +//1.2 Dimension parentSize = (((Dialog)aWindow).getOwner()).getSize(); + + if (parentSize.width > mySize.width) + { + x = ((parentSize.width - mySize.width)/2) + parentLocation.x; + } + if (parentSize.height > mySize.height) + { + y = ((parentSize.height - mySize.height)/2) + parentLocation.y; + } + break; + case CENTER: + if (screenSize.width > mySize.width) + { + x = (screenSize.width - mySize.width)/2; + } + if (screenSize.height > mySize.height) + { + y = (screenSize.height - mySize.height)/2; + } + break; + case CASCADE: + x = lastX + incrementX; + if ( x + mySize.width > screenSize.width ) + { + x = incrementX; + } + y = lastY + incrementY; + if ( y + mySize.height > screenSize.height ) + { + y = incrementY; + } + lastX = x; + lastY = y; + break; + default: + // don't move the frame + Point p = aWindow.getLocation(); + x = p.x; + y = p.y; + break; + } + aWindow.setLocation(x,y); + } + +/** + * Returns the first parent Window of the specified component. + * + * @param c the Component whose parent will be found. + * @return the Window that contains the component, + * or null if the component does not have a valid Frame parent. + */ + public static Window getWindowForComponent(Component c) + { + for(Component p = c; p != null; p = p.getParent()) { + if (p instanceof Window) { + return (Window) p; + } + } + return null; + } + + + +/** +* Prints out a list of all components to System.out. +* @param aContainer the Container whose components will be listed. +*/ + public static void dumpComponents( Container aContainer ) + { + dumpComponents( aContainer, "" ); + } + + protected static void dumpComponents( Container aContainer, String padding ) + { + Component c = null; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + if ( c instanceof javax.swing.JComponent ) + { + System.out.println( padding + c.getClass() + ": " + + ((javax.swing.JComponent)c).getAccessibleContext().getAccessibleName() ); + } + else + { + System.out.println( padding + c.getClass() + ": " + c.getName() ); + } + if ( c instanceof Container ) + { + dumpComponents( (Container) c, padding + " " ); + } + } + } + +/** +* Gets a list of all children of a specified container, sorted by position. +* Components are sorted from top to bottom and then left to right. +* @param aContainer The container whose children are to be returned. +* @result A List containing the sorted components. +*/ + public static List getSortedChildComponents( Container aContainer ) + { + List result = new ArrayList( getAllChildComponents( aContainer ) ); + Collections.sort( result, new PositionComparator( aContainer ) ); + return result; + } + + public static void dumpSortedChildComponents( Container aContainer ) + { + Component c = null; + Iterator it = getSortedChildComponents( aContainer ).iterator(); + while ( it.hasNext() ) + { + c = (Component) it.next(); + System.out.println( c.getLocation() + " : " + c.getClass() ); + } + } + + public static void dumpNamedChildComponents( Container aContainer ) + { + Iterator it = getUniqueNameMap( getSortedChildComponents( aContainer ) ).values().iterator(); + while ( it.hasNext() ) + { + System.out.println( it.next() ); + } + } + +/** +* Generates a unique name for each object in a list and returns +* a map that maps the objects to the names. The name is based +* on the class of the object in the object list. +* @param anObjectList A List of objects. +* @return A Map that maps the objects in the list to the generated names. +*/ + public static Map getUniqueNameMap( List anObjectList ) + { + Map namesToObjects = new HashMap(anObjectList.size(), 1F); + Map objectsToNames = new HashMap(anObjectList.size(), 1F); + + Object o = null; + String name = null; + int lastIndex = 0; + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + name = o.getClass().getName(); + lastIndex = name.lastIndexOf( "." ); + if ( lastIndex != -1 ) + { + name = name.substring( lastIndex+1 ); + } + name = incrementString( name ); + while ( namesToObjects.get( name ) != null ) + { + name = incrementString( name ); + } + namesToObjects.put( name, o ); + objectsToNames.put( o, name ); + } + + return objectsToNames; + } + +/** +* Numerically increments a string. For example, "hello" becomes "hello1" +* and "hello1" becomes "hello2" while "hello999" becomes "hello1000". +* @param aString a String to be incremented. +* @return The incremented String. +*/ + public static String incrementString( String aString ) + { + int i = aString.length()-1; + while ( ( i >= 0 ) && ( Character.isDigit( aString.charAt( i ) ) ) ) + { + i--; + } + + if ( i == aString.length()-1 ) + { // no numerics at end of string, increment manually + return aString + "1"; + } + + String alpha = aString.substring( 0, i+1 ); + String numeric = aString.substring( i+1 ); + numeric = Integer.toString( Integer.parseInt( numeric ) + 1 ); + + return alpha + numeric; + + } + +/** +* Gets all children of the specified container. +* @param aContainer the Container to be searched. +* @return A Collection containing all of the child components of the container. +*/ + public static Collection getAllChildComponents( Container aContainer ) + { + Collection result = new ArrayList(); + addAllChildComponents( aContainer, result ); + return result; + } + +/** +* Adds all children of the specified container to the specified collection. +* @param aContainer the Container to be searched. +* @param aCollection the Collection to which the child components will be added. +*/ + protected static void addAllChildComponents( Container aContainer, Collection aCollection ) + { + Component c = null; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + aCollection.add( c ); + if ( c instanceof Container ) + { + addAllChildComponents( (Container) c, aCollection ); + } + } + } + +/** +* Sets each child component's tooltip to show the name generated from +* getComponentNameMap(). +* (We're doing this so the tooltip authors can know how to reference +* the components.) +* @param aContainer the Container whose components will be labeled. +*/ + public static void labelComponents( Container aContainer ) + { + Map nameToComponent = getNameToComponentMap( aContainer ); + Map nameToName = new HashMap(nameToComponent.size(), 1F); + Iterator it = nameToComponent.keySet().iterator(); + String key; + while ( it.hasNext() ) + { + key = it.next().toString(); + nameToName.put( key, key ); + } + labelComponents( aContainer, nameToName ); + } + +/** +* Sets each child component's tooltip to show a given string retrieved +* from a map using the component's generated name as a key. +* @param aContainer the Container whose components will be labeled. +* @param aNameMap a Map of generated names to string values. +*/ + public static void labelComponents( Container aContainer, Map aNameMap ) + { + if ( aNameMap == null ) return; + + String key; + Object o ; + Iterator it = aNameMap.keySet().iterator(); + Map nameToComponent = getNameToComponentMap( aContainer ); + while ( it.hasNext() ) + { + key = it.next().toString(); + o = nameToComponent.get( key ); + if ( o instanceof javax.swing.JComponent ) + { + ((javax.swing.JComponent)o).setToolTipText( aNameMap.get( key ).toString() ); + } + } + } + +/** +* Generates a deterministically unique name for each component in a +* container. The name is based on the name of the class of the component +* followed by a number. Each class of component is numbered based on it's +* position in the container, sorted from top to bottom and left to right. +* @param aContainer the Container whose components will named. +* @return a Map that maps each component to its name. +*/ + public static Map getComponentToNameMap( Container aContainer ) + { + return getUniqueNameMap( getSortedChildComponents( aContainer ) ); + } + +/** +* Maps a deterministically unique name to a component in a +* container. The name is based on the name of the class of the component +* followed by a number. Each class of component is numbered based on it's +* position in the container, sorted from top to bottom and left to right. +* @param aContainer the Container whose components will named. +* @return a Map that maps each component to its name. +*/ + public static Map getNameToComponentMap( Container aContainer ) + { + Map componentToName = getComponentToNameMap( aContainer ); + Map result = new HashMap(componentToName.size(), 1F); + Iterator it = componentToName.keySet().iterator(); + Object key; + while ( it.hasNext() ) + { + key = it.next(); + result.put( componentToName.get( key ), key ); + } + return result; + } + +/** +* Sets the tooltips of all components in a container to the +* respective names of those components. (We're using this +* so the tooltip authors can know how to reference the components.) +* @param aContainer the Container whose components will be labeled. +*/ + public static void nameComponents( Container aContainer ) + { + nameComponents( aContainer, "" ); + } + + protected static void nameComponents( Container aContainer, String path ) + { + Component c = null; + String className = null; + int index = 0; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + className = c.getClass().getName(); + className = className.substring( className.lastIndexOf( '.' ) + 1 ); + System.out.println( path + className ); + if ( c instanceof javax.swing.JComponent ) + { + // ((javax.swing.JComponent)c).setToolTipText( path + className + " (" + c.getName() + ")" ); + ((javax.swing.JComponent)c).setToolTipText( c.getName() ); + } + if ( c instanceof Container ) + { + nameComponents( (Container) c, path + className + "." ); + } + } + } + +/** +* Sets the enabled state of a container and all of its components. +* @param aContainer the Container whose components will be enabled. +* @param isEnabled True if enabled, false id disabled. +*/ + public static void enableComponents( Container aContainer, boolean isEnabled ) + { + Component c = null; + String className = null; + int index = 0; + int count = aContainer.getComponentCount(); + for ( int i = 0; i < count; i++ ) + { + c = aContainer.getComponent( i ); + if ( c instanceof Container ) + { + enableComponents( (Container) c, isEnabled ); + } + else + { + c.setEnabled( isEnabled ); + } + } + aContainer.setEnabled( isEnabled ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:19:05 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.1.1.1 2000/12/21 15:51:55 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:46 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html new file mode 100644 index 0000000..97f1598 --- /dev/null +++ b/projects/net.wotonomy.ui.swing/src/main/java/net/wotonomy/ui/swing/util/package.html @@ -0,0 +1,6 @@ + +

+Contains utilities that ease Swing development +but don't quite qualify as components. +

+ diff --git a/projects/net.wotonomy.ui/.classpath b/projects/net.wotonomy.ui/.classpath new file mode 100644 index 0000000..dce51f2 --- /dev/null +++ b/projects/net.wotonomy.ui/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/projects/net.wotonomy.ui/.cvsignore b/projects/net.wotonomy.ui/.cvsignore new file mode 100644 index 0000000..e519f48 --- /dev/null +++ b/projects/net.wotonomy.ui/.cvsignore @@ -0,0 +1,2 @@ +target +cobertura.ser diff --git a/projects/net.wotonomy.ui/.project b/projects/net.wotonomy.ui/.project new file mode 100644 index 0000000..b8f2b64 --- /dev/null +++ b/projects/net.wotonomy.ui/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.ui + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.core.prefs b/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f5c4d5b --- /dev/null +++ b/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Thu Feb 16 21:52:51 EST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.ui.prefs b/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..7ca6ca2 --- /dev/null +++ b/projects/net.wotonomy.ui/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Thu Feb 16 21:52:51 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/projects/net.wotonomy.ui/pom.xml b/projects/net.wotonomy.ui/pom.xml new file mode 100644 index 0000000..2f64630 --- /dev/null +++ b/projects/net.wotonomy.ui/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-ui + Wotonomy - GUI + jar + + + ${project.groupId} + wotonomy-persistence + ${project.version} + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.ui + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.ui + + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DebuggingDelegate.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DebuggingDelegate.java new file mode 100644 index 0000000..8e9fae2 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DebuggingDelegate.java @@ -0,0 +1,333 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.List; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSNotification; + + /** + * A display group delegate that prints messages for each + * of the delegate methods. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ + public class DebuggingDelegate implements EODisplayGroup.Delegate + { + /** + * Called when the specified data source fails + * to create an object for the specified display group. + */ + public void displayGroupCreateObjectFailed ( + EODisplayGroup aDisplayGroup, + EODataSource aDataSource ) + { + report( "displayGroupCreateObjectFailed", + aDisplayGroup, + new Object[] { aDataSource } ); + } + + /** + * Called after the specified display group's + * data source is changed. + */ + public void displayGroupDidChangeDataSource ( + EODisplayGroup aDisplayGroup ) + { + report( "displayGroupDidChangeDataSource", + aDisplayGroup, + new Object[] { } ); + } + + /** + * Called after the specified display group's + * selection has changed. + */ + public void displayGroupDidChangeSelectedObjects ( + EODisplayGroup aDisplayGroup ) + { + report( "displayGroupDidChangeSelectedObjects", + aDisplayGroup, + new Object[] { } ); + } + + /** + * Called after the specified display group's + * selection has changed. + */ + public void displayGroupDidChangeSelection ( + EODisplayGroup aDisplayGroup ) + { + report( "displayGroupDidChangeSelection", + aDisplayGroup, + new Object[] { } ); + } + + /** + * Called after the specified object display group's + * selection has changed. + */ + public void displayGroupDidDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + report( "displayGroupDidDeleteObject", + aDisplayGroup, + new Object[] { anObject } ); + } + + /** + * Called after the specified display group + * has fetched the specified object list. + */ + public void displayGroupDidFetchObjects ( + EODisplayGroup aDisplayGroup, + List anObjectList ) + { + report( "displayGroupDidFetchObjects", + aDisplayGroup, + new Object[] { anObjectList } ); + } + + /** + * Called after the specified display group + * has inserted the specified object into + * its internal object list. + */ + public void displayGroupDidInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + report( "displayGroupDidInsertObject", + aDisplayGroup, + new Object[] { anObject } ); + } + + /** + * Called after the specified display group + * has set the specified value for the specified + * object and key. + */ + public void displayGroupDidSetValueForObject ( + EODisplayGroup aDisplayGroup, + Object aValue, + Object anObject, + String aKey ) + { + report( "displayGroupDidSetValueForObject", + aDisplayGroup, + new Object[] { aValue, anObject, aKey } ); + } + + /** + * Called by the specified display group to + * determine what objects should be displayed + * for the objects in the specified list. + * @return An NSArray containing the objects + * to be displayed for the objects in the + * specified list. + */ + public NSArray displayGroupDisplayArrayForObjects ( + EODisplayGroup aDisplayGroup, + List aList ) + { + return get( "displayGroupDisplayArrayForObjects", + aDisplayGroup, + aList ); + } + + /** + * Called by the specified display group before + * it attempts to change the selection. + * This implementation returns true. + * @return True to allow the selection to change, + * false otherwise. + */ + public boolean displayGroupShouldChangeSelection ( + EODisplayGroup aDisplayGroup, + List aSelectionList ) + { + return ask( "displayGroupShouldChangeSelection", + aDisplayGroup, + new Object[] { aSelectionList } ); + } + + /** + * Called by the specified display group before + * it attempts to delete the specified object. + * This implementation returns true. + * @return True to allow the object to be deleted + * false to prevent the deletion. + */ + public boolean displayGroupShouldDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + return ask( "displayGroupShouldDeleteObject", + aDisplayGroup, + new Object[] { anObject } ); + } + + /** + * Called by the specified display group before + * it attempts display the specified alert to + * the user. + * This implementation returns true. + * @return True to allow the message to be + * displayed, false if you want to handle the + * alert yourself and suppress the display group's + * notification. + */ + public boolean displayGroupShouldDisplayAlert ( + EODisplayGroup aDisplayGroup, + String aTitle, + String aMessage ) + { + return ask( "displayGroupShouldDisplayAlert", + aDisplayGroup, + new Object[] { aTitle, aMessage } ); + } + + /** + * Called by the specified display group before + * it attempts fetch objects. + * This implementation returns true. + * @return True to allow the fetch to take place, + * false to prevent the fetch. + */ + public boolean displayGroupShouldFetch ( + EODisplayGroup aDisplayGroup ) + { + return ask( "displayGroupShouldFetch", + aDisplayGroup, + new Object[] { } ); + } + + /** + * Called by the specified display group before + * it attempts to insert the specified object. + * This implementation returns true. + * @return True to allow the object to be inserted + * false to prevent the insertion. + */ + public boolean displayGroupShouldInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject, + int anIndex ) + { + return ask( "displayGroupShouldInsertObject", + aDisplayGroup, + new Object[] { anObject, new Integer( anIndex ) } ); + } + + /** + * No idea what this might indicate, + * nor what the notification indicates. + * This implementation returns true. + */ + public boolean displayGroupShouldRedisplay ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ) + { + return ask( "displayGroupShouldRedisplay", + aDisplayGroup, + new Object[] { aNotification } ); + } + + /** + * No idea what this might indicate, + * nor what the notification indicates. + */ + public boolean displayGroupShouldRefetch ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ) + { + return ask( "displayGroupShouldRefetch", + aDisplayGroup, + new Object[] { aNotification } ); + } + + /** + * This method is called by all delegate methods that + * return void. + * This implementation calls System.out.println. + * Override to customize or replace the output. + */ + protected void report( String aTitle, + EODisplayGroup aDisplayGroup, + Object[] aParameterArray ) + { + String result = aTitle + " : " + aDisplayGroup; + for ( int i = 0; i < aParameterArray.length; i++ ) + { + result += " : " + aParameterArray[i]; + } + System.out.println( result ); + } + + /** + * This method is called by displayGroupDisplayArrayForObjects. + * This implementation calls report + * and returns a copy of the specified list. + */ + protected NSArray get( String aTitle, + EODisplayGroup aDisplayGroup, + List anObjectList ) + { + report( aTitle, aDisplayGroup, new Object[] { anObjectList } ); + return new NSArray( anObjectList ); + } + + /** + * This method is called by the methods that + * return a boolean. + * This implementation calls report and return true. + */ + protected boolean ask( String aTitle, + EODisplayGroup aDisplayGroup, + Object[] aParameterArray ) + { + report( aTitle, aDisplayGroup, aParameterArray ); + return true; + } + + + } + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/01/24 14:37:24 mpowers + * Contributing a delegate useful for debugging. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DelegateAdapter.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DelegateAdapter.java new file mode 100644 index 0000000..ed572f4 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DelegateAdapter.java @@ -0,0 +1,257 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.List; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSNotification; + + /** + * A convenience class for creating display group delegates. + * All methods of the delegate interface are implemented + * to do nothing. + * + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 904 $ + */ + public class DelegateAdapter implements EODisplayGroup.Delegate + { + /** + * Called when the specified data source fails + * to create an object for the specified display group. + */ + public void displayGroupCreateObjectFailed ( + EODisplayGroup aDisplayGroup, + EODataSource aDataSource ) + { + + } + + /** + * Called after the specified display group's + * data source is changed. + */ + public void displayGroupDidChangeDataSource ( + EODisplayGroup aDisplayGroup ) + { + + } + + /** + * Called after the specified display group's + * selection has changed. + */ + public void displayGroupDidChangeSelectedObjects ( + EODisplayGroup aDisplayGroup ) + { + + } + + /** + * Called after the specified display group's + * selection has changed. + */ + public void displayGroupDidChangeSelection ( + EODisplayGroup aDisplayGroup ) + { + + } + + /** + * Called after the specified object display group's + * selection has changed. + */ + public void displayGroupDidDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + + } + + /** + * Called after the specified display group + * has fetched the specified object list. + */ + public void displayGroupDidFetchObjects ( + EODisplayGroup aDisplayGroup, + List anObjectList ) + { + + } + + /** + * Called after the specified display group + * has inserted the specified object into + * its internal object list. + */ + public void displayGroupDidInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + + } + + /** + * Called after the specified display group + * has set the specified value for the specified + * object and key. + */ + public void displayGroupDidSetValueForObject ( + EODisplayGroup aDisplayGroup, + Object aValue, + Object anObject, + String aKey ) + { + + } + + /** + * Called by the specified display group to + * determine what objects should be displayed + * for the objects in the specified list. + * @return An NSArray containing the objects + * to be displayed for the objects in the + * specified list. + */ + public NSArray displayGroupDisplayArrayForObjects ( + EODisplayGroup aDisplayGroup, + List aList ) + { + return new NSArray( aList ); + } + + /** + * Called by the specified display group before + * it attempts to change the selection. + * This implementation returns true. + * @return True to allow the selection to change, + * false otherwise. + */ + public boolean displayGroupShouldChangeSelection ( + EODisplayGroup aDisplayGroup, + List aSelectionList ) + { + return true; + } + + /** + * Called by the specified display group before + * it attempts to delete the specified object. + * This implementation returns true. + * @return True to allow the object to be deleted + * false to prevent the deletion. + */ + public boolean displayGroupShouldDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ) + { + return true; + } + + /** + * Called by the specified display group before + * it attempts display the specified alert to + * the user. + * This implementation returns true. + * @return True to allow the message to be + * displayed, false if you want to handle the + * alert yourself and suppress the display group's + * notification. + */ + public boolean displayGroupShouldDisplayAlert ( + EODisplayGroup aDisplayGroup, + String aTitle, + String aMessage ) + { + return true; + } + + /** + * Called by the specified display group before + * it attempts fetch objects. + * This implementation returns true. + * @return True to allow the fetch to take place, + * false to prevent the fetch. + */ + public boolean displayGroupShouldFetch ( + EODisplayGroup aDisplayGroup ) + { + return true; + } + + /** + * Called by the specified display group before + * it attempts to insert the specified object. + * This implementation returns true. + * @return True to allow the object to be inserted + * false to prevent the insertion. + */ + public boolean displayGroupShouldInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject, + int anIndex ) + { + return true; + } + + /** + * No idea what this might indicate, + * nor what the notification indicates. + * This implementation returns true. + */ + public boolean displayGroupShouldRedisplay ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ) + { + return true; + } + + /** + * No idea what this might indicate, + * nor what the notification indicates. + */ + public boolean displayGroupShouldRefetch ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ) + { + return true; + } + + } + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.1 2001/01/24 14:23:17 mpowers + * Contributing DelegateAdapter. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DisplayGroup.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DisplayGroup.java new file mode 100644 index 0000000..9fcbe34 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/DisplayGroup.java @@ -0,0 +1,300 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.List; +import java.util.Map; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOQualifier; + +/** +* DisplayGroup provides an abstraction of a user interface, +* comprising of an ordered collection of data objects, some +* of which are displayed, and of those some are selected. +* This class extends EODisplayGroup to provide java-friendly +* convenience methods. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class DisplayGroup extends EODisplayGroup +{ + /** + * Returns the current data source backing this display group, + * or null if no dataSource is currently used. + */ + public EODataSource getDataSource() + { + return super.dataSource(); + } + + /** + * Returns the current delegate for this display group, + * or null if no delegate is currently set. + */ + public Object getDelegate() + { + return delegate(); + } + + /** + * Returns the current string matching format. + * If not set, defaults to "%@*". + */ + public String getDefaultStringMatchFormat() + { + return defaultStringMatchFormat(); + } + + /** + * Returns the current string matching operator. + * If not set, defaults to "caseInsensitiveLike". + */ + public String getdefaultStringMatchOperator() + { + return defaultStringMatchOperator(); + } + + /** + * Returns a Map of default values that are applied + * to new objects that are inserted into the list. + */ + public Map getInsertedObjectDefaultValues() + { + return insertedObjectDefaultValues(); + } + + /** + * Returns the keys that were declared when read from + * an external resource file. + */ + public List getLocalKeys() + { + return localKeys(); + } + + /** + * Returns the List of sort orderings for this display group. + */ + public List getSortOrderings () + { + return sortOrderings(); + } + + /** + * Returns a qualifier that will be applied all the objects + * in this display group to determine which objects will + * be displayed. + */ + public EOQualifier getQualifier () + { + return qualifier(); + } + + /** + * Returns a new qualifier built from the three query + * value maps: greater than, equal to, and less than. + */ + public EOQualifier getQualifierFromQueryValues () + { + return qualifierFromQueryValues(); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public Map getQueryBindingValues() + { + return queryBindingValues(); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public Map getQueryOperatorValues() + { + return queryOperatorValues(); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for equality. + */ + public Map getEqualToQueryValues() + { + return equalToQueryValues(); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for greater value. + */ + public Map getGreaterThanQueryValues() + { + return greaterThanQueryValues(); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for lesser value. + */ + public Map getLessThanQueryValues() + { + return lessThanQueryValues(); + } + + /** + * Returns the association that is currently being edited, + * or null if no editing is taking place. + */ + public EOAssociation getEditingAssociation () + { + return editingAssociation(); + } + + /** + * Returns a List of associations that are observing + * this display group. + */ + public List getObservingAssociations() + { + return observingAssociations(); + } + + /** + * Returns a List containing all objects managed by the display group. + * This includes those objects not visible due to disqualification. + */ + public List getAllObjects() + { + return allObjects(); + } + + /** + * Returns a List of all objects in the display group + * that are currently displayed by the associations. + * The list is a copy of the internal displayed object list. + */ + public List getDisplayedObjects() + { + return displayedObjects(); + } + + /** + * Returns the currently selected object, or null if + * there is no selection. + */ + public Object getSelectedObject() + { + return selectedObject(); + } + + /** + * Returns a List containing all selected objects, if any. + * Returns an empty list if no objects are selected. + */ + public List getSelectedObjects() + { + return selectedObjects(); + } + + /** + * Returns a List containing the indexes of all selected + * objects, if any. The list contains instances of + * java.lang.Number; call intValue() retrieve the index. + */ + public List getSelectionIndexes() + { + return selectionIndexes(); + } + + /** + * Returns the index of the changed object. If more than + * one object has changed, -1 is returned. + */ + public int getUpdatedObjectIndex() + { + return updatedObjectIndex(); + } + + /** + * Returns a value on the selected object for the specified key. + */ + public Object getSelectedObjectValueForKey ( String aKey ) + { + return selectedObjectValueForKey( aKey ); + } + + /** + * Returns the value for the specified key on the specified object. + */ + public Object getValueForObject ( Object anObject, String aKey ) + { + return valueForObject( anObject, aKey ); + } + + /** + * Calls valueForObject() for the object at the specified index. + */ + public Object getValueForObjectAtIndex ( int anIndex, String aKey ) + { + return valueForObjectAtIndex( anIndex, aKey ); + } + + /** + * Specifies the default string matching format for all + * display groups. + */ + public static String getGlobalDefaultStringMatchFormat () + { + return globalDefaultStringMatchFormat(); + } + + /** + * Specifies the default string matching operator for all + * display groups. + */ + public static String getGlobalDefaultStringMatchOperator () + { + return globalDefaultStringMatchOperator(); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2002/05/17 15:01:49 mpowers + * Implemented dynamic lookup of delegate methods so delegates no longer + * need to implement the DisplayGroup.Delegate interface. + * + * Revision 1.1 2002/03/26 21:24:43 mpowers + * Contributing DisplayGroup as convenience for people coming from java. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EOAssociation.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EOAssociation.java new file mode 100644 index 0000000..635c5b9 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EOAssociation.java @@ -0,0 +1,565 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.Enumeration; + +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* Associations observe DisplayGroups and associate +* a user interface component with one or more keys +* on the objects in the display group.

+* +* Associations are created with a ui component in +* the constructor. Then, one or more aspects are +* bound to display groups and/or property keys with +* the bindAspect() method. Finally, the association +* is initialized with the establishConnection() +* method.

+* +* Per the openstep convention, you do not need to +* retain a reference to the association after it is +* created; the association will be garbage-collected +* when the ui component is garbage-collected.

+* +* (Because java components don't have delegates like +* openstep components do, java-based associations +* will likely need to implement some sort of listener +* and add itself to the component's list of listeners +* so that the component will have a strong reference +* (and the only reference) to the association.)

+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class EOAssociation extends EODelayedObserver +{ + // aspect constants + public static final String ActionAspect = "action"; + public static final String EnabledAspect = "enabled"; + public static final String SourceAspect = "source"; + public static final String ArgumentAspect = "argument"; + public static final String ParentAspect = "parent"; + public static final String TitlesAspect = "titles"; + public static final String BoldAspect = "bold"; + public static final String SelectedObjectAspect = "selectedObject"; + public static final String ValueAspect = "value"; + public static final String DestinationAspect = "destination"; + public static final String SelectedTitleAspect = "selectedTitle"; + public static final String URLAspect = "URL"; + public static final String ItalicAspect = "italic"; + public static final String ChildrenAspect = "children"; // not in spec + public static final String IsLeafAspect = "isLeaf"; // not in spec + public static final String EditableAspect = "editable"; // not in spec + public static final String VisibleAspect = "visible"; // not in spec + public static final String ObjectsAspect = "objects"; // not in spec + public static final String LabelAspect = "label"; // not in spec + public static final String IconAspect = "icon"; // not in spec + + public static final String AttributeAspectSignature = "A"; + public static final String NullAspectSignature = ""; + public static final String AttributeToOneAspectSignature = "A1"; + public static final String ToOneAspectSignature = "1"; + public static final String AttributeToOneToManyAspectSignature = "A1M"; + public static final String ToOneToManyAspectSignature = "1M"; + public static final String AttributeToManyAspectSignature = "AM"; + public static final String ToManyAspectSignature = "M"; + + protected Object control; + protected NSMutableDictionary aspectToGroup; + protected NSMutableDictionary aspectToKey; + + /** + * Default constructor. + */ + public EOAssociation () + { + aspectToGroup = new NSMutableDictionary(); + aspectToKey = new NSMutableDictionary(); + control = null; + } + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public EOAssociation ( Object anObject ) + { + this(); + control = anObject; + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + int size = aspects().count(); + NSMutableArray result = new NSMutableArray(); + for ( int i = 0; i < size; i++ ) + { + result.addObject( AttributeToOneToManyAspectSignature ); + } + return result; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + * + * TODO: Is this static in the WebObjects published API? If not, fix. + */ + public static NSArray aspects () + { + return new NSArray(); + } + + /** + * Returns all registered subclasses of EOAssociation + * for which usableWithObject with the specified object + * returns true. You should only call this method on + * the EOAssociation class. + */ + public static NSArray associationClassesForObject ( + Object anObject ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + * This implementation returns an empty list. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + + /** + * Binds the specified aspect of this association to the + * specified key on the specified display group. + */ + public void bindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey ) + { + // unattach old group, if any + EODisplayGroup oldGroup = displayGroupForAspect( anAspect ); + if ( oldGroup != null ) + { + EOObserverCenter.removeObserver( this, oldGroup ); + } + + // attach new group + if ( aDisplayGroup != null ) + { + aspectToGroup.setObjectForKey( aDisplayGroup, anAspect ); + } + else + { + aspectToGroup.removeObjectForKey( anAspect ); + } + // attach new key + if ( aKey != null ) + { + aspectToKey.setObjectForKey( aKey, anAspect ); + } + else + { + aspectToKey.removeObjectForKey( anAspect ); + } + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object, but remember to call this method. + * This implementation unregisters this association + * from observing each bound display group. + */ + public void breakConnection () + { + discardPendingNotification(); + Enumeration e = aspectToGroup.objectEnumerator(); + while ( e.hasMoreElements() ) + { + EOObserverCenter.removeObserver( this, e.nextElement() ); + } + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + * This implementation returns false. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return false; + } + + /** + * Copies the binding for each aspect in this association + * that has a matching aspect in the specified association. + */ + public void copyMatchingBindingsFromAssociation ( + EOAssociation anAssociation ) + { + //FIXME: this is broken: aspects() returns EOAssociation.aspects() + //NOTE: This is actually quite crazy - should this even be static? -ceg + NSMutableArray ourAspects = new NSMutableArray( this.aspects() ); + NSArray theirAspects = anAssociation.aspects(); + + String aspect; + Enumeration e = ourAspects.objectEnumerator(); + while ( e.hasMoreElements() ) + { + aspect = e.nextElement().toString(); + if ( theirAspects.containsObject( aspect ) ) + { + this.bindAspect( + aspect, + anAssociation.displayGroupForAspect( aspect ), + anAssociation.displayGroupKeyForAspect( aspect ) ); + } + } + } + + /** + * Returns the display group that is bound the specified + * aspect, or null if no display group is currently + * bound to that aspect. + */ + public EODisplayGroup displayGroupForAspect ( String anAspect ) + { + return (EODisplayGroup) aspectToGroup.objectForKey( anAspect ); + } + + /** + * Returns the key for the display group bound to the + * specified aspect, or null if no display group is currently + * bound to that aspect. + */ + public String displayGroupKeyForAspect ( String anAspect ) + { + return (String) aspectToKey.objectForKey( anAspect ); + } + + /** + * The human-readable descriptive name for this association. + * This implementation returns the class name. + */ + public String displayName () // was static - can static method get class name? + { + String className = getClass().getName(); + int index = className.lastIndexOf( "." ); + if ( index == -1 ) return className; + return className.substring( index+1 ); + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * This implementation returns true. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return true; + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should populate + * their controlled object from the display group and begin + * listening for events from their controlled object here, + * but remember to call this method. Any existing value + * in the controlled object will be overwritten. + * This implementation registers this assocation to + * observer changes to each of the bound display groups. + */ + public void establishConnection () + { + Enumeration e = aspectToGroup.objectEnumerator(); + while ( e.hasMoreElements() ) + { + EOObserverCenter.addObserver( this, e.nextElement() ); + } + } + + /** + * Returns whether this class can control the specified + * object. This implementation returns false. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return false; + } + + /** + * Returns the object that is currently controlled by this + * association, or null if no object is currently controlled. + */ + public Object object () + { + return control; + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + * This implementation returns an empty list. + */ + public static NSArray objectKeysTaken () + { + return new NSArray(); + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + * This implementation returns null. + */ + public static String primaryAspect () + { + return null; + } + + /** + * Writes the specified value for the display group + * and key currently bound to the specified aspect. + * This implementation calls + * EODisplayGroup.setSelectedObjectValue(). + * @return True if the value was successfully set, + * otherwise false. + */ + public boolean setValueForAspect ( + Object aValue, + String anAspect ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return false; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return false; + + //FIXME: is this the right method to call + return group.setSelectedObjectValue( aValue, key ); + } + + + /** + * Writes the specified value for the display group + * and key currently bound to the specified aspect, + * at the specified index (assuming it's an indexed + * property). + * This implementation calls + * EODisplayGroup.setValueForObjectAtIndex(). + * @return True if the value was successfully set, + * otherwise false. + */ + public boolean setValueForAspectAtIndex ( + Object aValue, + String anAspect, + int anIndex ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return false; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return false; + + //FIXME: is this the right method to call? + return group.setValueForObjectAtIndex( aValue, anIndex, key ); + } + + /** + * Called by subclasses to notify that the user's input + * failed validation. Notifies the display group for + * the aspect, returning the result. + * This implementation calls + * EODisplayGroup.associationFailedToValidateValue() + * with the display group's selected object. + * @return True if the user should be allowed to continue, + * meaning that the display group with handle user notification, + * otherwise false meaning the caller should notify the user. + */ + public boolean shouldEndEditing ( + String anAspect, + String anInvalidInput, + String anErrorDescription ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return false; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return false; + return group.associationFailedToValidateValue( + this, anInvalidInput, key, + group.selectedObject(), //FIXME: is this correct? + anErrorDescription ); + } + + /** + * Called by subclasses to notify that the user's input + * failed validation. Notifies the display group for + * the aspect, returning the result. + * This implementation calls + * EODisplayGroup.associationFailedToValidateValue() + * with the object in the display group's displayed + * objects array at the specified index. + * @return True if the user should be allowed to continue, + * meaning that the display group with handle user notification, + * otherwise false meaning the caller should notify the user. + */ + public boolean shouldEndEditingAtIndex ( + String anAspect, + String anInvalidInput, + String anErrorDescription, + int anIndex ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return false; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return false; + return group.associationFailedToValidateValue( + this, anInvalidInput, key, + group.displayedObjects().objectAtIndex( anIndex ), + //FIXME: is this correct? + anErrorDescription ); + } + + /** + * Called when either the selection or the contents of + * an associated display group have changed. + * This implementation does nothing. + */ + public void subjectChanged () + { + // does nothing + } + + /** + * Returns the current value for the display group + * and key associated with the specified aspect. + */ + public Object valueForAspect ( String anAspect ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return null; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return null; + + //FIXME: is this correct? use selected object? + return group.valueForObject( group.selectedObject(), key ); + } + + /** + * Returns the current value for the display group + * and key associated with the specified aspect, + * and the specified index (assuming multiple values). + */ + public Object valueForAspectAtIndex ( + String anAspect, int anIndex ) + { + EODisplayGroup group = (EODisplayGroup) + aspectToGroup.objectForKey( anAspect ); + if ( group == null ) return null; + String key = (String) + aspectToKey.objectForKey( anAspect ); + if ( key == null ) return null; + + //FIXME: is this the right method to call? + return group.valueForObjectAtIndex( anIndex, key ); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2005/05/11 15:21:53 cgruber + * Change enum to enumeration, since enum is now a keyword as of Java 5.0 + * + * A few other comments in the code. + * + * Revision 1.6 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.5 2003/06/06 20:29:45 mpowers + * Now dequeuing the association when connection is broken. + * Coalesced change events had been processed even after breaking connection. + * + * Revision 1.4 2001/03/06 23:43:46 mpowers + * Implemented icon aspect for text association. + * + * Revision 1.3 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.2 2001/01/25 17:46:11 mpowers + * Clarified usage and gc expectations. + * + * Revision 1.1.1.1 2000/12/21 15:48:07 mpowers + * Contributing wotonomy. + * + * Revision 1.11 2000/12/20 16:25:39 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EODisplayGroup.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EODisplayGroup.java new file mode 100644 index 0000000..ed65b1c --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/EODisplayGroup.java @@ -0,0 +1,2359 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Observable; + +import javax.swing.JOptionPane; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EODelayedObserver; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyValueCoding; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.control.OrderedDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.Duplicator; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* EODisplayGroup provides an abstraction of a user interface, +* comprising of an ordered collection of data objects, some +* of which are displayed, and of those some are selected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class EODisplayGroup extends Observable + implements EOObserving, EOEditingContext.Editor +{ + /** + * Notification sent when the display group is about to fetch. + */ + public static final String DisplayGroupWillFetchNotification + = "DisplayGroupWillFetchNotification"; + + private static boolean + globalDefaultForValidatesChangesImmediately = true; + private static String + globalDefaultStringMatchFormat = "caseInsensitiveLike"; + private static String + globalDefaultStringMatchOperator = "%@*"; + + protected NSMutableArray allObjects; + protected NSArray allObjectsProxy; + protected NSMutableArray displayedObjects; + protected NSArray displayedObjectsProxy; + protected NSMutableArray selectedObjects; + protected NSArray selectedObjectsProxy; + protected NSMutableArray selectedIndexes; + + private String defaultStringMatchOperator; + private String defaultStringMatchFormat; + + private boolean validatesChangesImmediately; + private Object delegate; + private EODataSource dataSource; + private EOAssociation editingAssociation; + private EOQualifier qualifier; + private NSMutableArray sortOrderings; + private NSArray sortOrderingsProxy; + + private NSArray localKeys; + private NSDictionary insertedObjectDefaultValues; + private boolean fetchesOnLoad; + private boolean selectsFirstObjectAfterFetch; + private boolean usesOptimisticRefresh; + private boolean inQueryMode; + + // change detection: package access for helper classes + boolean contentsChanged; + boolean selectionChanged; + int updatedObjectIndex; + + // this property is not in the spec + private boolean compareByReference = false; + + private EOObserving lastGroupObserver; + + /** + * Creates a new display group. + */ + public EODisplayGroup () + { + validatesChangesImmediately = + globalDefaultForValidatesChangesImmediately(); + defaultStringMatchOperator = + globalDefaultStringMatchFormat(); + defaultStringMatchFormat = + globalDefaultStringMatchOperator(); + + allObjects = new ObservableArray( this ); + allObjectsProxy = NSArray.arrayBackedByList( allObjects ); + displayedObjects = new NSMutableArray(); + displayedObjectsProxy = NSArray.arrayBackedByList( displayedObjects ); + selectedObjects = new NSMutableArray(); + selectedObjectsProxy = NSArray.arrayBackedByList( selectedObjects ); + sortOrderings = new NSMutableArray(); + sortOrderingsProxy = NSArray.arrayBackedByList( sortOrderings ); + selectedIndexes = new NSMutableArray(); + + delegate = null; + dataSource = null; + editingAssociation = null; + qualifier = null; + + localKeys = new NSArray(); // not implemented + insertedObjectDefaultValues = new NSDictionary(); + fetchesOnLoad = false; // not implemented + selectsFirstObjectAfterFetch = false; + usesOptimisticRefresh = false; + inQueryMode = false; // not implemented + + contentsChanged = false; + selectionChanged = false; + updatedObjectIndex = -1; + + // create our private delayed observer + lastGroupObserver = new LastGroupObserver( this ); + EOObserverCenter.addObserver( lastGroupObserver, this ); + } + + + + // specify optional data source + + /** + * Sets the data source that will be used by + * this display group. + */ + public void setDataSource ( EODataSource aDataSource ) + { + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // un-register for notifications from existing parent store + NSNotificationCenter.defaultCenter().removeObserver( + this, null, dataSource.editingContext() ); + dataSource.editingContext().removeEditor( this ); + if ( dataSource.editingContext().messageHandler() == this ) + { + dataSource.editingContext().setMessageHandler( null ); + } + + } + + dataSource = aDataSource; + + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // register for notifications from parent store + NSNotificationCenter.defaultCenter().addObserver( + this, new NSSelector( "objectsInvalidatedInEditingContext", + new Class[] { NSNotification.class } ), + null, dataSource.editingContext() ); + + // add ourselves as editor + dataSource.editingContext().addEditor( this ); + + // add ourselves as message handler if no such handler exists + if ( dataSource.editingContext().messageHandler() == null ) + { + dataSource.editingContext().setMessageHandler( this ); + } + } + } + + /** + * Returns the current data source backing this display group, + * or null if no dataSource is currently used. + */ + public EODataSource dataSource () + { + return dataSource; + } + + + + // specify optional delegate + + /** + * Sets the display group delegate that + * will be used by this display group. + */ + public void setDelegate ( Object aDelegate ) + { + delegate = aDelegate; + } + + /** + * Returns the current delegate for this display group, + * or null if no delegate is currently set. + */ + public Object delegate () + { + return delegate; + } + + + + // display group configuration + + /** + * Returns the current string matching format. + * If not set, defaults to "%@*". + */ + public String defaultStringMatchFormat () + { + return defaultStringMatchFormat; + } + + /** + * Returns the current string matching operator. + * If not set, defaults to "caseInsensitiveLike". + */ + public String defaultStringMatchOperator () + { + return defaultStringMatchOperator; + } + + /** + * Sets the display group and associations to edit a + * "query by example" query object. This method is + * used for target/action connections. + */ + public void enterQueryMode ( Object aSender ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether this display group should immediate + * fetch when loaded. + */ + public boolean fetchesOnLoad () + { + return fetchesOnLoad; + } + + /** + * Returns whether this display group is in "query by + * example" mode. + */ + public boolean inQueryMode () + { + return inQueryMode; + } + + /** + * Returns a Map of default values that are applied + * to new objects that are inserted into the list. + */ + public NSDictionary insertedObjectDefaultValues () + { + return insertedObjectDefaultValues; + } + + /** + * Returns the keys that were declared when read from + * an external resource file. + */ + public NSArray localKeys () + { + return localKeys; + } + + /** + * Sets whether this display group will select the + * first object in the list after a fetch. + */ + public boolean selectsFirstObjectAfterFetch () + { + return selectsFirstObjectAfterFetch; + } + + /** + * Sets the default string matching format that + * will be used by this display group. + */ + public void setDefaultStringMatchFormat ( String aFormat ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the default string matching operator that + * will be used by this display group. + */ + public void setDefaultStringMatchOperator ( String anOperator ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets whether this display group will fetch objects + * from its data source on load. + */ + public void setFetchesOnLoad ( boolean willFetch ) + { + fetchesOnLoad = willFetch; + } + + /** + * Sets whether this display group is in "query by example" + * mode. If true, all associations will bind to a special + * "example" object. + */ + public void setInQueryMode ( boolean isInQueryMode ) + { + inQueryMode = isInQueryMode; + } + + /** + * Sets the mapping that contains the values that will + * be applied to new objects inserted into the display group. + */ + public void setInsertedObjectDefaultValues ( Map aMap ) + { + insertedObjectDefaultValues = new NSDictionary( aMap ); + } + + /** + * Sets the keys that are declared when instantiated from + * an external resource file. + */ + public void setLocalKeys ( List aKeyList ) + { + localKeys = new NSArray( (Collection) aKeyList ); + } + + /** + * Sets whether the first object in the list will be + * selected after a fetch. + */ + public void setSelectsFirstObjectAfterFetch ( + boolean selectsFirst ) + { + selectsFirstObjectAfterFetch = selectsFirst; + } + + /** + * Sets the order of the keys by which this display group + * will be ordered after a fetch or after a call to + * updateDisplayedObjects(). The elements in the display + * group will be sorted first by the first key, within + * the first key, by the second key, and so on. + */ + public void setSortOrderings ( List aList ) + { + sortOrderings.removeAllObjects(); + + Object o; + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + // handle the convenience of specifying just a key + if ( ! ( o instanceof EOSortOrdering ) ) + { + o = new EOSortOrdering( + o.toString(), EOSortOrdering.CompareAscending ); + } + sortOrderings.add( o ); + } + } + + /** + * Sets whether only changed objects are refreshed (optimistic), + * or whether all objects are refreshed (pessimistic, default). + * By default, when the display group receives notification that + * one of its objects has changed, updateDisplayedObjects is called. + */ + public void setUsesOptimisticRefresh ( boolean isOptimistic ) + { + usesOptimisticRefresh = isOptimistic; + } + + /** + * Sets whether changes made by associations are validated + * immediately, or when changes are saved. + */ + public void setValidatesChangesImmediately ( + boolean validatesImmediately ) + { + validatesChangesImmediately = validatesImmediately; + } + + /** + * Returns a read-only List of sort orderings for this display group. + */ + public NSArray sortOrderings () + { + return sortOrderingsProxy; + } + + /** + * Returns whether this display group refreshes only + * the changed objects or all objects on refresh. + */ + public boolean usesOptimisticRefresh () + { + return usesOptimisticRefresh; + } + + /** + * Returns whether this display group validates changes + * immediately. Otherwise, validation should occur when + * changes are saved. Default is the global default, + * which is initially true. + */ + public boolean validatesChangesImmediately () + { + return validatesChangesImmediately; + } + + + // qualification + + /** + * Returns a qualifier that will be applied all the objects + * in this display group to determine which objects will + * be displayed. + */ + public EOQualifier qualifier () + { + return qualifier; + } + + /** + * Returns a new qualifier built from the three query + * value maps: greater than, equal to, and less than. + */ + public EOQualifier qualifierFromQueryValues () + { + //TODO: assemble qualifier from query values + + return new EOQualifier() + { + // use inner class until we actually implement one + public EOQualifier qualifierWithBindings( + Map aMap, + boolean requireAll ) + { + return null; + } + public Throwable + validateKeysWithRootClassDescription( Class aClass ) + { + return null; + } + public boolean evaluateWithObject(Object o) + { + return false; + } + }; + } + + /** + * Calls qualifierFromQueryValues(), applies the result + * to the data source, and calls fetch(). + */ + public void qualifyDataSource () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Calls qualifierFromQueryValues(), sets the qualifier + * with setQualifier(), and calls updateDisplayedObjects(). + */ + public void qualifyDisplayGroup () + { + setQualifier( qualifierFromQueryValues() ); + updateDisplayedObjects(); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public NSDictionary queryBindingValues () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public NSDictionary queryOperatorValues () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the qualifier that will be used by + * updateDisplayedObjects() to filter displayed objects. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + qualifier = aQualifier; + } + + /** + * Sets the mapping that contains the mappings of keys + * to binding values. + */ + public void setQueryBindingValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to operator values. + */ + public void setQueryOperatorValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + + + // qualifier query values + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for equality. + */ + public NSDictionary equalToQueryValues () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for greater value. + */ + public NSDictionary greaterThanQueryValues () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for lesser value. + */ + public NSDictionary lessThanQueryValues () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the Map that contains the mappings of keys + * to query values that will be used to test for equality. + */ + public void setEqualToQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for greater value. + */ + public void setGreaterThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for lesser value. + */ + public void setLessThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + + // interface to associations + + /** + * Called by an association when it begins editing. + */ + public void associationDidBeginEditing ( EOAssociation anAssociation ) + { // System.out.println( "EODisplayGroup.associationDidBeginEditing: " + anAssociation ); + if ( dataSource != null ) + { + if ( dataSource.editingContext() != null ) + { + dataSource.editingContext().setMessageHandler( this ); + } + } + editingAssociation = anAssociation; + } + + /** + * Called by an association when it is finished editing. + */ + public void associationDidEndEditing ( EOAssociation anAssociation ) + { // System.out.println( "EODisplayGroup.associationDidEndEditing: " + anAssociation ); + editingAssociation = null; + } + + /** + * Called by associations to determine whether the contents + * of any objects have been changed. Returns true if the + * contents have changed and not all observers have been + * notified. + */ + public boolean contentsChanged () + { + return contentsChanged; + } + + /** + * Called by associations to determine whether the + * selection has been changed. Returns true if the + * selection has changed and not all observers have + * been notified. + */ + public boolean selectionChanged () + { + return selectionChanged; + } + + /** + * Called by an association when a user-specified value fails the association's + * validation rules. This implementation returns true, unless the delegate + * prevents this. + * @return True to allow the association to handle user notification, + * otherwise return false to let the association know that the + * display group notified the user. + */ + public boolean associationFailedToValidateValue ( + EOAssociation anAssociation, + String aValue, + String aKey, + Object anObject, + String anErrorDescription ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { EODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Validation Failed", anErrorDescription } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + return true; + } + return false; + } + + /** + * Called by an association to determine whether it should enable + * a component that displayes a value for the specified key. + * @return true if an object is selected, or if + * the specified key is a query key. Otherwise false. + */ + public boolean enabledToSetSelectedObjectValueForKey ( String aKey ) + { + if ( ( aKey != null ) && ( aKey.startsWith( "@" ) ) ) return true; + return ( selectedObject() != null ); + } + + + // association management + + /** + * Returns the association that is currently being edited, + * or null if no editing is taking place. + */ + public EOAssociation editingAssociation () + { + return editingAssociation; + } + + /** + * Asks the association currently editing to stop editing. + * @returns true if editing was stopped, false if the + * association refused to stop editing (if a modal dialog + * is displayed or a value failed to validate). + */ + public boolean endEditing () + { + if ( editingAssociation == null ) return true; + return editingAssociation.endEditing(); + } + + /** + * Returns a read-only List of associations that are observing + * this display group. + */ + public NSArray observingAssociations () + { + NSArray observers = + EOObserverCenter.observersForObject( this ); + NSMutableArray result = new NSMutableArray(); + + Object o; + Enumeration e = observers.objectEnumerator(); + while ( e.hasMoreElements() ) + { + o = e.nextElement(); + if ( o instanceof EOAssociation ) + { + result.addObject( o ); + } + } + return result; + } + + + + // object management + + /** + * Returns a read-only List containing all objects managed by the display group. + * This includes those objects not visible due to disqualification. + */ + public NSArray allObjects () + { //System.out.println( "avoided allocation: allObjects" ); + return allObjectsProxy; + } + + /** + * Clears the current selection. + * @return True is the selection was cleared, + * False if the selection could not be cleared + * @see #setSelectionIndexes + */ + public boolean clearSelection () + { + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { EODisplayGroup.class, List.class }, + new Object[] { this, new NSArray( selectedObjects ) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + selectionChanged = true; + willChange(); + + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Deletes the object at the specified index, + * notifying the delegate before and after the operation, + * and then updating the selection if needed. + * @return True if delete was successful, false if the + * object was not deleted. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + Object result = notifyDelegate( + "displayGroupShouldDeleteObject", + new Class[] { EODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + contentsChanged = true; + + deleteObjectAtIndexNoNotify( anIndex ); + + if ( dataSource != null ) + { + dataSource.deleteObject( target ); + } + + notifyDelegate( + "displayGroupDidDeleteObject", + new Class[] { EODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + + return true; + } + + private void deleteObjectAtIndexNoNotify ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + int i; + + // remove from selected objects if necessary + i = indexOf( selectedObjects, target ); + if ( i != NSArray.NotFound ) + { + selectionChanged = true; + willChange(); // notify before removing + selectedObjects.removeObjectAtIndex( i ); + selectedIndexes.remove( new Integer( i ) ); // comps by value + } + else // notify - no selection change needed + { + willChange(); + } + + // remove from all objects + i = indexOf( allObjects, target ); + if ( i != NSArray.NotFound ) + { + allObjects.removeObjectAtIndex( i ); + } + else // otherwise should never happen + { +// throw new WotonomyException( +// "Displayed object not found in allObjects" ); + } + + // remove from displayed objects + displayedObjects.removeObjectAtIndex( anIndex ); + } + + /** + * Deletes the currently selected objects. + * This implementation calls deleteObjectAtIndex() for + * each index in the selection list, immediately returning + * false if any delete operation fails. + * @return True if all selected objects were deleted, + * false if any deletion failed. + */ + public boolean deleteSelection () + { + int i; + boolean result = true; + + Enumeration e = new NSArray( selectedObjects ).objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i == NSArray.NotFound ) + { + // should never happen + throw new WotonomyException( + "Selected object not found in displayedObjects" ); + } + result = result && deleteObjectAtIndex( i ); + } + + return result; + } + + /** + * Returns a read-only List of all objects in the display group + * that are currently displayed by the associations. + */ + public NSArray displayedObjects () + { // System.out.println( "avoided allocation: displayedObjects" ); + return displayedObjectsProxy; + } + + /** + * Requests a list of objects from the DataSource + * and calls setObjectArray to populate the list. + * More specifically, calls endEditing(), asks the + * delegate, fetches the objects, notifies the delegate, + * and populates the list. + */ + public boolean fetch () + { + endEditing(); + + if ( dataSource == null ) + { + return false; + } + + Object result = notifyDelegate( + "displayGroupShouldFetch", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + NSNotificationCenter.defaultCenter().postNotification( + DisplayGroupWillFetchNotification, this, new NSDictionary() ); + + NSArray objectList = dataSource.fetchObjects(); + + notifyDelegate( + "displayGroupDidFetchObjects", + new Class[] { EODisplayGroup.class, List.class }, + new Object[] { this, objectList } ); + + if ( selectsFirstObjectAfterFetch ) + { + //note: there's a good chance this logic ought to be in master-detail assoc: + // we're doing this because changes in the master object trigger a refetch + // on the child display group which annoyingly changes the selection. + NSArray original = new NSArray( allObjects ); + setObjectArray( objectList ); + if ( displayedObjects.size() > 0 + && !original.equals( allObjects ) ) // don't change if no change + { + setSelectionIndexes( new NSArray( new Integer( 0 ) ) ); + } + } + else + { + setObjectArray( objectList ); + } + + return true; + } + + /** + * Creates a new object at the specified index. + * Calls insertObjectAtIndex() with the result + * from sending createObject() to the data source. + * Presents a JOptionPane if the create fails, unless + * the delegate implements displayGroupCreateObjectFailed. + * @return the newly created object. + */ + public Object insertNewObjectAtIndex ( int anIndex ) + { + Object result = null; + if ( dataSource != null ) + { + result = dataSource.createObject(); + } + if ( result != null ) + { + if ( insertedObjectDefaultValues != null ) + { + Duplicator.writePropertiesForObject( + insertedObjectDefaultValues, result ); + } + insertObjectAtIndex( result, anIndex ); + } + else // create failed + { + if ( delegate() != null ) + { + NSSelector selector = new NSSelector( + "displayGroupCreateObjectFailed", + new Class[] { EODisplayGroup.class, EODataSource.class } ); + if ( selector.implementedByObject( delegate() ) ) + { + try + { + selector.invoke( delegate(), new Object[] { this, dataSource } ); + return result; + } + catch ( Exception exc ) + { + System.err.println( "Error notifying delegate: displayGroupCreateObjectFailed" ); + exc.printStackTrace(); + } + } + } + + // no delegate or delegate does not implement displayGroupCreateObjectFailed + + String message = "Data source could not create new object"; + Object delegateResult = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { EODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", message } ); + if ( ( delegateResult == null ) || ( Boolean.TRUE.equals( delegateResult ) ) ) + { + JOptionPane.showMessageDialog( null, message ); + } + } + return result; + } + + /** + * Inserts the specified object into the list at + * the specified index. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + Object result = notifyDelegate( + "displayGroupShouldInsertObject", + new Class[] { EODisplayGroup.class, Object.class, int.class }, + new Object[] { this, anObject, new Integer(anIndex) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return; + } + + contentsChanged = true; + updatedObjectIndex = anIndex; + willChange(); + + + // add to all objects + if ( anIndex == displayedObjects.size() ) + { + allObjects.addObject( anObject ); + } + else // insert before same object + { + Object target = displayedObjects.objectAtIndex( anIndex ); + int targetIndex = indexOf( allObjects, target ); + if ( targetIndex != NSArray.NotFound ) + { + allObjects.insertObjectAtIndex( anObject, targetIndex ); + } + else // should never happen + { + throw new WotonomyException( + "Could not find displayed object in all objects list: " + + target ); + } + } + + // add to displayed objects + displayedObjects.insertObjectAtIndex( anObject, anIndex ); + + if ( dataSource != null ) + { + if ( dataSource instanceof OrderedDataSource ) + { + ((OrderedDataSource)dataSource).insertObjectAtIndex( + anObject, anIndex ); + } + else + { + dataSource.insertObject( anObject ); + } + } + + notifyDelegate( + "displayGroupDidInsertObject", + new Class[] { EODisplayGroup.class, Object.class }, + new Object[] { this, anObject } ); + } + + /** + * Sets contentsChanged to true and notifies all observers. + */ + public void redisplay () + { + contentsChanged = true; + willChange(); + } + + /** + * Sets the selection to the next displayed object after the current + * selection. If the last object is selected, or if no object + * is selected, then the first object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return true if an object was selected. + */ + public boolean selectNext () + { + int count = displayedObjects.count(); + if ( count == 0 ) return false; + if ( count == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return true; + } + + int i = -1; + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = -1; + + // select next object + i++; + if ( i != displayedObjects.count() ) + { + // set to next object + selectedObject = displayedObjects.objectAtIndex( i ); + } + else // out of range + { + // set to null + selectedObject = displayedObjects.objectAtIndex( 0 ); + } + + return selectObject( selectedObject ); + } + + /** + * Sets the selection to the specified object. + * If the specified object is null or does not exist + * in the list of displayed objects, the selection + * will be cleared. + * @return true if the object was selected. + */ + public boolean selectObject ( Object anObject ) + { + if ( ( anObject == null ) || + ( indexOf( displayedObjects, anObject ) + == NSArray.NotFound ) ) + { + clearSelection(); + return false; + } + + selectObjectsIdenticalTo( new NSArray( new Object[] { anObject } ) ); + return true; + } + + /** + * Sets the selection to the specified objects. + * If the specified list is null or if none of the objects + * in the list exist in the list of displayed objects, the + * selection will be cleared. + * @return true if all specified objects were selected. + */ + public boolean selectObjectsIdenticalTo ( List anObjectList ) + { + // optimization: check for resetting of selection + if ( ( anObjectList != null ) && ( selectedObjects.size() == anObjectList.size() ) ) + { + boolean identical = true; + int size = selectedObjects.size(); + for ( int i = 0; ( i < size ) && identical; i++ ) + { + // compare by reference + if ( anObjectList.get( i ) != selectedObjects.get( i ) ) + { + identical = false; + } + else if ( displayedObjects.indexOfIdenticalObject( + anObjectList.get( i ) ) == NSArray.NotFound ) + { + identical = false; + } + } + if ( identical ) + { + return true; + } + } + + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { EODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + // need to notify the calling component + // to revert back to the previous selection + selectionChanged = true; + willChange(); + return false; + } + + int i; + selectionChanged = true; + willChange(); + Object o; + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + != NSArray.NotFound ) + { + selectedObjects.addObject( o ); + selectedIndexes.addObject( new Integer( i ) ); + } + } + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Sets the selection to the previous displayed object before the current + * selection. If the first object is selected, or if no object + * is selected, then the last object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return true if an object was selected. + */ + public boolean selectPrevious () + { + int i = displayedObjects.count(); + if ( i == 0 ) return false; + if ( i == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return true; + } + + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = displayedObjects.count(); + + // select next object + i--; + if ( i < 0 ) + { + // out of range - select last object + i = displayedObjects.count() - 1; + } + + return selectObject( displayedObjects.objectAtIndex( i ) ); + } + + /** + * Returns the currently selected object, or null if + * there is no selection. + */ + public Object selectedObject () + { + if ( selectedObjects.count() == 0 ) + { + return null; + } + return selectedObjects.objectAtIndex( 0 ); + } + + /** + * Returns a read-only List containing all selected objects, if any. + * Returns an empty list if no objects are selected. + */ + public NSArray selectedObjects () + { // System.out.println( "avoided allocation: selectedObjects" ); + return selectedObjectsProxy; + } + + /** + * Returns a read-only List containing the indexes of all selected + * objects, if any. The list contains instances of + * java.lang.Number; call intValue() to retrieve the index. + */ + public NSArray selectionIndexes () + { +// return selectedIndexes; + int i; + NSMutableArray result = new NSMutableArray(); + Enumeration e = selectedObjects.objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i != NSArray.NotFound ) + { + result.addObject( new Integer( i ) ); + } + else + { + System.err.println( + "Should never happen: selected objects not in displayed objects" ); + new RuntimeException().printStackTrace( System.err ); + } + } + return result; + } + + /** + * Sets the objects managed by this display group. + * updateDisplayedObjects() is called to filter the + * display objects. The previous selection will be + * maintained if possible. The data source is not + * notified. + */ + public void setObjectArray ( List anObjectList ) + { + if ( anObjectList == null ) anObjectList = new NSArray(); + + Object result = notifyDelegate( + "displayGroupDisplayArrayForObjects", + new Class[] { EODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( result != null ) + { + anObjectList = (List) result; + } + + contentsChanged = true; + willChange(); + + NSArray oldSelectedObjects = new NSArray( selectedObjects ); // copy + + // reset allObjects to new list + allObjects.removeAllObjects(); + allObjects.addObjectsFromArray( anObjectList ); + + // update the displayed object list + updateDisplayedObjects(); + + // restore the selection if possible + selectObjectsIdenticalTo( oldSelectedObjects ); + } + + /** + * Sets the currently selected object, or clears the + * selection if the object is not found or is null. + * Note: it's not clear how this differs from + * selectObject in the spec. It is recommended that + * you call selectObject for now. + */ + public void setSelectedObject ( Object anObject ) + { + selectObject( anObject ); + } + + /** + * Sets the current selection to the specified objects. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + * Note: it's not clear how this differs from + * selectObjectsIdenticalTo in the spec. + * It is recommended that you call that method for now. + */ + public void setSelectedObjects ( List aList ) + { + selectObjectsIdenticalTo( aList ); + } + + /** + * Sets the current selection to the objects at the + * specified indexes. Items in the list are assumed + * to be instances of java.lang.Number. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + */ + public boolean setSelectionIndexes ( List aList ) + { + Object o; + int index; + NSMutableArray objects = new NSMutableArray(); + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + index = ((Number)it.next()).intValue(); + if ( index < displayedObjects.count() ) + { + o = displayedObjects.objectAtIndex( index ); + if ( o != null ) + { + objects.add( o ); + } + } + } + return selectObjectsIdenticalTo( objects ); + } + + /** + * Applies the qualifier to all objects and sorts + * the results to update the list of displayed objects. + * Observing associations are notified to reflect the changes. + */ + public void updateDisplayedObjects () + { + contentsChanged = true; + updatedObjectIndex = -1; + willChange(); + + displayedObjects.removeAllObjects(); + + displayedObjects.addObjectsFromArray( allObjects ); + + // apply qualifier, if any + if ( qualifier() != null ) + { + EOQualifier.filterArrayWithQualifier( + displayedObjects, qualifier() ); + } + + // apply sort orderings, if any + NSArray orderings = sortOrderings(); + if ( orderings != null ) + { + if ( orderings.count() > 0 ) + { + selectionChanged = true; + willChange(); + EOSortOrdering.sortArrayUsingKeyOrderArray( + displayedObjects, orderings ); + } + } + + // make sure the selectedObjects is a subset of displayedObjects + int i; + Object o; + Iterator it = new LinkedList( selectedObjects ).iterator(); + boolean removeflag = false; + selectedIndexes.removeAllObjects(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + == NSArray.NotFound ) + { + selectedObjects.removeIdenticalObject( o ); + removeflag = true; + } + else + { + selectedIndexes.addObject( new Integer( i ) ); + } + } + + //Note: it is important to put the + //selectionChanged = true line below remove. + if (removeflag) + { + selectionChanged = true; + willChange(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { EODisplayGroup.class }, + new Object[] { this } ); + } + } + + /** + * Returns the index of the changed object. If more than + * one object has changed, -1 is returned. + */ + public int updatedObjectIndex () + { + return updatedObjectIndex; + } + + // getting and setting values in objects + + /** + * Returns a value on the selected object for the specified key. + */ + public Object selectedObjectValueForKey ( String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return null; + return valueForObject( selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * all selected objects. + */ + public boolean setSelectedObjectValue ( + Object aValue, String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return false; + return setValueForObject( aValue, selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * the specified object. Validations may be triggered, + * and error dialogs may appear to the user. + * @return True if the value was set successfully, + * false if the value could not be set and the update + * operation should not continue. + */ + public boolean setValueForObject ( + Object aValue, Object anObject, String aKey ) + { + // notify object's observers: + // this includes us, and will notify our observers + EOObserverCenter.notifyObserversObjectWillChange( anObject ); + + //TODO: if key is null, need to remove old object + // and add new object instead of simply replacing it. + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).takeValueForKey( aValue, aKey ); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( anObject, aValue, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { EODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return false; + } + + notifyDelegate( + "displayGroupDidSetValueForObject", + new Class[] { EODisplayGroup.class, Object.class, Object.class, String.class }, + new Object[] { this, aValue, anObject, aKey } ); + + return true; + } + + /** + * Calls setValueForObject() for the object at + * the specified index. + */ + public boolean setValueForObjectAtIndex ( + Object aValue, int anIndex, String aKey ) + { + return setValueForObject( + aValue, displayedObjects.objectAtIndex( anIndex ), aKey ); + } + + /** + * Returns the value for the specified key on the specified object. + */ + public Object valueForObject ( Object anObject, String aKey ) + { + // empty string is considered the identity property + if ( aKey == null ) return anObject; + if ( aKey.equals( "" ) ) return anObject; + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)anObject).valueForKey( aKey ); + } + else + { + return EOKeyValueCodingSupport.valueForKey( anObject, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { EODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return null; + } + } + + /** + * Calls valueForObject() for the object at the specified index. + */ + public Object valueForObjectAtIndex ( int anIndex, String aKey ) + { + Object o = displayedObjects.objectAtIndex( anIndex ); + return valueForObject( o, aKey ); + } + + /** + * Prints out the list of displayed objects. + */ + public String toString() + { + return displayedObjects.toString(); + } + + + /** + * Handles notifications from the data source's editing context, + * looking for InvalidatedAllObjectsInStoreNotification and + * ObjectsChangedInEditingContextNotification, refetching in + * the former case and updating displayed objects in the latter. + * Note: This method is not in the public specification. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRefetch", + new Class[] { EODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + fetch(); + } + } + else + if ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRedisplay", + new Class[] { EODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // inserts are ignored + + // mark updated objects as updated + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + e = updates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "EODisplayGroup: updated: " + index ); + if ( ! didChange ) + { + didChange = true; + contentsChanged = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // treat invalidated objects as updated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + e = invalidates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "EODisplayGroup: invalidated: " + index ); + if ( ! didChange ) + { + didChange = true; + contentsChanged = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // remove deletes from display group if they exist + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + e = deletes.objectEnumerator(); + Object o; + while ( e.hasMoreElements() ) + { + o = e.nextElement(); + index = indexOf( displayedObjects, o ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "EODisplayGroup: deleted: " + o ); + deleteObjectAtIndexNoNotify( index ); + } + } + + if ( !usesOptimisticRefresh() ) + { + updateDisplayedObjects(); + } + } + } + + } + + // static methods + + /** + * Specifies the default behavior for whether changes + * should be validated immediately for all display groups. + */ + public static boolean + globalDefaultForValidatesChangesImmediately () + { + return globalDefaultForValidatesChangesImmediately; + } + + /** + * Specifies the default string matching format for all + * display groups. + */ + public static String globalDefaultStringMatchFormat () + { + return globalDefaultStringMatchFormat; + } + + /** + * Specifies the default string matching operator for all + * display groups. + */ + public static String globalDefaultStringMatchOperator () + { + return globalDefaultStringMatchOperator; + } + + /** + * Sets the default behavior for validating changes + * for all display groups. + */ + public static void + setGlobalDefaultForValidatesChangesImmediately ( + boolean validatesImmediately ) + { + globalDefaultForValidatesChangesImmediately = + validatesImmediately; + } + + /** + * Sets the default string matching format that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchFormat ( String aFormat ) + { + globalDefaultStringMatchFormat = aFormat; + } + + /** + * Sets the default string matching operator that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchOperator ( String anOperator ) + { + globalDefaultStringMatchOperator = anOperator; + } + + /** + * Needed because we don't inherit from NSObject. + * Calls EOObserverCenter.notifyObserversObjectWillChange. + */ + protected void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + /** + * Called by LastGroupObserver to clear flags. + */ + protected void processRecentChanges() + { + contentsChanged = false; + selectionChanged = false; + } + + /** + * Returns the index of the specified object in the + * specified NSArray, comparing by value or by reference + * as determined by the private instance variable + * compareByReference. If not found, returns NSArray.NotFound. + */ + private int indexOf( NSArray anArray, Object anObject ) + { + if ( compareByReference ) + { + return anArray.indexOfIdenticalObject( anObject ); + } + else + { + return anArray.indexOf( anObject ); + } + } + + // interface EOObserving + + /** + * Receives notifications of changes from objects that + * are managed by this display group. This implementation + * sets updatedObjectIndex and contentsChanged as appropriate. + */ + public void objectWillChange(Object anObject) + { + int index = indexOf( displayedObjects, anObject ); + if ( index != NSArray.NotFound ) + { + updatedObjectIndex = index; + contentsChanged = true; + willChange(); + } + } + + // interface EOEditingContext.Editor + + /** + * Called before the editing context begins to save changes. + * This implementation calls endEditing(). + */ + public void editingContextWillSaveChanges( + EOEditingContext anEditingContext ) + { + endEditing(); + } + + /** + * Called to determine whether this editor has changes + * that have not been committed to the object in the context. + */ + public boolean editorHasChangesForEditingContext( + EOEditingContext anEditingContext ) + { + return ( editingAssociation() != null ); + } + + // interface EOEditingContext.MessageHandler + + /** + * Called to display a message for an error that occurred + * in the specified editing context. If the delegate allows, + * this implementation presents an informational JOptionPane. + * Override to customize. + */ + public void editingContextPresentErrorMessage( + EOEditingContext anEditingContext, + String aMessage ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { EODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", aMessage } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + JOptionPane.showMessageDialog( null, aMessage ); + } + } + + /** + * Called by the specified object store to determine whether + * fetching should continue, where count is the current count + * and limit is the limit as specified by the fetch specification. + * This implementation presents an JOptionPane allowing the user + * to specify whether to continue. Override to customize. + */ + public boolean editingContextShouldContinueFetching( + EOEditingContext anEditingContext, + int count, + int limit, + EOObjectStore anObjectStore ) + { + return ( JOptionPane.showConfirmDialog( null, + "Fetch limit reached: do you wish to continue?", + "Continue?", + JOptionPane.YES_NO_OPTION ) == JOptionPane.YES_OPTION ); + } + + /** + * Sends the specified message to the delegate. + * Returns the return value of the method, + * or null if no return value or no delegate + * or no implementation. + */ + private Object notifyDelegate( + String aMethodName, Class[] types, Object[] params ) + { + try + { + Object delegate = delegate(); + if ( delegate == null ) return null; + return NSSelector.invoke( + aMethodName, types, delegate, params ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( + "Error while messaging delegate: " + + delegate + " : " + aMethodName ); + exc.printStackTrace(); + } + + return null; + } + + /** + * DisplayGroups can delegate important decisions to a Delegate. + * Note that DisplayGroup doesn't require its delegates to implement + * this interface: rather, this interface defines the methods that + * DisplayGroup will attempt to invoke dynamically on its delegate. + * The delegate may choose to implement only a subset of the methods + * on the interface. + */ + public interface Delegate + { + /** + * Called when the specified data source fails + * to create an object for the specified display group. + */ + void displayGroupCreateObjectFailed ( + EODisplayGroup aDisplayGroup, + EODataSource aDataSource ); + + /** + * Called after the specified display group's + * data source is changed. + */ + void displayGroupDidChangeDataSource ( + EODisplayGroup aDisplayGroup ); + + /** + * Called after a change occurs in the specified + * display group's selected objects. + */ + void displayGroupDidChangeSelectedObjects ( + EODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group's + * selection has changed. + */ + void displayGroupDidChangeSelection ( + EODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group has + * deleted the specified object. + */ + void displayGroupDidDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has fetched the specified object list. + */ + void displayGroupDidFetchObjects ( + EODisplayGroup aDisplayGroup, + List anObjectList ); + + /** + * Called after the specified display group + * has inserted the specified object into + * its internal object list. + */ + void displayGroupDidInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has set the specified value for the specified + * object and key. + */ + void displayGroupDidSetValueForObject ( + EODisplayGroup aDisplayGroup, + Object aValue, + Object anObject, + String aKey ); + + /** + * Called by the specified display group to + * determine what objects should be displayed + * for the objects in the specified list. + * @return An NSArray containing the objects + * to be displayed for the objects in the + * specified list. + */ + NSArray displayGroupDisplayArrayForObjects ( + EODisplayGroup aDisplayGroup, + List aList ); + + /** + * Called by the specified display group before + * it attempts to change the selection. + * @return True to allow the selection to change, + * false otherwise. + */ + boolean displayGroupShouldChangeSelection ( + EODisplayGroup aDisplayGroup, + List aSelectionList ); + + /** + * Called by the specified display group before + * it attempts to delete the specified object. + * @return True to allow the object to be deleted + * false to prevent the deletion. + */ + boolean displayGroupShouldDeleteObject ( + EODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called by the specified display group before + * it attempts display the specified alert to + * the user. + * @return True to allow the message to be + * displayed, false if you want to handle the + * alert yourself and suppress the display group's + * notification. + */ + boolean displayGroupShouldDisplayAlert ( + EODisplayGroup aDisplayGroup, + String aTitle, + String aMessage ); + + /** + * Called by the specified display group before + * it attempts fetch objects. + * @return True to allow the fetch to take place, + * false to prevent the fetch. + */ + boolean displayGroupShouldFetch ( + EODisplayGroup aDisplayGroup ); + + /** + * Called by the specified display group before + * it attempts to insert the specified object. + * @return True to allow the object to be inserted + * false to prevent the insertion. + */ + boolean displayGroupShouldInsertObject ( + EODisplayGroup aDisplayGroup, + Object anObject, + int anIndex ); + + /** + * Called by the specified display group when + * it receives the specified + * ObjectsChangedInEditingContextNotification. + * @return True to allow the display group to + * update the display (recommended), false + * to prevent the update. + */ + boolean displayGroupShouldRedisplay ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + /** + * Called by the specified display group when + * it receives the specified + * InvalidatedAllObjectsInStoreNotification. + * @return True to allow the display group to + * refetch (recommended), false to prevent the + * refetch. + */ + boolean displayGroupShouldRefetch ( + EODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + } + +} + + /** + * A private class that will serve to clear the contentsChanged + * and selectionChanged flags after all Associations have been + * notified. + */ + class LastGroupObserver extends EODelayedObserver + { + Reference ref; + + public LastGroupObserver( EODisplayGroup aDisplayGroup ) + { + ref = new WeakReference( aDisplayGroup ); + } + + /** + * We want to be informed last, after all Associations + * have been notified to changes in the DisplayGroup. + */ + public int priority() + { + return ObserverPrioritySixth; + } + + /** + * After all Associations have been notified, + * clear the contentsChanged and selectionChanged flags. + */ + public void subjectChanged () + { + EODisplayGroup group = (EODisplayGroup) ref.get(); + if ( group != null ) + { + group.processRecentChanges(); + } + } + } + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.51 2004/01/28 18:35:40 mpowers + * Slight optimization: only comparing list in fetch if we have to. + * + * Revision 1.50 2004/01/27 20:42:30 mpowers + * No longer reselecting first after fetch if contents are identical. + * + * Revision 1.49 2003/12/18 11:37:45 mpowers + * Now calling qualifier internally. + * + * Revision 1.48 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.47 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.46 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.45 2002/10/24 18:20:20 mpowers + * Because NSArray is read-only, we are returning our internal representations + * to callers of allObjects(), displayedObjects(), and selectedObjects(). + * + * Revision 1.44 2002/08/06 18:20:25 mpowers + * Now posting DisplayGroupWillFetch notifications before fetch. + * Implemented support for usesOptimisticRefresh. + * No longer supporting inserted/updated/deleted lists: not part of spec. + * + * Revision 1.43 2002/05/17 15:01:49 mpowers + * Implemented dynamic lookup of delegate methods so delegates no longer + * need to implement the DisplayGroup.Delegate interface. + * + * Revision 1.42 2002/03/26 21:46:06 mpowers + * Contributing EditingContext as a java-friendly convenience. + * + * Revision 1.41 2002/03/11 03:17:56 mpowers + * Provided control point for coalesced changes. + * + * Revision 1.40 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.39 2002/02/19 22:26:04 mpowers + * Implemented EOEditingContext.MessageHandler support. + * + * Revision 1.38 2002/02/19 16:37:38 mpowers + * Implemented support for EOEditingContext.Editor + * + * Revision 1.37 2001/12/11 22:17:48 mpowers + * Now properly handling exceptions in valueForObject. + * No longer trying to retain selection based only on index. + * + * Revision 1.36 2001/11/08 21:42:00 mpowers + * Now we know what to do with shouldRefetch and shouldRedisplay. + * + * Revision 1.35 2001/11/04 18:26:58 mpowers + * Fixed bug where exceptions were not properly reported when updating + * a value and the display group did not have a delegate. + * + * Revision 1.34 2001/11/02 20:59:36 mpowers + * Now correctly ensuring selected objects are a subset of displayed objects. + * + * Revision 1.33 2001/10/30 22:56:45 mpowers + * Added support for EOQualifier. + * + * Revision 1.32 2001/10/23 22:27:53 mpowers + * Now running at ObserverPrioritySixth. + * + * Revision 1.31 2001/10/23 18:45:05 mpowers + * Rolling back changes. + * + * Revision 1.28 2001/08/22 19:23:41 mpowers + * No longer asserting objects in all objects list. + * + * Revision 1.27 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.26 2001/07/10 22:49:07 mpowers + * Fixed bug in optimization for selectObjectsIdenticalTo (found by Dongzhi). + * + * Revision 1.25 2001/06/19 15:40:21 mpowers + * Now only changing the selection if the new selection is different + * from the old. + * + * Revision 1.24 2001/05/24 17:36:15 mpowers + * Fixed problem with selectedObjectsIdenticalTo: it was using compare + * by value instead of compare by reference. + * + * Revision 1.23 2001/05/18 21:09:19 mpowers + * Now throwing exceptions if the delegate cannot handle error from update. + * + * Revision 1.22 2001/05/14 15:26:12 mpowers + * Now checking for null delegate before and after selection change. + * + * Revision 1.21 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.20 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.19 2001/04/13 16:38:09 mpowers + * Alpha3 release. + * + * Revision 1.18 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.17 2001/03/29 03:31:13 mpowers + * No longer using Introspector. + * + * Revision 1.16 2001/02/27 03:32:18 mpowers + * Implemented default values for new objects. + * + * Revision 1.15 2001/02/27 02:11:17 mpowers + * Now throwing exception when cloning fails. + * Removed debugging printlns. + * + * Revision 1.14 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * Revision 1.13 2001/02/26 15:53:22 mpowers + * Fine-tuning notification firing. + * Child display groups now update properly after parent save or invalidate. + * + * Revision 1.12 2001/02/22 20:55:06 mpowers + * Implemented notification handling. + * + * Revision 1.11 2001/02/21 20:40:42 mpowers + * setObjectArray now falls back to index when trying to retain the + * same selection. + * + * Revision 1.10 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.9 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.8 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.7 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.6 2001/01/24 14:23:05 mpowers + * Added support for OrderedDataSource. + * + * Revision 1.5 2001/01/12 17:21:37 mpowers + * Implicit creation of EOSortOrderings now happens in setSortOrderings. + * + * Revision 1.4 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.3 2001/01/10 22:49:44 mpowers + * Implemented similarly named selection methods instead of + * throwing exceptions. + * + * Revision 1.2 2001/01/09 20:12:52 mpowers + * Moved inner classes to package access. + * + * Revision 1.1.1.1 2000/12/21 15:48:20 mpowers + * Contributing wotonomy. + * + * Revision 1.21 2000/12/20 16:25:39 michael + * Added log to all files. + * + * Revision 1.20 2000/12/15 15:04:42 michael + * Added doc. + * + * Revision 1.19 2000/12/11 13:32:48 michael + * Finish the much better TreeAssociation implementation. + * TreeAssociation now has no gui dependencies. + * + * Revision 1.18 2000/12/05 17:41:46 michael + * Broadcasts selection change after delegate refuses selection change + * so the initiating association gets refreshed. + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/GenericAssociation.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/GenericAssociation.java new file mode 100644 index 0000000..3a4ff08 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/GenericAssociation.java @@ -0,0 +1,373 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.wotonomy.control.EOKeyValueCoding; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.foundation.NSArray; + +/** +* GenericAssociation binds one or more properties on an +* observable object to a display group. The controlled +* object is expected to use the ObserverCenter and will +* be observed by this association.

+* +* Bindings for this association are generic: the +* name of the aspect will be treated as a property key +* on the displayed object(s) and synchronized with the +* bound property on the controlled object.

+* +* NOTE: because we cannot assume that the controlled +* object will retain a reference to this association, +* you must explicitly retain a reference to prevent +* the association from getting garbage collected. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class GenericAssociation extends EOAssociation +{ + protected boolean objectModified; + protected Set aspectsModified; + + static final NSArray aspects = + new NSArray( new Object[] { + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + } ); + + /** + * Constructor specifying the object to be controlled by this + * association. Does not establish connection. + */ + public GenericAssociation ( Object anObject ) + { + super( anObject ); + objectModified = false; + aspectsModified = new HashSet(); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return true; + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ""; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return true; + } + + /** + * Establishes a connection between this association + * and the controlled object. This implementation + * registers with ObserverCenter for change notifications + * from the controlled object. + */ + public void establishConnection () + { + EOObserverCenter.addObserver( this, object() ); + super.establishConnection(); + + // forces update from bindings + subjectChanged(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + EOObserverCenter.removeObserver( this, object() ); + super.breakConnection(); + } + + /** + * Overridden to track which observed object is changing. + */ + public void objectWillChange( Object anObject ) + { + if ( object() == anObject ) + { + objectModified = true; + } + else + { + aspectsModified.add( aspectToGroup.allKeysForObject( anObject ) ); + } + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + String aspect; + String key; + Object value; + EODisplayGroup displayGroup; + Iterator iterator; + + iterator = aspectsModified.iterator(); + while ( iterator.hasNext() ) + { + aspect = (String) iterator.next(); + key = displayGroupKeyForAspect( aspect ); + displayGroup = displayGroupForAspect( aspect ); + + value = readValueFromDisplayGroupForKey( displayGroup, key ); + writeValueForKey( object(), value, key ); + } + aspectsModified.clear(); + + if ( objectModified ) + { + iterator = aspectToGroup.keySet().iterator(); + while ( iterator.hasNext() ) + { + aspect = (String) iterator.next(); + key = displayGroupKeyForAspect( aspect ); + value = readValueForKey( object(), key ); + displayGroup = displayGroupForAspect( aspect ); + displayGroup.setSelectedObjectValue( value, key ); + } + } + } + + protected Object readValueForKey( Object object, String key ) + { + if ( object instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)object).valueForKey( key ); + } + return EOKeyValueCodingSupport.valueForKey( object, key ); + } + + protected void writeValueForKey( Object object, Object value, String key ) + { + if ( object instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)object).takeValueForKey( value, key ); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( object, value, key ); + } + } + + protected Object readValueFromDisplayGroupForKey( + EODisplayGroup displayGroup, String key ) + { + Object value; + + if ( displayGroup.selectedObjects().size() > 1 ) + { + // if there're more than one object selected, set + // the value to blank for all of them. + Object previousValue; + + Iterator indexIterator = displayGroup.selectionIndexes(). + iterator(); + + // get value for the first selected object. + int initialIndex = ( (Integer)indexIterator.next() ).intValue(); + previousValue = displayGroup.valueForObjectAtIndex( + initialIndex, key ); + value = null; + + // go through the rest of the selected objects, compare each + // value with the previous one. continue comparing if two + // values are equal, break the while loop if they're different. + // the final value will be the common value of all selected objects + // if there is one, or be blank if there is not. + while ( indexIterator.hasNext() ) + { + int index = ( (Integer)indexIterator.next() ).intValue(); + Object currentValue = displayGroup.valueForObjectAtIndex( + index, key ); + if ( currentValue != null && !currentValue.equals( previousValue ) ) + { + value = null; + break; + } + else + { + // currentValue is the same as the previous one + value = currentValue; + } + + } // end while + + } else { + + value = displayGroup.selectedObjectValueForKey( key ); + } // end checking size of displayGroup + + return value; + } + + /** + * Writes the value currently in the component + * to the selected object in the display group + * bound to the value aspect. + * @return false if there were problems validating, + * or true to continue. + */ + protected boolean writeValueForAspect( Object value, String aspect ) + { + EODisplayGroup displayGroup = + displayGroupForAspect( aspect ); + if ( displayGroup != null ) + { + String key = displayGroupKeyForAspect( aspect ); + + boolean returnValue = true; + Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); + while ( selectedIterator.hasNext() ) + { + int index = ( (Integer)selectedIterator.next() ).intValue(); + + if ( !displayGroup.setValueForObjectAtIndex( value, index, key ) ) + { + returnValue = false; + } + } + return returnValue; + } + return false; + } + + /** + * Forces this association to cause the object to + * stop editing and validate the user's input. + * @return false if there were problems validating, + * or true to continue. + */ + public boolean endEditing () + { + return false; +//! return writeValueToDisplayGroup(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2001/11/08 19:51:24 mpowers + * Draft implementation. + * + * Revision 1.1 2001/11/02 23:15:04 mpowers + * Contributing "generic association". + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MasterDetailAssociation.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MasterDetailAssociation.java new file mode 100644 index 0000000..2aea8d3 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MasterDetailAssociation.java @@ -0,0 +1,406 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import net.wotonomy.control.EOClassDescription; +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSMutableArray; + +/** +* MasterDetailAssociation binds a display group to a property +* on the selected object of another display group. +* Bindings are: +*
    +*
  • parent: The property on the selected object of the +* bound display group that is expected to be an indexed property.
  • +*
+* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 904 $ +*/ +public class MasterDetailAssociation extends EOAssociation +{ + static final NSArray aspects = + new NSArray( new Object[] { + ParentAspect + } ); + static final NSArray aspectSignatures = + new NSArray( new Object[] { + AttributeToOneAspectSignature + } ); + static final NSArray objectKeysTaken = + new NSArray( new Object[] { + "allObjects" + } ); + + /** + * Used to be notified of changes to objects in the + * controlled display group. requalify() should place + * all objects fetched into the controlled group into + * this array. + * Otherwise, the parent object is only marked as + * changed for inserts and deletes. + */ + protected NSMutableArray observableArray; + + /** + * Constructor expecting an EODisplayGroup. + * If the controlled display group does not have a data source, + * a new PropertyDataSource will be used. + */ + public MasterDetailAssociation ( Object anObject ) + { + super( anObject ); + observableArray = new ObservableArray( this ); + } + + /** + * Returns a List of aspect signatures whose contents + * correspond with the aspects list. Each element is + * a string whose characters represent a capability of + * the corresponding aspect.
    + *
  • "A" attribute: the aspect can be bound to + * an attribute.
  • + *
  • "1" to-one: the aspect can be bound to a + * property that returns a single object.
  • + *
  • "M" to-one: the aspect can be bound to a + * property that returns multiple objects.
  • + *
+ * An empty signature "" means that the aspect can + * bind without needing a key. + * This implementation returns "A1M" for each + * element in the aspects array. + */ + public static NSArray aspectSignatures () + { + return aspectSignatures; + } + + /** + * Returns a List that describes the aspects supported + * by this class. Each element in the list is the string + * name of the aspect. This implementation returns an + * empty list. + */ + public static NSArray aspects () + { + return aspects; + } + + /** + * Returns a List of EOAssociation subclasses that, + * for the objects that are usable for this association, + * are less suitable than this association. + */ + public static NSArray associationClassesSuperseded () + { + return new NSArray(); + } + + /** + * Returns whether this class can control the specified + * object. + */ + public static boolean isUsableWithObject ( Object anObject ) + { + return ( anObject instanceof EODisplayGroup ); + } + + /** + * Returns a List of properties of the controlled object + * that are controlled by this class. For example, + * "stringValue", or "selected". + */ + public static NSArray objectKeysTaken () + { + return objectKeysTaken; + } + + /** + * Returns the aspect that is considered primary + * or default. This is typically "value" or somesuch. + */ + public static String primaryAspect () + { + return ParentAspect; + } + + /** + * Returns whether this association can bind to the + * specified display group on the specified key for + * the specified aspect. + */ + public boolean canBindAspect ( + String anAspect, EODisplayGroup aDisplayGroup, String aKey) + { + return ( aspects.containsObject( anAspect ) ); + } + + /** + * Establishes a connection between this association + * and the controlled object. Subclasses should begin + * listening for events from their controlled object here. + */ + public void establishConnection () + { + //NOTE: if nothing refers to this assocation, it gets gc'd. + // otherwise, this is not needed. + component().addObserver( this ); + + EODisplayGroup displayGroup = + displayGroupForAspect( ParentAspect ); + String key = + displayGroupKeyForAspect( ParentAspect ); + + // obtain and qualify new data source from existing source if necessary + if ( component().dataSource() == null ) + { + if ( ( displayGroup != null ) && ( displayGroup.dataSource() != null ) ) + { + component().setDataSource( + displayGroup.dataSource(). + dataSourceQualifiedByKey( key ) ); + } + } + + // set up proxy data source if necessary + if ( component().dataSource() == null ) + { + // get context and class desc from master group + EOEditingContext editingContext = null; + EOClassDescription classDesc = null; + if ( displayGroup != null ) + { + EODataSource dataSource = displayGroup.dataSource(); + if ( dataSource != null ) + { + editingContext = dataSource.editingContext(); + EOClassDescription parentDesc = dataSource.classDescriptionForObjects(); + if ( parentDesc != null ) + { + classDesc = parentDesc.classDescriptionForDestinationKey( key ); + } + } + } + + //FIXME: should this be called DetailDataSource? + component().setDataSource( + new PropertyDataSource( editingContext, classDesc ) ); + } + + super.establishConnection(); + requalify(); + } + + /** + * Breaks the connection between this association and + * its object. Override to stop listening for events + * from the object. + */ + public void breakConnection () + { + //NOTE: if nothing refers to this assocation, it gets gc'd. + // otherwise, this is not needed. + component().deleteObserver( this ); + + super.breakConnection(); + } + + /** + * Called when either the selection or the contents + * of an associated display group have changed. + */ + public void subjectChanged () + { + EODisplayGroup displayGroup; + + // parent aspect + displayGroup = displayGroupForAspect( ParentAspect ); + if ( displayGroup != null ) + { + if ( displayGroup.selectionChanged() ) + { + requalify(); + } + else + if ( displayGroup.contentsChanged() ) + { + requalify(); + } + } + } + + /** + * Overridden to intercept notifications of changes to objects + * in the controlled display group and broadcast a change on the + * parent group's selected object. All other notifications are + * passed to the super implementation. + */ + public void objectWillChange ( Object anObject ) + { + // if child display group is notifying + if ( ! ( anObject instanceof EODisplayGroup ) ) + { + // mark parent group's object as changed + EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect ); + if ( displayGroup != null ) + { + Object selected = displayGroup.selectedObject(); + if ( selected != null ) + { + // only notify if childrenKey is an attribute of parentDesc + // (and therefore not a toOne or toMany relationship) + EOClassDescription parentDesc = + EOClassDescription.classDescriptionForClass( + selected.getClass() ); + String key = displayGroupKeyForAspect( ParentAspect ); + if ( key != null ) + { + int idx = key.indexOf( '.' ); + if ( idx != -1 ) key = key.substring( 0, idx ); + if ( parentDesc.attributeKeys().contains( key ) ) + { + // only notify if we are an attribute key + EOObserverCenter.notifyObserversObjectWillChange( selected ); + } + } + } + } + } + else // display group is notifying + { + // call super so subjectChanged will be called + super.objectWillChange( anObject ); + } + } + + /** + * Called by subjectChanged() to requalify the controlled + * display group with the selected object and the bound key. + */ + protected void requalify() + { + EODisplayGroup component = component(); + EODisplayGroup displayGroup = displayGroupForAspect( ParentAspect ); + String key = displayGroupKeyForAspect( ParentAspect ); + + if ( ( displayGroup.selectedObject() != null ) + && ( component.dataSource() != null ) ) + { + component.dataSource().qualifyWithRelationshipKey( + key, displayGroup.selectedObject() ); + component.fetch(); + observableArray.setArray( component.allObjects() ); + } + else // no selection or no data source, clear + { + component.setObjectArray( null ); + observableArray.removeAllObjects(); + } + component.updateDisplayedObjects(); + } + + /** + * This implementation returns ObserverPrioritySecond + * so that master detail assocations are notified before + * other associations. + */ + public int priority () + { + return ObserverPrioritySecond; + } + + // convenience + + private EODisplayGroup component() + { + return (EODisplayGroup) object(); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.14 2004/02/04 20:00:49 mpowers + * Improved change notification for dotted key paths. + * + * Revision 1.13 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.12 2001/10/26 18:39:05 mpowers + * Now a delayed observer with a higher priority, so that it is processed + * before other associations. + * + * Revision 1.11 2001/06/26 21:39:33 mpowers + * Added check for null component data source before requalifying. + * + * Revision 1.10 2001/05/21 14:04:15 mpowers + * No longer changing a detail group's data source if it's already specified. + * + * Revision 1.9 2001/05/18 21:08:46 mpowers + * Now calling updateDisplayedObjects on detail after master changes. + * + * Revision 1.8 2001/05/14 15:26:42 mpowers + * Modified logic for controlled groups that have no data source already set. + * + * Revision 1.7 2001/05/04 14:42:58 mpowers + * Now getting stored values in KeyValueCoding. + * MasterDetail now marks dirty based on whether it's an attribute + * or relation. + * Implemented editing context marker. + * + * Revision 1.6 2001/04/29 02:29:31 mpowers + * Debugging relationship faulting. + * + * Revision 1.4 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.3 2001/01/18 16:57:18 mpowers + * Fixed problem with losing connection: the association was getting + * garbage collected because nothing referred to it. All other associations + * make themselves listeners of their controlled object, and that has been + * the only thing keeping them from getting gc'd. This will need to be fixed. + * + * Revision 1.2 2001/01/17 23:06:09 mpowers + * TreeAssociation now modifies the contents of the children display + * group rather than adding items to the titles display group. + * + * Revision 1.1.1.1 2000/12/21 15:48:23 mpowers + * Contributing wotonomy. + * + * Revision 1.5 2000/12/20 16:25:40 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MirrorDetailAssociation.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MirrorDetailAssociation.java new file mode 100644 index 0000000..aeac376 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/MirrorDetailAssociation.java @@ -0,0 +1,106 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; +import net.wotonomy.foundation.NSArray; + +/** +* This master detail association synchronizes the contents +* and selection of the master group into the detail group. +*/ +public class MirrorDetailAssociation extends MasterDetailAssociation{ + + /** + * Standard constructor specifying the detail display group. + * @param displayGroup the detail display group of this + * Master-Detail Association + */ + public MirrorDetailAssociation(EODisplayGroup displayGroup){ + super(displayGroup); + } + + /** + * Called by subjectChanged() to requalify the controlled + * display group with the indexed object and the bound key. + * This implementation ignores both and sets the object array + * of the detail group to the displayed objects of the master + * and sets the selection to match. + */ + protected void requalify() + { + EODisplayGroup detail = (EODisplayGroup) object(); + EODisplayGroup master = + displayGroupForAspect( ParentAspect ); + + if ( master != null ) + { + NSArray masterObjects = master.displayedObjects(); + NSArray detailObjects = detail.displayedObjects(); + int size = masterObjects.size(); + boolean different = false; + + // see if lists contain the same object instances + if ( size == detailObjects.size() ) + { + for ( int i = 0; i < size; i++ ) + { + if ( masterObjects.objectAtIndex(i) + != detailObjects.objectAtIndex(i) ) + { + different = true; + break; + } + } + } + else // different sizes + { + different = true; + } + + // if different, sync contents and selection with master + if ( different ) + { + detail.setObjectArray( masterObjects ); + detail.setSelectionIndexes( master.selectionIndexes() ); + } + else // if selection changed, sync selection with master + if ( master.selectionChanged() ) + { + detail.setSelectionIndexes( master.selectionIndexes() ); + } + } + else // no bound display group, clear + { + detail.setObjectArray( null ); + } + } +} +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/07/05 22:13:28 mpowers + * Now only updating if master has actually changed. + * + * Revision 1.1 2001/05/29 19:57:47 mpowers + * Added some neglected files. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/ObservableArray.java b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/ObservableArray.java new file mode 100644 index 0000000..a398e97 --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/ObservableArray.java @@ -0,0 +1,349 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.ui; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSRange; + +/** +* A package class that extends NSMutableArray but makes use +* of the fact that wotonomy's implementation extends ArrayList +* to intercept insertions and deletion and register and +* unregister objects for change notifications as appropriate. +* Since we can't be sure of ArrayList's implementation, we're +* forced to override each and every add and remove method, +* some of which probably call each other. However, +* EOObserverCenter will only register us once per object. +*/ +class ObservableArray extends NSMutableArray +{ + EOObserving observer; + + ObservableArray( EOObserving anObserver ) + { + observer = anObserver; + } + + /** + * Removes the last object from the array. + */ + public void removeLastObject () + { + remove( count() - 1 ); + } + + /** + * Removes the object at the specified index. + */ + public void removeObjectAtIndex (int index) + { + remove( index ); + } + + /** + * Adds all objects in the specified collection. + */ + public void addObjectsFromArray (Collection aCollection) + { + addAll( aCollection ); + } + + /** + * Removes all objects from the array. + */ + public void removeAllObjects () + { + clear(); + } + + /** + * Removes all objects equivalent to the specified object + * within the range of specified indices. + */ + public void removeObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( get( i ) ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all instances of the specified object within the + * range of specified indices, comparing by reference. + */ + public void removeIdenticalObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == get( i ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all objects in the specified collection from the array. + */ + public void removeObjectsInArray (Collection aCollection) + { + removeAll( aCollection ); + } + + /** + * Removes all objects in the indices within the specified range + * from the array. + */ + public void removeObjectsInRange (NSRange aRange) + { + if ( aRange == null ) return; + + for ( int i = 0; i < aRange.length(); i++ ) + { + remove( aRange.location() ); + } + } + + /** + * Replaces objects in the current range with objects from + * the specified range of the specified array. If currentRange + * is larger than otherRange, the extra objects are removed. + * If otherRange is larger than currentRange, the extra objects + * are added. + */ + public void replaceObjectsInRange (NSRange currentRange, + List otherArray, NSRange otherRange) + { + if ( ( currentRange == null ) || ( otherArray == null ) || + ( otherRange == null ) ) return; + + // transform otherRange if out of bounds for array + if ( otherRange.maxRange() > otherArray.size() ) + { + // TODO: Test this logic. + int loc = Math.min( otherRange.location(), otherArray.size() - 1 ); + otherRange = new NSRange( loc, otherArray.size() - loc ); + } + + Object o; + List subList = subList( + currentRange.location(), currentRange.maxRange() ); + int otherIndex = otherRange.location(); + // TODO: Test this logic. + for ( int i = 0; i < subList.size(); i++ ) + { + if ( otherIndex < otherRange.maxRange() ) + { // set object + subList.set( i, otherArray.get( otherIndex ) ); + } + else + { // remove extra elements from currentRange + subList.remove( i ); + i--; + } + otherIndex++; + } + // TODO: Test this logic. + for ( int i = otherIndex; i < otherRange.maxRange(); i++ ) + { + add( otherArray.get( i ) ); + } + } + + /** + * Clears the current array and then populates it with the + * contents of the specified collection. + */ + public void setArray (Collection aCollection) + { + clear(); + addAll( aCollection ); + } + + /** + * Removes all objects equivalent to the specified object. + */ + public void removeObject (Object anObject) + { + remove( anObject ); + } + + /** + * Removes all occurences of the specified object, + * comparing by reference. + */ + public void removeIdenticalObject (Object anObject) + { + EOObserverCenter.removeObserver( observer, anObject ); + super.removeIdenticalObject( anObject ); + } + + /** + * Inserts the specified object into this array at the + * specified index. + */ + public void insertObjectAtIndex (Object anObject, int anIndex) + { + add( anIndex, anObject ); + } + + /** + * Replaces the object at the specified index with the + * specified object. + */ + public void replaceObjectAtIndex (int anIndex, Object anObject) + { + set( anIndex, anObject ); + } + + /** + * Adds the specified object to the end of this array. + */ + public void addObject (Object anObject) + { + add( anObject ); + } + + // interface List: mutators + + public void add(int index, Object element) + { + EOObserverCenter.addObserver( observer, element ); + super.add( index, element ); + } + + public boolean add(Object o) + { + EOObserverCenter.addObserver( observer, o ); + return super.add(o); + } + + public boolean addAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll( index, c ); + } + + public void clear() + { + Iterator it = iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + super.clear(); + } + + public Object remove(int index) + { + EOObserverCenter.removeObserver( observer, get(index) ); + return super.remove( index ); + } + + public boolean remove(Object o) + { + EOObserverCenter.removeObserver( observer, o ); + return super.remove(o); + } + + public boolean removeAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + return super.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + throw new UnsupportedOperationException(); + } + + public Object set(int index, Object element) + { + EOObserverCenter.removeObserver( observer, get(index) ); + EOObserverCenter.addObserver( observer, element ); + return super.set( index, element ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/18 23:14:35 cgruber + * Update imports and maven dependencies. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.3 2003/08/06 23:07:52 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.2 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.1 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.1 2001/01/24 14:37:24 mpowers + * Contributing a delegate useful for debugging. + * + * + */ + diff --git a/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/package.html b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/package.html new file mode 100644 index 0000000..710c57b --- /dev/null +++ b/projects/net.wotonomy.ui/src/main/java/net/wotonomy/ui/package.html @@ -0,0 +1,14 @@ + +

+An implementation of the JavaClient +framework. It tries to be API-compatible at the +EODisplayGroup and EOAssociation layer. The +associations themselves are inspired by their EO +counterparts, but are extended so as to be +more useful for java application development. +

+

+This package has dependencies on the control, +foundation, and util packages. +

+ diff --git a/projects/net.wotonomy.web/.classpath b/projects/net.wotonomy.web/.classpath new file mode 100644 index 0000000..cf9357f --- /dev/null +++ b/projects/net.wotonomy.web/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/projects/net.wotonomy.web/.cvsignore b/projects/net.wotonomy.web/.cvsignore new file mode 100644 index 0000000..e519f48 --- /dev/null +++ b/projects/net.wotonomy.web/.cvsignore @@ -0,0 +1,2 @@ +target +cobertura.ser diff --git a/projects/net.wotonomy.web/.project b/projects/net.wotonomy.web/.project new file mode 100644 index 0000000..59b6177 --- /dev/null +++ b/projects/net.wotonomy.web/.project @@ -0,0 +1,23 @@ + + + net.wotonomy.web + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/projects/net.wotonomy.web/.settings/org.eclipse.jdt.core.prefs b/projects/net.wotonomy.web/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..f0418e2 --- /dev/null +++ b/projects/net.wotonomy.web/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Thu Feb 16 21:51:56 EST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/projects/net.wotonomy.web/.settings/org.eclipse.jdt.ui.prefs b/projects/net.wotonomy.web/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000..ad23fed --- /dev/null +++ b/projects/net.wotonomy.web/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Thu Feb 16 21:51:56 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/projects/net.wotonomy.web/pom.xml b/projects/net.wotonomy.web/pom.xml new file mode 100644 index 0000000..77739e9 --- /dev/null +++ b/projects/net.wotonomy.web/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + + net.wotonomy + wotonomy-all + 1.0-alpha5-SNAPSHOT + + wotonomy-web + Wotonomy - MVC Web Application Framework + jar + + + ${project.groupId} + wotonomy-persistence + ${project.version} + + + jetty + org.mortbay.jetty + 5.1.9 + + + org.mortbay.jetty + servlet-api-2.5 + 0PR + + + dom4j + dom4j + 1.6.1 + + + pull-parser + pull-parser + + + xml-apis + xml-apis + + + + + junit + junit + 3.8.1 + test + + + + + + maven-javadoc-plugin + + + org.codehaus.mojo + surefire-report-maven-plugin + + + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.web + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects/net.wotonomy.web + + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java new file mode 100644 index 0000000..cef1372 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/ObservableArray.java @@ -0,0 +1,351 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSRange; + +/** +* A package class that extends NSMutableArray but makes use +* of the fact that wotonomy's implementation extends ArrayList +* to intercept insertions and deletion and register and +* unregister objects for change notifications as appropriate. +* Since we can't be sure of ArrayList's implementation, we're +* forced to override each and every add and remove method, +* some of which probably call each other. However, +* EOObserverCenter will only register us once per object. +*/ +class ObservableArray extends NSMutableArray +{ + EOObserving observer; + + ObservableArray( EOObserving anObserver ) + { + observer = anObserver; + } + + /** + * Removes the last object from the array. + */ + public void removeLastObject () + { + remove( count() - 1 ); + } + + /** + * Removes the object at the specified index. + */ + public void removeObjectAtIndex (int index) + { + remove( index ); + } + + /** + * Adds all objects in the specified collection. + */ + public void addObjectsFromArray (Collection aCollection) + { + addAll( aCollection ); + } + + /** + * Removes all objects from the array. + */ + public void removeAllObjects () + { + clear(); + } + + /** + * Removes all objects equivalent to the specified object + * within the range of specified indices. + */ + public void removeObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject.equals( get( i ) ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all instances of the specified object within the + * range of specified indices, comparing by reference. + */ + public void removeIdenticalObject (Object anObject, NSRange aRange) + { + if ( ( anObject == null ) || ( aRange == null ) ) return; + + int loc = aRange.location(); + int max = aRange.maxRange(); + for ( int i = loc; i < max; i++ ) + { + if ( anObject == get( i ) ) + { + remove( i ); + i = i - 1; + max = max - 1; + } + } + } + + /** + * Removes all objects in the specified collection from the array. + */ + public void removeObjectsInArray (Collection aCollection) + { + removeAll( aCollection ); + } + + /** + * Removes all objects in the indices within the specified range + * from the array. + */ + public void removeObjectsInRange (NSRange aRange) + { + if ( aRange == null ) return; + + for ( int i = 0; i < aRange.length(); i++ ) + { + remove( aRange.location() ); + } + } + + /** + * Replaces objects in the current range with objects from + * the specified range of the specified array. If currentRange + * is larger than otherRange, the extra objects are removed. + * If otherRange is larger than currentRange, the extra objects + * are added. + */ + public void replaceObjectsInRange (NSRange currentRange, + List otherArray, NSRange otherRange) + { + if ( ( currentRange == null ) || ( otherArray == null ) || + ( otherRange == null ) ) return; + + // transform otherRange if out of bounds for array + if ( otherRange.maxRange() > otherArray.size() ) + { + // TODO: Test this logic. + int loc = Math.min( otherRange.location(), otherArray.size() - 1 ); + otherRange = new NSRange( loc, otherArray.size() - loc ); + } + + Object o; + List subList = subList( + currentRange.location(), currentRange.maxRange() ); + int otherIndex = otherRange.location(); + // TODO: Test this logic. + for ( int i = 0; i < subList.size(); i++ ) + { + if ( otherIndex < otherRange.maxRange() ) + { // set object + subList.set( i, otherArray.get( otherIndex ) ); + } + else + { // remove extra elements from currentRange + subList.remove( i ); + i--; + } + otherIndex++; + } + // TODO: Test this logic. + for ( int i = otherIndex; i < otherRange.maxRange(); i++ ) + { + add( otherArray.get( i ) ); + } + } + + /** + * Clears the current array and then populates it with the + * contents of the specified collection. + */ + public void setArray (Collection aCollection) + { + clear(); + addAll( aCollection ); + } + + /** + * Removes all objects equivalent to the specified object. + */ + public void removeObject (Object anObject) + { + remove( anObject ); + } + + /** + * Removes all occurences of the specified object, + * comparing by reference. + */ + public void removeIdenticalObject (Object anObject) + { + EOObserverCenter.removeObserver( observer, anObject ); + super.removeIdenticalObject( anObject ); + } + + /** + * Inserts the specified object into this array at the + * specified index. + */ + public void insertObjectAtIndex (Object anObject, int anIndex) + { + add( anIndex, anObject ); + } + + /** + * Replaces the object at the specified index with the + * specified object. + */ + public void replaceObjectAtIndex (int anIndex, Object anObject) + { + set( anIndex, anObject ); + } + + /** + * Adds the specified object to the end of this array. + */ + public void addObject (Object anObject) + { + add( anObject ); + } + + // interface List: mutators + + public void add(int index, Object element) + { + EOObserverCenter.addObserver( observer, element ); + super.add( index, element ); + } + + public boolean add(Object o) + { + EOObserverCenter.addObserver( observer, o ); + return super.add(o); + } + + public boolean addAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll(coll); + } + + public boolean addAll(int index, Collection c) + { + Iterator it = c.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.addObserver( observer, it.next() ); + } + return super.addAll( index, c ); + } + + public void clear() + { + Iterator it = iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + super.clear(); + } + + public Object remove(int index) + { + EOObserverCenter.removeObserver( observer, get(index) ); + return super.remove( index ); + } + + public boolean remove(Object o) + { + EOObserverCenter.removeObserver( observer, o ); + return super.remove(o); + } + + public boolean removeAll(Collection coll) + { + Iterator it = coll.iterator(); + while ( it.hasNext() ) + { + EOObserverCenter.removeObserver( observer, it.next() ); + } + return super.removeAll(coll); + } + + public boolean retainAll(Collection coll) + { + throw new UnsupportedOperationException(); + } + + public Object set(int index, Object element) + { + EOObserverCenter.removeObserver( observer, get(index) ); + EOObserverCenter.addObserver( observer, element ); + return super.set( index, element ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/18 23:31:32 mpowers + * Needed for WODisplayGroup. + * + * Revision 1.2 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.1 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.1 2001/01/24 14:37:24 mpowers + * Contributing a delegate useful for debugging. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java new file mode 100644 index 0000000..41f77f5 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/URI.java @@ -0,0 +1,3464 @@ +/* + * $Header$ + * $Revision: 905 $ + * $Date: 2006-02-18 20:44:03 -0500 (Sat, 18 Feb 2006) $ + * + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002 the Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "HttpClient", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ + +// excellent class borrowed from Apache Commons project: +//package org.apache.commons.httpclient; + +package net.wotonomy.web; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.security.AccessController; +import java.util.BitSet; +import java.util.Hashtable; +import java.util.Locale; + +import sun.security.action.GetPropertyAction; + +/** + * The interface for the URI(Uniform Resource Identifiers) version of RFC 2396. + * This class has the purpose of supportting of parsing a URI reference to + * extend any specific protocols, the character encoding of the protocol to + * be transported and the charset of the document. + *

+ * A URI is always in an "escaped" form, since escaping or unescaping a + * completed URI might change its semantics. + *

+ * Implementers should be careful not to escape or unescape the same string + * more than once, since unescaping an already unescaped string might lead to + * misinterpreting a percent data character as another escaped character, + * or vice versa in the case of escaping an already escaped string. + *

+ * In order to avoid these problems, data types used as follows: + *

+ *   URI character sequence: char
+ *   octet sequence: byte
+ *   original character sequence: String
+ * 

+ * + * So, a URI is a sequence of characters as an array of a char type, which + * is not always represented as a sequence of octets as an array of byte. + *

+ * + * URI Syntactic Components + *

+ * - In general, written as follows:
+ *   Absolute URI = <scheme>:<scheme-specific-part>
+ *   Generic URI = <scheme>://<authority><path>?<query>
+ *
+ * - Syntax
+ *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+ *   hier_part     = ( net_path | abs_path ) [ "?" query ]
+ *   net_path      = "//" authority [ abs_path ]
+ *   abs_path      = "/"  path_segments
+ * 

+ * + * The following examples illustrate URI that are in common use. + *

+ * ftp://ftp.is.co.za/rfc/rfc1808.txt
+ *    -- ftp scheme for File Transfer Protocol services
+ * gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles
+ *    -- gopher scheme for Gopher and Gopher+ Protocol services
+ * http://www.math.uio.no/faq/compression-faq/part1.html
+ *    -- http scheme for Hypertext Transfer Protocol services
+ * mailto:mduerst@ifi.unizh.ch
+ *    -- mailto scheme for electronic mail addresses
+ * news:comp.infosystems.www.servers.unix
+ *    -- news scheme for USENET news groups and articles
+ * telnet://melvyl.ucop.edu/
+ *    -- telnet scheme for interactive services via the TELNET Protocol
+ * 
+ * Please, notice that there are many modifications from URL(RFC 1738) and + * relative URL(RFC 1808). + *

+ * The expressions for a URI + *

+ * For escaped URI forms
+ *  - URI(char[]) // constructor
+ *  - char[] getRawXxx() // method
+ *  - String getEscapedXxx() // method
+ *  - String toString() // method
+ * 

+ * For unescaped URI forms + * - URI(String) // constructor + * - String getXXX() // method + *

+ * + * @author Sung-Gu + * @version $Revision: 905 $ $Date: 2002/03/14 15:14:01 + */ +class URI implements Cloneable, Comparable, Serializable { + + + // ----------------------------------------------------------- Constructors + + protected URI() { + } + + /** + * Construct a URI as an escaped form of a character array. + * An URI can be placed within double-quotes or angle brackets like + * "http://test.com/" and <http://test.com/> + * + * @param escaped the URI character sequence + * @exception IOException + * @throws NullPointerException if escaped is null + */ + public URI(char[] escaped) throws IOException { + parseUriReference(new String(escaped), true); + } + + + /** + * Construct a URI from the given string. + *

+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     * 

+ * An URI can be placed within double-quotes or angle brackets like + * "http://test.com/" and <http://test.com/> + * + * @param original the string to be represented to URI character sequence + * It is one of absoluteURI and relativeURI. + * @exception IOException + */ + public URI(String original) throws IOException { + parseUriReference(original, false); + } + + /** + * Construct a URI from a URL. + * + * @param url a valid URL. + * @throws IOException + * @since 2.0 + */ + public URI(URL url) throws IOException { + this(url.toString()); + } + + + /** + * Construct a general URI from the given components. + *

+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     *   opaque_part   = uric_no_slash *uric
+     * 

+ * It's for absolute URI = <scheme>:<scheme-specific-part># + * <fragment>. + * + * @param scheme the scheme string + * @param scheme_specific_part scheme_specific_part + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String scheme_specific_part, String fragment) + throws IOException { + + // validate and contruct the URI character sequence + if (scheme == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: scheme required"); + } + char[] s = scheme.toLowerCase().toCharArray(); + if (validate(s, URI.scheme)) { + _scheme = s; // is_absoluteURI + } else { + throw new IOException(/*IOException.PARSING,*/ "URI: incorrect scheme"); + } + _opaque = encode(scheme_specific_part, allowed_opaque_part); + // Set flag + _is_opaque_part = true; + setUriReference(); + } + + + /** + * Construct a general URI from the given components. + *

+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     *   relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     *   hier_part     = ( net_path | abs_path ) [ "?" query ]
+     * 

+ * It's for absolute URI = <scheme>:<path>?<query>#< + * fragment> and relative URI = <path>?<query>#<fragment + * >. + * + * @param scheme the scheme string + * @param authority the authority string + * @param path the path string + * @param query the query string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String authority, String path, String query, + String fragment) throws IOException { + + // validate and contruct the URI character sequence + StringBuffer buff = new StringBuffer(); + if (scheme != null) { + buff.append(scheme); + buff.append(':'); + } + if (authority != null) { + buff.append("//"); + buff.append(authority); + } + if (path != null) { // accept empty path + if ((scheme != null || authority != null) + && !path.startsWith("/")) { + throw new IOException(/*IOException.PARSING*,*/ + "URI: abs_path requested"); + } + buff.append(path); + } + if (query != null) { + buff.append('?'); + buff.append(query); + } + if (fragment != null) { + buff.append('#'); + buff.append(fragment); + } + parseUriReference(buff.toString(), false); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port) + throws IOException { + + this(scheme, userinfo, host, port, null, null, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path) throws IOException { + + this(scheme, userinfo, host, port, path, null, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @param query the query string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path, String query) throws IOException { + + this(scheme, userinfo, host, port, path, query, null); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param userinfo the userinfo string + * @param host the host string + * @param port the port number + * @param path the path string + * @param query the query string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String userinfo, String host, int port, + String path, String query, String fragment) throws IOException { + + this(scheme, (host == null) ? null : + ((userinfo != null) ? userinfo + '@' : "") + host + + ((port != -1) ? ":" + port : ""), path, query, fragment); + } + + + /** + * Construct a general URI from the given components. + * + * @param scheme the scheme string + * @param host the host string + * @param path the path string + * @param fragment the fragment string + * @exception IOException + */ + public URI(String scheme, String host, String path, String fragment) + throws IOException { + + this(scheme, host, path, null, fragment); + } + + + /** + * Construct a general URI with the given relative URI string. + * + * @param base the base URI + * @param relative the relative URI string + * @exception IOException + */ + public URI(URI base, String relative) throws IOException { + this(base, new URI(relative)); + } + + + /** + * Construct a general URI with the given relative URI. + *

+     *   URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     *   relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     * 

+ * Resolving Relative References to Absolute Form. + * + * Examples of Resolving Relative URI References + * + * Within an object with a well-defined base URI of + *

+     *   http://a/b/c/d;p?q
+     * 

+ * the relative URI would be resolved as follows: + * + * Normal Examples + * + *

+     *   g:h           =  g:h
+     *   g             =  http://a/b/c/g
+     *   ./g           =  http://a/b/c/g
+     *   g/            =  http://a/b/c/g/
+     *   /g            =  http://a/g
+     *   //g           =  http://g
+     *   ?y            =  http://a/b/c/?y
+     *   g?y           =  http://a/b/c/g?y
+     *   #s            =  (current document)#s
+     *   g#s           =  http://a/b/c/g#s
+     *   g?y#s         =  http://a/b/c/g?y#s
+     *   ;x            =  http://a/b/c/;x
+     *   g;x           =  http://a/b/c/g;x
+     *   g;x?y#s       =  http://a/b/c/g;x?y#s
+     *   .             =  http://a/b/c/
+     *   ./            =  http://a/b/c/
+     *   ..            =  http://a/b/
+     *   ../           =  http://a/b/
+     *   ../g          =  http://a/b/g
+     *   ../..         =  http://a/
+     *   ../../        =  http://a/ 
+     *   ../../g       =  http://a/g
+     * 

+ * + * Some URI schemes do not allow a hierarchical syntax matching the + * syntax, and thus cannot use relative references. + * + * @param base the base URI + * @param relative the relative URI + * @exception IOException + */ + public URI(URI base, URI relative) throws IOException { + + if (base._scheme == null) { + throw new IOException(/* IOException.PARSING,*/ "URI: base URI required"); + } + if (base._scheme != null) { + this._scheme = base._scheme; + this._authority = base._authority; + } + if (base._is_opaque_part || relative._is_opaque_part) { + this._scheme = base._scheme; + this._is_opaque_part = relative._is_opaque_part; + this._opaque = relative._opaque; + this._fragment = relative._fragment; + this.setUriReference(); + return; + } + if (relative._scheme != null) { + this._scheme = relative._scheme; + this._is_net_path = relative._is_net_path; + this._authority = relative._authority; + if (relative._is_server) { + this._userinfo = relative._userinfo; + this._host = relative._host; + this._port = relative._port; + } else if (relative._is_reg_name) { + this._is_reg_name = relative._is_reg_name; + } + this._is_abs_path = relative._is_abs_path; + this._is_rel_path = relative._is_rel_path; + this._path = relative._path; + } else if (base._authority != null && relative._scheme == null) { + this._is_net_path = base._is_net_path; + this._authority = base._authority; + if (base._is_server) { + this._userinfo = base._userinfo; + this._host = base._host; + this._port = base._port; + } else if (base._is_reg_name) { + this._is_reg_name = base._is_reg_name; + } + } + if (relative._authority != null) { + this._is_net_path = relative._is_net_path; + this._authority = relative._authority; + if (relative._is_server) { + this._is_server = relative._is_server; + this._userinfo = relative._userinfo; + this._host = relative._host; + this._port = relative._port; + } else if (relative._is_reg_name) { + this._is_reg_name = relative._is_reg_name; + } + this._is_abs_path = relative._is_abs_path; + this._is_rel_path = relative._is_rel_path; + this._path = relative._path; + } + // resolve the path + if (relative._scheme == null && relative._authority == null || + equals(base._scheme, relative._scheme)) { + this._path = resolvePath(base._path, relative._path); + } + // base._query removed + if (relative._query != null) { + this._query = relative._query; + } + // base._fragment removed + if (relative._fragment != null) { + this._fragment = relative._fragment; + } + this.setUriReference(); + } + + // --------------------------------------------------- Instance Variables + + static final long serialVersionUID = 604752400577948726L; + + + /** + * This Uniform Resource Identifier (URI). + * The URI is always in an "escaped" form, since escaping or unescaping + * a completed URI might change its semantics. + */ + protected char[] _uri = null; + + + /** + * The default charset of the protocol. RFC 2277, 2396 + */ + protected static String _protocolCharset = "UTF-8"; + + + /** + * The default charset of the document. RFC 2277, 2396 + * The platform's charset is used for the document by default. + */ + protected static String _documentCharset = null; + // Static initializer for _documentCharset + static { + Locale locale = Locale.getDefault(); + if (locale != null) { + // in order to support backward compatiblity + _documentCharset = LocaleToCharsetMap.getCharset(locale); + } else { + _documentCharset = (String)AccessController.doPrivileged( + new GetPropertyAction("file.encoding")); + } + } + + /** + * The scheme. + */ + protected char[] _scheme = null; + + + /** + * The opaque. + */ + protected char[] _opaque = null; + + + /** + * The authority. + */ + protected char[] _authority = null; + + + /** + * The userinfo. + */ + protected char[] _userinfo = null; + + + /** + * The host. + */ + protected char[] _host = null; + + + /** + * The port. + */ + protected int _port = -1; + + + /** + * The path. + */ + protected char[] _path = null; + + + /** + * The query. + */ + protected char[] _query = null; + + + /** + * The fragment. + */ + protected char[] _fragment = null; + + + /** + * The root path. + */ + protected static char[] rootPath = { '/' }; + + // ---------------------- Generous characters for each component validation + + /** + * The percent "%" character always has the reserved purpose of being the + * escape indicator, it must be escaped as "%25" in order to be used as + * data within a URI. + */ + protected static final BitSet percent = new BitSet(256); + // Static initializer for percent + static { + percent.set('%'); + } + + + /** + * BitSet for digit. + *

+     * digit    = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" |
+     *            "8" | "9"
+     * 

+ */ + protected static final BitSet digit = new BitSet(256); + // Static initializer for digit + static { + for(int i = '0'; i <= '9'; i++) { + digit.set(i); + } + } + + + /** + * BitSet for alpha. + *

+     * alpha         = lowalpha | upalpha
+     * 

+ */ + protected static final BitSet alpha = new BitSet(256); + // Static initializer for alpha + static { + for (int i = 'a'; i <= 'z'; i++) { + alpha.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + alpha.set(i); + } + } + + + /** + * BitSet for alphanum (join of alpha & digit). + *

+     *  alphanum      = alpha | digit
+     * 

+ */ + protected static final BitSet alphanum = new BitSet(256); + // Static initializer for alphanum + static { + alphanum.or(alpha); + alphanum.or(digit); + } + + + /** + * BitSet for hex. + *

+     * hex           = digit | "A" | "B" | "C" | "D" | "E" | "F" |
+     *                         "a" | "b" | "c" | "d" | "e" | "f"
+     * 

+ */ + protected static final BitSet hex = new BitSet(256); + // Static initializer for hex + static { + hex.or(digit); + for(int i = 'a'; i <= 'f'; i++) { + hex.set(i); + } + for(int i = 'A'; i <= 'F'; i++) { + hex.set(i); + } + } + + + /** + * BitSet for escaped. + *

+     * escaped       = "%" hex hex
+     * 

+ */ + protected static final BitSet escaped = new BitSet(256); + // Static initializer for escaped + static { + escaped.or(percent); + escaped.or(hex); + } + + + /** + * BitSet for mark. + *

+     * mark          = "-" | "_" | "." | "!" | "~" | "*" | "'" |
+     *                 "(" | ")"
+     * 

+ */ + protected static final BitSet mark = new BitSet(256); + // Static initializer for mark + static { + mark.set('-'); + mark.set('_'); + mark.set('.'); + mark.set('!'); + mark.set('~'); + mark.set('*'); + mark.set('\''); + mark.set('('); + mark.set(')'); + } + + + /** + * Data characters that are allowed in a URI but do not have a reserved + * purpose are called unreserved. + *

+     * unreserved    = alphanum | mark
+     * 

+ */ + protected static final BitSet unreserved = new BitSet(256); + // Static initializer for unreserved + static { + unreserved.or(alphanum); + unreserved.or(mark); + } + + + /** + * BitSet for reserved. + *

+     * reserved      = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+     *                 "$" | ","
+     * 

+ */ + protected static final BitSet reserved = new BitSet(256); + // Static initializer for reserved + static { + reserved.set(';'); + reserved.set('/'); + reserved.set('?'); + reserved.set(':'); + reserved.set('@'); + reserved.set('&'); + reserved.set('='); + reserved.set('+'); + reserved.set('$'); + reserved.set(','); + } + + + /** + * BitSet for uric. + *

+     * uric          = reserved | unreserved | escaped
+     * 

+ */ + protected static final BitSet uric = new BitSet(256); + // Static initializer for uric + static { + uric.or(reserved); + uric.or(unreserved); + uric.or(escaped); + } + + + /** + * BitSet for fragment (alias for uric). + *

+     * fragment      = *uric
+     * 

+ */ + protected static final BitSet fragment = uric; + + + /** + * BitSet for query (alias for uric). + *

+     * query         = *uric
+     * 

+ */ + protected static final BitSet query = uric; + + + /** + * BitSet for pchar. + *

+     * pchar         = unreserved | escaped |
+     *                 ":" | "@" | "&" | "=" | "+" | "$" | ","
+     * 

+ */ + protected static final BitSet pchar = new BitSet(256); + // Static initializer for pchar + static { + pchar.or(unreserved); + pchar.or(escaped); + pchar.set(':'); + pchar.set('@'); + pchar.set('&'); + pchar.set('='); + pchar.set('+'); + pchar.set('$'); + pchar.set(','); + } + + + /** + * BitSet for param (alias for pchar). + *

+     * param         = *pchar
+     * 

+ */ + protected static final BitSet param = pchar; + + + /** + * BitSet for segment. + *

+     * segment       = *pchar *( ";" param )
+     * 

+ */ + protected static final BitSet segment = new BitSet(256); + // Static initializer for segment + static { + segment.or(pchar); + segment.set(';'); + segment.or(param); + } + + + /** + * BitSet for path segments. + *

+     * path_segments = segment *( "/" segment )
+     * 

+ */ + protected static final BitSet path_segments = new BitSet(256); + // Static initializer for path_segments + static { + path_segments.set('/'); + path_segments.or(segment); + } + + + /** + * URI absolute path. + *

+     * abs_path      = "/"  path_segments
+     * 

+ */ + protected static final BitSet abs_path = new BitSet(256); + // Static initializer for abs_path + static { + abs_path.set('/'); + abs_path.or(path_segments); + } + + + /** + * URI bitset for encoding typical non-slash characters. + *

+     * uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
+     *                 "&" | "=" | "+" | "$" | ","
+     * 

+ */ + protected static final BitSet uric_no_slash = new BitSet(256); + // Static initializer for uric_no_slash + static { + uric_no_slash.or(unreserved); + uric_no_slash.or(escaped); + uric_no_slash.set(';'); + uric_no_slash.set('?'); + uric_no_slash.set(';'); + uric_no_slash.set('@'); + uric_no_slash.set('&'); + uric_no_slash.set('='); + uric_no_slash.set('+'); + uric_no_slash.set('$'); + uric_no_slash.set(','); + } + + + /** + * URI bitset that combines uric_no_slash and uric. + *

+     * opaque_part   = uric_no_slash *uric
+     * 

+ */ + protected static final BitSet opaque_part = new BitSet(256); + // Static initializer for opaque_part + static { + opaque_part.or(uric_no_slash); + opaque_part.or(uric); + } + + + /** + * URI bitset that combines absolute path and opaque part. + *

+     * path          = [ abs_path | opaque_part ]
+     * 

+ */ + protected static final BitSet path = new BitSet(256); + // Static initializer for path + static { + path.or(abs_path); + path.or(opaque_part); + } + + + /** + * Port, a logical alias for digit. + */ + protected static final BitSet port = digit; + + + /** + * Bitset that combines digit and dot fo IPv$address. + *

+     * IPv4address   = 1*digit "." 1*digit "." 1*digit "." 1*digit
+     * 

+ */ + protected static final BitSet IPv4address = new BitSet(256); + // Static initializer for IPv4address + static { + IPv4address.or(digit); + IPv4address.set('.'); + } + + + /** + * RFC 2373. + *

+     * IPv6address = hexpart [ ":" IPv4address ]
+     * 

+ */ + protected static final BitSet IPv6address = new BitSet(256); + // Static initializer for IPv6address reference + static { + IPv6address.or(hex); // hexpart + IPv6address.set(':'); + IPv6address.or(IPv4address); + } + + + /** + * RFC 2732, 2373. + *

+     * IPv6reference   = "[" IPv6address "]"
+     * 

+ */ + protected static final BitSet IPv6reference = new BitSet(256); + // Static initializer for IPv6reference + static { + IPv6reference.set('['); + IPv6reference.or(IPv6address); + IPv6reference.set(']'); + } + + + /** + * BitSet for toplabel. + *

+     * toplabel      = alpha | alpha *( alphanum | "-" ) alphanum
+     * 

+ */ + protected static final BitSet toplabel = new BitSet(256); + // Static initializer for toplabel + static { + toplabel.or(alphanum); + toplabel.set('-'); + } + + + /** + * BitSet for domainlabel. + *

+     * domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
+     * 

+ */ + protected static final BitSet domainlabel = toplabel; + + + /** + * BitSet for hostname. + *

+     * hostname      = *( domainlabel "." ) toplabel [ "." ]
+     * 

+ */ + protected static final BitSet hostname = new BitSet(256); + // Static initializer for hostname + static { + hostname.or(toplabel); + // hostname.or(domainlabel); + hostname.set('.'); + } + + + /** + * BitSet for host. + *

+     * host          = hostname | IPv4address | IPv6reference
+     * 

+ */ + protected static final BitSet host = new BitSet(256); + // Static initializer for host + static { + host.or(hostname); + // host.or(IPv4address); + host.or(IPv6reference); // IPv4address + } + + + /** + * BitSet for hostport. + *

+     * hostport      = host [ ":" port ]
+     * 

+ */ + protected static final BitSet hostport = new BitSet(256); + // Static initializer for hostport + static { + hostport.or(host); + hostport.set(':'); + hostport.or(port); + } + + + /** + * Bitset for userinfo. + *

+     * userinfo      = *( unreserved | escaped |
+     *                    ";" | ":" | "&" | "=" | "+" | "$" | "," )
+     * 

+ */ + protected static final BitSet userinfo = new BitSet(256); + // Static initializer for userinfo + static { + userinfo.or(unreserved); + userinfo.or(escaped); + userinfo.set(';'); + userinfo.set(':'); + userinfo.set('&'); + userinfo.set('='); + userinfo.set('+'); + userinfo.set('$'); + userinfo.set(','); + } + + + /** + * BitSet for within the userinfo component like user and password. + */ + public static final BitSet within_userinfo = new BitSet(256); + // Static initializer for within_userinfo + static { + within_userinfo.or(userinfo); + within_userinfo.clear(';'); // reserved within authority + within_userinfo.clear(':'); + within_userinfo.clear('@'); + within_userinfo.clear('?'); + within_userinfo.clear('/'); + } + + + /** + * Bitset for server. + *

+     * server        = [ [ userinfo "@" ] hostport ]
+     * 

+ */ + protected static final BitSet server = new BitSet(256); + // Static initializer for server + static { + server.or(userinfo); + server.set('@'); + server.or(hostport); + } + + + /** + * BitSet for reg_name. + *

+     * reg_name      = 1*( unreserved | escaped | "$" | "," |
+     *                     ";" | ":" | "@" | "&" | "=" | "+" )
+     * 

+ */ + protected static final BitSet reg_name = new BitSet(256); + // Static initializer for reg_name + static { + reg_name.or(unreserved); + reg_name.or(escaped); + reg_name.set('$'); + reg_name.set(','); + reg_name.set(';'); + reg_name.set(':'); + reg_name.set('@'); + reg_name.set('&'); + reg_name.set('='); + reg_name.set('+'); + } + + + /** + * BitSet for authority. + *

+     * authority     = server | reg_name
+     * 

+ */ + protected static final BitSet authority = new BitSet(256); + // Static initializer for authority + static { + authority.or(server); + authority.or(reg_name); + } + + + /** + * BitSet for scheme. + *

+     * scheme        = alpha *( alpha | digit | "+" | "-" | "." )
+     * 

+ */ + protected static final BitSet scheme = new BitSet(256); + // Static initializer for scheme + static { + scheme.or(alpha); + scheme.or(digit); + scheme.set('+'); + scheme.set('-'); + scheme.set('.'); + } + + + /** + * BitSet for rel_segment. + *

+     * rel_segment   = 1*( unreserved | escaped |
+     *                     ";" | "@" | "&" | "=" | "+" | "$" | "," )
+     * 

+ */ + protected static final BitSet rel_segment = new BitSet(256); + // Static initializer for rel_segment + static { + rel_segment.or(unreserved); + rel_segment.or(escaped); + rel_segment.set(';'); + rel_segment.set('@'); + rel_segment.set('&'); + rel_segment.set('='); + rel_segment.set('+'); + rel_segment.set('$'); + rel_segment.set(','); + } + + + /** + * BitSet for rel_path. + *

+     * rel_path      = rel_segment [ abs_path ]
+     * 

+ */ + protected static final BitSet rel_path = new BitSet(256); + // Static initializer for rel_path + static { + rel_path.or(rel_segment); + rel_path.or(abs_path); + } + + + /** + * BitSet for net_path. + *

+     * net_path      = "//" authority [ abs_path ]
+     * 

+ */ + protected static final BitSet net_path = new BitSet(256); + // Static initializer for net_path + static { + net_path.set('/'); + net_path.or(authority); + net_path.or(abs_path); + } + + + /** + * BitSet for hier_part. + *

+     * hier_part     = ( net_path | abs_path ) [ "?" query ]
+     * 

+ */ + protected static final BitSet hier_part = new BitSet(256); + // Static initializer for hier_part + static { + hier_part.or(net_path); + hier_part.or(abs_path); + // hier_part.set('?'); aleady included + hier_part.or(query); + } + + + /** + * BitSet for relativeURI. + *

+     * relativeURI   = ( net_path | abs_path | rel_path ) [ "?" query ]
+     * 

+ */ + protected static final BitSet relativeURI = new BitSet(256); + // Static initializer for relativeURI + static { + relativeURI.or(net_path); + relativeURI.or(abs_path); + relativeURI.or(rel_path); + // relativeURI.set('?'); aleady included + relativeURI.or(query); + } + + + /** + * BitSet for absoluteURI. + *

+     * absoluteURI   = scheme ":" ( hier_part | opaque_part )
+     * 

+ */ + protected static final BitSet absoluteURI = new BitSet(256); + // Static initializer for absoluteURI + static { + absoluteURI.or(scheme); + absoluteURI.set(':'); + absoluteURI.or(hier_part); + absoluteURI.or(opaque_part); + } + + + /** + * BitSet for URI-reference. + *

+     * URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+     * 

+ */ + protected static final BitSet URI_reference = new BitSet(256); + // Static initializer for URI_reference + static { + URI_reference.or(absoluteURI); + URI_reference.or(relativeURI); + URI_reference.set('#'); + URI_reference.or(fragment); + } + + // ---------------------------- Characters disallowed within the URI syntax + // Excluded US-ASCII Characters are like control, space, delims and unwise + + /** + * BitSet for control. + */ + public static final BitSet control = new BitSet(256); + // Static initializer for control + static { + for (int i = 0; i <= 0x1F; i++) { + control.set(i); + } + control.set(0x7F); + } + + /** + * BitSet for space. + */ + public static final BitSet space = new BitSet(256); + // Static initializer for space + static { + space.set(0x20); + } + + + /** + * BitSet for delims. + */ + public static final BitSet delims = new BitSet(256); + // Static initializer for delims + static { + delims.set('<'); + delims.set('>'); + delims.set('#'); + delims.set('%'); + delims.set('"'); + } + + + /** + * BitSet for unwise. + */ + public static final BitSet unwise = new BitSet(256); + // Static initializer for unwise + static { + unwise.set('{'); + unwise.set('}'); + unwise.set('|'); + unwise.set('\\'); + unwise.set('^'); + unwise.set('['); + unwise.set(']'); + unwise.set('`'); + } + + + /** + * Disallowed rel_path before escaping. + */ + public static final BitSet disallowed_rel_path = new BitSet(256); + // Static initializer for disallowed_rel_path + static { + disallowed_rel_path.or(uric); + disallowed_rel_path.andNot(rel_path); + } + + + /** + * Disallowed opaque_part before escaping. + */ + public static final BitSet disallowed_opaque_part = new BitSet(256); + // Static initializer for disallowed_opaque_part + static { + disallowed_opaque_part.or(uric); + disallowed_opaque_part.andNot(opaque_part); + } + + // ----------------------- Characters allowed within and for each component + + /** + * Those characters that are allowed for the authority component. + */ + public static final BitSet allowed_authority = new BitSet(256); + // Static initializer for allowed_authority + static { + allowed_authority.or(authority); + allowed_authority.clear('%'); + } + + + /** + * Those characters that are allowed for the opaque_part. + */ + public static final BitSet allowed_opaque_part = new BitSet(256); + // Static initializer for allowed_opaque_part + static { + allowed_opaque_part.or(opaque_part); + allowed_opaque_part.clear('%'); + } + + + /** + * Those characters that are allowed for the reg_name. + */ + public static final BitSet allowed_reg_name = new BitSet(256); + // Static initializer for allowed_reg_name + static { + allowed_reg_name.or(reg_name); + // allowed_reg_name.andNot(percent); + allowed_reg_name.clear('%'); + } + + + /** + * Those characters that are allowed for the userinfo component. + */ + public static final BitSet allowed_userinfo = new BitSet(256); + // Static initializer for allowed_userinfo + static { + allowed_userinfo.or(userinfo); + // allowed_userinfo.andNot(percent); + allowed_userinfo.clear('%'); + } + + + /** + * Those characters that are allowed for within the userinfo component. + */ + public static final BitSet allowed_within_userinfo = new BitSet(256); + // Static initializer for allowed_within_userinfo + static { + allowed_within_userinfo.or(within_userinfo); + allowed_within_userinfo.clear('%'); + } + + + /** + * Those characters that are allowed for the IPv6reference component. + * The characters '[', ']' in IPv6reference should be excluded. + */ + public static final BitSet allowed_IPv6reference = new BitSet(256); + // Static initializer for allowed_IPv6reference + static { + allowed_IPv6reference.or(IPv6reference); + // allowed_IPv6reference.andNot(unwise); + allowed_IPv6reference.clear('['); + allowed_IPv6reference.clear(']'); + } + + + /** + * Those characters that are allowed for the host component. + * The characters '[', ']' in IPv6reference should be excluded. + */ + public static final BitSet allowed_host = new BitSet(256); + // Static initializer for allowed_host + static { + allowed_host.or(hostname); + allowed_host.or(allowed_IPv6reference); + } + + + /** + * Those characters that are allowed for the authority component. + */ + public static final BitSet allowed_within_authority = new BitSet(256); + // Static initializer for allowed_within_authority + static { + allowed_within_authority.or(server); + allowed_within_authority.or(reg_name); + allowed_within_authority.clear(';'); + allowed_within_authority.clear(':'); + allowed_within_authority.clear('@'); + allowed_within_authority.clear('?'); + allowed_within_authority.clear('/'); + } + + + /** + * Those characters that are allowed for the abs_path. + */ + public static final BitSet allowed_abs_path = new BitSet(256); + // Static initializer for allowed_abs_path + static { + allowed_abs_path.or(abs_path); + // allowed_abs_path.set('/'); // aleady included + allowed_abs_path.andNot(percent); + } + + + /** + * Those characters that are allowed for the rel_path. + */ + public static final BitSet allowed_rel_path = new BitSet(256); + // Static initializer for allowed_rel_path + static { + allowed_rel_path.or(rel_path); + allowed_rel_path.clear('%'); + } + + + /** + * Those characters that are allowed within the path. + */ + public static final BitSet allowed_within_path = new BitSet(256); + // Static initializer for allowed_within_path + static { + allowed_within_path.or(abs_path); + allowed_within_path.clear('/'); + allowed_within_path.clear(';'); + allowed_within_path.clear('='); + allowed_within_path.clear('?'); + } + + + /** + * Those characters that are allowed for the query component. + */ + public static final BitSet allowed_query = new BitSet(256); + // Static initializer for allowed_query + static { + allowed_query.or(uric); + allowed_query.clear('%'); + } + + + /** + * Those characters that are allowed within the query component. + */ + public static final BitSet allowed_within_query = new BitSet(256); + // Static initializer for allowed_within_query + static { + allowed_within_query.or(allowed_query); + allowed_within_query.andNot(reserved); // excluded 'reserved' + allowed_within_query.clear('#'); // avoid confict with the fragment + } + + + /** + * Those characters that are allowed for the fragment component. + */ + public static final BitSet allowed_fragment = new BitSet(256); + // Static initializer for allowed_fragment + static { + allowed_fragment.or(uric); + allowed_fragment.clear('%'); + } + + // ------------------------------------------- Flags for this URI-reference + + // URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + // absoluteURI = scheme ":" ( hier_part | opaque_part ) + protected boolean _is_hier_part; + protected boolean _is_opaque_part; + // relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + // hier_part = ( net_path | abs_path ) [ "?" query ] + protected boolean _is_net_path; + protected boolean _is_abs_path; + protected boolean _is_rel_path; + // net_path = "//" authority [ abs_path ] + // authority = server | reg_name + protected boolean _is_reg_name; + protected boolean _is_server; // = _has_server + // server = [ [ userinfo "@" ] hostport ] + // host = hostname | IPv4address | IPv6reference + protected boolean _is_hostname; + protected boolean _is_IPv4address; + protected boolean _is_IPv6reference; + + // ------------------------------------------ Character and escape encoding + + /** + * Encode with the default protocol charset. + * + * @param original the original character sequence + * @param allowed those characters that are allowed within a component + * @return URI character sequence + * @exception IOException null component or unsupported character encoding + */ + protected static char[] encode(String original, BitSet allowed) + throws IOException { + + return encode(original, allowed, _protocolCharset); + } + + + /** + * Encodes URI string. + * + * This is a two mapping, one from original characters to octets, and + * subsequently a second from octets to URI characters: + *

+     *   original character sequence->octet sequence->URI character sequence
+     * 

+ * + * An escaped octet is encoded as a character triplet, consisting of the + * percent character "%" followed by the two hexadecimal digits + * representing the octet code. For example, "%20" is the escaped + * encoding for the US-ASCII space character. + *

+ * Conversion from the local filesystem character set to UTF-8 will + * normally involve a two step process. First convert the local character + * set to the UCS; then convert the UCS to UTF-8. + * The first step in the process can be performed by maintaining a mapping + * table that includes the local character set code and the corresponding + * UCS code. + * The next step is to convert the UCS character code to the UTF-8 encoding. + *

+ * Mapping between vendor codepages can be done in a very similar manner + * as described above. + *

+ * The only time escape encodings can allowedly be made is when a URI is + * being created from its component parts. The escape and validate methods + * are internally performed within this method. + * + * @param original the original character sequence + * @param allowed those characters that are allowed within a component + * @param charset the protocol charset + * @return URI character sequence + * @exception IOException null component or unsupported character encoding + */ + protected static char[] encode(String original, BitSet allowed, + String charset) throws IOException { + + // encode original to uri characters. + if (original == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: null"); + } + // escape octet to uri characters. + if (allowed == null) { + throw new IOException(/*IOException.PARSING,*/ + "URI: null allowed characters"); + } + byte[] octets; + try { + octets = original.getBytes(charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/*IOException.UNSUPPORTED_ENCODING,*/ "Unsupported Encoding: " + charset); + } + StringBuffer buf = new StringBuffer(octets.length); + for (int i = 0; i < octets.length; i++) { + char c = (char) octets[i]; + if (allowed.get(c)) { + buf.append(c); + } else { + buf.append('%'); + byte b = octets[i]; // use the original byte value + char hexadecimal = Character.forDigit((b >> 4) & 0xF, 16); + buf.append(Character.toUpperCase(hexadecimal)); // high + hexadecimal = Character.forDigit(b & 0xF, 16); + buf.append(Character.toUpperCase(hexadecimal)); // low + } + } + + return buf.toString().toCharArray(); + } + + + /** + * Decode with the default protocol charset. + * + * @param component the URI character sequence + * @return original character sequence + * @exception IOException incomplete trailing escape pattern + * or unsupported character encoding + */ + protected static String decode(char[] component) throws IOException { + return decode(component, _protocolCharset); + } + + + /** + * Decodes URI encoded string. + * + * This is a two mapping, one from URI characters to octets, and + * subsequently a second from octets to original characters: + *

+     *   URI character sequence->octet sequence->original character sequence
+     * 

+ * + * A URI must be separated into its components before the escaped + * characters within those components can be allowedly decoded. + *

+ * Notice that there is a chance that URI characters that are non UTF-8 + * may be parsed as valid UTF-8. A recent non-scientific analysis found + * that EUC encoded Japanese words had a 2.7% false reading; SJIS had a + * 0.0005% false reading; other encoding such as ASCII or KOI-8 have a 0% + * false reading. + *

+ * The percent "%" character always has the reserved purpose of being + * the escape indicator, it must be escaped as "%25" in order to be used + * as data within a URI. + *

+ * The unescape method is internally performed within this method. + * + * @param component the URI character sequence + * @param charset the protocol charset + * @return original character sequence + * @exception IOException incomplete trailing escape pattern + * or unsupported character encoding + */ + protected static String decode(char[] component, String charset) + throws IOException { + + // unescape uri characters to octets + if (component == null) return null; + + byte[] octets; + try { + octets = new String(component).getBytes(charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/* IOException.UNSUPPORTED_ENCODING, */ + "URI: not supported " + charset + " encoding"); + } + int length = octets.length; + int oi = 0; // output index + for (int ii = 0; ii < length; oi++) { + byte aByte = (byte) octets[ii++]; + if (aByte == '%' && ii+2 <= length) { + byte high = (byte) Character.digit((char) octets[ii++], 16); + byte low = (byte) Character.digit((char) octets[ii++], 16); + if (high == -1 || low == -1) { + throw new IOException(/* IOException.ESCAPING, */ + "URI: incomplete trailing escape pattern"); + + } + aByte = (byte) ((high << 4) + low); + } + octets[oi] = (byte) aByte; + } + + String result; + try { + result = new String(octets, 0, oi, charset); + } catch (UnsupportedEncodingException error) { + throw new IOException(/* IOException.UNSUPPORTED_ENCODING, */ + "URI: not supported " + charset + " encoding"); + } + + return result; + } + + + /** + * Pre-validate the unescaped URI string within a specific component. + * + * @param component the component string within the component + * @param disallowed those characters disallowed within the component + * @return if true, it doesn't have the disallowed characters + * if false, the component is undefined or an incorrect one + */ + protected boolean prevalidate(String component, BitSet disallowed) { + // prevalidate the given component by disallowed characters + if (component == null) { + return false; // undefined + } + char[] target = component.toCharArray(); + for (int i = 0; i < target.length; i++) { + if (disallowed.get(target[i])) { + return false; + } + } + return true; + } + + + /** + * Validate the URI characters within a specific component. + * The component must be performed after escape encoding. Or it doesn't + * include escaped characters. + * + * @param component the characters sequence within the component + * @param generous those characters that are allowed within a component + * @return if true, it's the correct URI character sequence + */ + protected boolean validate(char[] component, BitSet generous) { + // validate each component by generous characters + return validate(component, 0, -1, generous); + } + + + /** + * Validate the URI characters within a specific component. + * The component must be performed after escape encoding. Or it doesn't + * include escaped characters. + *

+ * It's not that much strict, generous. The strict validation might be + * performed before being called this method. + * + * @param component the characters sequence within the component + * @param soffset the starting offset of the given component + * @param eoffset the ending offset of the given component + * if -1, it means the length of the component + * @param generous those characters that are allowed within a component + * @return if true, it's the correct URI character sequence + * @throws NullPointerException null component + */ + protected boolean validate(char[] component, int soffset, int eoffset, + BitSet generous) { + // validate each component by generous characters + if (eoffset == -1) { + eoffset = component.length -1; + } + for (int i = soffset; i <= eoffset; i++) { + if (!generous.get(component[i])) return false; + } + return true; + } + + + /** + * In order to avoid any possilbity of conflict with non-ASCII characters, + * Parse a URI reference as a String with the character + * encoding of the local system or the document. + *

+ * The following line is the regular expression for breaking-down a URI + * reference into its components. + *

+     *   ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+     *    12            3  4          5       6  7        8 9
+     * 

+ * For example, matching the above expression to + * http://jakarta.apache.org/ietf/uri/#Related + * results in the following subexpression matches: + *

+     *               $1 = http:
+     *  scheme    =  $2 = http
+     *               $3 = //jakarta.apache.org
+     *  authority =  $4 = jakarta.apache.org
+     *  path      =  $5 = /ietf/uri/
+     *               $6 = 
+     *  query     =  $7 = 
+     *               $8 = #Related
+     *  fragment  =  $9 = Related
+     * 

+ * + * @param original the original character sequence + * @param escaped true if original is escaped + * @return the original character sequence + * @exception IOException + */ + protected void parseUriReference(String original, boolean escaped) + throws IOException { + + // validate and contruct the URI character sequence + if (original == null || original.length() == 0) { + throw new IOException("URI-Reference required"); + } + + /** @ + * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + */ + String tmp = original.trim(); + + /** + * The length of the string sequence of characters. + * It may not be equal to the length of the byte array. + */ + int length = tmp.length(); + + /** + * Remove the delimiters like angle brackets around an URI. + */ + char[] firstDelimiter = { tmp.charAt(0) }; + if (validate(firstDelimiter, delims)) { + if (length >= 2) { + char[] lastDelimiter = { tmp.charAt(length - 1) }; + if (validate(lastDelimiter, delims)) { + tmp = tmp.substring(1, length - 1); + length = length - 2; + } + } + } + + /** + * The starting index + */ + int from = 0; + + /** + * The test flag whether the URI is started from the path component. + */ + boolean isStartedFromPath = false; + int atColon = tmp.indexOf(':'); + int atSlash = tmp.indexOf('/'); + if (atColon < 0 || (atSlash >= 0 && atSlash < atColon)) { + isStartedFromPath = true; + } + + /** + *

+         *     @@@@@@@@
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + int at = indexFirstOf(tmp, isStartedFromPath ? "/?#" : ":/?#", from); + if (at == -1) at = 0; + + /** + * Parse the scheme. + *

+         *  scheme    =  $2 = http
+         *              @
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + if (at < length && tmp.charAt(at) == ':') { + char[] target = tmp.substring(0, at).toLowerCase().toCharArray(); + if (validate(target, scheme)) { + _scheme = target; + } else { + throw new IOException("incorrect scheme"); + } + from = ++at; + } + + /** + * Parse the authority component. + *

+         *  authority =  $4 = jakarta.apache.org
+         *                  @@
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + // Reset flags + _is_net_path = _is_abs_path = _is_rel_path = _is_hier_part = false; + if (0 <= at && at < length && tmp.charAt(at) == '/') { + // Set flag + _is_hier_part = true; + if (at + 2 < length && tmp.charAt(at + 1) == '/') { + // the temporary index to start the search from + int next = indexFirstOf(tmp, "/?#", at + 2); + if (next == -1) { + next = (tmp.substring(at + 2).length() == 0) ? at + 2 : + tmp.length(); + } + parseAuthority(tmp.substring(at + 2, next), escaped); + from = at = next; + // Set flag + _is_net_path = true; + } + if (from == at) { + // Set flag + _is_abs_path = true; + } + } + + /** + * Parse the path component. + *

+         *  path      =  $5 = /ietf/uri/
+         *                                @@@@@@
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + if (from < length) { + // rel_path = rel_segment [ abs_path ] + int next = indexFirstOf(tmp, "?#", from); + if (next == -1) { + next = tmp.length(); + } + if (!_is_abs_path) { + if (!escaped && prevalidate(tmp.substring(from, next), + disallowed_rel_path) || escaped && + validate(tmp.substring(from, next).toCharArray(), + rel_path)) { + // Set flag + _is_rel_path = true; + } else if (!escaped && prevalidate(tmp.substring(from, next), + disallowed_opaque_part) || escaped && + validate(tmp.substring(from, next).toCharArray(), + opaque_part)) { + // Set flag + _is_opaque_part = true; + } else { + // the path component may be empty + _path = null; + } + } + setPath(tmp.substring(from, next)); + at = next; + } + + /** + * Parse the query component. + *

+         *  query     =  $7 = 
+         *                                        @@@@@@@@@
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + if (0 <= at && at+1 < length && tmp.charAt(at) == '?') { + int next = tmp.indexOf('#', at + 1); + if (next == -1) { + next = tmp.length(); + } + _query = (escaped) ? tmp.substring(at + 1, next).toCharArray() : + encode(tmp.substring(at + 1, next), allowed_query); + at = next; + } + + /** + * Parse the fragment component. + *

+         *  fragment  =  $9 = Related
+         *                                                   @@@@@@@@
+         *  ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+         * 

+ */ + if (0 <= at && at+1 < length && tmp.charAt(at) == '#') { + _fragment = (escaped) ? tmp.substring(at + 1).toCharArray() : + encode(tmp.substring(at + 1), allowed_fragment); + } + + // set this URI. + setUriReference(); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given string. + * + * @param s the string to be indexed + * @param delims the delimiters used to index + * @return the earlier index if there are delimiters + */ + protected int indexFirstOf(String s, String delims) { + return indexFirstOf(s, delims, -1); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given string. + * + * @param s the string to be indexed + * @param delims the delimiters used to index + * @param offset the from index + * @return the earlier index if there are delimiters + */ + protected int indexFirstOf(String s, String delims, int offset) { + if (s == null || s.length() == 0) { + return -1; + } + if (delims == null || delims.length() == 0) { + return -1; + } + // check boundaries + if (offset < 0) { + offset = 0; + } else if (offset > s.length()) { + return -1; + } + // s is never null + int min = s.length(); + char[] delim = delims.toCharArray(); + for (int i = 0; i < delim.length; i++) { + int at = s.indexOf(delim[i], offset); + if (at >= 0 && at < min) { + min = at; + } + } + return (min == s.length()) ? -1 : min; + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given array. + * + * @param s the character array to be indexed + * @param delim the delimiter used to index + * @return the ealier index if there are a delimiter + */ + protected int indexFirstOf(char[] s, char delim) { + return indexFirstOf(s, delim, 0); + } + + + /** + * Get the earlier index that to be searched for the first occurrance in + * one of any of the given array. + * + * @param s the character array to be indexed + * @param delim the delimiter used to index + * @return the ealier index if there is a delimiter + */ + protected int indexFirstOf(char[] s, char delim, int offset) { + if (s == null || s.length == 0) { + return -1; + } + // check boundaries + if (offset < 0) { + offset = 0; + } else if (offset > s.length) { + return -1; + } + for (int i = offset; i < s.length; i++) { + if (s[i] == delim) { + return i; + } + } + return -1; + } + + + /** + * Parse the authority component. + * + * @param original the original character sequence of authority component + * @param escaped true if original is escaped + * @exception IOException + */ + protected void parseAuthority(String original, boolean escaped) + throws IOException { + + // Reset flags + _is_reg_name = _is_server = + _is_hostname = _is_IPv4address = _is_IPv6reference = false; + + boolean has_port = true; + int from = 0; + int next = original.indexOf('@'); + if (next != -1) { // neither -1 and 0 + // each protocol extented from URI supports the specific userinfo + _userinfo = (escaped) ? original.substring(0, next).toCharArray() : + encode(original.substring(0, next), allowed_userinfo); + from = next + 1; + } + next = original.indexOf('[', from); + if (next >= from) { + next = original.indexOf(']', from); + if (next == -1) { + throw new IOException(/* IOException.PARSING,*/ "URI: IPv6reference"); + } else { + next++; + } + // In IPv6reference, '[', ']' should be excluded + _host = (escaped) ? original.substring(from, next).toCharArray() : + encode(original.substring(from, next), allowed_IPv6reference); + // Set flag + _is_IPv6reference = true; + } else { // only for !_is_IPv6reference + next = original.indexOf(':', from); + if (next == -1) { + next = original.length(); + has_port = false; + } + // REMINDME: it doesn't need the pre-validation + _host = original.substring(from, next).toCharArray(); + if (validate(_host, IPv4address)) { + // Set flag + _is_IPv4address = true; + } else if (validate(_host, hostname)) { + // Set flag + _is_hostname = true; + } else { + // Set flag + _is_reg_name = true; + } + } + if (_is_reg_name) { + // Reset flags for a server-based naming authority + _is_server = _is_hostname = _is_IPv4address = + _is_IPv6reference = false; + // set a registry-based naming authority + _authority = (escaped) ? original.toString().toCharArray() : + encode(original.toString(), allowed_reg_name); + } else { + if (original.length()-1 > next && has_port && + original.charAt(next) == ':') { // not empty + from = next + 1; + try { + _port = Integer.parseInt(original.substring(from)); + } catch (NumberFormatException error) { + throw new IOException(/*IOException.PARSING, */ + "URI: invalid port number"); + } + } + // set a server-based naming authority + StringBuffer buf = new StringBuffer(); + if (_userinfo != null) { // has_userinfo + buf.append(_userinfo); + buf.append('@'); + } + if (_host != null) { + buf.append(_host); + if (_port != -1) { + buf.append(':'); + buf.append(_port); + } + } + _authority = buf.toString().toCharArray(); + // Set flag + _is_server = true; + } + } + + + /** + * Once it's parsed successfully, set this URI. + * + * @see #getRawURI + */ + protected void setUriReference() { + // set _uri + StringBuffer buf = new StringBuffer(); + // ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + if (_scheme != null) { + buf.append(_scheme); + buf.append(':'); + } + if (_is_net_path) { + buf.append("//"); + if (_authority != null) { // has_authority + if (_userinfo != null) { // by default, remove userinfo part + if (_host != null) { + buf.append(_host); + if (_port != -1) { + buf.append(':'); + buf.append(_port); + } + } + } else { + buf.append(_authority); + } + } + } + if (_opaque != null && _is_opaque_part) { + buf.append(_opaque); + } else if (_path != null) { + // _is_hier_part or _is_relativeURI + if (_path.length != 0) { + buf.append(_path); + } + } + if (_query != null) { // has_query + buf.append('?'); + buf.append(_query); + } + if (_fragment != null) { // has_fragment + buf.append('#'); + buf.append(_fragment); + } + + _uri = buf.toString().toCharArray(); + } + + // ----------------------------------------------------------- Test methods + + + /** + * Tell whether or not this URI is absolute. + * + * @return true iif this URI is absoluteURI + */ + public boolean isAbsoluteURI() { + return (_scheme != null); + } + + + /** + * Tell whether or not this URI is relative. + * + * @return true iif this URI is relativeURI + */ + public boolean isRelativeURI() { + return (_scheme == null); + } + + + /** + * Tell whether or not the absoluteURI of this URI is hier_part. + * + * @return true iif the absoluteURI is hier_part + */ + public boolean isHierPart() { + return _is_hier_part; + } + + + /** + * Tell whether or not the absoluteURI of this URI is opaque_part. + * + * @return true iif the absoluteURI is opaque_part + */ + public boolean isOpaquePart() { + return _is_opaque_part; + } + + + /** + * Tell whether or not the relativeURI or heir_part of this URI is net_path. + * It's the same function as the has_authority() method. + * + * @return true iif the relativeURI or heir_part is net_path + * @see #hasAuthority + */ + public boolean isNetPath() { + return _is_net_path || (_authority != null); + } + + + /** + * Tell whether or not the relativeURI or hier_part of this URI is abs_path. + * + * @return true iif the relativeURI or hier_part is abs_path + */ + public boolean isAbsPath() { + return _is_abs_path; + } + + + /** + * Tell whether or not the relativeURI of this URI is rel_path. + * + * @return true iif the relativeURI is rel_path + */ + public boolean isRelPath() { + return _is_rel_path; + } + + + /** + * Tell whether or not this URI has authority. + * It's the same function as the is_net_path() method. + * + * @return true iif this URI has authority + * @see #isNetPath + */ + public boolean hasAuthority() { + return (_authority != null) || _is_net_path; + } + + /** + * Tell whether or not the authority component of this URI is reg_name. + * + * @return true iif the authority component is reg_name + */ + public boolean isRegName() { + return _is_reg_name; + } + + + /** + * Tell whether or not the authority component of this URI is server. + * + * @return true iif the authority component is server + */ + public boolean isServer() { + return _is_server; + } + + + /** + * Tell whether or not this URI has userinfo. + * + * @return true iif this URI has userinfo + */ + public boolean hasUserinfo() { + return (_userinfo != null); + } + + + /** + * Tell whether or not the host part of this URI is hostname. + * + * @return true iif the host part is hostname + */ + public boolean isHostname() { + return _is_hostname; + } + + + /** + * Tell whether or not the host part of this URI is IPv4address. + * + * @return true iif the host part is IPv4address + */ + public boolean isIPv4address() { + return _is_IPv4address; + } + + + /** + * Tell whether or not the host part of this URI is IPv6reference. + * + * @return true iif the host part is IPv6reference + */ + public boolean isIPv6reference() { + return _is_IPv6reference; + } + + + /** + * Tell whether or not this URI has query. + * + * @return true iif this URI has query + */ + public boolean hasQuery() { + return (_query != null); + } + + + /** + * Tell whether or not this URI has fragment. + * + * @return true iif this URI has fragment + */ + public boolean hasFragment() { + return (_fragment != null); + } + + + // ---------------------------------------------------------------- Charset + + + /** + * Set the default charset of the protocol. + *

+ * The character set used to store files SHALL remain a local decision and + * MAY depend on the capability of local operating systems. Prior to the + * exchange of URIs they SHOULD be converted into a ISO/IEC 10646 format + * and UTF-8 encoded. This approach, while allowing international exchange + * of URIs, will still allow backward compatibility with older systems + * because the code set positions for ASCII characters are identical to the + * one byte sequence in UTF-8. + *

+ * An individual URI scheme may require a single charset, define a default + * charset, or provide a way to indicate the charset used. + * + * @param charset the default charset for each protocol + */ + public static void setProtocolCharset(String charset) { + _protocolCharset = charset; + } + + + /** + * Get the default charset of the protocol. + *

+ * An individual URI scheme may require a single charset, define a default + * charset, or provide a way to indicate the charset used. + *

+ * To work globally either requires support of a number of character sets + * and to be able to convert between them, or the use of a single preferred + * character set. + * For support of global compatibility it is STRONGLY RECOMMENDED that + * clients and servers use UTF-8 encoding when exchanging URIs. + * + * @return the charset string + */ + public static String getProtocolCharset() { + return _protocolCharset; + } + + + /** + * Set the default charset of the document. + *

+ * Notice that it will be possible to contain mixed characters (e.g. + * ftp://host/KoreanNamespace/ChineseResource). To handle the Bi-directional + * display of these character sets, the protocol charset could be simply + * used again. Because it's not yet implemented that the insertion of BIDI + * control characters at different points during composition is extracted. + * + * @param charset the default charset for the document + */ + public static void setDocumentCharset(String charset) { + _documentCharset = charset; + } + + + /** + * Get the default charset of the document. + * + * @return the charset string + */ + public static String getDocumentCharset() { + return _documentCharset; + } + + // ------------------------------------------------------------- The scheme + + /** + * Get the scheme. + * + * @return the scheme + */ + public char[] getRawScheme() { + return _scheme; + } + + + /** + * Get the scheme. + * + * @return the scheme + * null if undefined scheme + */ + public String getScheme() { + return (_scheme == null) ? null : new String(_scheme); + } + + // ---------------------------------------------------------- The authority + + /** + * Set the authority. It can be one type of server, hostport, hostname, + * IPv4address, IPv6reference and reg_name. + *

+     *   authority     = server | reg_name
+     * 

+ * + * @param escapedAuthority the raw escaped authority + * @exception IOException + * @throws NullPointerException null authority + */ + public void setRawAuthority(char[] escapedAuthority) throws IOException { + parseAuthority(new String(escapedAuthority), true); + setUriReference(); + } + + + /** + * Set the authority. It can be one type of server, hostport, hostname, + * IPv4address, IPv6reference and reg_name. + * Note that there is no setAuthority method by the escape encoding reason. + * + * @param escapedAuthority the escaped authority string + * @exception IOException + */ + public void setEscapedAuthority(String escapedAuthority) + throws IOException { + + parseAuthority(escapedAuthority, true); + setUriReference(); + } + + + /** + * Get the raw-escaped authority. + * + * @return the raw-escaped authority + */ + public char[] getRawAuthority() { + return _authority; + } + + + /** + * Get the escaped authority. + * + * @return the escaped authority + */ + public String getEscapedAuthority() { + return (_authority == null) ? null : new String(_authority); + } + + + /** + * Get the authority. + * + * @return the authority + * @exception IOException + * @see #decode + */ + public String getAuthority() throws IOException { + return (_authority == null) ? null : decode(_authority); + } + + // ----------------------------------------------------------- The userinfo + + /** + * Get the raw-escaped userinfo. + * + * @return the raw-escaped userinfo + * @see #getAuthority + */ + public char[] getRawUserinfo() { + return _userinfo; + } + + + /** + * Get the escaped userinfo. + * + * @return the escaped userinfo + * @see #getAuthority + */ + public String getEscapedUserinfo() { + return (_userinfo == null) ? null : new String(_userinfo); + } + + + /** + * Get the userinfo. + * + * @return the userinfo + * @exception IOException + * @see #decode + * @see #getAuthority + */ + public String getUserinfo() throws IOException { + return (_userinfo == null) ? null : decode(_userinfo); + } + + // --------------------------------------------------------------- The host + + /** + * Get the host. + *

+     *   host          = hostname | IPv4address | IPv6reference
+     * 

+ * + * @return the host + * @see #getAuthority + */ + public char[] getRawHost() { + return _host; + } + + + /** + * Get the host. + *

+     *   host          = hostname | IPv4address | IPv6reference
+     * 

+ * + * @return the host + * @exception IOException + * @see #decode + * @see #getAuthority + */ + public String getHost() throws IOException { + return decode(_host); + } + + // --------------------------------------------------------------- The port + + /** + * Get the port. In order to get the specfic default port, the specific + * protocol-supported class extended from the URI class should be used. + * It has the server-based naming authority. + * + * @return the port + * if -1, it has the default port for the scheme or the server-based + * naming authority is not supported in the specific URI. + */ + public int getPort() { + return _port; + } + + // --------------------------------------------------------------- The path + + /** + * Set the path. The method couldn't be used by API programmers. + * + * @param path the path string + * @exception IOException set incorrectly or fragment only + * @see #encode + */ + protected void setPath(String path) throws IOException { + + // set path + if (_is_net_path || _is_abs_path) { + _path = encode(path, allowed_abs_path); + } else if (_is_rel_path) { + StringBuffer buff = new StringBuffer(path.length()); + int at = path.indexOf('/'); + if (at > 0) { // never 0 + buff.append(encode(path.substring(0, at), allowed_rel_path)); + buff.append(encode(path.substring(at), allowed_abs_path)); + } else { + buff.append(encode(path, allowed_rel_path)); + } + _path = buff.toString().toCharArray(); + } else if (_is_opaque_part) { + _opaque = encode(path, allowed_opaque_part); + } else { + throw new IOException(/*IOException.PARSING, */"URI: incorrect path"); + } + } + + + /** + * Resolve the base and relative path. + * + * @param base_path a character array of the base_path + * @param rel_path a character array of the rel_path + * @return the resolved path + */ + protected char[] resolvePath(char[] base_path, char[] rel_path) { + + // REMINDME: paths are never null + String base = (base_path == null) ? "" : new String(base_path); + int at = base.lastIndexOf('/'); + if (at != -1) { + base_path = base.substring(0, at + 1).toCharArray(); + } + // _path could be empty + if (rel_path == null || rel_path.length == 0) { + return normalize(base_path); + } else if (rel_path[0] == '/') { + return rel_path; + } else { + StringBuffer buff = new StringBuffer(base.length() + + rel_path.length); + if (at != -1) { + buff.append(base.substring(0, at + 1)); + buff.append(rel_path); + } + return normalize(buff.toString().toCharArray()); + } + } + + + /** + * Get the raw-escaped current hierarchy level in the given path. + * If the last namespace is a collection, the slash mark ('/') should be + * ended with at the last character of the path string. + * + * @param path the path + * @return the current hierarchy level + * @exception IOException no hierarchy level + */ + protected char[] getRawCurrentHierPath(char[] path) throws IOException { + + if (_is_opaque_part) { + throw new IOException(/*IOException.PARSING,*/ "URI: no hierarchy level"); + } + if (path == null) { + throw new IOException(/*IOException.PARSING,*/ "URI: emtpy path"); + } + String buff = new String(path); + int first = buff.indexOf('/'); + int last = buff.lastIndexOf('/'); + if (last == 0) { + return rootPath; + } else if (first != last && last != -1) { + return buff.substring(0, last).toCharArray(); + } + // FIXME: it could be a document on the server side + return path; + } + + + /** + * Get the raw-escaped current hierarchy level. + * + * @return the raw-escaped current hierarchy level + * @exception IOException no hierarchy level + */ + public char[] getRawCurrentHierPath() throws IOException { + return (_path == null) ? null : getRawCurrentHierPath(_path); + } + + + /** + * Get the escaped current hierarchy level. + * + * @return the escaped current hierarchy level + * @exception IOException no hierarchy level + */ + public String getEscapedCurrentHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the current hierarchy level. + * + * @return the current hierarchy level + * @exception IOException + * @see #decode + */ + public String getCurrentHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the raw above hierarchy level + * @exception IOException + */ + public char[] getRawAboveHierPath() throws IOException { + char[] path = getRawCurrentHierPath(); + return (path == null) ? null : getRawCurrentHierPath(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the raw above hierarchy level + * @exception IOException + */ + public String getEscapedAboveHierPath() throws IOException { + char[] path = getRawAboveHierPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the level above the this hierarchy level. + * + * @return the above hierarchy level + * @exception IOException + * @see #decode + */ + public String getAboveHierPath() throws IOException { + char[] path = getRawAboveHierPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the raw-escaped path. + *

+     *   path          = [ abs_path | opaque_part ]
+     * 

+ * + * @return the raw-escaped path + */ + public char[] getRawPath() { + return _is_opaque_part ? _opaque : _path; + } + + + /** + * Get the escaped path. + *

+     *   path          = [ abs_path | opaque_part ]
+     *   abs_path      = "/"  path_segments 
+     *   opaque_part   = uric_no_slash *uric
+     * 

+ * + * @return the escaped path string + */ + public String getEscapedPath() { + char[] path = getRawPath(); + return (path == null) ? null : new String(path); + } + + + /** + * Get the path. + *

+     *   path          = [ abs_path | opaque_part ]
+     * 

+ * @return the path string + * @exception IOException + * @see #decode + */ + public String getPath() throws IOException { + char[] path = getRawPath(); + return (path == null) ? null : decode(path); + } + + + /** + * Get the raw-escaped basename of the path. + * + * @return the raw-escaped basename + */ + public char[] getRawName() { + if (_path == null) return null; + + int at = 0; + for (int i = _path.length - 1; i >= 0; i--) { + if (_path[i] == '/') { + at = i + 1; + break; + } + } + int len = _path.length - at; + char[] basename = new char[len]; + System.arraycopy(_path, at, basename, 0, len); + return basename; + } + + + /** + * Get the escaped basename of the path. + * + * @return the escaped basename string + */ + public String getEscapedName() { + char[] basename = getRawName(); + return (basename == null) ? null : new String(basename); + } + + + /** + * Get the basename of the path. + * + * @return the basename string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getName() throws IOException { + char[] basename = getRawName(); + return (basename == null) ? null : decode(getRawName()); + } + + // ----------------------------------------------------- The path and query + + /** + * Get the raw-escaped path and query. + * + * @return the raw-escaped path and query + */ + public char[] getRawPathQuery() { + + if (_path == null && _query == null) { + return null; + } + StringBuffer buff = new StringBuffer(); + if (_path != null) { + buff.append(_path); + } + if (_query != null) { + buff.append('?'); + buff.append(_query); + } + return buff.toString().toCharArray(); + } + + + /** + * Get the escaped query. + * + * @return the escaped path and query string + */ + public String getEscapedPathQuery() { + char[] rawPathQuery = getRawPathQuery(); + return (rawPathQuery == null) ? null : new String(rawPathQuery); + } + + + /** + * Get the path and query. + * + * @return the path and query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getPathQuery() throws IOException { + char[] rawPathQuery = getRawPathQuery(); + return (rawPathQuery == null) ? null : decode(rawPathQuery); + } + + // -------------------------------------------------------------- The query + + /** + * Set the raw-escaped query. + * + * @param escapedQuery the raw-escaped query + * @exception IOException escaped query not valid + * @throws NullPointerException null query + */ + public void setRawQuery(char[] escapedQuery) throws IOException { + if (!validate(escapedQuery, query)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped query not valid"); + _query = escapedQuery; + setUriReference(); + } + + + /** + * Set the escaped query string. + * + * @param escapedQuery the escaped query string + * @exception IOException escaped query not valid + * @throws NullPointerException null query + */ + public void setEscapedQuery(String escapedQuery) throws IOException { + setRawQuery(escapedQuery.toCharArray()); + } + + + /** + * Set the query. + * When a query string is not misunderstood the reserved special characters + * ("&", "=", "+", ",", and "$") within a query component, it is + * recommended to use in encoding the whole query with this method. + * + * @param query the query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @throws NullPointerException null query + * @see #encode + */ + public void setQuery(String query) throws IOException { + setRawQuery(encode(query, allowed_query)); + } + + + /** + * Get the raw-escaped query. + * + * @return the raw-escaped query + */ + public char[] getRawQuery() { + return _query; + } + + + /** + * Get the escaped query. + * + * @return the escaped query string + */ + public String getEscapedQuery() { + return (_query == null) ? null : new String(_query); + } + + + /** + * Get the query. + * + * @return the query string. + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getQuery() throws IOException { + return (_query == null) ? null : decode(_query); + } + + // ----------------------------------------------------------- The fragment + + /** + * Set the raw-escaped fragment. + * + * @param escapedFragment the raw-escaped fragment + * @exception IOException escaped fragment not valid + * @throws NullPointerException null fragment + */ + public void setRawFragment(char[] escapedFragment) throws IOException { + if (!validate(escapedFragment, fragment)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped fragment not valid"); + _fragment = escapedFragment; + setUriReference(); + } + + + /** + * Set the escaped fragment string. + * + * @param escapedFragment the escaped fragment string + * @exception IOException escaped fragment not valid + * @throws NullPointerException null fragment + */ + public void setEscapedFragment(String escapedFragment) throws IOException { + char[] fragmentSequence = escapedFragment.toCharArray(); + if (!validate(fragmentSequence, fragment)) + throw new IOException(/*IOException.ESCAPING,*/ + "URI: escaped fragment not valid"); + _fragment = fragmentSequence; + setUriReference(); + } + + + /** + * Set the fragment. + * + * @param the fragment string. + * @exception IOException + * Or unsupported character encoding + * @throws NullPointerException null fragment + */ + public void setFragment(String fragment) throws IOException { + _fragment = encode(fragment, allowed_fragment); + setUriReference(); + } + + + /** + * Get the raw-escaped fragment. + *

+ * The optional fragment identifier is not part of a URI, but is often used + * in conjunction with a URI. + *

+ * The format and interpretation of fragment identifiers is dependent on + * the media type [RFC2046] of the retrieval result. + *

+ * A fragment identifier is only meaningful when a URI reference is + * intended for retrieval and the result of that retrieval is a document + * for which the identified fragment is consistently defined. + * + * @return the raw-escaped fragment + */ + public char[] getRawFragment() { + return _fragment; + } + + + /** + * Get the escaped fragment. + * + * @return the escaped fragment string + */ + public String getEscapedFragment() { + return (_fragment == null) ? null : new String(_fragment); + } + + + /** + * Get the fragment. + * + * @return the fragment string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getFragment() throws IOException { + return (_fragment == null) ? null : decode(_fragment); + } + + // ------------------------------------------------------------- Utilities + + /** + * Normalize the given hier path part. + * + * @param path the path to normalize + * @return the normalized path + */ + protected char[] normalize(char[] path) { + + if (path == null) return null; + + String normalized = new String(path); + boolean endsWithSlash = true; + // precondition + if (!normalized.endsWith("/")) { + normalized += '/'; + endsWithSlash = false; + } + if (normalized.endsWith("/./") || normalized.endsWith("/../")) { + endsWithSlash = true; + } + // Resolve occurrences of "/./" in the normalized path + while (true) { + int at = normalized.indexOf("/./"); + if (at == -1) { + break; + } + normalized = normalized.substring(0, at) + + normalized.substring(at + 2); + } + // Resolve occurrences of "/../" in the normalized path + while (true) { + int at = normalized.indexOf("/../"); + if (at == -1) { + break; + } + if (at == 0) { + normalized = "/"; + break; + } + int backward = normalized.lastIndexOf('/', at - 1); + if (backward == -1) { + // consider the rel_path + normalized = normalized.substring(at + 4); + } else { + normalized = normalized.substring(0, backward) + + normalized.substring(at + 3); + } + } + // Resolve occurrences of "//" in the normalized path + while (true) { + int at = normalized.indexOf("//"); + if (at == -1) { + break; + } + normalized = normalized.substring(0, at) + + normalized.substring(at + 1); + } + if (!endsWithSlash && normalized.endsWith("/")) { + normalized = normalized.substring(0, normalized.length()-1); + } else if (endsWithSlash && !normalized.endsWith("/")) { + normalized = normalized + "/"; + } + // Set the normalized path that we have completed + return normalized.toCharArray(); + } + + + /** + * Normalize the path part of this URI. + */ + public void normalize() { + _path = normalize(_path); + } + + + /** + * Test if the first array is equal to the second array. + * + * @param first the first character array + * @param second the second character array + * @return true if they're equal + */ + protected boolean equals(char[] first, char[] second) { + + if (first == null && second == null) { + return true; + } + if (first == null || second == null) { + return false; + } + if (first.length != second.length) { + return false; + } + for (int i = 0; i < first.length; i++) { + if (first[i] != second[i]) { + return false; + } + } + return true; + } + + + /** + * Test an object if this URI is equal to another. + * + * @param obj an object to compare + * @return true if two URI objects are equal + */ + public boolean equals(Object obj) { + + // normalize and test each components + if (obj == this) { + return true; + } + if (!(obj instanceof URI)) { + return false; + } + URI another = (URI) obj; + // scheme + if (!equals(_scheme, another._scheme)) { + return false; + } + // is_opaque_part or is_hier_part? and opaque + if (!equals(_opaque, another._opaque)) { + return false; + } + // is_hier_part + // has_authority + if (!equals(_authority, another._authority)) { + return false; + } + // path + if (!equals(_path, another._path)) { + return false; + } + // has_query + if (!equals(_query, another._query)) { + return false; + } + // has_fragment? should be careful of the only fragment case. + if (!equals(_fragment, another._fragment)) { + return false; + } + return true; + } + + // ---------------------------------------------------------- Serialization + + /** + * Write the content of this URI. + * + * @param oos the object-output stream + */ + protected void writeObject(java.io.ObjectOutputStream oos) + throws IOException { + + oos.defaultWriteObject(); + } + + + /** + * Read a URI. + * + * @param ois the object-input stream + */ + protected void readObject(java.io.ObjectInputStream ois) + throws ClassNotFoundException, IOException { + + ois.defaultReadObject(); + } + + // ------------------------------------------------------------- Comparison + + /** + * Compare this URI to another object. + * + * @param obj the object to be compared. + * @return 0, if it's same, + * -1, if failed, first being compared with in the authority component + * @exception ClassCastException not URI argument + * @throws NullPointerException null object + */ + public int compareTo(Object obj) { + + URI another = (URI) obj; + if (!equals(_authority, another.getRawAuthority())) return -1; + return toString().compareTo(another.toString()); + } + + // ------------------------------------------------------------------ Clone + + /** + * Create and return a copy of this object, the URI-reference containing + * the userinfo component. Notice that the whole URI-reference including + * the userinfo component counld not be gotten as a String. + *

+ * To copy the identical URI object including the userinfo + * component, it should be used. + * + * @return a clone of this instance + */ + public synchronized Object clone() { + + URI instance = new URI(); + + instance._uri = _uri; + instance._scheme = _scheme; + instance._opaque = _opaque; + instance._authority = _authority; + instance._userinfo = _userinfo; + instance._host = _host; + instance._port = _port; + instance._path = _path; + instance._query = _query; + instance._fragment = _fragment; + // flags + instance._is_hier_part = _is_hier_part; + instance._is_opaque_part = _is_opaque_part; + instance._is_net_path = _is_net_path; + instance._is_abs_path = _is_abs_path; + instance._is_rel_path = _is_rel_path; + instance._is_reg_name = _is_reg_name; + instance._is_server = _is_server; + instance._is_hostname = _is_hostname; + instance._is_IPv4address = _is_IPv4address; + instance._is_IPv6reference = _is_IPv6reference; + + return instance; + } + + // ------------------------------------------------------------ Get the URI + + /** + * It can be gotten the URI character sequence. It's raw-escaped. + * For the purpose of the protocol to be transported, it will be useful. + *

+ * It is clearly unwise to use a URL that contains a password which is + * intended to be secret. In particular, the use of a password within + * the 'userinfo' component of a URL is strongly disrecommended except + * in those rare cases where the 'password' parameter is intended to be + * public. + *

+ * When you want to get each part of the userinfo, you need to use the + * specific methods in the specific URL. It depends on the specific URL. + * + * @return URI character sequence + */ + public char[] getRawURI() { + return _uri; + } + + + /** + * It can be gotten the URI character sequence. It's escaped. + * For the purpose of the protocol to be transported, it will be useful. + * + * @return the URI string + */ + public String getEscapedURI() { + return (_uri == null) ? null : new String(_uri); + } + + + /** + * It can be gotten the URI character sequence. + * + * @return the URI string + * @exception IOException incomplete trailing escape pattern + * Or unsupported character encoding + * @see #decode + */ + public String getURI() throws IOException { + return (_uri == null) ? null : decode(_uri); + } + + + /** + * Get the escaped URI string. + *

+ * On the document, the URI-reference form is only used without the userinfo + * component like http://jakarta.apache.org/ by the security reason. + * But the URI-reference form with the userinfo component could be parsed. + *

+ * In other words, this URI and any its subclasses must not expose the + * URI-reference expression with the userinfo component like + * http://user:password@hostport/restricted_zone.
+ * It means that the API client programmer should extract each user and + * password to access manually. Probably it will be supported in the each + * subclass, however, not a whole URI-reference expression. + * + * @return the URI string + * @see #clone() + */ + public String toString() { + return getEscapedURI(); + } + + + // ------------------------------------------------------------ Inner class + + /** + * A mapping to determine the (somewhat arbitrarily) preferred charset for + * a given locale. Supports all locales recognized in JDK 1.1. + *

+ * The distribution of this class is Servlets.com. It was originally + * written by Jason Hunter [jhunter at acm.org] and used by with permission. + */ + public static class LocaleToCharsetMap { + + private static Hashtable map; + static { + map = new Hashtable(); + map.put("ar", "ISO-8859-6"); + map.put("be", "ISO-8859-5"); + map.put("bg", "ISO-8859-5"); + map.put("ca", "ISO-8859-1"); + map.put("cs", "ISO-8859-2"); + map.put("da", "ISO-8859-1"); + map.put("de", "ISO-8859-1"); + map.put("el", "ISO-8859-7"); + map.put("en", "ISO-8859-1"); + map.put("es", "ISO-8859-1"); + map.put("et", "ISO-8859-1"); + map.put("fi", "ISO-8859-1"); + map.put("fr", "ISO-8859-1"); + map.put("hr", "ISO-8859-2"); + map.put("hu", "ISO-8859-2"); + map.put("is", "ISO-8859-1"); + map.put("it", "ISO-8859-1"); + map.put("iw", "ISO-8859-8"); + map.put("ja", "Shift_JIS"); + map.put("ko", "EUC-KR"); + map.put("lt", "ISO-8859-2"); + map.put("lv", "ISO-8859-2"); + map.put("mk", "ISO-8859-5"); + map.put("nl", "ISO-8859-1"); + map.put("no", "ISO-8859-1"); + map.put("pl", "ISO-8859-2"); + map.put("pt", "ISO-8859-1"); + map.put("ro", "ISO-8859-2"); + map.put("ru", "ISO-8859-5"); + map.put("sh", "ISO-8859-5"); + map.put("sk", "ISO-8859-2"); + map.put("sl", "ISO-8859-2"); + map.put("sq", "ISO-8859-2"); + map.put("sr", "ISO-8859-5"); + map.put("sv", "ISO-8859-1"); + map.put("tr", "ISO-8859-9"); + map.put("uk", "ISO-8859-5"); + map.put("zh", "GB2312"); + map.put("zh_TW", "Big5"); + } + + /** + * Get the preferred charset for the given locale. + * + * @param locale the locale + * @return the preferred charset + * or null if the locale is not recognized + */ + public static String getCharset(Locale locale) { + // try for an full name match (may include country) + String charset = (String) map.get(locale.toString()); + if (charset != null) return charset; + + // if a full name didn't match, try just the language + charset = (String) map.get(locale.getLanguage()); + return charset; // may be null + } + + } + +} + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java new file mode 100644 index 0000000..377345c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionResults.java @@ -0,0 +1,51 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +/** +* An interface defining the results of a DirectAction. +* Implemented by both WOResponse and WOComponent so both +* can be returned from a DirectAction. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public interface WOActionResults +{ +/** +* Returns a response object as appropriate for the target. +*/ + WOResponse generateResponse (); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:52:44 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java new file mode 100644 index 0000000..78191b6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActionURL.java @@ -0,0 +1,57 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** + * This dynamic element renders only the URL of a hyperlink. + * Bindings are: + *

    + *
  • href: The URL that the hyperlink should point to.
  • + *
  • pageName: The name of the WOComponent that the hyperlink should point to.
  • + *
  • directActionName: The name of the direct action to call when the link is activated.
  • + *
  • actionClass: The name of the WODirectAction subclass where the direct action resides.
  • + *
  • action: A pointer to a method on the component that contains this element. If the link is activated, + * the method will be called. + *
  • ref: The name of the anchor to go to inside the resulting page.
  • + *
+ * + * The href, pageName and directActionName/actionClass and name properties are mutually exclusive and you should + * only use at most one of them simultaneously. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOActionURL extends WOHyperlink { + + public WOActionURL() { + super(); + } + + public WOActionURL(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(actionURL(c)); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java new file mode 100644 index 0000000..cad6f64 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOActiveImage.java @@ -0,0 +1,81 @@ + +package net.wotonomy.web; + +import java.util.Enumeration; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** + * WOActiveImage renders a dynamically generated IMG tag, enclosed in a hyperlink. + * Internally, it uses a WOImage and a WOHyperlink to do the actual work. + * + * The bindings are those of WOImage and WOHyperlink combined. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOActiveImage extends WODynamicElement { + + protected WOActiveImage() { + super(); + } + + public WOActiveImage(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void appendToResponse(WOResponse r, WOContext c) { + NSMutableDictionary atribs = new NSMutableDictionary(5); + if (associations.objectForKey("mimeType") != null) + atribs.setObjectForKey(associations.objectForKey("mimeType"), "mimeType"); + if (associations.objectForKey("data") != null) + atribs.setObjectForKey(associations.objectForKey("data"), "data"); + if (associations.objectForKey("src") != null) + atribs.setObjectForKey(associations.objectForKey("src"), "src"); + if (associations.objectForKey("framework") != null) + atribs.setObjectForKey(associations.objectForKey("framework"), "framework"); + if (associations.objectForKey("filename") != null) + atribs.setObjectForKey(associations.objectForKey("filename"), "filename"); + if (associations.objectForKey("alt") != null) + atribs.setObjectForKey(associations.objectForKey("alt"), "alt"); + if (associations.objectForKey("border") != null) + atribs.setObjectForKey(associations.objectForKey("border"), "border"); + if (associations.objectForKey("width") != null) + atribs.setObjectForKey(associations.objectForKey("width"), "width"); + if (associations.objectForKey("height") != null) + atribs.setObjectForKey(associations.objectForKey("height"), "height"); + WODynamicElement img = new WOImage("WOImage_" + name, atribs, null); + NSMutableDictionary uf = new NSMutableDictionary(); + Enumeration enumeration = associations.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (key.startsWith("?")) + uf.setObjectForKey(associations.objectForKey(key), key); + } + createLink(img).appendToResponse(r, c); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + return createLink(null).invokeAction(r, c); + } + + protected WOHyperlink createLink(WOElement e) { + NSMutableDictionary atribs = new NSMutableDictionary(5); + if (associations.objectForKey("href") != null) + atribs.setObjectForKey(associations.objectForKey("href"), "href"); + if (associations.objectForKey("pageName") != null) + atribs.setObjectForKey(associations.objectForKey("pageName"), "pageName"); + if (associations.objectForKey("action") != null) + atribs.setObjectForKey(associations.objectForKey("action"), "action"); + if (associations.objectForKey("directActionName") != null) + atribs.setObjectForKey(associations.objectForKey("directActionName"), "directActionName"); + if (associations.objectForKey("actionClass") != null) + atribs.setObjectForKey(associations.objectForKey("actionClass"), "actionClass"); + if (associations.objectForKey("target") != null) + atribs.setObjectForKey(associations.objectForKey("target"), "target"); + return new WOHyperlink(name, atribs, e); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java new file mode 100644 index 0000000..90e40c6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOApplication.java @@ -0,0 +1,1193 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.lang.reflect.Constructor; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.web.util.BrowserLauncher; + +import org.mortbay.http.HttpListener; +import org.mortbay.http.HttpServer; +import org.mortbay.jetty.servlet.ServletHandler; +import org.mortbay.util.InetAddrPort; + +/** +* A pure java implementation of WOApplication.

+* +* The application is responsible for creating and managing sessions +* and dispatching requests to the appropriate handlers.

+* +* This implementation extends HttpServlet, so the application itself +* is a servlet and can be configured and managed as such by the servlet +* container. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOApplication + extends HttpServlet +{ + /** + * A tricky way to allow multiple WOApplications + * in the same servlet container. + */ + static private ThreadLocal threadLocal; + //static private WOApplication application; + + /** + * Determines application-wide page caching. + * Pages may individually prevent caching. + */ + static private boolean cachingEnabled = false; + private static boolean autoOpenInBrowser = true; + + private String name; + private WORequestHandler defaultRequestHandler; + private NSMutableDictionary requestHandlers; + private WOSessionStore sessionStore; + private WOResourceManager resourceManager; + private boolean pageRefreshOnBacktrack; + private int pageCacheSize; + private int permanentPageCacheSize; + + public static final String WOApplicationWillFinishLaunchingNotification + = "WOApplicationWillFinishLaunchingNotification"; + public static final String WOApplicationDidFinishLaunchingNotification + = "WOApplicationDidFinishLaunchingNotification"; + public static final String WOGarbageCollectionPeriodKey + = "WOGarbageCollectionPeriodKey"; + + static String _DirectActionRequestHandlerKey = "_DirectActionRequestHandlerKey"; + static String _ComponentRequestHandlerKey = "_ComponentRequestHandlerKey"; + static String _ResourceRequestHandlerKey = "_ResourceRequestHandlerKey"; + static String WOPort = "WOPort"; + static String WOSMTPHost = "WOSMTPHost"; + static final String ELEMENT_CLASS = "elementClass"; + + public WOApplication () + { + if ( threadLocal == null ) + { + threadLocal = new ThreadLocal(); + } + threadLocal.set( this ); + + //application = this; + resourceManager = createResourceManager(); + requestHandlers = new NSMutableDictionary(); + defaultRequestHandler = new WODirectActionRequestHandler(); + registerRequestHandler( defaultRequestHandler, directActionRequestHandlerKey() ); + registerRequestHandler( new WOComponentRequestHandler(), componentRequestHandlerKey() ); + registerRequestHandler( new WOResourceRequestHandler(), resourceRequestHandlerKey() ); + sessionStore = WOSessionStore.serverSessionStore(); + + pageRefreshOnBacktrack = true; + pageCacheSize = 30; + permanentPageCacheSize = 30; + + threadLocal.set( null ); + } + + /** + * Dispatches the request and updates the specified response + * as appropriate. This implementation creates a new WORequest + * and WOContext from the request, sends the response to the + * appropriate WORequestHandler, and then updates the servlet + * response from the resulting WOResponse. + */ + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, java.io.IOException + { + threadLocal.set( this ); + + WORequest request = new WORequest( req, this ); + WOResponse response = dispatchRequest( request ); + response.generateServletResponse( resp ); + } + + /** + * Handles post requests by calling doGet(), since the framework + * handles both gets and posts similarly. Override to handle + * post requests in a different manner. + */ + protected void doPost(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, java.io.IOException + { + doGet( req, resp ); + } + + // obtaining attributes + + /** + * Returns the singleton instance of this application. + */ + public static WOApplication application() + { + return (WOApplication) threadLocal.get(); + //return application; + } + + /** + * Returns the name of the application. This implementation returns + * the name the jar file or directory from which the class was loaded, + * with no extensions. + */ + public String name() + { + if ( name == null ) + { + name = path(); + int i; + if ( name.endsWith( "/" ) ) + { // path + name = name.substring( 0, name.length() - 1 ); + } + else + { // jar file + i = name.lastIndexOf( '.' ); + if ( i != -1 ) name = name.substring( 0, i ); + } + i = name.lastIndexOf( '/' ); + if ( i != -1 ) name = name.substring( i+1 ); + } + return name; + } + + /** + * Returns the absolute path to the application on the local file system. + * Note that the application might be embedded inside of a jar file. + */ + public String path () + { + return getClass().getProtectionDomain().getCodeSource().getLocation().toString(); + } + + /** + * Returns the path to the application on the local file system + * relative to the server's document root. + */ + public String baseURL () + { + String root = getServletContext().getRealPath( "/" ); + String result = path(); + if ( result.endsWith("/") ) + { // path + if ( result.startsWith( root ) ) + { // relative to root + result = result.substring( root.length() ); + } + // else leave as absolute reference + } + // jar or war file: leave as absolute reference + return result; + } + + // concurrent request handling + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + * Subclasses may override to return false to force + * single-threaded request handling, although this + * is not implemented. + */ + public boolean allowsConcurrentRequestHandling () + { + return true; + } + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + */ + public boolean adaptorsDispatchRequestsConcurrently () + { + return true; + } + + /** + * Returns whether this application allows request + * to be handled concurrently. + * This implementation returns true. + */ + public boolean isConcurrentRequestHandlingEnabled () + { + return true; + } + + // handling requests + + /** + * Invoked first in the request-response cycle. + * Override to perform any kind of initialization at the + * start of a request. This implementation does nothing. + */ + public void awake () + { + + } + + /** + * Invoked to start the first phase of the request-response cycle, + * after all calls to awake() have been completed. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + aContext.session().takeValuesFromRequest( aRequest, aContext ); + } + + /** + * Invoked to start the second phase of the request-response cycle, + * after all calls to takeValuesFromRequest have finished. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return aContext.session().invokeAction( aRequest, aContext ); + } + + /** + * Invoked to start the third phase of the request-response cycle, + * after invokeAction() has completed and returned a WOResponse. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + aContext.session().appendToResponse( aResponse, aContext ); + } + + /** + * Invoked last in the request-response cycle. + * Override to perform any kind of clean-up at the + * end of a request. This implementation does nothing. + */ + public void sleep () + { + + } + + /** + * Dispatches the request to the appropriate handler. + */ + public WOResponse dispatchRequest (WORequest aRequest) + { + return handlerForRequest( aRequest ).handleRequest( aRequest ); + } + + // request handling + + /** + * Returns the default request handler used if the requested + * handler isn't specified or cannot be found. (This defaults + * to the WODirectActionRequestHandler.) + */ + public WORequestHandler defaultRequestHandler () + { + return defaultRequestHandler; + } + + /** + * Sets the default request handler used if the requested + * handler isn't specified or cannot be found. + */ + public void setDefaultRequestHandler (WORequestHandler aRequestHandler) + { + defaultRequestHandler = aRequestHandler; + } + + /** + * Registers the specified request handler for the specified key. + */ + public void registerRequestHandler (WORequestHandler aRequestHandler, String aKey) + { + requestHandlers.setObjectForKey( aRequestHandler, aKey ); + } + + /** + * Unregisters any existing request handler for the specified key + * returning the existing request handler, if any. + */ + public WORequestHandler removeRequestHandlerForKey (String aKey) + { + WORequestHandler result = requestHandlerForKey( aKey ); + requestHandlers.removeObjectForKey( aKey ); + return result; + } + + /** + * Returns the keys under which request handlers are registered. + */ + public NSArray registeredRequestHandlerKeys () + { + return requestHandlers.allKeys(); + } + + /** + * Returns the request handler registered for the specified key, + * or null if no request handler is registered for that key. + */ + public WORequestHandler requestHandlerForKey (String aKey) + { + return (WORequestHandler) requestHandlers.objectForKey( aKey ); + } + + /** + * Returns the request handler that would best service the specified request. + */ + public WORequestHandler handlerForRequest (WORequest aRequest) + { + WORequestHandler result = requestHandlerForKey( aRequest.requestHandlerKey() ); + if ( aRequest == null ) result = defaultRequestHandler(); + return result; + } + + // handling errors + + public WOResponse handleSessionCreationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to create session: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handleSessionRestorationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to restore session: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handlePageRestorationErrorInContext( WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + //TODO: add more useful information to the response + System.err.println( "Failed to restore page: " + aContext ); +new RuntimeException().printStackTrace(); // remove me + return response; + } + + public WOResponse handleException( Throwable aThrowable, WOContext aContext ) + { + WOResponse response = new WOResponse(); + response.setStatus( 500 ); // internal server error + System.err.println( "Exception occurred: " + aContext ); + if ( aThrowable.getMessage() != null ) + { + response.appendContentString( aThrowable.getMessage() ); + aThrowable.printStackTrace(); + } + else + { + response.appendContentString( aThrowable.toString() ); + aThrowable.printStackTrace(); + } + aThrowable.printStackTrace(); + return response; + } + + // managing pages + + /** + * Sets the number of pages that will be retained + * in the user's session. Set to zero to disable page caching. + */ + public void setPageCacheSize (int aPositiveInt) + { + pageCacheSize = aPositiveInt; + } + + /** + * Returns the number of pages that will be retained + * in the user's session. The default page cache size is 30. + */ + public int pageCacheSize () + { + return pageCacheSize; + } + + /** + * Returns the number of pages that will be retained in the + * longer-term "permanent" page cache in the user's session, + * which is typically used for navigation bars in frames, etc. + * The default permanent page cache size is 30. + */ + public int permanentPageCacheSize () + { + return permanentPageCacheSize; + } + + /** + * Returns the number of pages that will be retained in the + * longer-term "permanent" page cache in the user's session, + * which is typically used for navigation bars in frames, etc. + * Set to zero to disable permanent page caching. + */ + public void setPermanentPageCacheSize (int aPositiveInt) + { + permanentPageCacheSize = aPositiveInt; + } + + /** + * Returns whether a "backtrack" for an existing page should + * simply call generateResponse() on the existing page instance. + * If false, a new page is created instead. The default is true. + */ + public void setPageRefreshOnBacktrackEnabled (boolean enabled) + { + pageRefreshOnBacktrack = enabled; + } + + /** + * Returns whether a "backtrack" for an existing page should + * simply call generateResponse() on the existing page instance. + * If false, a new page is created instead. The default is true. + */ + public boolean isPageRefreshOnBacktrackEnabled () + { + return pageRefreshOnBacktrack; + } + + // managing sessions + + /** + * Sets the session store used by this application to persist + * sessions between request-response transactions. + */ + public void setSessionStore(WOSessionStore aSessionStore) + { + sessionStore = aSessionStore; + } + + /** + * Returns the session store used by this application to persist + * sessions between request-response transactions. + */ + public WOSessionStore sessionStore() + { + return sessionStore; + } + + /** + * Called at the end of the request-response cycle + * to persist the current session until the user's next request. + */ + public void saveSessionForContext(WOContext aContext) + { + sessionStore.saveSessionForContext( aContext ); + } + + /** + * Called at the beginning of the request-response cycle + * to obtain the current session from the user's last request. + * Returns null if no such session has been created. + * This method sets the context of the session to the specified context. + */ + public WOSession restoreSessionWithID(String aSessionID, WOContext aContext) + { + WORequest request = aContext.request(); + WOSession session = sessionStore.restoreSessionWithID( aSessionID, request ); + if ( session != null ) + { + session.setContext( aContext ); + session.setServletSession( request.servletRequest().getSession() ); + } + return session; + } + + /** + * Called to create a session for a new request. This implementation + * looks for a class in the same package as the application class + * called "Session" and failing that returns a WOSession. + */ + public WOSession createSessionForRequest(WORequest aRequest) + { + WOSession result = null; + try + { + // using our class loader, which is hopefully dynamic. + result = (WOSession) getLocalClass( "Session" ).newInstance(); + } + catch ( Throwable t ) + { + // ignore: fall back to WOSession + //t.printStackTrace(); + } + + if ( result == null ) + { + result = new WOSession(); + } + + result.setServletSession( aRequest.servletRequest().getSession( true ) ); + return result; + } + + /** + * Returns the page component with the specified name. + * A context is created with the specified request, + * along with a session if necessary. + */ + public WOComponent pageWithName (String aName, WORequest aRequest) + { + return pageWithName( aName, WOContext.contextWithRequest( aRequest ) ); + } + + /** + * Called to retrieve a component for the specified context. + */ + public WOComponent pageWithName (String aName, WOContext aContext) + { + if ( aName == null ) + { + throw new IllegalArgumentException( + "WOApplication.pageWithName: name is null" ); + } + + WOComponent result = null; + try + { + // using our class loader, which is hopefully dynamic. + Class c = getLocalClass( aName ); + + if ( c != null ) + { + // get constructor + Constructor ctor; + try + { + ctor = c.getConstructor( new Class[] { WOContext.class } ); + } + catch ( NoSuchMethodException nsme ) + { + ctor = null; + } + + // create instance of class + if ( ctor != null ) + { + result = (WOComponent) ctor.newInstance( new Object[] { aContext } ); + } + else // call back on default constructor (deprecated) + { + result = (WOComponent) c.newInstance(); + } + } + } + catch ( Throwable t ) + { + // ignore for now + //TODO: Throw appropriate exception here + //System.err.println( "Not found: pageWithName: " + aName ); + t.printStackTrace(); + } + + if ( result != null && aContext != null ) + { + // this is where components get their context + result.ensureAwakeInContext( aContext ); + } + else + if ( result == null ) + { + System.err.println( "Not found: pageWithName: " + aName ); + } + + return result; + } + + /** + * Returns a class in the same package as the Application class, + * or, failing that, from the WOApplication package, or finally + * from the root of the class path. Returns null if not found. + */ + Class getLocalClass( String aName ) + { + Class result = null; + if ( getClass() != WOApplication.class ) + { + result = loadLocalClass( getClass(), aName ); + } + if ( result == null ) + { + result = loadLocalClass( WOApplication.class, aName ); + } + if ( result == null ) + { + result = loadLocalClass( null, aName ); + } + return result; + } + + private static final Class loadLocalClass( Class aClass, String aName ) + { + ClassLoader loader; + String packageName = ""; + if ( aClass != null ) + { + loader = aClass.getClassLoader(); + packageName = aClass.getName(); + int index = packageName.lastIndexOf( "." ); + if ( index > -1 ) + { + packageName = packageName.substring( 0, index+1 ); + } + else + { + packageName = ""; + } + } + else + { + loader = WOApplication.class.getClassLoader(); + } + + try + { + return loader.loadClass( packageName + aName ); + } + catch ( ClassNotFoundException e ) + { + return null; + } + } + + // creating elements + + /** + * Returns either a dynamic element or a component + * for the specified name. + */ + public WOElement dynamicElementWithName( + String anElementName, NSDictionary anAssociationMap, + WOElement aBodyElement, List aLanguageList) + { + WOElement element = null; + Class c = null; + try + { + c = getLocalClass( anElementName ); + if ( c == null ) + { + System.out.println( "Not found: dynamicElementWithName: " + + "could not find WODynamicElement subclass: " + anElementName ); + c = WODynamicElement.class; + } + + // get constructor + Class[] params = new Class[] + { String.class, NSDictionary.class, WOElement.class }; + Constructor ctor = c.getConstructor( params ); + + // create instance of class + if ( ctor != null ) + { + element = (WODynamicElement) ctor.newInstance( + new Object[] { anElementName, anAssociationMap, aBodyElement } ); + } + } + catch ( Throwable t ) + { + // ignore: not a dynamic element + //System.out.println( "Not a dynamic element: dynamicElementWithName: " + t ); + //exc.printStackTrace(); + } + + // no dynamic element found: look for a component + if ( element == null ) + { + WOComponent component = (WOComponent) pageWithName( anElementName, (WOContext) null ); + + // this seems hackish: + // I don't see another way of setting the bindings. + component.associations = anAssociationMap; + component.rootElement = aBodyElement; + + element = component; + } + + return element; + } + + // resource handling + + /** + * Called to create the application's resource manager. + * Override to create a custom resource manager. + */ + public WOResourceManager createResourceManager() + { + return new WOResourceManager(); + } + + /** + * Returns the application's current resource manager. + */ + public WOResourceManager resourceManager() + { + return resourceManager; + } + + /** + * Installs a custom resource manager into the current application. + * @deprecated Override createResourceManager() instead. + */ + public void setResourceManager(WOResourceManager aResourceManager) + { + resourceManager = aResourceManager; + } + +/* + // request handling undocumented + + public WOComponent pageWithName (String); + public void savePage (WOComponent); + public WOComponent restorePageForContextID (String); + public WOContext context (); + public WOSession session (); + public WOSession createSession (); + public WOSession restoreSession (); + public void saveSession (WOSession); + + public WOResponse handleRequest (WORequest aRequest) + { + } + + // error handling undocumented + + WOResponse handleSessionCreationError (); + WOResponse handleSessionRestorationError (); + WOResponse handlePageRestorationError (); + WOResponse handleException (Throwable); + + // running + + public NSRunLoop runLoop (); + public void run (); + public void setTimeOut (double); + public double timeOut (); + public void terminate (); + public boolean isTerminating (); + + // script debugging + + public void traceScriptedMessages (boolean); + public void traceAssignments (boolean); + public void traceStatements (boolean); + public void traceObjectiveCMessages (boolean); + public void trace (boolean); + public void logTakeValueForDeclarationNamed (String, String, String, String, Object); + public void logSetValueForDeclarationNamed (String, String, String, String, Object); + + // script handling + + public String scriptedClassNameWithPath (String); + public String scriptedClassNameWithPathEncoding (String, int); + + // statistics report + + public void setStatisticsStore (WOStatisticsStore); + public WOStatisticsStore statisticsStore (); + public NSDictionary statistics (); + + // managing adaptors + + public WOAdaptor adaptorWithName (String, NSDictionary); + public NSArray adaptors (); + + // monitor support + + public boolean monitoringEnabled (); + public int activeSessionsCount (); + public void refuseNewSessions (boolean); + public boolean isRefusingNewSessions (); + public void setMinimumActiveSessionsCount (int); + public int minimumActiveSessionsCount (); + public void terminateAfterTimeInterval (double); + + // garbage collection undocumented + + int garbageCollectionPeriod (); + void setGarbageCollectionPeriod (int); + + // backwards compatibility + + public boolean requiresWOF35RequestHandling (); + public boolean requiresWOF35TemplateParser (); + + public void setPrintsHTMLParserDiagnostics (boolean); + public boolean printsHTMLParserDiagnostics (); + + // configuration and defaults + + public static NSArray loadFrameworks (); + public static void setLoadFrameworks (NSArray); +*/ + static boolean debuggingEnabled = false; + /** + * Returns whether the application is in "debug mode". + */ + public static boolean isDebuggingEnabled() + { + return debuggingEnabled; + } + + /** + * Sets whether the application is in "debug mode". + */ + public static void setDebuggingEnabled( boolean enabled ) + { + debuggingEnabled = enabled; + } + + /** + * Sets whether templates are cached. If true, templates will + * only be read once per application lifetime. Otherwise, templates + * will be read each time this class is instantiated. Defaults to false. + */ + public static void setCachingEnabled (boolean enabled) + { + cachingEnabled = enabled; + } + + /** + * Returns whether templates are cached. If true, templates are + * read once per application lifetime. Otherwise, templates are + * read each time this class is instantiated. + */ + public static boolean isCachingEnabled () + { + return cachingEnabled; + } + + // configuration + + /** + * Returns the component request handler key, + * which is defined by the system property _ComponentRequestHandlerKey. + * The default is "wo". + */ + public static String componentRequestHandlerKey() + { + return System.getProperty( _ComponentRequestHandlerKey, "wo" ); + } + + /** + * Sets the component request handler key. + * @deprecated Set the system property _ComponentRequestHandlerKey. + */ + public static void setComponentRequestHandlerKey(String aKey) + { + System.setProperty( _ComponentRequestHandlerKey, aKey ); + } + + /** + * Returns the direct action request handler key, + * which is defined by the system property _DirectActionRequestHandlerKey. + * The default is "wa". + */ + public static String directActionRequestHandlerKey() + { + return System.getProperty( _DirectActionRequestHandlerKey, "wa" ); + } + + /** + * Sets the direct action request handler key. + * @deprecated Set the system property _DirectActionRequestHandlerKey. + */ + public static void setDirectActionRequestHandlerKey(String aKey) + { + System.setProperty( _DirectActionRequestHandlerKey, aKey ); + } + + /** + * Returns the resource request handler key, + * which is defined by the system property _ResourceRequestHandlerKey. + * The default is "wr". + */ + public static String resourceRequestHandlerKey() + { + return System.getProperty( _ResourceRequestHandlerKey, "wr" ); + } + + /** + * Sets the resource request handler key. + * @deprecated Set the system property _ResourceRequestHandlerKey. + */ + public static void setResourceRequestHandlerKey(String aKey) + { + System.setProperty( _ResourceRequestHandlerKey, aKey ); + } + + /** + * Returns whether this application should attempt to open + * a web browser on the host machine when launched standalone. + * The default is true. + */ + public static boolean autoOpenInBrowser() + { + return autoOpenInBrowser; + } + + /** + * Sets whether this application should attempt to open + * a web browser on the host machine when launched standalone. + */ + public static void setAutoOpenInBrowser(boolean autoOpen) + { + autoOpenInBrowser = autoOpen; + } + + /** + * Gets the port used when run as a standalone server. + * Returns the value of the system property WOPort. + * By default, this is zero, which causes the application + * to automatically select an available port. + */ + public static Number port () + { + return Integer.getInteger( WOPort, 0 ); + } + + /** + * Gets the smtp server that will be used to send email. + * Returns the system property WOSMTPHost. + */ + public static String SMTPHost() + { + return System.getProperty( WOSMTPHost ); + } + + /** + * Sets the smtp server that will be used to send email. + * @deprecated Set the system property WOSMTPHost. + */ + public static void setSMTPHost( String aHost ) + { + System.setProperty( WOSMTPHost, aHost ); + } +/* + public static boolean isDirectConnectEnabled (); + public static void setDirectConnectEnabled (boolean); + public static String cgiAdaptorURL (); + public static void setCGIAdaptorURL (String); + public static String applicationBaseURL (); + public static void setApplicationBaseURL (String); + public static String frameworksBaseURL (); + public static void setFrameworksBaseURL (String); + public static String recordingPath (); + public static void setRecordingPath (String); + public static NSArray projectSearchPath (); + public static void setProjectSearchPath (NSArray); + public static boolean isMonitorEnabled (); + public static void setMonitorEnabled (boolean); + public static String monitorHost (); + public static String adaptor (); + public String number (); // deprecated + public static Number listenQueueSize (); + public static void setListenQueueSize (Number); + public static NSArray additionalAdaptors (); + public static void setAdditionalAdaptors (NSArray); + public static boolean includeCommentsInResponses (); + public static void setIncludeCommentsInResponses (boolean); + public static void setSessionTimeOut (Number); + public static Number sessionTimeOut (); + public static void logString (String); + public static void debugString (String); + public static void logToMonitorString (String); +*/ + + /** + * Main entry point for applications that do not subclass WOApplication. + */ + public static void main( String[] argv ) + { + main( argv, WOApplication.class ); + } + + /** + * Subclasses may call this method to start a self-hosted + * web server to serve themselves directly (for testing). + */ + public static void main( String[] argv, Class subclass ) + { + try + { + int port = 0; + boolean open = false; + try + { + port = ((Number)subclass.getMethod( "port", + new Class[0]).invoke(subclass,new Object[0])).intValue(); + open = ((Boolean)subclass.getMethod( "autoOpenInBrowser", + new Class[0]).invoke(subclass,new Object[0])).booleanValue(); + } + catch ( Throwable t ) + { + System.err.print("Error reading configuration:" ); + t.printStackTrace(); + } + + HttpServer server = new HttpServer(); + HttpListener listener = server.addListener(new InetAddrPort(port)); + org.mortbay.http.HttpContext context = server.getContext("/"); + ServletHandler handler = new ServletHandler(); + handler.addServlet("/",subclass.getName()); + context.addHandler(handler); + server.start(); + port = listener.getPort(); + System.out.println("Waiting for requests: http://127.0.0.1:" + port); + if ( open ) + { + BrowserLauncher.openURL( "http://127.0.0.1:" + port ); + } + } + catch ( Throwable t ) + { + t.printStackTrace(); + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.30 2003/03/28 18:01:19 mpowers + * Now defaulting port to zero. + * + * Revision 1.29 2003/03/28 17:31:58 mpowers + * Implemented support for autoselection of free port. (thanks gmuth!) + * + * Revision 1.28 2003/03/28 17:26:17 mpowers + * Implemented package support: Applications can now live in packages. + * Better support for locating package local classes. + * + * Revision 1.27 2003/02/21 16:40:22 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.26 2003/02/14 22:33:18 mpowers + * Better handling for standalone mode. + * + * Revision 1.25 2003/02/14 15:18:27 mpowers + * Now launching standalone app as a servlet, not a webapp. + * Disabled jetty's event logging. + * + * Revision 1.24 2003/02/13 22:41:04 mpowers + * WOApplications can now be self-serving. Added configuration params too. + * + * Revision 1.23 2003/01/28 19:33:51 mpowers + * Implemented the rest of WOResourceManager. + * Implemented support for java-style i18n. + * Components now use the resource manager to load templates. + * + * Revision 1.22 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * Revision 1.21 2003/01/24 20:13:22 mpowers + * Now accepting immutable NSDictionary in constructor, not Map. + * + * Revision 1.20 2003/01/20 17:50:11 mpowers + * Caught a loop condition when same declaration was used twice. + * + * Revision 1.19 2003/01/19 22:33:25 mpowers + * Fixed problems with classpath and dynamic class loading. + * Dynamic elements now pass on ensureAwakeInContext. + * Parser how handles tags. + * + * Revision 1.18 2003/01/18 23:54:50 mpowers + * Implemented debugging enabled. + * + * Revision 1.17 2003/01/17 20:58:18 mpowers + * Fixed up WOHyperlink. + * + * Revision 1.16 2003/01/17 20:34:17 mpowers + * Rudimentary support for resource requests. + * + * Revision 1.15 2003/01/17 15:31:56 mpowers + * Removed spurious error message. + * + * Revision 1.14 2003/01/17 14:39:00 mpowers + * Now calling preferred constructor: WOComponent(WOContext) + * + * Revision 1.13 2003/01/16 20:10:46 mpowers + * - components now synchronize bindings + * - support for WOComponentContent + * - implemented performParentAction + * + * Revision 1.12 2003/01/16 15:50:43 mpowers + * More robust declaration parsing. + * Subcomponents are now supported. + * dynamicElementWithName can now return subcomponents. + * + * Revision 1.11 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.10 2003/01/13 22:24:18 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.9 2003/01/10 20:17:41 mpowers + * Component action urls are now working. + * + * Revision 1.8 2003/01/10 19:16:40 mpowers + * Implemented support for page caching. + * + * Revision 1.4 2002/12/19 17:58:52 mpowers + * Rewrote the template parsing - no longer confused about "root element". + * + * Revision 1.3 2002/12/18 14:12:38 mpowers + * Support for differentiated request handlers. + * Support url generation for WOContext and WORequest. + * + * Revision 1.2 2002/12/17 14:57:41 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:52:50 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java new file mode 100644 index 0000000..608f9fa --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOAssociation.java @@ -0,0 +1,168 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +/** +* A pure java implementation of WOAssociation.

+* +* A WOAssociation represents the mapping of a property on a +* WOComponent to a property on a WOAssociation. For example:

+* +* MyAssociation: WOString { value = currentCustomer.location.city };

+* +* This example represents a WOAssociation between the value field +* on a WOString element and the city property of the location property +* of the currentCustomer property of a WOComponent.

+* +* To resolve values, a property accessor method will be used in +* preference to a public field, if both exist. Any null value +* in the path will produce null.

+* +* A mapping represented in quotation marks: { value = "This is a test." } +* is considered a constant value. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public class WOAssociation implements java.io.Serializable +{ + protected Object value; + protected String path; + + /** + * The default constructor. The static factory methods should + * be used to create instances of WOAssociation. + */ + protected WOAssociation () + { + value = null; + path = null; + } + + /** + * Creates a WOAssociation that maps to a constant value. + */ + public static WOAssociation associationWithValue (Object anObject) + { + WOAssociation result = new WOAssociation(); + result.value = anObject; + return result; + } + + /** + * Creates a WOAssociation that maps to the specified key path. + * If the path is null, the association will map to null. + * Throws an exception if the property cannot be resolved. + */ + public static WOAssociation associationWithKeyPath (String aString) + { + WOAssociation result = new WOAssociation(); + result.path = aString; + return result; + } + + /** + * Returns the value for this association's key path in the + * specified component, or null if any value in the path is + * null or if the key path is null. + */ + public Object valueInComponent (WOComponent aComponent) + { + if ( aComponent == null ) return null; + if ( value != null ) return value; + if ( path != null ) return aComponent.valueForKey( path ); + throw new RuntimeException( + "WOAssociation: neither value nor path specified!" ); + } + + /** + * Sets the property in the specified component to the specified value. + * Throws an exception if the property cannot be resolved. + */ + public void setValue (Object aValue, WOComponent aComponent) + { + if ( path != null ) + { + aComponent.takeValueForKey( aValue, path ); + return; + } + throw new RuntimeException( + "WOAssociation: tried to set value but no path was specified!" ); + } + + /** + * Returns true if this association is writable; that is, + * returns true if this association is not constant. + */ + public boolean isValueSettable () + { + return ( path != null ); + } + + /** + * Returns true if this association is constant + * and therefore read-only. + */ + public boolean isValueConstant () + { + return ( path == null ); + } + + /** + * For debugging purposes. + */ + public String toString() + { + if ( path != null ) + { + return "[WOAssociation:" + path + "]"; + } + return "[WOAssociation:\"" + value + "\"]"; + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/01/24 20:13:22 mpowers + * Now accepting immutable NSDictionary in constructor, not Map. + * + * Revision 1.5 2003/01/17 22:55:08 mpowers + * Straighted out the parent binding issue (I think). + * Fixes for woextensions compatibility. + * + * Revision 1.3 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.2 2003/01/14 15:51:48 mpowers + * Removed value() method from WOAssociaton. + * + * Revision 1.1.1.1 2000/12/21 15:52:50 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java new file mode 100644 index 0000000..9f0707d --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOBody.java @@ -0,0 +1,121 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOBody represents a page's "body" tag and is used to dynamically +* set the background image for a page, and therefore works much like WOImage. +* +* Bindings are: +*
    +*
  • src: A static URL for the image source.
  • +*
  • data: A NSData object with the image content. Must be used with mimeType.
  • +*
  • mimeType: The MIME type for the image data. Can be used with filename or data bindings.
  • +*
  • filename: The path to a file containing an image.
  • +*
  • framework: The optional framework from whence the image should be retrieved (used in conjunction with filename).
  • +*
  • key: A key under which this resource will be cached for repeated access.
  • +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOBody extends WOImage { + + protected String src; + protected String filename; + protected String framework; + protected NSData data; + protected String mimeType; + + protected WOBody() { + super(); + } + + public WOBody(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + void ensureAwakeInContext (WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.ensureAwakeInContext( aContext ); + } + } + + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.takeValuesFromRequest( aRequest, aContext ); + } + } + + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + return rootElement.invokeAction( aRequest, aContext ); + } + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) + { + r.appendContentString(""); + if ( rootElement != null ) + { + rootElement.appendToResponse( r, c ); + } + r.appendContentString(""); + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.4 2003/08/07 00:15:14 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.3 2003/01/27 15:57:28 mpowers + * Now participates in all parts of request/response cycle. + * + * Revision 1.2 2003/01/27 15:08:23 mpowers + * Now appending to response. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java new file mode 100644 index 0000000..5d22d36 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCheckBox.java @@ -0,0 +1,81 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +public class WOCheckBox extends WOInput { + + protected boolean checked = false; + + public WOCheckBox() { + super(); + } + + public WOCheckBox(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "CHECKBOX"; + } + + protected Object value(WOContext c) { + Object val = null; + boolean checked = false; + if (associations.objectForKey("value") != null) { + val = valueForProperty("value", c.component()); + Object sel = valueForProperty("selection", c.component()); + if (sel != null && val != null && sel.equals(val)) + checked = true; + } + if (val == null) { + val = c.elementID(); + } + return val; + } + + protected void appendExtras(WOResponse r, WOContext c) { + checked |= booleanForProperty("checked", c.component()); + if (checked) + r.appendContentString(" CHECKED"); + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.addObject("checked"); + a.addObject("selection"); + return a; + } + + public void appendToResponse(WOResponse r, WOContext c) { + checked = false; + super.appendToResponse(r, c); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if (disabled(c)) + return; + NSArray values = r.formValuesForKey(inputName(c)); + Object val = valueForProperty("value", c.component()); + if (val == null) + val = c.elementID(); + java.util.Enumeration enumerator = values.objectEnumerator(); + checked = false; + while (enumerator.hasMoreElements()) { + Object nextval = enumerator.nextElement(); + if (nextval.equals(val)) + checked = true; + } + if (associations.objectForKey("value") != null && associations.objectForKey("selection") != null) { + if (checked) + setValueForProperty("selection", val, c.component()); + else if (valueForProperty("selection", c.component()) != null) + setValueForProperty("selection", null, c.component()); + } + if (associations.objectForKey("checked") != null) + setValueForProperty("checked", checked ? Boolean.TRUE : Boolean.FALSE, c.component()); + } + +} \ No newline at end of file diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java new file mode 100644 index 0000000..20d8b0a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponent.java @@ -0,0 +1,1312 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.io.PushbackInputStream; +import java.io.StringReader; +import java.lang.reflect.InvocationTargetException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSSelector; + +/** +* Pure java implementation of WOComponent. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOComponent + extends WOElement + implements WOActionResults, + net.wotonomy.control.EOKeyValueCodingAdditions, + net.wotonomy.control.EOKeyValueCoding +{ + WOElement rootElement; + + private static final String DIRECTORY_SUFFIX = ".wo"; + private static final String TEMPLATE_SUFFIX = ".html"; + private static final String DECLARATION_SUFFIX = ".wod"; + + private static final String OPEN_TAG = "webobject"; + private static final String CLOSE_TAG = "/webobject"; + private static final String NAME_KEY = "name"; + + protected transient WOContext context; // don't persist + protected boolean cachingEnabled; + protected WOElement template; + protected WOComponent parent; + + /** + * Default constructor. Deprecated in latest spec. + */ + public WOComponent () + { + parent = null; + cachingEnabled = true; + template = null; + } + + /** + * Constructor specifying a context. + */ + public WOComponent( WOContext aContext ) + { + this(); + context = aContext; + } + + /** + * Returns the name of the component, which is usually just the class name. + */ + public String name () + { + return justTheClassName(); + } + + /** + * Returns the system-dependent file path to the current component + * directory, including the ".wo" extension. + */ + public String path () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns the URL for this component, relative to the server's + * document root on the server's file system. + * This is not an http url. + */ + public String baseURL () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns the name of the framework that contains this component, + * or null if the component does not belong to a framework. + * This currently returns the package path of the class, or + * null if it does not belong to a package. + */ + public String frameworkName () + { + return justTheResourcePath(); + } + + /** + * Sets whether templates are cached. If true, templates will + * only be read once per application lifetime. Otherwise, templates + * will be read each time this class is instantiated. Defaults to false. + */ + public void setCachingEnabled (boolean enabled) + { + cachingEnabled = enabled; + } + + /** + * Returns whether templates are cached. If true, templates are + * read once per application lifetime. Otherwise, templates are + * read each time this class is instantiated. + */ + public boolean isCachingEnabled () + { + return cachingEnabled && WOApplication.application().isCachingEnabled(); + } + + /** + * Returns the root of the tree of elements produced by parsing + * the templates in the component directory for this component. + */ + public WOElement template() + { + return template; + } + + /** + * Returns the root of the tree of elements produced by parsing + * the templates in the component directory for the named component. + * @deprecated Use template() instead. + */ + public WOElement templateWithName(String aComponentName) + { + return templateWithName( aComponentName, null ); + } + + /** + * Returns the root of the tree of elements produced by parsing + * the templates in the component directory for the named component. + */ + WOElement templateWithName(String aComponentName, String aFramework) + { + NSArray languages = null; + WOContext context = context(); + if ( context != null ) + { + languages = context.request().browserLanguages(); + } + WOElement result = templateWithHTMLString( + readTemplateResource( aComponentName, aFramework, TEMPLATE_SUFFIX, languages ), + readTemplateResource( aComponentName, aFramework, DECLARATION_SUFFIX, languages ), + languages ); + if ( result == null ) + { + System.out.println( "WOComponent.templateWithName: failed for " + aComponentName ); + } + return result; + } + + /** + * Returns the root of the tree of elements produced by parsing + * the specfified HTML string and bindings declaration string. + * Note: language list is currently ignored. + */ + public static WOElement templateWithHTMLString ( + String anHTMLString, String aDeclaration, List aLanguageList) + { + if ( anHTMLString == null ) return null; + WOElement result = null; + try + { + NSDictionary bindings = processDeclaration( aDeclaration ); + List elements = new LinkedList(); + int index = processTemplate( elements, anHTMLString, 0, bindings, aLanguageList ); + if ( index == -1 ) + { + if ( elements.size() == 1 ) + { + result = (WOElement) elements.get(0); + } + else + { + result = new WOParentElement( elements ); + } + } + else // entire template did not process + { + throw new RuntimeException( "No closing tag: " + anHTMLString.substring( index ) ); + } + } + catch ( Exception exc ) + { + exc.printStackTrace(); + } + return result; + } + + /** + * Called at the beginning of a request-response cycle. + * Override to perform any necessary initialization. + * This implementation does nothing. + */ + public void awake () + { + } + + /** + * Package access only. Called to initialize the component with + * the proper context before the start of the request-response cycle. + * If the context has a current component, that component becomes + * this component's parent. + */ + void ensureAwakeInContext (WOContext aContext) + { + context = aContext; + parent = aContext.parent(); + if ( template == null ) + { + template = templateWithName( name(), frameworkName() ); + } + if ( template != null ) + { + template.ensureAwakeInContext( aContext ); + } + awake(); + } + + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + if ( synchronizesVariablesWithBindings() ) + { + pullValuesFromParent(); + if ( template != null ) + { + template.takeValuesFromRequest( aRequest, aContext ); + } + pushValuesToParent(); + } + else + if ( template != null ) + { + template.takeValuesFromRequest( aRequest, aContext ); + } + } + + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + WOActionResults result = null; + if ( synchronizesVariablesWithBindings() ) + { + pullValuesFromParent(); + if ( template != null ) + { + result = template.invokeAction( aRequest, aContext ); + } + pushValuesToParent(); + } + else + if ( template != null ) + { + result = template.invokeAction( aRequest, aContext ); + } + return result; + } + + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + if ( synchronizesVariablesWithBindings() ) + { + pullValuesFromParent(); + if ( template != null ) + { + template.appendToResponse( aResponse, aContext ); + } + pushValuesToParent(); + } + else + if ( template != null ) + { + template.appendToResponse( aResponse, aContext ); + } + context = null; + } + + /** + * Called at the end of a request-response cycle. + * Override to perform any necessary clean-up. + * This implementation does nothing. + */ + public void sleep () + { + } + + /** + * Generates a WOResponse and calls appendToResponse() on it. + */ + public WOResponse generateResponse () + { + WOResponse response = new WOResponse(); + WOContext context = context(); + appendToResponse( response, context ); // nulls out context + context.session().savePage( this ); //?is this the right place for this? + return response; + } + + /** + * Returns this component's parent component, or null if none. + */ + public WOComponent parent() + { + return parent; + } + + /** + * Invokes the specified action on this component's parent. + * Variables will be synchronized when this method returns. + */ + public WOActionResults performParentAction(String anAction) + { + WOActionResults result = parent().performAction( anAction ); + if ( synchronizesVariablesWithBindings() ) + { + pullValuesFromParent(); + } + return result; + } + + /** + * Invokes the specified action on this component. + */ + WOActionResults performAction( String anAction ) + { + try + { + return (WOActionResults) NSSelector.invoke( anAction, this ); + } + catch ( NoSuchMethodException exc ) + { + // returns below + } + catch ( InvocationTargetException exc ) + { + Throwable t = exc.getTargetException(); + exc.printStackTrace(); + throw new RuntimeException( t.toString() ); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + throw new RuntimeException( exc.toString() ); + } + return null; + } + + /** + * Called before each phase of the request-response cycle, + * if synchronizesVariablesWithBindings is true and the + * component is not stateless. + */ + public void pullValuesFromParent() + { + if ( associations == null ) return; + String key; + Enumeration e = associations.keyEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement().toString(); + takeValueForKey( valueForBinding( key ), key ); + } + } + + /** + * Called after each phase of the request-response cycle, + * if synchronizesVariablesWithBindings is true and the + * component is not stateless. + */ + public void pushValuesToParent() + { + if ( associations == null ) return; + String key; + Enumeration e = associations.keyEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement().toString(); + setValueForBinding( valueForKey( key ), key ); + } + } + + /** + * Returns whether this component should be considered stateless. + * Stateless components are shared between sessions to conserve memory. + * This implementation returns false; override to return true. + */ + public boolean isStateless() + { + return false; + } + + /** + * Called only on stateless components to tell themselves to reset + * themselves for another invocation using a different context. + * This implementation does nothing. + */ + public void reset() + { + // does nothing + } + + /** + * Returns the application containing this instance of the class. + */ + public WOApplication application () + { + return context.application(); + } + + /** + * Returns whether a session has been created for this user. + */ + public boolean hasSession () + { + return context.hasSession(); + } + + /** + * Returns the current session object, creating it if it doesn't exist. + */ + public WOSession session () + { + return context.session(); + } + + /** + * Returns the current context for this component. + */ + public WOContext context () + { + return context; + } + + /** + * Returns a new WOComponent with the specified name. + * If null, returns the component named "Main". + * If the named component doesn't exist, returns null. + */ + public WOComponent pageWithName (String aName) + { + return application().pageWithName( aName, context() ); + } + + /** + * Called when exceptions are raised by assigning values + * to this object. This implementation does nothing, but + * subclasses may override to do something useful. + */ + public void validationFailedWithException ( + Throwable anException, Object aValue, String aPath) + { + // does nothing + } + + /** + * Called on the component that represents the requested page. + * Override to return logging information specific to your + * component. This implementation returns the component's name. + */ + public String descriptionForResponse ( + WOResponse aResponse, WOContext aContext) + { + return name(); + } + + /** + * Returns true if this component should get and set values + * in its parent. This implementation returns true. + * Override to create a component that does not automatically + * synchronize bindings with its parent, useful if you wish + * to handle synchronization manually. + */ + public boolean synchronizesVariablesWithBindings () + { + return true; + } + + /** + * Returns whether this component has a readable value that maps + * to the specified binding. This implementation calls + * hasBinding(aBinding). + */ + public boolean canGetValueForBinding(String aBinding) + { + return hasBinding( aBinding ); + } + + /** + * Returns whether this component has a writable value that maps + * to the specified binding. + */ + public boolean canSetValueForBinding(String aBinding) + { + WOAssociation assoc = + (WOAssociation)associations.objectForKey(aBinding); + if (assoc != null) + { + if ( assoc.isValueSettable() ) return true; + } + return false; + } + + /** + * Returns whether this component has the specified binding. + */ + public boolean hasBinding (String aBinding) + { + if ( associations == null ) return false; + return associations.containsKey( aBinding ); + } + + /** + * Returns the value for the specified binding for this component. + * The parent component is expected to have set the binding for + * this component. If no such binding exists, the binding is + * treated as a property is and obtained using valueForKey. + * If the property is not found, this method returns null. + */ + public Object valueForBinding (String aBinding) + { + WOComponent parent = parent(); + if ( associations != null ) + { + WOAssociation assoc = + (WOAssociation)associations.objectForKey(aBinding); + if (assoc != null && parent != null) + { + return assoc.valueInComponent( parent ); + } + } + if ( parent != null ) + { + return parent.valueForKey( aBinding ); + } + return null; + } + + /** + * Sets the value for the specified binding for this component. + * The parent component is expected to have set the binding + * for this component. If no such binding exists, the binding + * is treated as a property and is set using takeValueForKey. + * If the property is not found, this method fails silently. + */ + public void setValueForBinding (Object aValue, String aBinding) + { + if ( associations == null ) return; + + WOComponent parent = parent(); + + if ( associations != null ) + { + WOAssociation assoc = + (WOAssociation)associations.objectForKey(aBinding); + if (assoc != null && parent != null) + { + if ( assoc.isValueSettable() ) + { + assoc.setValue( aValue, parent ); + return; + } + } + } + if ( parent != null ) + { + parent.takeValueForKey( aValue, aBinding ); + } + } + + public static void logString (String aString) + { + System.out.println( aString ); + } + + public static void debugString (String aString) + { + System.err.println( aString ); + } + + public Object valueForKeyPath (String aPath) + { + // currently key value coding support also handles keypaths + return valueForKey( aPath ); + } + + public void takeValueForKeyPath (Object aValue, String aPath) + { + // currently key value coding support also handles keypaths + takeValueForKey( aValue, aPath ); + } + + public NSDictionary valuesForKeys (List aKeyList) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public void takeValuesFromDictionary (Map aValueMap) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public Object valueForKey (String aKey) + { // System.out.println( "valueForKey: " + aKey + "->" + this ); + // handle "^" property keys + if ( aKey.startsWith( "^" ) ) + { + return valueForBinding( aKey.substring(1) ); + } + return EOKeyValueCodingSupport.valueForKey( this, aKey ); + } + + public void takeValueForKey (Object aValue, String aKey) + { // System.out.println( "takeValueForKey: " + aKey + " : " + aValue + "->" + this ); + // handle "^" property keys + if ( aKey.startsWith( "^" ) ) + { + setValueForBinding( aValue, aKey.substring(1) ); + return; + } + EOKeyValueCodingSupport.takeValueForKey( this, aValue, aKey ); + } + + public Object storedValueForKey (String aKey) + { + return EOKeyValueCodingSupport.storedValueForKey( this, aKey ); + } + + public void takeStoredValueForKey (Object aValue, String aKey) + { + EOKeyValueCodingSupport.takeStoredValueForKey( this, aValue, aKey ); + } + + public Object handleQueryWithUnboundKey (String aKey) + { + return EOKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + public void handleTakeValueForUnboundKey (Object aValue, String aKey) + { + EOKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + public void unableToSetNullForKey (String aKey) + { + EOKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + public Object validateTakeValueForKeyPath (Object aValue, String aKey) + { + throw new RuntimeException( "Not implemented yet." ); + } + + + // Template Processing + + /** + * Takes a template string and a location to begin parsing, + * looking only for interesting tags, and calling itself recursively + * as necessary. Returns the index to resume parsing, or -1 if done. + */ + static private int processTemplate( + List elements, String template, int index, + Map bindings, List aLanguageList ) + throws java.io.IOException + { //System.out.println( "processTemplate: " + index ); + if ( template == null ) return -1; + + int start = index; + + while ( true ) + { + // search for start of next tag + start = template.indexOf( '<', start ); + + if ( start == -1 ) + { + // if no tags, send output and return + elements.add( new WOStaticElement( template.substring( index ) ) ); + return -1; + } + + // search for end of opening tag + int end = template.indexOf( ">", start + 1 ); + if ( end == -1 ) + { + // if no end to tag + throw new RuntimeException( "No end to tag: " + + template.substring( start ) ); + } + + boolean hasBody = true; + if ( template.charAt( end - 1 ) == '/' ) + { + // tag is standalone - no body + end = end - 1; + hasBody = false; + } + + // search for name of tag + int endName = start + 1; + while ( endName < end ) + { + if ( Character.isWhitespace( + template.charAt(endName) ) ) break; + endName++; + } + + String name = template.substring( start + 1, endName ); + + if ( name.toLowerCase().startsWith( OPEN_TAG ) ) + { + // add the contents before the tag + //System.out.println( index + " : " + start + " : " + hasBody ); + elements.add( new WOStaticElement( template.substring( index, start ) ) ); + + // interesting tag; parse parameters + Map params = new HashMap( 5 ); // arbitrary init length + if ( endName < end ) + { + // delimit by whitespace + StringTokenizer tokens = new StringTokenizer( + template.substring( endName+1, end ) ); + int equals; + String token; + String value; + while ( tokens.hasMoreTokens() ) + { + token = tokens.nextToken(); + equals = token.indexOf( '=' ); + if ( equals != -1 ) + { + value = token.substring( equals+1 ); + + if ( value.startsWith( "\"" ) ) + { + // handle spaces within parameter names + while ( ! value.endsWith( "\"" ) ) + { + value = value + " " + tokens.nextToken(); + } + + // strip quotation marks + if ( value.endsWith( "\"" ) ) + { + value = value.substring( 1, value.length()-1 ); + } + } + + // register key with specified value + params.put( + token.substring( 0, equals ).toLowerCase(), value ); + + } + else + { + // no value found, register the key name + params.put( token.toLowerCase(), "" ); + } + } + } + + index = end + (hasBody?1:2); + + WOElement body = null; + if ( hasBody ) + { + List childElements = new LinkedList(); + + index = processTemplate( + childElements, template, index, + bindings, aLanguageList ); + start = index; + + if ( index == -1 ) + { + throw new RuntimeException( + "No closing tag found: " + template.substring( end ) ); + } + + if ( childElements.size() == 1 ) + { + body = (WOElement) childElements.get(0); + } + else + { + body = new WOParentElement( childElements ); + } + } + + WOElement element = null; + String nameProperty = (String) params.get( NAME_KEY ); + NSDictionary original = (NSDictionary) bindings.get( nameProperty ); + //System.out.println( nameProperty + " : " + associations ); + if ( original == null ) + { + original = NSDictionary.EmptyDictionary; + System.err.println( "No associations for: " + nameProperty ); + System.err.println( bindings ); + } + + NSDictionary associations = new NSMutableDictionary( original ); + String elementClass = (String) associations.remove( WOApplication.ELEMENT_CLASS ); + + WOApplication application = WOApplication.application(); + element = application.dynamicElementWithName( + elementClass, associations, body, aLanguageList ); + if ( element == null ) + { + // unable to create element: show assocs in static element + element = new WOStaticElement( associations.toString() ); + } + + //System.out.println( element ); + elements.add( element ); + + if ( !hasBody ) + { + start = end + 2; + } + } + else + if ( name.toLowerCase().startsWith( CLOSE_TAG ) ) + { + // add any contents before the tag + elements.add( new WOStaticElement( template.substring( index, start ) ) ); + +// return end + name.length() + 1; // "<" + ">" - 1 = 1 + return end + (hasBody?1:2); // "<" + ">" - 1 = 1 + } + else + { + // tag not interesting: continue + start = end + (hasBody?1:2); + } + } + } + + + // Utility Methods + + static private void rewriteTag( String tagName, + Map properties, String body, StringBuffer context ) + throws java.io.IOException + { + context.append( "<"+tagName ); + Iterator it = properties.keySet().iterator(); + String key; + while ( it.hasNext() ) + { + key = (String) it.next(); + context.append( " " + key + "=\"" + properties.get( key ) + "\"" ); + } + + if ( body == null ) + { + context.append( "/>" ); + return; + } + + context.append( ">" + body + "" ); + } + + private String justTheClassName() + { + String className = getClass().getName(); + int index = className.lastIndexOf( "." ); + if ( index == -1 ) return className; + return className.substring( index+1 ); + } + + private String justTheResourcePath() + { + int last = -1; + char[] src = getClass().getName().toCharArray(); + char[] dst = new char[ src.length ]; + for ( int i = 0; i < src.length; i++ ) + { + if ( src[i] == '.' ) + { + dst[i] = '/'; + last = i; + } + else + { + dst[i] = src[i]; + } + } + if ( last == -1 ) return null; + return new String( dst, 0, last ); + } + + private String readTemplateResource( String name, String framework, String suffix, NSArray languages ) + { + if ( name == null ) return null; + name = name + DIRECTORY_SUFFIX + '/' + name + suffix; + InputStream is = null; + if ( isCachingEnabled() ) + { + byte[] data = WOApplication.application().resourceManager().bytesForResourceNamed( + name, framework, languages ); + if ( data != null ) + { + is = new ByteArrayInputStream( data ); + } + } + else + { + is = WOApplication.application().resourceManager().inputStreamForResourceNamed( + name, framework, languages ); + } + if ( is == null ) + { + System.err.println( "No resources found for: " + name ); + return null; + } + + // try to autodetect encoding + String encoding = "ISO8859_1"; + try + { + byte[] header = new byte[4]; + is = new PushbackInputStream( is, 4 ); + is.read( header ); + if ( header[0] < 33 || header[0] > 126 ) + { + // if any funny characters, presume UTF-16 + encoding = "UTF-16"; + if (!( header[1] < 33 || header[1] > 126 )) + { + // if second character is valid, presume UTF-8 + encoding = "UTF-8"; + } + } + // check byte-order-mark + if (header[0] == 0xef && header[1] == 0xbb && header[2] == 0xbf) // utf-8 + { + encoding = "UTF-8"; + } + else + if (header[0] == 0xfe && header[1] == 0xff) // utf-16 + { + encoding = "UTF-16"; + } + else + if (header[0] == 0 && header[1] == 0 && header[2] == 0xfe && header[3] == 0xff) // ucs-4 + { + encoding = "UCS-4"; //?? + } + else + if (header[0] == 0xff && header[1] == 0xfe) // ucs-2le, ucs-4le, and + { + encoding = "UCS-16le"; //?? + } + // put back the header + ((PushbackInputStream)is).unread( header ); + } + catch ( Throwable t ) + { + t.printStackTrace(); + System.err.println( + "Error while autodetecting encoding: should never happen" ); + } + + try + { + String line; + StringBuffer buf = new StringBuffer(); + BufferedReader r = new BufferedReader( new InputStreamReader( is, encoding ) ); + while ( ( line = r.readLine() ) != null ) + { + buf.append( line ); + buf.append( '\n' ); + } + is.close(); // release the resource + return buf.toString(); + } + catch ( IOException exc ) + { + System.err.println( "Error while reading: " + name ); + exc.printStackTrace(); + return null; + } + } + + // Declaration Parsing + + /** + * Parses the declarations in the specified content and returns a map of element names + * to maps of attribute names to WOAssociations. + */ + private static NSDictionary processDeclaration( String content ) + { + int index; + NSMutableDictionary result = new NSMutableDictionary(); + + // strip out comments + StringBuffer stripped = new StringBuffer(); + try + { + LineNumberReader reader = + new LineNumberReader( new StringReader( content ) ); + String line; + while ( ( line = reader.readLine() ) != null ) + { + index = line.indexOf("//"); + while (index > -1) { + //(chochos) This used to truncate lines with quoted URLs + //in them. We have to check that the "//" is not inside quotes. + boolean quoted = false; + if (index > 0) { + for (int _position = 0; _position < index; _position++) + if (line.charAt(_position) == '"') + quoted = !quoted; + } + if (!quoted) { + line = line.substring( 0, index ); + index = -1; + } else { + //if we didn't truncate the line it's because the // + //were quoted. let's look for more, and check if they're not quoted... + index = line.indexOf("\"", index); + if (index > 0) { + index = line.indexOf("//", index); + } + } + } + stripped.append( line ); + } + } + catch ( IOException exc ) + { + throw new RuntimeException( + "Error while stripping comments from declaration: " + stripped ); + } + while ( (index = stripped.toString().indexOf( "/*" )) != -1 ) + { + int j = stripped.toString().indexOf( "*/", index+1 ); + if ( j == -1 ) break; + stripped.delete( index, j+2 ); + } + + String token; + StringTokenizer tokens = new StringTokenizer( stripped.toString(), "{}", true ); + while ( tokens.hasMoreTokens() ) + { + token = tokens.nextToken(); + + // next token is the name and class + String name, cl; + index = token.indexOf( ":" ); + if ( index > -1 ) + { + name = token.substring( 0, index ).trim(); + cl = token.substring( index+1 ).trim(); + } + else + { + System.err.println( "Could not parse declaration:" ); + System.err.println( content ); + throw new RuntimeException( + "Could not parse declaration: " + token ); + } + + // next token is the declaration for the name and class + if ( ! tokens.hasMoreTokens() ) + { + System.err.println( "Could not find associations for declaration:" ); + System.err.println( content ); + throw new RuntimeException( + "Could not find associations for declaration: " + name ); + } + + token = tokens.nextToken(); + if ( token.equals( "{" ) ) + { + if ( !tokens.hasMoreTokens() ) throw new RuntimeException( + "Error parsing declaration: expected { but found: '" + token + "'" ); + token = tokens.nextToken(); + } + + NSMutableDictionary associations = new NSMutableDictionary(); + + if ( !token.equals( "}" ) ) + { + String line, key, value; + StringTokenizer lines = + new StringTokenizer( token, ";" ); + while ( lines.hasMoreElements() ) + { + line = lines.nextToken(); + index = line.indexOf( "=" ); + if ( index > -1 ) + { + if ( line.length() == index+ 1 ) line += " "; + key = line.substring( 0, index ).trim(); + value = line.substring( index+1 ).trim(); + } + else + { + // not a valid key: skip + key = null; + value = null; + } + + if ( key != null ) + { + // if in quotation marks + if ( ( value.startsWith( "\"" ) ) && ( value.endsWith( "\"" ) ) ) + { + // it's a constant value association + value = value.substring( 1, value.length()-1 ); + associations.put( key, + WOAssociation.associationWithValue( value ) ); + } + else + if ( value.equalsIgnoreCase( "true" ) || value.equalsIgnoreCase( "false" ) ) + { + //HACK: needed to be compatible with woextensions + // apparently true and false are allowed without quotes + associations.put( key, + WOAssociation.associationWithValue( value ) ); + } + else + { + //HACK: needed to be compatible with woextensions: + // apparently a standalone integer is allowed without quotes. + try + { + Integer.parseInt( value ); // does it parse? + associations.put( key, + WOAssociation.associationWithValue( value ) ); + } + catch ( NumberFormatException nfe ) + { + // did not parse: + // it's a key path association + associations.put( key, + WOAssociation.associationWithKeyPath( value ) ); + } + } + } + } + if ( tokens.hasMoreTokens() ) + { + token = tokens.nextToken(); + if ( !token.equals( "}" ) ) throw new RuntimeException( + "Error parsing declaration: expected } but found: '" + token + "'" ); + } + } + associations.put( WOApplication.ELEMENT_CLASS, cl ); // store classname + result.put( name, associations ); + + } + //System.out.println( "processDeclaration: " + result ); + return result; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.32 2003/08/07 00:15:14 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.31 2003/07/24 00:23:21 chochos + * fixed problem with parsing wod files that have //-type comments. Quotes URL's would be truncated. + * + * Revision 1.30 2003/03/28 15:33:11 mpowers + * Now using a PushBackInputStream for auto detection of content encoding. + * No longer relying on markSupported() since jar input streams don't have it. + * + * Revision 1.29 2003/03/03 16:41:52 mpowers + * Bad characters in cvs log. + * + * Revision 1.28 2003/03/03 16:37:35 mpowers + * Better handling for string encodings. + * Now trying to autodetect unicode-formatted templates and declarations. + * Now handlings block-style comments in declarations. + * + * Revision 1.27 2003/01/28 19:33:51 mpowers + * Implemented the rest of WOResourceManager. + * Implemented support for java-style i18n. + * Components now use the resource manager to load templates. + * + * Revision 1.26 2003/01/24 20:13:22 mpowers + * Now accepting immutable NSDictionary in constructor, not Map. + * + * Revision 1.25 2003/01/21 17:53:45 mpowers + * Now correctly reporting error for missing bindings. + * + * Revision 1.24 2003/01/20 17:50:11 mpowers + * Caught a loop condition when same declaration was used twice. + * + * Revision 1.23 2003/01/19 22:33:25 mpowers + * Fixed problems with classpath and dynamic class loading. + * Dynamic elements now pass on ensureAwakeInContext. + * Parser how handles tags. + * + * Revision 1.22 2003/01/17 22:55:09 mpowers + * Straighted out the parent binding issue (I think). + * Fixes for woextensions compatibility. + * + * Revision 1.21 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.19 2003/01/17 15:32:22 mpowers + * Changes to better support generic elements and containers. + * Now preserving newlines in templates. + * + * Revision 1.17 2003/01/16 22:47:30 mpowers + * Compatibility changes to support compiling woextensions source. + * (34 out of 56 classes compile!) + * + * Revision 1.15 2003/01/16 15:50:43 mpowers + * More robust declaration parsing. + * Subcomponents are now supported. + * dynamicElementWithName can now return subcomponents. + * + * Revision 1.14 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.13 2003/01/15 14:33:48 mpowers + * Refactoring: element id handling is now confined to WOParentElement. + * Other elements/components should not have to do element id incrementing. + * + * Revision 1.12 2003/01/14 16:05:12 mpowers + * Removed extraneous printlns. + * + * Revision 1.11 2003/01/13 22:24:25 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.10 2003/01/10 19:33:28 mpowers + * Added contextID for the component url generation. + * + * Revision 1.9 2003/01/10 19:16:40 mpowers + * Implemented support for page caching. + * + * Revision 1.8 2003/01/09 21:16:48 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.7 2003/01/09 16:13:55 mpowers + * Implemented WOComponentRequestHandler: + * Bringing the request-response cycle more into conformance. + * + * Revision 1.6 2002/12/20 22:56:33 mpowers + * Reimplemented the template parsing again. + * Nested components are now correctly parsed. + * ElementID numbering is now working. + * + * Revision 1.3 2002/12/18 14:12:38 mpowers + * Support for differentiated request handlers. + * Support url generation for WOContext and WORequest. + * + * Revision 1.2 2002/11/07 18:52:33 mpowers + * New components courtesy of ezamudio@nasoft.com. Many thanks! + * + * Revision 1.1.1.1 2000/12/21 15:53:01 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java new file mode 100644 index 0000000..1544934 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentContent.java @@ -0,0 +1,45 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** + * Used to include a component's content inside another component. This way, you can create reusable components + * with content that surrounds the component it's in. In reality all it does is forward the appendToResponse + * method to the WOElement containing the HTML between the WEBOBJECT tags that define this element. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOComponentContent extends WODynamicElement { + + public WOComponentContent() { + super(); + } + + public WOComponentContent(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + c.component().rootElement.appendToResponse(r, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java new file mode 100644 index 0000000..9f79987 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOComponentRequestHandler.java @@ -0,0 +1,229 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + + +/** +* An implementation of WORequestHandler that dispatches Component actions. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOComponentRequestHandler + extends WORequestHandler +{ + public WOResponse handleRequest( WORequest aRequest ) + { + WOApplication application = aRequest.application(); + WOContext context = WOContext.contextWithRequest( aRequest ); + WOResponse response = null; + + // no concurrent access is allowed to a user's session: + // a user/browser/machine with multiple requests will have to wait + synchronized ( aRequest.request.getSession() ) + { + try + { + application.awake(); + + WOSession session = null; + /* + // the NeXT way + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + if ( session == null ) + { + response = application.handleSessionRestorationErrorInContext( context ); + } + } + else + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + */ + // the servlet way + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + } + + if ( session == null ) + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + + context.setSession( session ); + + session.awake(); + + if ( response == null ) + { + + WOComponent page; + String contextID = aRequest.contextID(); + + if ( contextID != null ) + { + page = session.restorePageForContextID( contextID ); + } + else + { + page = application.pageWithName( aRequest.pageName(), context ); + } + + if ( page == null ) + { + //FIXME: should we call this a restoration error, or do we + // allow a bookmark with an expired context id to resume gracefully? + page = application.pageWithName( aRequest.pageName(), context ); + //!response = application.handlePageRestorationErrorInContext( context ); + } + //!else + { + context.pushElement( page ); //? needed? + page.ensureAwakeInContext( context ); //? shouldn't this be in WOApplication? + + // only take values from request if there are values in the request + System.out.println("should I takeValuesFromRequest ? " + ( aRequest.formValueKeys().count() > 0 )); + if ( aRequest.formValueKeys().count() > 0 ) + { + application.takeValuesFromRequest( aRequest, context ); + } + + // only invoke action if there is a sender id to invoke + WOActionResults result; + System.out.println("senderID: " + aRequest.senderID()); + if ( aRequest.senderID() != null ) + { + result = application.invokeAction( aRequest, context ); + } + else + { + result = null; + } + + if ( result == null || result == page ) // same page is returned + { + result = page; + + // generate response + response = new WOResponse(); + application.appendToResponse( response, context ); + page.sleep(); + } + else // different page is returned + if ( result instanceof WOComponent ) + { + page.sleep(); + page = (WOComponent) result; + page.ensureAwakeInContext( context ); + context.popElement(); // removes page + context.pushElement( page ); + + // generate response + response = new WOResponse(); + application.appendToResponse( response, context ); + page.sleep(); + } + else // WOResponse was returnd + { + response = (WOResponse) result; + } + + context.popElement(); + session.sleep(); + session.savePage( page ); + session.setContext( null ); + application.saveSessionForContext( context ); + } + } + } + catch ( Throwable t ) + { + response = application.handleException( t, context ); + } + + application.sleep(); + } + return response; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.8 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.7 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.6 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.4 2003/01/14 15:51:09 mpowers + * No longer calling takeValues or invokeAction if there are no request params + * + * Revision 1.3 2003/01/13 22:24:34 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.1 2003/01/09 16:13:59 mpowers + * Implemented WOComponentRequestHandler: + * Bringing the request-response cycle more into conformance. + * + * Revision 1.2 2002/12/17 14:57:44 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:19 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java new file mode 100644 index 0000000..124ca11 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOConditional.java @@ -0,0 +1,99 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** +* WOConditional renders whatever is inside its opening and closing tags + * only if a condition is met. + * Bindings are: + *
      + *
    • condition: a boolean property that indicates whether the contents of the element + * should be displayed, invoked, or passed the request form values.
    • + *
    • negate: if this is true, then the behavior of the element is reversed, showing its + * contents only if the condition is NOT met.
    • + *
    + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 893 $ + */ +public class WOConditional extends WODynamicElement { + + public boolean condition; + public boolean negate; + + protected WOConditional() { + super(); + } + + public WOConditional(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setCondition(boolean value) { + condition = value; + } + public boolean condition() { + return condition; + } + + public void setNegate(boolean value) { + negate = value; + } + public boolean negate() { + return negate; + } + + protected void pullValuesFromParent(WOComponent c) { + condition = booleanForProperty("condition", c); + negate = booleanForProperty("negate", c); + } + + public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { + if (rootElement == null) + return; + pullValuesFromParent(aContext.component()); + if ((condition && !negate) || (!condition && negate)) { + rootElement.takeValuesFromRequest(aRequest, aContext); + } + } + + public WOActionResults invokeAction(WORequest aRequest, WOContext aContext) { + if (rootElement == null) + return null; + pullValuesFromParent(aContext.component()); + WOActionResults el = null; + if ((condition && !negate) || (!condition && negate)) { + el = rootElement.invokeAction(aRequest, aContext); + } + return el; + } + + public void appendToResponse(WOResponse aResponse, WOContext aContext) { + if (rootElement == null) + return; + pullValuesFromParent(aContext.component()); + if ((condition && !negate) || (!condition && negate)) { + rootElement.appendToResponse(aResponse, aContext); + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java new file mode 100644 index 0000000..4f774e6 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOContext.java @@ -0,0 +1,572 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** +* A pure java implementation of WOContext. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOContext +{ + private WOSession session; + private WORequest request; + private WOResponse response; + private List elementStack; + private boolean isInForm; + private boolean isDistributionEnabled; + private static final String EMPTY = ""; + private static final String SEP = "."; + private static final String ZERO = "0"; + private static final String HTTP = "http://"; + private static final String HTTPS = "https://"; + + // package access + WOComponent page; + WOComponent component; + StringBuffer elementID; + + private static volatile int contextCounter = 0; + private String contextID; + + /** + * Default constructor performs necessary initialization. + * Subclassers should override the static factory method below. + */ + public WOContext () + { + contextID = null; + elementID = new StringBuffer(); + elementStack = new LinkedList(); + } + + /** + * Preferred constructor performs necessary initialization. + * Subclassers should override this method. + */ + public WOContext (WORequest aRequest) + { + this(); + request = aRequest; + response = new WOResponse(); + } + + /** + * Simply calls the preferred constructor. + * Included for compatibility. + */ + public static WOContext contextWithRequest (WORequest aRequest) + { + String id = Integer.toString( contextCounter++ ); + WOContext result = new WOContext( aRequest ); + result.contextID = id; + return result; + } + + /** + * Returns the context id. + */ + public String contextID () + { + return contextID; + } + + /** + * Returns the sender id, or null if it doesn't exist. + */ + public String senderID () + { + return request.senderID(); + } + + /** + * Returns the element id, or null if it doesn't exist. + */ + public String elementID () + { + return elementID.toString(); + } + + /** + * Returns this context's application, or null if it doesn't exist. + */ + public WOApplication application () + { + return request.application(); + } + + /** + * Returns whether a session has been created for the associated request. + */ + public boolean hasSession () + { + return ( session != null ); + } + + /** + * Returns this context's session, creating one if it doesn't exist. + */ + public WOSession session () + { + if ( session == null ) + { + // so far we can't figure out how the direct action handler can avoid creating a session + throw new RuntimeException( "WOContext.session: Lazy instantiation not yet implemented." ); +/* + session = application().restoreSessionWithID( + request.sessionID(), this ); + if ( session == null ) + { + session = application().createSessionForRequest( request ); + session.setContext( this ); + } +*/ + } + return session; + } + + /** + * Package access only. + */ + void setSession( WOSession aSession ) + { + session = aSession; + } + + /** + * Returns this context's request. + */ + public WORequest request () + { + return request; + } + + /** + * Returns this context's response. + */ + public WOResponse response () + { + return response; + } + + /** + * Returns the current page. + */ + public WOComponent page () + { + return (WOComponent) elementStack.get( elementStack.size()-1 ); + } + + /** + * Returns the current component. + */ + public WOComponent component () + { + Object o; + Iterator i = elementStack.iterator(); + while ( i.hasNext() ) + { + o = i.next(); + if ( o instanceof WOComponent ) return (WOComponent) o; + } + return null; + } + + /** + * Returns the current component's parent. + */ + WOComponent parent () + { + Object o; + Iterator i = elementStack.iterator(); + if ( i.hasNext() ) + { + // skip current component + o = i.next(); + } + while ( i.hasNext() ) + { + o = i.next(); + if ( o instanceof WOComponent ) + { + return (WOComponent) o; + } + } + return null; + } + + /** + * Pushes an element onto the stack. + * Package access only. + */ + void pushElement( WOElement aComponent ) + { + elementStack.add( 0, aComponent ); + } + + /** + * Pops an element off the stack. + * Package access only. + */ + WOElement popElement() + { + return (WOElement) elementStack.remove(0); + } + + /** + * Returns whether the current context is in a form. + */ + public boolean isInForm () + { + return isInForm; + } + + /** + * Sets whether the current context is in a WOForm. + */ + public void setInForm (boolean inForm) + { + isInForm = inForm; + } + + /** + * Appends the specified string to the end of the element id. + * For example, if the element id is "0.1", sending "Test" + * changes the element id to "0.1.Test". + */ + public void appendElementIDComponent (String aString) + { + if ( elementID.length() > 0 ) + { + elementID.append( SEP ); + } + elementID.append( aString ); + } + + /** + * Appends a zero to the element id to represent the first + * new child component. For example, if the element id is "0.1", + * calling this changes the element id to "0.1.0". + */ + public void appendZeroElementIDComponent () + { + if ( elementID.length() > 0 ) + { + elementID.append( SEP ); + } + elementID.append( ZERO ); + } + + /** + * Increments the last component of the element id. + * For example, if the element id is "0.1", calling this + * changes the element id to "0.2". + */ + public void incrementLastElementIDComponent () + { + String last; + String id = elementID.toString(); + int index = id.lastIndexOf( SEP ); + if ( index == -1 ) + { + last = id; + } + else + { + last = id.substring( index + 1 ); + } + + deleteLastElementIDComponent(); + + try + { + appendElementIDComponent( + Integer.toString( Integer.parseInt( last ) + 1 ) ); + } + catch ( Exception exc ) + { + System.err.println( "Error parsing id: " + last ); + appendZeroElementIDComponent(); + } + //System.out.println( "WOContext: " + elementID ); + } + + /** + * Deletes the last component of the element id. + * For example, if the element id is "0.1", callling this + * changes the element id to "0". + */ + public void deleteLastElementIDComponent () + { + int index = elementID.toString().lastIndexOf( SEP ); + if ( index == -1 ) + { + elementID.setLength( 0 ); + } + else + { + elementID.setLength( index ); + } + } + + /** + * Deletes all components of the element id. + * This makes the element id an empty string. + */ + public void deleteAllElementIDComponents () + { + elementID.setLength( 0 ); + } + + /** + * Returns a URL for the named action with query parameters + * as specified by the dictionary. + */ + public String directActionURLForActionNamed ( + String anActionName, Map aQueryDict) + { + StringBuffer query = new StringBuffer(); + + try + { + String key; + Iterator i = aQueryDict.keySet().iterator(); + while ( i.hasNext() ) + { + key = i.next().toString(); + query.append( URI.encode( key, + URI.allowed_within_query ) ); + query.append( '=' ); + query.append( URI.encode( aQueryDict.get( key ).toString(), + URI.allowed_within_query ) ); + if ( i.hasNext() ) + { + query.append( '&' ); + } + } + } + catch ( IOException exc ) + { + // report error + System.err.println( + "directActionURLForActionNamed: " + anActionName + " : " + aQueryDict ); + System.err.println( exc ); + + // delete query string + query = new StringBuffer(); + } + + return urlWithRequestHandlerKey( + WOApplication.directActionRequestHandlerKey(), + anActionName, query.toString() ); + } + + /** + * Returns the complete URL for the current component action. + */ + public String componentActionURL () + { + StringBuffer buffer = new StringBuffer(); + buffer.append( request().applicationName() ); + buffer.append( '/' ); + buffer.append( WOApplication.application().componentRequestHandlerKey() ); + buffer.append( '/' ); + buffer.append( page().name() ); + buffer.append( '/' ); + buffer.append( contextID ); + buffer.append( '/' ); + buffer.append( elementID ); + return buffer.toString(); + } + + /** + * Returns a URL relative to the servlet for the specified + * request handler, action, and query string. + */ + public String urlWithRequestHandlerKey ( + String aRequestHandlerKey, String aPath, String aQueryString) + { + StringBuffer buffer = new StringBuffer(); + buffer.append( request().applicationName() ); + buffer.append( '/' ); + buffer.append( aRequestHandlerKey ); + buffer.append( '/' ); + buffer.append( aPath ); + if ( aQueryString != null && aQueryString.trim().length() > 0 ) + { + buffer.append( '?' ); + buffer.append( aQueryString ); + } + return buffer.toString(); + } + + /** + * Returns the complete URL for the specified request handler, + * path, and query string. isSecure determines the protocol: + * http or https. port is appended to the protocol, if zero, + * the port is omitted from the URL. + */ + public String completeURLWithRequestHandlerKey ( + String aRequestHandlerKey, String aPath, String aQueryString, + boolean isSecure, int port) + { + StringBuffer buffer = new StringBuffer(); + + if ( isSecure ) + { + buffer.append( HTTPS ); + } + else + { + buffer.append( HTTP ); + } + + buffer.append( request.applicationHost() ); + + if ( port != 0 && port != 80 ) + { + buffer.append( ':' ); + buffer.append( Integer.toString( port ) ); + } + + buffer.append( urlWithRequestHandlerKey( + aRequestHandlerKey, aPath, aQueryString ) ); + + return buffer.toString(); + } + + /** + * Sets whether distribution is enabled. + */ + public void setDistributionEnabled (boolean distributionEnabled) + { + isDistributionEnabled = distributionEnabled; + } + + /** + * Returns whether distribution is enabled. + */ + public boolean isDistributionEnabled () + { + return isDistributionEnabled; + } + + /** + * This method is not included in the WOContext specification. + * This implementation returns "" since only cookie ids are + * currently supported. + */ + String urlSessionPrefix () + { + return EMPTY; // "/" + sessionid; + } + + /** + * Returns the relative URL for the current page. + */ + String url () + { + throw new RuntimeException( "Not implemented yet." ); + } + + public String toString() + { + return "[WOContext@"+Integer.toHexString(System.identityHashCode(this)) + +":id=" + contextID +":page=" + page + ":component=" + component + ":element=" + elementID + "]"; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.19 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.18 2003/02/21 16:40:23 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.17 2003/01/24 20:12:54 mpowers + * Better parent determination. + * + * Revision 1.16 2003/01/21 22:27:02 mpowers + * Corrected context id usage. + * Implemented backtracking. + * + * Revision 1.15 2003/01/17 20:58:19 mpowers + * Fixed up WOHyperlink. + * + * Revision 1.14 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.13 2003/01/14 19:48:36 mpowers + * - fixes to property synchronization + * - forms now pass takeValuesFromRequest to children + * - fixes to action invocation + * - now supporting multipleSubmit in forms + * + * Revision 1.12 2003/01/13 22:24:44 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.11 2003/01/10 20:17:41 mpowers + * Component action urls are now working. + * + * Revision 1.8 2003/01/09 21:16:48 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.7 2003/01/09 16:13:59 mpowers + * Implemented WOComponentRequestHandler: + * Bringing the request-response cycle more into conformance. + * + * Revision 1.4 2002/12/20 22:56:33 mpowers + * Reimplemented the template parsing again. + * Nested components are now correctly parsed. + * ElementID numbering is now working. + * + * Revision 1.3 2002/12/18 14:12:38 mpowers + * Support for differentiated request handlers. + * Support url generation for WOContext and WORequest. + * + * Revision 1.2 2002/12/17 14:57:42 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:04 mpowers + * Contributing wotonomy. + * + * Revision 1.3 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java new file mode 100644 index 0000000..78427f0 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOCookie.java @@ -0,0 +1,203 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDate; + +/** +* A pure java implementation of WOCookie that extends +* javax.servlet.httpd.Cookie for greater compatibility. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOCookie + extends javax.servlet.http.Cookie +{ + /** + * Default constructor. + */ + public WOCookie () + { + super( "", "" ); + } + + /** + * Constructs a cookie with the specified name and value. + */ + public WOCookie( String aName, String aValue ) + { + super( aName, aValue ); + } + + /** + * Constructs a cookie with the specified name and value. + * Also sets the path to the current application's path. + */ + public static WOCookie cookieWithName (String aName, String aValue) + { + WOCookie result = new WOCookie( aName, aValue ); + //TODO: Set the path to the current application's path. + return result; + } + + /** + * Constructs a cookie with the specified attributes. + */ + public static WOCookie cookieWithName (String aName, String aValue, + String aPath, String aDomain, NSDate expirationDate, boolean secure) + { + WOCookie result = new WOCookie( aName, aValue ); + result.setPath( aPath ); + result.setDomain( aDomain ); + result.setExpires( expirationDate ); + result.setSecure( secure ); + return result; + } + + /** + * Returns the name of the cookie. + */ + public String name () + { + return this.getName(); + } + + /** + * Sets the name of the cookie. + */ + public void setName (String aString) + { + // super.setName( aString ); + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Returns the value of the cookie. + */ + public String value () + { + return this.getValue(); + } + + /** + * Sets the value of the cookie. + */ + public void setValue (String aString) + { + super.setValue( aString ); + } + + /** + * Gets the domain of the cookie. + */ + public String domain () + { + return this.getDomain(); + } + + /** + * Sets the domain of the cookie. + */ + public void setDomain (String aString) + { + super.setDomain( aString ); + } + + /** + * Gets the path of the cookie. + */ + public String path () + { + return this.getPath(); + } + + /** + * Sets the path of the cookie. + */ + public void setPath (String aString) + { + super.setPath( aString ); + } + + /** + * Gets the expiration date of the cookie. + * If in the past, the cookie will persist until browser shutdown. + */ + public NSDate expires () + { + return new NSDate( this.getMaxAge() ); + } + + /** + * Sets the expiration date of the cookie. + */ + public void setExpires (NSDate aDate) + { + this.setMaxAge( (int) aDate.timeIntervalSinceNow() ); + } + + /** + * Returns whether the cookie will only be sent over a secure protocol. + */ + public boolean isSecure () + { + return this.getSecure(); + } + + /** + * Sets whether the cookie will only be sent over a secure protocol. + */ + public void setIsSecure (boolean isSecure) + { + this.setSecure( isSecure ); + } + + /** + * Returns the string as it appears in the HTTP header of the response. + * This would normally be called by WOResponse, but is handled automatically + * by the servlet implementation. + */ + public String headerString () + { + new RuntimeException( "Not implemented yet." ); + return null; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1.1.1 2000/12/21 15:53:04 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java new file mode 100644 index 0000000..09d0131 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectAction.java @@ -0,0 +1,317 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.servlet.http.HttpSession; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.ValueConverter; + +/** +* A pure java implementation of WODirectAction. +* This class provides a number of services to subclasses, +* including access to the query parameters, the request, +* the session, and logging and debugging facilities. +* A new instance of the class is created and then destroyed +* for every request-response cycle.

    +* +* Subclass this to implement direct actions for your +* application. Subclasses would typically override the +* constructor to initialize state based on the request, +* and then provide additional methods that would be invoked +* based on the value at the end of the URI.

    +* +* Example: "http://www/MyApp.woa/MyActions/search" would call +* the method named "searchAction" on the DirectAction subclass +* named "MyActions". If the subclass name is omitted, a subclass +* named "DirectAction" is assumed to exist within the application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODirectAction +{ + private WORequest request; + WOContext context; + + /** + * Default constructor. This is called implicitly by + * subclasses in all cases. Package access only. + */ + WODirectAction () + { + + } + + /** + * Request constructor. This is the constructor used + * to create an action in response to a user request. + * Override to perform any necessary initialization in + * your subclass. + */ + public WODirectAction (WORequest aRequest) + { + this(); + request = aRequest; + } + + /** + * Returns the response from the component named "Main". + */ + public WOActionResults defaultAction() + { + return pageWithName("Main").generateResponse(); + } + + /** + * Returns the WORequest object for the current request. + */ + public WORequest request () + { + return request; + } + + /** + * Returns the existing session, or null if no session exists. + */ + public WOSession existingSession () + { + //FIXME: this is incorrect + HttpSession session = request.servletRequest().getSession(); + if ( session == null ) return null; + WOSession wosession = new WOSession(); + wosession.setServletSession( session ); + return wosession; + } + + /** + * Returns the existing session if it exists. If no session + * exists, returns a newly created session. Note that if the + * user client does not support session ids/cookies, this will + * create a new session with each request. + */ + public WOSession session () + { + //FIXME: this is incorrect + WOSession wosession = new WOSession(); + wosession.setServletSession( + request.servletRequest().getSession( true ) ); + return wosession; + } + + /** + * Returns the named WOComponent. + */ + public WOComponent pageWithName (String aString) + { + return request.application().pageWithName( aString, context ); + } + + /** + * Appends "Action" to the specified string and tries to invoke + * method with that name and no arguments. Returns null if + * the method does not exist. If anAction is null, "defaultAction" + * will be assumed. + */ + public WOActionResults performActionNamed (String anAction) + { + if ( anAction == null ) anAction = "default"; + try + { + NSSelector sel = new NSSelector( anAction+"Action" ); + return (WOActionResults) sel.invoke( this ); + } + catch ( NoSuchMethodException exc ) + { + // returns below + } + catch ( InvocationTargetException exc ) + { + Throwable e = exc.getTargetException(); + exc.printStackTrace(); + throw new RuntimeException( e.toString() ); + } + catch ( Exception exc ) + { + exc.printStackTrace(); + throw new RuntimeException( exc.toString() ); + } + WOResponse error = new WOResponse(); + error.setStatus( 404 ); // not found + error.appendContentString( + "Could not find method named \"" + anAction + "Action\"." ); + return error; + } + + /** + * Assigns the arrays of form values for the specified keys + * to properties on this object with matching names whose type + * is NSArray or is convertable from a Collection. + */ + public void takeFormValueArraysForKeyArray (NSArray anArray) + { + if ( anArray == null ) return; + + Method m; + Object key; + Object value; + java.util.Enumeration e = anArray.objectEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.formValuesForKey( key.toString() ); + try + { + // obtain setter method for this class + m = Introspector.getPropertyWriteMethod( + this.getClass(), key.toString(), + new Class[] { Introspector.WILD } ); + if ( m != null ) + { + // if value isn't null, try to convert it to type + if ( value != null ) + { + Class[] paramTypes = m.getParameterTypes(); + if ( ! paramTypes[0].isAssignableFrom( + value.getClass() ) ) + { + //TODO: find a constructor whose parameter + // is assignable from value.getClass() + // and instantiate it in the place of value. + } + } + // set property to value + m.invoke( this, new Object[] { value } ); + } + } + catch ( Exception exc ) + { + // show error and continue + debugString( "WODirectAction.takeFormValuesForKeyArray: " + exc ); + } + } + + } + + /** + * Assigns the form values for the specified keys to properties + * on this object with matching names. + */ + public void takeFormValuesForKeyArray (NSArray anArray) + { + if ( anArray == null ) return; + + Method m; + Object key; + Object value; + java.util.Enumeration e = anArray.objectEnumerator(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.formValueForKey( key.toString() ); + try + { + // obtain setter method for this class + m = Introspector.getPropertyWriteMethod( + this.getClass(), key.toString(), + new Class[] { Introspector.WILD } ); + if ( m != null ) + { + // if value isn't null, try to convert it to type + if ( value != null ) + { + Class[] paramTypes = m.getParameterTypes(); + Object convertedValue = + ValueConverter.convertObjectToClass( + value, paramTypes[0] ); + if ( convertedValue != null ) + { + value = convertedValue; + } + } + // set property to value + m.invoke( this, new Object[] { value } ); + } + } + catch ( Exception exc ) + { + // show error and continue + debugString( "WODirectAction.takeFormValuesForKeyArray: " + exc ); + } + } + + } + + /** + * Writes a message to the standard error stream. + */ + public static void logString (String aString) + { + System.err.println( aString ); + } + + /** + * Writes a message to the standard error stream + * if debugging is activated. + */ + public static void debugString (String aString) + { + // TODO: Check to see if debugging is enabled. + System.err.println( aString ); + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/01/13 22:24:51 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.4 2003/01/09 21:16:48 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.3 2003/01/07 15:58:11 mpowers + * Implementing the request-response cycle. + * WOSession refactored to support distribution. + * + * Revision 1.2 2002/12/17 14:57:43 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:18 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java new file mode 100644 index 0000000..eac826b --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODirectActionRequestHandler.java @@ -0,0 +1,221 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; + +/** +* An implementation of WORequestHandler that dispatches +* DirectActions.

    +* +* See WODirectAction for the rules for parsing a request. +* In short, className defaults to "DirectAction", and the +* action defaults to "default". The action class is expected +* to have a constructor that takes a WORequest parameter. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODirectActionRequestHandler + extends WORequestHandler +{ + public WOResponse handleRequest (WORequest aRequest) + { + WOResponse response = null; + + // no concurrent access is allowed to a user's session: + // a user/browser/machine with multiple requests will have to wait. + // NOTE: this forces a session creation for any direct action (!) + synchronized ( aRequest.request.getSession() ) + { + WOApplication application = aRequest.application(); + WOContext context = WOContext.contextWithRequest( aRequest ); + + String className = "DirectAction"; + String actionName = "default"; + NSArray path = aRequest.requestHandlerPathArray(); + if ( path.count() > 0 ) + { + className = path.objectAtIndex( 0 ).toString(); + if ( path.count() > 1 ) + { + actionName = path.objectAtIndex( path.count() - 1 ).toString(); + } + if ( path.count() > 2 ) + { + for ( int i = 1; i < path.count()-1; i++ ) + { + className = className + "." + + path.objectAtIndex( i ).toString(); + } + } + } + + //FIXME: sessions are supposed to be lazily created for direct actions + + WOSession session = null; + + String sessionID = aRequest.sessionID(); + if ( sessionID != null ) + { + session = application.restoreSessionWithID( sessionID, context ); + } + + if ( session == null ) + { + session = application.createSessionForRequest( aRequest ); + if ( session == null ) + { + response = application.handleSessionCreationErrorInContext( context ); + } + else + { + session.setContext( context ); + } + } + + context.setSession( session ); + + application.awake(); + session.awake(); + + try + { + + if ( response == null ) + { + Class c = null; + c = application.getLocalClass( className ); + if ( ( c == null ) && ( path.count() == 1 ) ) + { + actionName = className; + className = "DirectAction"; + c = application.getLocalClass( className ); + } + if ( c == null ) + { + throw new RuntimeException( + "Could not find class named \"" + className + + "\": " ); + } + java.lang.reflect.Constructor ctor = + c.getConstructor( new Class[] { WORequest.class } ); + + WODirectAction action = (WODirectAction) + ctor.newInstance( new Object[] { aRequest } ); + action.context = context; //HACK: how else can action call pageWithName? + + WOActionResults result = action.performActionNamed( actionName ); + if ( result instanceof WOComponent ) + { + //HACK: I'm not sure where this should have gone: seems hackish here. + context.pushElement( (WOComponent) result ); + ((WOComponent)result).ensureAwakeInContext( context ); + //context.popElement(); + } + response = result.generateResponse(); // calls session.savePage (?) + if ( result instanceof WOComponent ) + { + //HACK: I'm not sure where this should have gone: seems hackish here. + ((WOComponent)result).sleep(); + } + } + } + catch ( NoSuchMethodException exc ) + { + WOResponse error = new WOResponse(); + exc.printStackTrace(); + error.setStatus( 404 ); // not found + error.appendContentString( + "Could not find request constructor for class named \"" + + className + "\": " ); + response = error; + } + catch ( Exception exc ) + { + WOResponse error = new WOResponse(); + error.setStatus( 500 ); // error + if ( exc.getMessage() != null ) + { + error.appendContentString( exc.getMessage() ); + exc.printStackTrace(); + } + else + { + error.appendContentString( exc.toString() ); + exc.printStackTrace(); + } + response = error; + } + + session.sleep(); + session.setContext( null ); + application.saveSessionForContext( context ); + application.sleep(); + } + return response; + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.10 2003/03/28 17:26:18 mpowers + * Implemented package support: Applications can now live in packages. + * Better support for locating package local classes. + * + * Revision 1.9 2003/01/19 22:33:26 mpowers + * Fixed problems with classpath and dynamic class loading. + * Dynamic elements now pass on ensureAwakeInContext. + * Parser how handles tags. + * + * Revision 1.8 2003/01/17 20:34:57 mpowers + * Better handling for components and parents in the context's element stack. + * + * Revision 1.7 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.5 2003/01/13 22:25:00 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.4 2003/01/09 21:16:49 mpowers + * Bringing request-response cycle more into conformance. + * + * Revision 1.2 2002/12/17 14:57:44 mpowers + * Minor corrections to WORequests's parsing, and updated javadocs. + * + * Revision 1.1.1.1 2000/12/21 15:53:19 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:50 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java new file mode 100644 index 0000000..bda1dd5 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODisplayGroup.java @@ -0,0 +1,2455 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Observable; + +import net.wotonomy.control.EODataSource; +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.EOKeyValueCoding; +import net.wotonomy.control.EOKeyValueCodingSupport; +import net.wotonomy.control.EOObjectStore; +import net.wotonomy.control.EOObserverCenter; +import net.wotonomy.control.EOObserving; +import net.wotonomy.control.EOQualifier; +import net.wotonomy.control.EOSortOrdering; +import net.wotonomy.control.OrderedDataSource; +import net.wotonomy.control.PropertyDataSource; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.NSNotification; +import net.wotonomy.foundation.NSNotificationCenter; +import net.wotonomy.foundation.NSRange; +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.Duplicator; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* WODisplayGroup manages a set of objects, +* allowing them to be sorted, batched, and filtered. +* WODisplay also acts as a bridge to the wotonomy's +* control package, including WODisplayGroup and +* EOEditingContext. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WODisplayGroup extends Observable + implements EOObserving, EOEditingContext.Editor, + java.io.Serializable +{ + /** + * Notification sent when the display group is about to fetch. + */ + public static final String DisplayGroupWillFetchNotification + = "DisplayGroupWillFetchNotification"; + + private static boolean + globalDefaultForValidatesChangesImmediately = true; + private static String + globalDefaultStringMatchFormat = "caseInsensitiveLike"; + private static String + globalDefaultStringMatchOperator = "%@*"; + + protected NSMutableArray allObjects; + protected NSArray allObjectsProxy; + protected NSMutableArray displayedObjects; + protected NSArray displayedObjectsProxy; + protected NSMutableArray selectedObjects; + protected NSArray selectedObjectsProxy; + protected NSMutableArray selectedIndexes; + + private String defaultStringMatchOperator; + private String defaultStringMatchFormat; + + private boolean validatesChangesImmediately; + private Object delegate; + private EODataSource dataSource; + private EOQualifier qualifier; + private NSMutableArray sortOrderings; + private NSArray sortOrderingsProxy; + + private NSArray localKeys; + private NSDictionary insertedObjectDefaultValues; + private boolean fetchesOnLoad; + private boolean selectsFirstObjectAfterFetch; + private boolean usesOptimisticRefresh; + private boolean inQueryMode; + + // change detection: package access for helper classes + boolean selectionChanged; + int updatedObjectIndex; + + // batching + private int batchIndex; + private int batchSize; + + // this property is not in the spec + private boolean compareByReference = false; + + private EOObserving lastGroupObserver; + + /** + * Creates a new display group. + */ + public WODisplayGroup() + { + validatesChangesImmediately = + globalDefaultForValidatesChangesImmediately(); + defaultStringMatchOperator = + globalDefaultStringMatchFormat(); + defaultStringMatchFormat = + globalDefaultStringMatchOperator(); + + allObjects = new ObservableArray( this ); + allObjectsProxy = NSArray.arrayBackedByList( allObjects ); + displayedObjects = new NSMutableArray(); + displayedObjectsProxy = NSArray.arrayBackedByList( displayedObjects ); + selectedObjects = new NSMutableArray(); + selectedObjectsProxy = NSArray.arrayBackedByList( selectedObjects ); + sortOrderings = new NSMutableArray(); + sortOrderingsProxy = NSArray.arrayBackedByList( sortOrderings ); + selectedIndexes = new NSMutableArray(); + + delegate = null; + dataSource = null; + qualifier = null; + + localKeys = new NSArray(); // not implemented + insertedObjectDefaultValues = new NSDictionary(); + fetchesOnLoad = false; // not implemented + selectsFirstObjectAfterFetch = false; + usesOptimisticRefresh = false; + inQueryMode = false; // not implemented + + selectionChanged = false; + updatedObjectIndex = -1; + + batchIndex = 0; + batchSize = 0; + } + + + + // specify optional data source + + /** + * Sets the data source that will be used by + * this display group. + */ + public void setDataSource ( EODataSource aDataSource ) + { + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // un-register for notifications from existing parent store + NSNotificationCenter.defaultCenter().removeObserver( + this, null, dataSource.editingContext() ); + dataSource.editingContext().removeEditor( this ); + if ( dataSource.editingContext().messageHandler() == this ) + { + dataSource.editingContext().setMessageHandler( null ); + } + + } + + dataSource = aDataSource; + + if ( ( dataSource != null ) + && ( dataSource.editingContext() != null ) ) + { + // register for notifications from parent store + NSNotificationCenter.defaultCenter().addObserver( + this, new NSSelector( "objectsInvalidatedInEditingContext", + new Class[] { NSNotification.class } ), + null, dataSource.editingContext() ); + + // add ourselves as editor + dataSource.editingContext().addEditor( this ); + + // add ourselves as message handler if no such handler exists + if ( dataSource.editingContext().messageHandler() == null ) + { + dataSource.editingContext().setMessageHandler( this ); + } + } + } + + /** + * Returns the current data source backing this display group, + * or null if no dataSource is currently used. + */ + public EODataSource dataSource() + { + return dataSource; + } + + /** + * Returns the key by which this display group is bound a master + * display group, or null if this is not a detail display group. + */ + public String detailKey() + { + if ( dataSource instanceof PropertyDataSource ) + { + return ((PropertyDataSource)dataSource).key(); + } + return null; + } + + /** + * Sets the key by which this display group is bound a master + * display group. Does nothing if this is not a detail display group. + */ + public void setDetailKey( String aKey ) + { + if ( dataSource instanceof PropertyDataSource ) + { + ((PropertyDataSource)dataSource).setKey( aKey ); + } + } + + /** + * Returns whether the data source is a detail data source, + * suggesting that this is a detail display group. + */ + public boolean hasDetailDataSource() + { + return ( dataSource instanceof PropertyDataSource ); + } + + /** + * Returns the selected object in the master display group, + * or null if this is not a detail display group. + */ + public Object masterObject() + { + if ( dataSource instanceof PropertyDataSource ) + { + return ((PropertyDataSource)dataSource).source(); + } + return null; + } + + /** + * Sets the master object in the detail data source. + * Does nothing if there is no detail data source. + */ + public void setMasterObject( Object anObject ) + { + if ( dataSource instanceof PropertyDataSource ) + { + ((PropertyDataSource)dataSource).setSource( anObject ); + } + } + + // specify optional delegate + + /** + * Sets the display group delegate that + * will be used by this display group. + */ + public void setDelegate ( Object aDelegate ) + { + delegate = aDelegate; + } + + /** + * Returns the current delegate for this display group, + * or null if no delegate is currently set. + */ + public Object delegate() + { + return delegate; + } + + + + // display group configuration + + /** + * Returns the current string matching format. + * If not set, defaults to "%@*". + */ + public String defaultStringMatchFormat() + { + return defaultStringMatchFormat; + } + + /** + * Returns the current string matching operator. + * If not set, defaults to "caseInsensitiveLike". + */ + public String defaultStringMatchOperator() + { + return defaultStringMatchOperator; + } + + /** + * Sets the display group and associations to edit a + * "query by example" query object. This method is + * used for target/action connections. + */ + public void enterQueryMode ( Object aSender ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether this display group should immediate + * fetch when loaded. + */ + public boolean fetchesOnLoad() + { + return fetchesOnLoad; + } + + /** + * Returns whether this display group is in "query by + * example" mode. + */ + public boolean inQueryMode() + { + return inQueryMode; + } + + /** + * Returns a Map of default values that are applied + * to new objects that are inserted into the list. + */ + public NSDictionary insertedObjectDefaultValues() + { + return insertedObjectDefaultValues; + } + + /** + * Returns the keys that were declared when read from + * an external resource file. + */ + public NSArray localKeys() + { + return localKeys; + } + + /** + * Sets whether this display group will select the + * first object in the list after a fetch. + */ + public boolean selectsFirstObjectAfterFetch() + { + return selectsFirstObjectAfterFetch; + } + + /** + * Sets the default string matching format that + * will be used by this display group. + */ + public void setDefaultStringMatchFormat ( String aFormat ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the default string matching operator that + * will be used by this display group. + */ + public void setDefaultStringMatchOperator ( String anOperator ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets whether this display group will fetch objects + * from its data source on load. + */ + public void setFetchesOnLoad ( boolean willFetch ) + { + fetchesOnLoad = willFetch; + } + + /** + * Sets whether this display group is in "query by example" + * mode. If true, all associations will bind to a special + * "example" object. + */ + public void setInQueryMode ( boolean isInQueryMode ) + { + inQueryMode = isInQueryMode; + } + + /** + * Sets the mapping that contains the values that will + * be applied to new objects inserted into the display group. + */ + public void setInsertedObjectDefaultValues ( Map aMap ) + { + insertedObjectDefaultValues = new NSDictionary( aMap ); + } + + /** + * Sets the keys that are declared when instantiated from + * an external resource file. + */ + public void setLocalKeys ( List aKeyList ) + { + localKeys = new NSArray( (Collection) aKeyList ); + } + + /** + * Sets whether the first object in the list will be + * selected after a fetch. + */ + public void setSelectsFirstObjectAfterFetch ( + boolean selectsFirst ) + { + selectsFirstObjectAfterFetch = selectsFirst; + } + + /** + * Sets the order of the keys by which this display group + * will be ordered after a fetch or after a call to + * updateDisplayedObjects(). The elements in the display + * group will be sorted first by the first key, within + * the first key, by the second key, and so on. + */ + public void setSortOrderings ( List aList ) + { + sortOrderings.removeAllObjects(); + + Object o; + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + // handle the convenience of specifying just a key + if ( ! ( o instanceof EOSortOrdering ) ) + { + o = new EOSortOrdering( + o.toString(), EOSortOrdering.CompareAscending ); + } + sortOrderings.add( o ); + } + } + + /** + * Sets whether only changed objects are refreshed (optimistic), + * or whether all objects are refreshed (pessimistic, default). + * By default, when the display group receives notification that + * one of its objects has changed, updateDisplayedObjects is called. + */ + public void setUsesOptimisticRefresh ( boolean isOptimistic ) + { + usesOptimisticRefresh = isOptimistic; + } + + /** + * Sets whether changes made by associations are validated + * immediately, or when changes are saved. + */ + public void setValidatesChangesImmediately ( + boolean validatesImmediately ) + { + validatesChangesImmediately = validatesImmediately; + } + + /** + * Returns a read-only List of sort orderings for this display group. + */ + public NSArray sortOrderings() + { + return sortOrderingsProxy; + } + + /** + * Returns whether this display group refreshes only + * the changed objects or all objects on refresh. + */ + public boolean usesOptimisticRefresh() + { + return usesOptimisticRefresh; + } + + /** + * Returns whether this display group validates changes + * immediately. Otherwise, validation should occur when + * changes are saved. Default is the global default, + * which is initially true. + */ + public boolean validatesChangesImmediately() + { + return validatesChangesImmediately; + } + + + // qualification + + /** + * Returns a qualifier that will be applied all the objects + * in this display group to determine which objects will + * be displayed. + */ + public EOQualifier qualifier() + { + return qualifier; + } + + /** + * Returns a new qualifier built from the three query + * value maps: greater than, equal to, and less than. + */ + public EOQualifier qualifierFromQueryValues() + { + //TODO: assemble qualifier from query values + + return new EOQualifier() + { + // use inner class until we actually implement one + public EOQualifier qualifierWithBindings( + Map aMap, + boolean requireAll ) + { + return null; + } + public Throwable + validateKeysWithRootClassDescription( Class aClass ) + { + return null; + } + public boolean evaluateWithObject(Object o) + { + return false; + } + }; + } + + /** + * Calls qualifierFromQueryValues(), applies the result + * to the data source, and calls fetch(). + */ + public void qualifyDataSource() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Calls qualifierFromQueryValues(), sets the qualifier + * with setQualifier(), and calls updateDisplayedObjects(). + */ + public void qualifyDisplayGroup() + { + setQualifier( qualifierFromQueryValues() ); + updateDisplayedObjects(); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public NSDictionary queryBindingValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values. + */ + public NSMutableDictionary queryBindings() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a matching query. + */ + public NSMutableDictionary queryMatch() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a minimum value query. + */ + public NSMutableDictionary queryMin() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to binding query values for a maximum value query. + */ + public NSMutableDictionary queryMax() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public NSMutableDictionary queryOperator() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a list containing all supported qualifier operators. + */ + public NSArray allQualifierOperators() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to operator values. + */ + public NSDictionary queryOperatorValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the qualifier that will be used by + * updateDisplayedObjects() to filter displayed objects. + */ + public void setQualifier ( EOQualifier aQualifier ) + { + qualifier = aQualifier; + } + + /** + * Sets the mapping that contains the mappings of keys + * to binding values. + */ + public void setQueryBindingValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to operator values. + */ + public void setQueryOperatorValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + + + // qualifier query values + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for equality. + */ + public NSDictionary equalToQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for greater value. + */ + public NSDictionary greaterThanQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns a Map containing the mappings of keys + * to query values that will be used to test for lesser value. + */ + public NSDictionary lessThanQueryValues() + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the Map that contains the mappings of keys + * to query values that will be used to test for equality. + */ + public void setEqualToQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for greater value. + */ + public void setGreaterThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets the mapping that contains the mappings of keys + * to query values that will be used to test for lesser value. + */ + public void setLessThanQueryValues ( Map aMap ) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Deprecated: returns true. + */ + public boolean endEditing() + { + return true; + } + + + // object management + + /** + * Returns a read-only List containing all objects managed by the display group. + * This includes those objects not visible due to disqualification. + */ + public NSArray allObjects() + { //System.out.println( "avoided allocation: allObjects" ); + return allObjectsProxy; + } + + /** + * Returns the total number of batches held by this display group. + */ + public int batchCount() + { + if ( batchSize < 1 ) return 1; + int count = displayedObjects.count(); + if ( count == 0 ) return 1; + return ((count-1) / batchSize) + 1; + } + + /** + * Returns the index of the currently displayed batch. + */ + public int currentBatchIndex() + { + return batchIndex; + } + + /** + * Sets the index of the currently displayed batch. + */ + public void setCurrentBatchIndex( int aBatchIndex ) + { + batchIndex = aBatchIndex; + updateDisplayedObjects(); + } + + /** + * Sets the displayed objects to the batch containing + * the first selected object, and updates the current + * batch index, and returns null to force a page reload. + * Displays the first batch is there is no selection. + */ + public Object displayBatchContainingSelectedObject() + { + if ( batchSize < 1 ) return null; + NSArray indexes = selectionIndexes(); + if ( indexes.count() > 0 ) + { + batchIndex = + ((Number)indexes.objectAtIndex( 0 )).intValue() / batchSize; + } + else + { + batchIndex = 0; + } + updateDisplayedObjects(); + return null; + } + + /** + * Sets the displayed objects to the next batch + * and updates the current batch index, and returns null + * to force a page reload. If there is no next batch + * the first batch is displayed. + */ + public Object displayNextBatch() + { + batchIndex = (batchIndex + 1) % batchCount(); + updateDisplayedObjects(); + return null; + } + + /** + * Sets the displayed objects to the next batch + * and updates the current batch index, and returns null + * to force a page reload. If there is no previous + * batch, the last batch is displayed. + */ + public Object displayPreviousBatch() + { + batchIndex--; + if ( batchIndex < 0 ) batchIndex = batchCount() - 1; + updateDisplayedObjects(); + return null; + } + + /** + * Returns whether the displayed objects have been batched. + */ + public boolean hasMultipleBatches() + { + return batchCount() > 1; + } + + /** + * Returns the one-based index within the displayed objects list + * of the first displayed object in the current batch. + */ + public int indexOfFirstDisplayedObject() + { + if ( batchSize < 1 ) return 1; + return batchIndex * batchSize + 1; + } + + /** + * Returns the one-based index within the displayed objects list + * of the first last object in the current batch. + */ + public int indexOfLastDisplayedObject() + { + if ( batchSize < 1 ) return displayedObjects.count(); + return Math.min( + ((batchIndex+1) * batchSize), + displayedObjects.count() ); + } + + /** + * Returns the number of objects per batch. + */ + public int numberOfObjectsPerBatch() + { + return batchSize; + } + + /** + * Returns the number of objects per batch. + */ + public void setNumberOfObjectsPerBatch( int aSize ) + { + batchSize = aSize; + updateDisplayedObjects(); + } + + /** + * Clears the current selection. + * @return True is the selection was cleared, + * False if the selection could not be cleared + * @see #setSelectionIndexes + */ + public boolean clearSelection() + { + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, new NSArray( selectedObjects ) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + selectionChanged = true; + willChange(); + + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Convenience for binding to a component action: + * calls deleteSelection() and then displayBatchContainingSelectedObject() + * and returns null, which is a suitable result from a component action. + */ + public Object delete() + { + deleteSelection(); + displayBatchContainingSelectedObject(); + return null; + } + + /** + * Deletes the object at the specified index, + * notifying the delegate before and after the operation, + * and then updating the selection if needed. + * @return True if delete was successful, false if the + * object was not deleted. + */ + public boolean deleteObjectAtIndex ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + Object result = notifyDelegate( + "displayGroupShouldDeleteObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return false; + } + + deleteObjectAtIndexNoNotify( anIndex ); + + if ( dataSource != null ) + { + dataSource.deleteObject( target ); + } + + notifyDelegate( + "displayGroupDidDeleteObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, target } ); + + return true; + } + + private void deleteObjectAtIndexNoNotify ( int anIndex ) + { + Object target = displayedObjects.objectAtIndex( anIndex ); + + int i; + + // remove from selected objects if necessary + i = indexOf( selectedObjects, target ); + if ( i != NSArray.NotFound ) + { + selectionChanged = true; + willChange(); // notify before removing + selectedObjects.removeObjectAtIndex( i ); + selectedIndexes.remove( new Integer( i ) ); // comps by value + } + else // notify - no selection change needed + { + willChange(); + } + + // remove from all objects + i = indexOf( allObjects, target ); + if ( i != NSArray.NotFound ) + { + allObjects.removeObjectAtIndex( i ); + } + else // otherwise should never happen + { +// throw new WotonomyException( +// "Displayed object not found in allObjects" ); + } + + // remove from displayed objects + displayedObjects.removeObjectAtIndex( anIndex ); + } + + /** + * Deletes the currently selected objects. + * This implementation calls deleteObjectAtIndex() for + * each index in the selection list, immediately returning + * false if any delete operation fails. + * @return True if all selected objects were deleted, + * false if any deletion failed. + */ + public boolean deleteSelection() + { + int i; + boolean result = true; + + Enumeration e = new NSArray( selectedObjects ).objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i == NSArray.NotFound ) + { + // should never happen + throw new WotonomyException( + "Selected object not found in displayedObjects" ); + } + result = result && deleteObjectAtIndex( i ); + } + + return result; + } + + /** + * Returns a read-only List of all objects in the display group + * that are currently displayed by the associations. + */ + public NSArray displayedObjects() + { // System.out.println( "avoided allocation: displayedObjects" ); + if ( batchSize < 1 ) return displayedObjectsProxy; + return displayedObjectsProxy.subarrayWithRange( + new NSRange( batchIndex * batchSize, batchSize ) ); + } + + /** + * Requests a list of objects from the DataSource + * and calls setObjectArray to populate the list. + * More specifically, calls endEditing(), asks the + * delegate, fetches the objects, notifies the delegate, + * and populates the list. Returns null to force a + * page reload. + */ + public Object fetch() + { + endEditing(); + + if ( dataSource == null ) + { + return null; + } + + Object result = notifyDelegate( + "displayGroupShouldFetch", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return null; + } + + NSNotificationCenter.defaultCenter().postNotification( + DisplayGroupWillFetchNotification, this, new NSDictionary() ); + + NSArray objectList = dataSource.fetchObjects(); + + notifyDelegate( + "displayGroupDidFetchObjects", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, objectList } ); + + setObjectArray( objectList ); + + if ( ( selectsFirstObjectAfterFetch ) && ( displayedObjects.size() > 0 ) ) + { + setSelectionIndexes( new NSArray( new Integer( 0 ) ) ); + } + + return null; + } + + /** + * Convenience to call insertNewObjectAtIndex with the current selection plus one, + * or at the end of the list if there is no selection. + * Returns null to force a page reload. + */ + public Object insert() + { + NSArray indexes = selectionIndexes(); + int size = indexes.count(); + if ( size == 0 ) + { + insertNewObjectAtIndex( displayedObjects.count() ); + } + else + { + insertNewObjectAtIndex( + ((Number)selectedIndexes.objectAtIndex( size-1 )).intValue()+1 ); + } + return null; + } + + /** + * Creates a new object at the specified index. + * Calls insertObjectAtIndex() with the result + * from sending createObject() to the data source. + * @return the newly created object. + */ + public Object insertNewObjectAtIndex ( int anIndex ) + { + Object result = null; + if ( dataSource != null ) + { + result = dataSource.createObject(); + } + if ( result != null ) + { + if ( insertedObjectDefaultValues != null ) + { + Duplicator.writePropertiesForObject( + insertedObjectDefaultValues, result ); + } + insertObjectAtIndex( result, anIndex ); + } + else // create failed + { + if ( delegate() != null ) + { + NSSelector selector = new NSSelector( + "displayGroupCreateObjectFailed", + new Class[] { WODisplayGroup.class, EODataSource.class } ); + if ( selector.implementedByObject( delegate() ) ) + { + try + { + selector.invoke( delegate(), new Object[] { this, dataSource } ); + return result; + } + catch ( Exception exc ) + { + System.err.println( "Error notifying delegate: displayGroupCreateObjectFailed" ); + exc.printStackTrace(); + } + } + } + } + return result; + } + + /** + * Inserts the specified object into the list at + * the specified index. + */ + public void insertObjectAtIndex ( Object anObject, int anIndex ) + { + Object result = notifyDelegate( + "displayGroupShouldInsertObject", + new Class[] { WODisplayGroup.class, Object.class, int.class }, + new Object[] { this, anObject, new Integer(anIndex) } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + return; + } + + updatedObjectIndex = anIndex; + willChange(); + + int i; + + // add to all objects + if ( anIndex == displayedObjects.size() ) + { + allObjects.addObject( anObject ); + } + else // insert before same object + { + Object target = displayedObjects.objectAtIndex( anIndex ); + int targetIndex = indexOf( allObjects, target ); + if ( targetIndex != NSArray.NotFound ) + { + allObjects.insertObjectAtIndex( anObject, targetIndex ); + } + else // should never happen + { + throw new WotonomyException( + "Could not find displayed object in all objects list: " + + target ); + } + } + + // add to displayed objects + displayedObjects.insertObjectAtIndex( anObject, anIndex ); + + if ( dataSource != null ) + { + if ( dataSource instanceof OrderedDataSource ) + { + ((OrderedDataSource)dataSource).insertObjectAtIndex( + anObject, anIndex ); + } + else + { + dataSource.insertObject( anObject ); + } + } + + notifyDelegate( + "displayGroupDidInsertObject", + new Class[] { WODisplayGroup.class, Object.class }, + new Object[] { this, anObject } ); + } + + /** + * Sets contentsChanged to true and notifies all observers. + */ + public void redisplay() + { + willChange(); + } + + /** + * Sets the selection to the next displayed object after the current + * selection. If the last object is selected, or if no object + * is selected, then the first object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return null to force a page reload. + */ + public Object selectNext() + { + int count = displayedObjects.count(); + if ( count == 0 ) return null; + if ( count == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return null; + } + + int i = -1; + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = -1; + + // select next object + i++; + if ( i != displayedObjects.count() ) + { + // set to next object + selectedObject = displayedObjects.objectAtIndex( i ); + } + else // out of range + { + // set to null + selectedObject = displayedObjects.objectAtIndex( 0 ); + } + + selectObject( selectedObject ); + return null; + } + + /** + * Sets the selection to the specified object. + * If the specified object is null or does not exist + * in the list of displayed objects, the selection + * will be cleared. + * @return true if the object was selected. + */ + public boolean selectObject ( Object anObject ) + { + if ( ( anObject == null ) || + ( indexOf( displayedObjects, anObject ) + == NSArray.NotFound ) ) + { + clearSelection(); + return false; + } + + selectObjectsIdenticalTo( new NSArray( new Object[] { anObject } ) ); + return true; + } + + /** + * Sets the selection to the specified objects. + * If the specified list is null or if none of the objects + * in the list exist in the list of displayed objects, the + * selection will be cleared. + * @return true if all specified objects were selected. + */ + public boolean selectObjectsIdenticalTo ( List anObjectList ) + { + // optimization: check for resetting of selection + if ( ( anObjectList != null ) && ( selectedObjects.size() == anObjectList.size() ) ) + { + boolean identical = true; + int size = selectedObjects.size(); + for ( int i = 0; ( i < size ) && identical; i++ ) + { + // compare by reference + if ( anObjectList.get( i ) != selectedObjects.get( i ) ) + { + identical = false; + } + else if ( displayedObjects.indexOfIdenticalObject( + anObjectList.get( i ) ) == NSArray.NotFound ) + { + identical = false; + } + } + if ( identical ) + { + return true; + } + } + + Object result = notifyDelegate( + "displayGroupShouldChangeSelection", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( ( result != null ) && ( Boolean.FALSE.equals( result ) ) ) + { + // need to notify the calling component + // to revert back to the previous selection + selectionChanged = true; + willChange(); + return false; + } + + int i; + selectionChanged = true; + willChange(); + Object o; + selectedObjects.removeAllObjects(); + selectedIndexes.removeAllObjects(); + Iterator it = anObjectList.iterator(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + != NSArray.NotFound ) + { + selectedObjects.addObject( o ); + selectedIndexes.addObject( new Integer( i ) ); + } + } + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + + return true; + } + + /** + * Calls selectObjectsIdenticalTo and if false is returned + * and selectFirstIfNoMatch is true, selects the first object. + */ + public boolean selectObjectsIdenticalToSelectFirstOnNoMatch( + List anObjectList, boolean selectFirstIfNoMatch ) + { + if ( selectObjectsIdenticalTo( anObjectList ) ) + { + return true; + } + if ( selectFirstIfNoMatch ) + { + clearSelection(); + selectNext(); + return true; + } + return false; + } + + /** + * Sets the selection to the previous displayed object before the current + * selection. If the first object is selected, or if no object + * is selected, then the last object becomes selected. + * If multiple items are selected, the first selected item is + * considered the selected item for the purposes of this method. + * Does not call redisplay(). + * @return null to force a page reload. + */ + public Object selectPrevious() + { + int i = displayedObjects.count(); + if ( i == 0 ) return null; + if ( i == 1 ) + { + selectObject( displayedObjects.objectAtIndex( 0 ) ); + return null; + } + + Object selectedObject = selectedObject(); + if ( selectedObject != null ) + { + i = indexOf( displayedObjects, selectedObject ); + } + if ( i == NSArray.NotFound ) i = displayedObjects.count(); + + // select next object + i--; + if ( i < 0 ) + { + // out of range - select last object + i = displayedObjects.count() - 1; + } + + selectObject( displayedObjects.objectAtIndex( i ) ); + return null; + } + + /** + * Returns the currently selected object, or null if + * there is no selection. + */ + public Object selectedObject() + { + if ( selectedObjects.count() == 0 ) + { + return null; + } + return selectedObjects.objectAtIndex( 0 ); + } + + /** + * Returns a read-only List containing all selected objects, if any. + * Returns an empty list if no objects are selected. + */ + public NSArray selectedObjects() + { // System.out.println( "avoided allocation: selectedObjects" ); + return selectedObjectsProxy; + } + + /** + * Returns a read-only List containing the indexes of all selected + * objects, if any. The list contains instances of + * java.lang.Number; call intValue() to retrieve the index. + */ + public NSArray selectionIndexes() + { +// return selectedIndexes; + int i; + NSMutableArray result = new NSMutableArray(); + Enumeration e = selectedObjects.objectEnumerator(); + while ( e.hasMoreElements() ) + { + i = indexOf( displayedObjects, e.nextElement() ); + if ( i != NSArray.NotFound ) + { + result.addObject( new Integer( i ) ); + } + else + { + System.err.println( + "Should never happen: selected objects not in displayed objects" ); + new RuntimeException().printStackTrace( System.err ); + } + } + return result; + } + + /** + * Sets the objects managed by this display group. + * updateDisplayedObjects() is called to filter the + * display objects. The previous selection will be + * maintained if possible. The data source is not + * notified. + */ + public void setObjectArray ( List anObjectList ) + { + if ( anObjectList == null ) anObjectList = new NSArray(); + + Object result = notifyDelegate( + "displayGroupDisplayArrayForObjects", + new Class[] { WODisplayGroup.class, List.class }, + new Object[] { this, anObjectList } ); + if ( result != null ) + { + anObjectList = (List) result; + } + + willChange(); + + NSArray oldSelectedObjects = new NSArray( selectedObjects ); // copy + + // reset allObjects to new list + allObjects.removeAllObjects(); + allObjects.addObjectsFromArray( anObjectList ); + + // update the displayed object list + updateDisplayedObjects(); + + // restore the selection if possible + selectObjectsIdenticalTo( oldSelectedObjects ); + + batchIndex = 0; + displayBatchContainingSelectedObject(); + } + + /** + * Sets the currently selected object, or clears the + * selection if the object is not found or is null. + * Note: it's not clear how this differs from + * selectObject in the spec. It is recommended that + * you call selectObject for now. + */ + public void setSelectedObject ( Object anObject ) + { + selectObject( anObject ); + } + + /** + * Sets the current selection to the specified objects. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + * Note: it's not clear how this differs from + * selectObjectsIdenticalTo in the spec. + * It is recommended that you call that method for now. + */ + public void setSelectedObjects ( List aList ) + { + selectObjectsIdenticalTo( aList ); + } + + /** + * Sets the current selection to the objects at the + * specified indexes. Items in the list are assumed + * to be instances of java.lang.Number. + * The previous selection is cleared, and any objects + * in the display group that are in the specified list + * are then selected. If no items in the specified list + * are found in the display group, then the selection is + * effectively cleared. + */ + public boolean setSelectionIndexes ( List aList ) + { + Object o; + int index; + NSMutableArray objects = new NSMutableArray(); + Iterator it = aList.iterator(); + while ( it.hasNext() ) + { + index = ((Number)it.next()).intValue(); + if ( index < displayedObjects.count() ) + { + o = displayedObjects.objectAtIndex( index ); + if ( o != null ) + { + objects.add( o ); + } + } + } + return selectObjectsIdenticalTo( objects ); + } + + /** + * Applies the qualifier to all objects and sorts + * the results to update the list of displayed objects. + * Observing associations are notified to reflect the changes. + */ + public void updateDisplayedObjects() + { + updatedObjectIndex = -1; + willChange(); + + displayedObjects.removeAllObjects(); + + displayedObjects.addObjectsFromArray( allObjects ); + + // apply qualifier, if any + if ( qualifier != null ) + { + EOQualifier.filterArrayWithQualifier( + displayedObjects, qualifier ); + } + + // apply sort orderings, if any + NSArray orderings = sortOrderings(); + if ( orderings != null ) + { + if ( orderings.count() > 0 ) + { + selectionChanged = true; + willChange(); + EOSortOrdering.sortArrayUsingKeyOrderArray( + displayedObjects, orderings ); + } + } + + // make sure the selectedObjects is a subset of displayedObjects + int i; + Object o; + Iterator it = new LinkedList( selectedObjects ).iterator(); + boolean removeflag = false; + selectedIndexes.removeAllObjects(); + while ( it.hasNext() ) + { + o = it.next(); + if ( ( i = displayedObjects.indexOfIdenticalObject( o ) ) + == NSArray.NotFound ) + { + selectedObjects.removeIdenticalObject( o ); + removeflag = true; + } + else + { + selectedIndexes.addObject( new Integer( i ) ); + } + } + + //Note: it is important to put the + //selectionChanged = true line below remove. + if (removeflag) + { + selectionChanged = true; + willChange(); + + notifyDelegate( + "displayGroupDidChangeSelection", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + notifyDelegate( + "displayGroupDidChangeSelectedObjects", + new Class[] { WODisplayGroup.class }, + new Object[] { this } ); + } + } + + /** + * Returns the index of the changed object. If more than + * one object has changed, -1 is returned. + */ + public int updatedObjectIndex() + { + return updatedObjectIndex; + } + + // getting and setting values in objects + + /** + * Returns a value on the selected object for the specified key. + */ + public Object selectedObjectValueForKey ( String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return null; + return valueForObject( selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * all selected objects. + */ + public boolean setSelectedObjectValue ( + Object aValue, String aKey ) + { + Object selectedObject = selectedObject(); + if ( selectedObject == null ) return false; + return setValueForObject( aValue, selectedObject, aKey ); + } + + /** + * Sets the specified value for the specified key on + * the specified object. Validations may be triggered, + * and error dialogs may appear to the user. + * @return True if the value was set successfully, + * false if the value could not be set and the update + * operation should not continue. + */ + public boolean setValueForObject ( + Object aValue, Object anObject, String aKey ) + { + // notify object's observers: + // this includes us, and will notify our observers + EOObserverCenter.notifyObserversObjectWillChange( anObject ); + + //TODO: if key is null, need to remove old object + // and add new object instead of simply replacing it. + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + ((EOKeyValueCoding)anObject).takeValueForKey( aValue, aKey ); + } + else + { + EOKeyValueCodingSupport.takeValueForKey( anObject, aValue, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return false; + } + + notifyDelegate( + "displayGroupDidSetValueForObject", + new Class[] { WODisplayGroup.class, Object.class, Object.class, String.class }, + new Object[] { this, aValue, anObject, aKey } ); + + return true; + } + + /** + * Calls setValueForObject() for the object at + * the specified index. + */ + public boolean setValueForObjectAtIndex ( + Object aValue, int anIndex, String aKey ) + { + return setValueForObject( + aValue, displayedObjects.objectAtIndex( anIndex ), aKey ); + } + + /** + * Returns the value for the specified key on the specified object. + */ + public Object valueForObject ( Object anObject, String aKey ) + { + // empty string is considered the identity property + if ( aKey == null ) return anObject; + if ( aKey.equals( "" ) ) return anObject; + + try + { + if ( anObject instanceof EOKeyValueCoding ) + { + return ((EOKeyValueCoding)anObject).valueForKey( aKey ); + } + else + { + return EOKeyValueCodingSupport.valueForKey( anObject, aKey ); + } + } + catch ( RuntimeException exc ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", exc.getMessage() } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + throw exc; + } + return null; + } + } + + /** + * Calls valueForObject() for the object at the specified index. + * Returns null if out of bounds. + */ + public Object valueForObjectAtIndex ( int anIndex, String aKey ) + { + if ( displayedObjects.count() <= anIndex ) return null; + Object o = displayedObjects.objectAtIndex( anIndex ); + return valueForObject( o, aKey ); + } + + /** + * Prints out the list of displayed objects. + */ + public String toString() + { + return displayedObjects.toString(); + } + + + /** + * Handles notifications from the data source's editing context, + * looking for InvalidatedAllObjectsInStoreNotification and + * ObjectsChangedInEditingContextNotification, refetching in + * the former case and updating displayed objects in the latter. + * Note: This method is not in the public specification. + */ + public void objectsInvalidatedInEditingContext( NSNotification aNotification ) + { + if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRefetch", + new Class[] { WODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + fetch(); + } + } + else + if ( EOEditingContext.ObjectsChangedInEditingContextNotification + .equals( aNotification.name() ) ) + { + Object result = notifyDelegate( + "displayGroupShouldRedisplay", + new Class[] { WODisplayGroup.class, NSNotification.class }, + new Object[] { this, aNotification } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + int index; + Enumeration e; + boolean didChange = false; + NSDictionary userInfo = aNotification.userInfo(); + + // inserts are ignored + + // mark updated objects as updated + NSArray updates = (NSArray) userInfo.objectForKey( + EOObjectStore.UpdatedKey ); + e = updates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: updated: " + index ); + if ( ! didChange ) + { + didChange = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // treat invalidated objects as updated + NSArray invalidates = (NSArray) userInfo.objectForKey( + EOObjectStore.InvalidatedKey ); + e = invalidates.objectEnumerator(); + while ( e.hasMoreElements() ) + { + index = indexOf( displayedObjects, e.nextElement() ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: invalidated: " + index ); + if ( ! didChange ) + { + didChange = true; + willChange(); + updatedObjectIndex = index; + } + else + { + updatedObjectIndex = -1; + } + } + } + + // remove deletes from display group if they exist + NSArray deletes = (NSArray) userInfo.objectForKey( + EOObjectStore.DeletedKey ); + e = deletes.objectEnumerator(); + Object o; + while ( e.hasMoreElements() ) + { + o = e.nextElement(); + index = indexOf( displayedObjects, o ); + if ( index != NSArray.NotFound ) + { + //System.out.println( "WODisplayGroup: deleted: " + o ); + deleteObjectAtIndexNoNotify( index ); + } + } + + if ( !usesOptimisticRefresh() ) + { + updateDisplayedObjects(); + } + } + } + + } + + // static methods + + /** + * Specifies the default behavior for whether changes + * should be validated immediately for all display groups. + */ + public static boolean + globalDefaultForValidatesChangesImmediately() + { + return globalDefaultForValidatesChangesImmediately; + } + + /** + * Specifies the default string matching format for all + * display groups. + */ + public static String globalDefaultStringMatchFormat() + { + return globalDefaultStringMatchFormat; + } + + /** + * Specifies the default string matching operator for all + * display groups. + */ + public static String globalDefaultStringMatchOperator() + { + return globalDefaultStringMatchOperator; + } + + /** + * Sets the default behavior for validating changes + * for all display groups. + */ + public static void + setGlobalDefaultForValidatesChangesImmediately ( + boolean validatesImmediately ) + { + globalDefaultForValidatesChangesImmediately = + validatesImmediately; + } + + /** + * Sets the default string matching format that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchFormat ( String aFormat ) + { + globalDefaultStringMatchFormat = aFormat; + } + + /** + * Sets the default string matching operator that + * will be used by all display groups. + */ + public static void + setGlobalDefaultStringMatchOperator ( String anOperator ) + { + globalDefaultStringMatchOperator = anOperator; + } + + /** + * Needed because we don't inherit from NSObject. + * Calls EOObserverCenter.notifyObserversObjectWillChange. + */ + protected void willChange() + { + EOObserverCenter.notifyObserversObjectWillChange( this ); + } + + /** + * Returns the index of the specified object in the + * specified NSArray, comparing by value or by reference + * as determined by the private instance variable + * compareByReference. If not found, returns NSArray.NotFound. + */ + private int indexOf( NSArray anArray, Object anObject ) + { + if ( compareByReference ) + { + return anArray.indexOfIdenticalObject( anObject ); + } + else + { + return anArray.indexOf( anObject ); + } + } + + // interface EOObserving + + /** + * Receives notifications of changes from objects that + * are managed by this display group. This implementation + * sets updatedObjectIndex as appropriate. + */ + public void objectWillChange(Object anObject) + { + int index = indexOf( displayedObjects, anObject ); + if ( index != NSArray.NotFound ) + { + updatedObjectIndex = index; + willChange(); + } + } + + // interface EOEditingContext.Editor + + /** + * Called before the editing context begins to save changes. + * This implementation calls endEditing(). + */ + public void editingContextWillSaveChanges( + EOEditingContext anEditingContext ) + { + endEditing(); + } + + /** + * Called to determine whether this editor has changes + * that have not been committed to the object in the context. + * This implementation returns false. + */ + public boolean editorHasChangesForEditingContext( + EOEditingContext anEditingContext ) + { + return false; + } + + // interface EOEditingContext.MessageHandler + + /** + * Called to display a message for an error that occurred + * in the specified editing context. If the delegate allows, + * this implementation writes a message to the standard output. + * Override to customize. + */ + public void editingContextPresentErrorMessage( + EOEditingContext anEditingContext, + String aMessage ) + { + Object result = notifyDelegate( + "displayGroupShouldDisplayAlert", + new Class[] { WODisplayGroup.class, String.class, String.class }, + new Object[] { this, "Error", aMessage } ); + if ( ( result == null ) || ( Boolean.TRUE.equals( result ) ) ) + { + System.out.println( aMessage ); + } + } + + /** + * Called by the specified object store to determine whether + * fetching should continue, where count is the current count + * and limit is the limit as specified by the fetch specification. + * This implementation returns true. Override to customize. + */ + public boolean editingContextShouldContinueFetching( + EOEditingContext anEditingContext, + int count, + int limit, + EOObjectStore anObjectStore ) + { + return true; + } + + /** + * Sends the specified message to the delegate. + * Returns the return value of the method, + * or null if no return value or no delegate + * or no implementation. + */ + private Object notifyDelegate( + String aMethodName, Class[] types, Object[] params ) + { + try + { + Object delegate = delegate(); + if ( delegate == null ) return null; + return NSSelector.invoke( + aMethodName, types, delegate, params ); + } + catch ( NoSuchMethodException e ) + { + // ignore: not implemented + } + catch ( Exception exc ) + { + // log to standard error + System.err.println( + "Error while messaging delegate: " + + delegate + " : " + aMethodName ); + exc.printStackTrace(); + } + + return null; + } + + /** + * DisplayGroups can delegate important decisions to a Delegate. + * Note that DisplayGroup doesn't require its delegates to implement + * this interface: rather, this interface defines the methods that + * DisplayGroup will attempt to invoke dynamically on its delegate. + * The delegate may choose to implement only a subset of the methods + * on the interface. + */ + public interface Delegate + { + /** + * Called when the specified data source fails + * to create an object for the specified display group. + */ + void displayGroupCreateObjectFailed ( + WODisplayGroup aDisplayGroup, + EODataSource aDataSource ); + + /** + * Called after the specified display group's + * data source is changed. + */ + void displayGroupDidChangeDataSource ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after a change occurs in the specified + * display group's selected objects. + */ + void displayGroupDidChangeSelectedObjects ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group's + * selection has changed. + */ + void displayGroupDidChangeSelection ( + WODisplayGroup aDisplayGroup ); + + /** + * Called after the specified display group has + * deleted the specified object. + */ + void displayGroupDidDeleteObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has fetched the specified object list. + */ + void displayGroupDidFetchObjects ( + WODisplayGroup aDisplayGroup, + List anObjectList ); + + /** + * Called after the specified display group + * has inserted the specified object into + * its internal object list. + */ + void displayGroupDidInsertObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called after the specified display group + * has set the specified value for the specified + * object and key. + */ + void displayGroupDidSetValueForObject ( + WODisplayGroup aDisplayGroup, + Object aValue, + Object anObject, + String aKey ); + + /** + * Called by the specified display group to + * determine what objects should be displayed + * for the objects in the specified list. + * @return An NSArray containing the objects + * to be displayed for the objects in the + * specified list. + */ + NSArray displayGroupDisplayArrayForObjects ( + WODisplayGroup aDisplayGroup, + List aList ); + + /** + * Called by the specified display group before + * it attempts to change the selection. + * @return True to allow the selection to change, + * false otherwise. + */ + boolean displayGroupShouldChangeSelection ( + WODisplayGroup aDisplayGroup, + List aSelectionList ); + + /** + * Called by the specified display group before + * it attempts to delete the specified object. + * @return True to allow the object to be deleted + * false to prevent the deletion. + */ + boolean displayGroupShouldDeleteObject ( + WODisplayGroup aDisplayGroup, + Object anObject ); + + /** + * Called by the specified display group before + * it attempts display the specified alert to + * the user. + * @return True to allow the message to be + * displayed, false if you want to handle the + * alert yourself and suppress the display group's + * notification. + */ + boolean displayGroupShouldDisplayAlert ( + WODisplayGroup aDisplayGroup, + String aTitle, + String aMessage ); + + /** + * Called by the specified display group before + * it attempts fetch objects. + * @return True to allow the fetch to take place, + * false to prevent the fetch. + */ + boolean displayGroupShouldFetch ( + WODisplayGroup aDisplayGroup ); + + /** + * Called by the specified display group before + * it attempts to insert the specified object. + * @return True to allow the object to be inserted + * false to prevent the insertion. + */ + boolean displayGroupShouldInsertObject ( + WODisplayGroup aDisplayGroup, + Object anObject, + int anIndex ); + + /** + * Called by the specified display group when + * it receives the specified + * ObjectsChangedInEditingContextNotification. + * @return True to allow the display group to + * update the display (recommended), false + * to prevent the update. + */ + boolean displayGroupShouldRedisplay ( + WODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + /** + * Called by the specified display group when + * it receives the specified + * InvalidatedAllObjectsInStoreNotification. + * @return True to allow the display group to + * refetch (recommended), false to prevent the + * refetch. + */ + boolean displayGroupShouldRefetch ( + WODisplayGroup aDisplayGroup, + NSNotification aNotification ); + + } + +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.5 2003/01/22 23:01:06 mpowers + * Better handling for index out of bounds. + * + * Revision 1.4 2003/01/21 22:27:02 mpowers + * Corrected context id usage. + * Implemented backtracking. + * + * Revision 1.3 2003/01/21 17:54:01 mpowers + * Batch indices are one-based, not zero-based. + * + * Revision 1.2 2003/01/21 14:38:42 mpowers + * Fixed batching. + * + * Revision 1.1 2003/01/18 23:30:42 mpowers + * WODisplayGroup now compiles. + * + * Revision 1.46 2002/10/24 21:15:36 mpowers + * New implementations of NSArray and subclasses. + * + * Revision 1.45 2002/10/24 18:20:20 mpowers + * Because NSArray is read-only, we are returning our internal representations + * to callers of allObjects(), displayedObjects(), and selectedObjects(). + * + * Revision 1.44 2002/08/06 18:20:25 mpowers + * Now posting DisplayGroupWillFetch notifications before fetch. + * Implemented support for usesOptimisticRefresh. + * No longer supporting inserted/updated/deleted lists: not part of spec. + * + * Revision 1.43 2002/05/17 15:01:49 mpowers + * Implemented dynamic lookup of delegate methods so delegates no longer + * need to implement the DisplayGroup.Delegate interface. + * + * Revision 1.42 2002/03/26 21:46:06 mpowers + * Contributing EditingContext as a java-friendly convenience. + * + * Revision 1.41 2002/03/11 03:17:56 mpowers + * Provided control point for coalesced changes. + * + * Revision 1.40 2002/03/05 23:18:28 mpowers + * Added documentation. + * Added isSelectionPaintedImmediate and isSelectionTracking attributes + * to TableAssociation. + * Added getTableAssociation to TableColumnAssociation. + * + * Revision 1.39 2002/02/19 22:26:04 mpowers + * Implemented EOEditingContext.MessageHandler support. + * + * Revision 1.38 2002/02/19 16:37:38 mpowers + * Implemented support for EOEditingContext.Editor + * + * Revision 1.37 2001/12/11 22:17:48 mpowers + * Now properly handling exceptions in valueForObject. + * No longer trying to retain selection based only on index. + * + * Revision 1.36 2001/11/08 21:42:00 mpowers + * Now we know what to do with shouldRefetch and shouldRedisplay. + * + * Revision 1.35 2001/11/04 18:26:58 mpowers + * Fixed bug where exceptions were not properly reported when updating + * a value and the display group did not have a delegate. + * + * Revision 1.34 2001/11/02 20:59:36 mpowers + * Now correctly ensuring selected objects are a subset of displayed objects. + * + * Revision 1.33 2001/10/30 22:56:45 mpowers + * Added support for EOQualifier. + * + * Revision 1.32 2001/10/23 22:27:53 mpowers + * Now running at ObserverPrioritySixth. + * + * Revision 1.31 2001/10/23 18:45:05 mpowers + * Rolling back changes. + * + * Revision 1.28 2001/08/22 19:23:41 mpowers + * No longer asserting objects in all objects list. + * + * Revision 1.27 2001/07/30 16:17:01 mpowers + * Minor code cleanup. + * + * Revision 1.26 2001/07/10 22:49:07 mpowers + * Fixed bug in optimization for selectObjectsIdenticalTo (found by Dongzhi). + * + * Revision 1.25 2001/06/19 15:40:21 mpowers + * Now only changing the selection if the new selection is different + * from the old. + * + * Revision 1.24 2001/05/24 17:36:15 mpowers + * Fixed problem with selectedObjectsIdenticalTo: it was using compare + * by value instead of compare by reference. + * + * Revision 1.23 2001/05/18 21:09:19 mpowers + * Now throwing exceptions if the delegate cannot handle error from update. + * + * Revision 1.22 2001/05/14 15:26:12 mpowers + * Now checking for null delegate before and after selection change. + * + * Revision 1.21 2001/05/08 18:47:34 mpowers + * Minor fixes for d3. + * + * Revision 1.20 2001/04/29 22:02:45 mpowers + * Work on id transposing between editing contexts. + * + * Revision 1.19 2001/04/13 16:38:09 mpowers + * Alpha3 release. + * + * Revision 1.18 2001/04/03 20:36:01 mpowers + * Fixed refaulting/reverting/invalidating to be self-consistent. + * + * Revision 1.17 2001/03/29 03:31:13 mpowers + * No longer using Introspector. + * + * Revision 1.16 2001/02/27 03:32:18 mpowers + * Implemented default values for new objects. + * + * Revision 1.15 2001/02/27 02:11:17 mpowers + * Now throwing exception when cloning fails. + * Removed debugging printlns. + * + * Revision 1.14 2001/02/26 22:41:51 mpowers + * Implemented null placeholder classes. + * Duplicator now uses NSNull. + * No longer catching base exception class. + * + * Revision 1.13 2001/02/26 15:53:22 mpowers + * Fine-tuning notification firing. + * Child display groups now update properly after parent save or invalidate. + * + * Revision 1.12 2001/02/22 20:55:06 mpowers + * Implemented notification handling. + * + * Revision 1.11 2001/02/21 20:40:42 mpowers + * setObjectArray now falls back to index when trying to retain the + * same selection. + * + * Revision 1.10 2001/02/20 16:38:55 mpowers + * MasterDetailAssociations now observe their controlled display group's + * objects for changes to that the parent object will be marked as updated. + * Before, only inserts and deletes to an object's items are registered. + * Also, moved ObservableArray to package access. + * + * Revision 1.9 2001/02/17 17:23:49 mpowers + * More changes to support compiling with jdk1.1 collections. + * + * Revision 1.8 2001/02/17 16:52:05 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.7 2001/01/24 16:35:37 mpowers + * Improved documentation on TreeAssociation. + * SortOrderings are now inherited from parent nodes. + * Updates after sorting are still lost on TreeController. + * + * Revision 1.6 2001/01/24 14:23:05 mpowers + * Added support for OrderedDataSource. + * + * Revision 1.5 2001/01/12 17:21:37 mpowers + * Implicit creation of EOSortOrderings now happens in setSortOrderings. + * + * Revision 1.4 2001/01/11 20:34:26 mpowers + * Implemented EOSortOrdering and added support in framework. + * Added header-click to sort table columns. + * + * Revision 1.3 2001/01/10 22:49:44 mpowers + * Implemented similarly named selection methods instead of + * throwing exceptions. + * + * Revision 1.2 2001/01/09 20:12:52 mpowers + * Moved inner classes to package access. + * + * Revision 1.1.1.1 2000/12/21 15:48:20 mpowers + * Contributing wotonomy. + * + * Revision 1.21 2000/12/20 16:25:39 michael + * Added log to all files. + * + * Revision 1.20 2000/12/15 15:04:42 michael + * Added doc. + * + * Revision 1.19 2000/12/11 13:32:48 michael + * Finish the much better TreeAssociation implementation. + * TreeAssociation now has no gui dependencies. + * + * Revision 1.18 2000/12/05 17:41:46 michael + * Broadcasts selection change after delegate refuses selection change + * so the initiating association gets refreshed. + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java new file mode 100644 index 0000000..6e449c3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WODynamicElement.java @@ -0,0 +1,208 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* The base class for dynamic WOElements. Dynamic elements +* are expected to do something useful with user-entered data +* in the request and with any binding associations with the +* context's current WOComponent. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WODynamicElement + extends WOElement +{ + protected String name; + protected WOElement rootElement; + protected NSDictionary associations; + + /** + * The default constructor. + */ + protected WODynamicElement () + { + name = null; + associations = new NSMutableDictionary(); + rootElement = null; + } + + /** + * Required constructor specifying the class name of the component, + * a map of associations, and the root element of the tree that + * contains this element (which may be null). The map keys + * correspond to properties of this element, and the values are + * associations to be applied to the context's current component. + */ + public WODynamicElement ( + String aName, NSDictionary anAssociationMap, WOElement aRootElement) + { + this(); + name = aName; + associations = anAssociationMap; + rootElement = aRootElement; + } + + /** + * Package access only. Called to initialize the component with + * the proper context before the start of the request-response cycle. + * If the context has a current component, that component becomes + * this component's parent. + */ + void ensureAwakeInContext (WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.ensureAwakeInContext( aContext ); + } + } + + /** + * Use this method to get a map with the properties that start with + * a question mark. These are supposed to go at the end of a URL, and it is + * very useful for components that generate URLs, specially with direct + * actions. + * @param c The component where the values of the properties have to be + * retrieved from. + */ + Map urlFields(WOComponent c) { + HashMap map = new HashMap(associations.count()); + Enumeration enumeration = associations.keyEnumerator(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (key.charAt(0) == '?') { + map.put(key.substring(1), valueForProperty(key, c)); + } + } + return map; + } + + /** Convenience method for getting the value of an association. */ + Object valueForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + if (a != null) + return a.valueInComponent(c); + return null; + } + + /** Convenience method for getting the string value of an association. */ + String stringForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + Object result = null; + if (a != null) result = a.valueInComponent(c); + if ( result == null ) return null; + return result.toString(); + } + + /** Convenience method for getting the string value of an association. */ + boolean booleanForProperty(String key, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + Object result = null; + if (a != null) result = a.valueInComponent(c); + if ( result == null ) return false; + if ( result.toString().toLowerCase().equals( "true" ) ) return true; + return Boolean.TRUE.equals( result ); + } + + /** Convenience method for setting the value of an association. */ + void setValueForProperty(String key, Object value, WOComponent c) { + WOAssociation a = (WOAssociation)associations.objectForKey(key); + if ( a != null && a.isValueSettable() ) + a.setValue(value, c); + } + + /** this method composes a String suitable for inclusion inside a HTML tag. It includes + the key-value pairs of all the associations not mentioned in the standardProperties + parameter. This is very useful for including extra properties in tags without having to worry + if the HTML specification has changed or if non-standard tags are being used. + @param c The component where the associations' values should be retrieved from. + @param standardProperties An array of Strings with all the associations that should be + excluded from the resulting string. */ + String additionalHTMLProperties(WOComponent c, NSArray standardProperties) { + Enumeration enumeration = associations.keyEnumerator(); + StringBuffer buf = new StringBuffer(); + while (enumeration.hasMoreElements()) { + String key = (String)enumeration.nextElement(); + if (!(standardProperties.containsObject(key) || key.charAt(0)=='?')) { + buf.append(' '); + buf.append(key); + buf.append("=\""); + buf.append(valueForProperty(key, c)); + buf.append('\"'); + } + } + return buf.toString(); + } + + /** + * This method is called to retrieve user-entered data from + * the request. WOElements should retrieve data from the + * request based on their elementID and set values in the + * context's current WOComponent, typically those values that + * are associated with the element in the binding. This + * implementation does nothing. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + + } + + /** + * This method is called on all objects and elements of the + * application until a non-null value is returned. + * WOElements should first check to see if they are the + * target of an action by checking the WOContext's senderID + * to see if it matches this element's elementID. + * If this element is the target, it should perform an + * appropriate action on the context's current WOComponent, + * usually the action specified in the binding, and return + * the result of that action. This implementation returns null. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return null; + } + + /** + * This method is called on all elements of the content tree + * to build a response to a user request. The message should + * be forwarded to any child elements so that the entire tree + * is traversed. This implementation does nothing. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + // does nothing + } + + public WOResponse generateResponse() + { + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java new file mode 100644 index 0000000..11944d3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOElement.java @@ -0,0 +1,97 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.Serializable; + +import net.wotonomy.foundation.NSDictionary; + +/** +* This class represents a static or dynamic portion of the +* content returned to a request. Each request walks a tree +* of WOElements to generate a response. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WOElement implements WOActionResults, Serializable +{ + NSDictionary associations; + + /** + * Default constructor. Performs necessary initialization. + */ + public WOElement() + { + } + + /** + * This method is called to retrieve user-entered data from + * the request. WOElements should retrieve data from the + * request based on their elementID and set values in the + * context's current WOComponent, typically those values that + * are associated with the element in the binding. This + * implementation does nothing. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + // does nothing + } + + /** + * This method is called on all objects and elements of the + * application until a non-null value is returned. + * WOElements should first check to see if they are the + * target of an action by checking the WOContext's senderID + * to see if it matches this element's elementID. + * If this element is the target, it should perform an + * appropriate action on the context's current WOComponent, + * usually the action specified in the binding, and return + * the result of that action. This implementation returns null. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return null; + } + + /** + * This method is called on all elements of the content tree + * to build a response to a user request. The message should + * be forwarded to any child elements so that the entire tree + * is traversed. This implementation does nothing. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + // does nothing + } + + /** + * Package access only. Called to initialize the component with + * the proper context before the start of the request-response cycle. + * If the context has a current component, that component becomes + * this component's parent. + */ + void ensureAwakeInContext (WOContext aContext) + { + // does nothing + } + + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java new file mode 100644 index 0000000..887d1a3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOForm.java @@ -0,0 +1,124 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* Implements a FORM element with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOForm extends WODynamicElement { + + public WOForm() { + super(); + } + + public WOForm(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public String href(WOContext c) { + return (String)valueForProperty("href", c.component()); + } + + public String directActionName(WOContext c) { + return (String)valueForProperty("directAction", c.component()); + } + + public String directActionClass(WOContext c) { + return (String)valueForProperty("actionClass", c.component()); + } + + public boolean multipleSubmit(WOContext c) { + return booleanForProperty("multipleSubmit", c.component()); + } + + public void takeValuesFromRequest ( + WORequest aRequest, WOContext aContext) + { + if ( rootElement != null ) + { + rootElement.takeValuesFromRequest( aRequest, aContext ); + } + } + + public void appendToResponse(WOResponse r, WOContext c) { + //Append the opening tag + r.appendContentString(" 0) + r.appendContentString(link); + r.appendContentString(">"); + //Notify that we're inside a form now + c.setInForm(true); + //Append the inner template + rootElement.appendToResponse(r, c); + //Close the tag + r.appendContentString(""); +// c.deleteLastElementIDComponent(); + c.setInForm(false); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + //We only process the request if it's not a multipleSubmit (otherwise we leave it to the buttons) + if (!multipleSubmit(c) + && c.elementID().equals(c.senderID()) + && associations.objectForKey("action") != null ) { + return (WOActionResults)valueForProperty("action", c.component()); + } + WOActionResults res = rootElement.invokeAction(r, c); + return res; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java new file mode 100644 index 0000000..30dd4bc --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOFrame.java @@ -0,0 +1,60 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +public class WOFrame extends WODynamicElement { + + public WOFrame() { + super(); + } + + public WOFrame(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + public String frameName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public String url(WOContext c) { + //Check if the href property is set + String href = stringForProperty("href", c.component()); + if (href != null) + return href; + href = stringForProperty("pageName", c.component()); + if (href != null || associations.objectForKey("action") != null) { //write this component's URL + return c.componentActionURL(); + } + href = stringForProperty("directActionName", c.component()); + if (href != null) { //compose the direct action URL + String fullActionName = stringForProperty("actionClass", c.component()); + if (fullActionName != null) + fullActionName = fullActionName + "/" + href; + else + fullActionName = href; + return c.directActionURLForActionNamed(fullActionName, + urlFields(c.component())); + } + //Coded needed here to support filename/framework and data/mimeType. + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(" 0) + r.appendContentString(moreFields); + r.appendContentString(">"); + } + +} \ No newline at end of file diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java new file mode 100644 index 0000000..9af5460 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericContainer.java @@ -0,0 +1,61 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** + * Used to dynamically generate HTML containers (elements with opening and closing tags and something in between). + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOGenericContainer extends WOGenericElement { + + public WOGenericContainer() { + super(); + } + + public WOGenericContainer(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void appendToResponse(WOResponse r, WOContext c) { + super.appendToResponse(r, c); + rootElement.appendToResponse(r, c); + r.appendContentString(""); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + super.takeValuesFromRequest( r, c ); + rootElement.takeValuesFromRequest(r, c); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + WOActionResults result = super.invokeAction( r, c ); + if ( result == null ) + { + result = rootElement.invokeAction(r, c); + } + return result; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java new file mode 100644 index 0000000..8894428 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOGenericElement.java @@ -0,0 +1,95 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** + * Used to generate any HTML element dynamically. It only creates the opening tag without a closing tag. + * To generate HTML elements that have opening and closing tags, as well as some content in between, + * use WOGenericContainer instead. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOGenericElement extends WODynamicElement { + + static NSArray bindings = new NSArray( new Object[] + { "elementName", "omitTags", "elementID", "otherTagString", + "formValue", "formValues", "invokeAction" } ); + + public WOGenericElement() { + super(); + } + + public WOGenericElement(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public String elementName(WOContext c) { + String x = (String)valueForProperty("elementName", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if ( c.elementID().equals( c.senderID() ) ) + { + Object value; + value = r.formValueForKey( c.elementID() ); + setValueForProperty( "formValue", value, c.component() ); + value = r.formValuesForKey( c.elementID() ); + setValueForProperty( "formValues", value, c.component() ); + } + } + + public WOActionResults invokeAction(WORequest r, WOContext c) + { + WOActionResults result = null; + String action = stringForProperty( "invokeAction", c.component() ); + if ( action != null && c.elementID().equals( c.senderID() ) ) + { + result = c.component().performAction( action ); + } + return result; + } + + public void appendToResponse(WOResponse r, WOContext c) { + WOComponent component = c.component(); + if ( !booleanForProperty( "omitTags", component ) ) + { + r.appendContentString("<"); + r.appendContentString(elementName(c)); + String other = stringForProperty( "otherTagString", component ); + if ( other != null ) + { + r.appendContentString( " " ); + r.appendContentString( other ); + } + String add = additionalHTMLProperties(component, bindings); + if (add.length() > 0) + r.appendContentString(add); + r.appendContentString(">"); + } + setValueForProperty( "elementID", c.elementID(), component ); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java new file mode 100644 index 0000000..c5d5711 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHiddenField.java @@ -0,0 +1,42 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** +* Used to dynamically generate a hidden field within a form. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOHiddenField extends WOTextField { + + public WOHiddenField() { + super(); + } + + public WOHiddenField(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java new file mode 100644 index 0000000..d0f3ff7 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOHyperlink.java @@ -0,0 +1,249 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import java.util.Iterator; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOHyperlink renders a dynamically generated hyperlink in the output. + * Bindings are: + *
      + *
    • string: a string to be included between the hyperlink tags (optional).
    • + *
    • escapeHTML: a property returning a value convertable to a Boolean + * indicating whether the any html characters in the output should be + * escaped so they are shown as html characters rather than interpreted + * as html.
    • + *
    • href: The URL that the hyperlink should point to.
    • + *
    • pageName: The name of the WOComponent that the hyperlink should point to.
    • + *
    • directActionName: The name of the direct action to call when the link is activated.
    • + *
    • actionClass: The name of the WODirectAction subclass where the direct action resides.
    • + *
    • anchorName: The name of the link, for anchor tags.
    • + *
    • action: A pointer to a method on the component that contains this element. If the link is activated, + * the method will be called. + *
    • ref: The name of the anchor to go to inside the resulting page.
    • + *
    + * + * The href, pageName and directActionName/actionClass and name properties are mutually exclusive and you should + * only use at most one of them simultaneously. + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOHyperlink extends WODynamicElement { + + protected String string; + protected String href; + protected String pageName; + protected String directActionName; + protected String actionClass; + protected String action; + protected boolean escapeHTML; + protected String anchorName; + protected String ref; + + protected WOHyperlink() { + super(); + } + + public WOHyperlink(String aName, NSDictionary aMap, WOElement aRootElement) { + super(aName, aMap, aRootElement); + escapeHTML = true; + } + + public void setString(String value) { + string = value; + } + public String string() { + return string; + } + + public void setHref(String value) { + href = value; + } + public String href() { + return href; + } + + public void setAnchorName(String value) { + anchorName = value; + } + public String anchorName() { + return anchorName; + } + + public void setPageName(String value) { + pageName = value; + } + public String pageName() { + return pageName; + } + + public void setDirectActionName(String value) { + directActionName = value; + } + public String directActionName() { + return directActionName; + } + + public void setActionClass(String value) { + actionClass = value; + } + public String actionClass() { + return actionClass; + } + + /** Sets the escapeHTML property. */ + public void setEscapeHTML(boolean escape) { + escapeHTML = escape; + } + + /** If true, inserts escape codes in to the string string so + * that HTML special characters (greater-than, less-than, etc.) + * appear correctly. If false, those characters will get + * interpreted by the browser. Defaults to true. + */ + public boolean escapeHTML() { + return escapeHTML; + } + + public String actionURL(WOContext c) { + //Check if the href property is set + if (href() != null) { + return href(); + } else if (pageName() != null || associations.objectForKey("action") != null) { //write this component's URL + StringBuffer retval = new StringBuffer(c.componentActionURL()); + Map addFields = urlFields(c.component()); + if (addFields.size() > 0) { + Iterator enumeration = addFields.keySet().iterator(); + retval.append('?'); + while (enumeration.hasNext()) { + String encoding = c.response() != null ? c.response().contentEncoding() : c.request().contentEncoding(); + String key = (String)enumeration.next(); + try { + retval.append(java.net.URLEncoder.encode(key, encoding)); + } catch (java.io.UnsupportedEncodingException ex) { + retval.append(key); + } + retval.append("="); + try { + retval.append(java.net.URLEncoder.encode(addFields.get(key).toString(), encoding)); + } catch (java.io.UnsupportedEncodingException e) { + retval.append(addFields.get(key).toString()); + } + if (enumeration.hasNext()) + retval.append('&'); + } + } + return retval.toString(); + } else if (directActionName() != null) { //compose the direct action URL + String fullActionName = null; + if (actionClass() != null ) + fullActionName = actionClass() + "/" + directActionName(); + else + fullActionName = directActionName(); + return c.directActionURLForActionNamed(fullActionName, urlFields(c.component())); + } + return null; + } + + protected void pullValuesFromParent(WOComponent c) { + string = stringForProperty("string", c); + href = stringForProperty("href", c); + pageName = stringForProperty("pageName", c); + directActionName = stringForProperty("directActionName", c); + actionClass = stringForProperty("actionClass", c); + //action = stringForProperty("action", c); + escapeHTML = booleanForProperty("escapeHTML", c); + anchorName = stringForProperty("anchorName", c); + ref = stringForProperty("ref", c); + } + + public void appendToResponse(WOResponse r, WOContext c) { + pullValuesFromParent( c.component() ); + r.appendContentString(""); + //Append the string if present + if (string() != null) { + if (escapeHTML()) + r.appendContentHTMLString(string()); + else + r.appendContentString(string()); + } + //If there is a template, call appendToResponse on it + if (rootElement != null) { + rootElement.appendToResponse(r, c); + } + //Close the tag + r.appendContentString(""); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + System.out.println("invoke action with elementID=" + c.elementID() + " senderID=" + c.senderID()); + //Check if this element is the target + if (c.senderID().equals(c.elementID())) { + if (pageName() != null) + { + return WOApplication.application().pageWithName(pageName(), r); + } + else + { + WOAssociation ass = (WOAssociation) associations.objectForKey("action"); + if ( ass != null && ass.path != null ) //?? + return (WOActionResults)c.component().performAction( ass.path ); + } + } + return null; + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + System.out.println("takeValuesFromRequest elementID=" + c.elementID() + " senderID=" + c.senderID()); + super.takeValuesFromRequest(r, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java new file mode 100644 index 0000000..2673cd1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImage.java @@ -0,0 +1,150 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +/** +* WOImage renders a dynamically generated IMG tag. The URL for the image SRC can be +* static, or it can be generated dynamically to return a NSData object (with a mime-type) +* or even return the contents of a file (not implemented yet). +* +* Bindings are: +*
    • src: A static URL for the image source.
    • +*
    • data: A NSData object with the image content. Must be used with mimeType.
    • +*
    • mimeType: The MIME type for the image data. Can be used with filename or data bindings.
    • +*
    • filename: The path to a file containing an image.
    • +*
    • framework: The optional framework from whence the image should be retrieved (used in conjunction with filename).
    • +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOImage extends WODynamicElement { + + protected String src; + protected String filename; + protected String framework; + protected NSData data; + protected String mimeType; + + protected WOImage() { + super(); + } + + public WOImage(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setSrc(String value) { + src = value; + } + public String src() { + return src; + } + + public void setFilename(String value) { + filename = value; + } + + public String filename() { + return filename; + } + + public void setFramework(String value) { + framework = value; + } + public String framework() { + return framework; + } + + public void setData(NSData value) { + data = value; + } + public NSData data() { + return data; + } + + public void setMimeType(String value) { + mimeType = value; + } + public String mimeType() { + return mimeType(); + } + + public String sourceURL(WOContext c) { + if (associations.objectForKey("src") != null) + return (String)valueForProperty("src", c.component()); + if (associations.objectForKey("data") != null) { + return c.componentActionURL(); + } + if (associations.objectForKey("filename") != null) { + WOComponent component = c.component(); + + String framework = stringForProperty("framework", component); + String filename = stringForProperty( "filename", component ); + if ( filename != null && framework == null ) + { + if ( filename.startsWith("/" ) ) + { + int i = filename.lastIndexOf( "/" ); + if ( i > 0 ) + { + framework = filename.substring( 0, i ); + if ( i < filename.length() ) + { + filename = filename.substring( i+1 ); + } + } + } + else + { + // just until we figure out how we're handling bundles/localization + framework = component.frameworkName(); + if ( framework != null ) + { + framework = framework + '/' + component.name() + ".wo"; + } + else + { + framework = '/' + component.name() + ".wo"; + } + } + } + return WOApplication.application().resourceManager().urlForResourceNamed( + filename, framework, c.request().browserLanguages(), c.request() ); + } + return "NO SOURCE"; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(""); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + if (c.senderID().equals(c.elementID())) { + Object data = valueForProperty("data", c.component()); + if (data instanceof byte[]) data = new NSData( (byte[]) data ); + if (data instanceof NSData) { + String mt = stringForProperty("mimeType", c.component()); + if (mt == null) + throw new IllegalArgumentException("WOImage: No mimeType specified for data."); + WOResponse img = new WOResponse(); + img.setContent((NSData)data); + img.setHeader(mt, "content-type"); + return img; + } else if (filename() != null) { + //will this thing use frameworks, or regular JAR files with resources? + // mp: both/either, depending on which resource manager implementation + } + } + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java new file mode 100644 index 0000000..7c9f22e --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOImageButton.java @@ -0,0 +1,58 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** + * WOImageButton renders a dynamically generated IMG tag or an INPUT tag, depending on whether the + * element is inside a WOForm (in which case an INPUT of type IMAGE is generated) or not (in which + * case the equivalent of a WOActiveImage) + * + * Bindings are: + *
        + *
      • src: A static URL for the image source.
      • + *
      • data: A NSData object with the image content. Must be used with mimeType.
      • + *
      • mimeType: The MIME type for the image data. Can be used with filename or data bindings.
      • + *
      • filename: The path to a file containing an image.
      • + *
      • framework: The framework where the image should be retrieved from (used in conjunction with filename).
      • + * + * @author ezamudio@nasoft.com + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOImageButton extends WOImage { + + protected WOImageButton() { + super(); + } + + public WOImageButton(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public String buttonName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + public void appendToResponse(WOResponse r, WOContext c) { + if (c.isInForm()) { + //generate an INPUT + r.appendContentString(""); + } else { + //generate a WOActiveImage + new WOActiveImage(name, associations, rootElement).appendToResponse(r, c); + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java new file mode 100644 index 0000000..a8c7daa --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOInput.java @@ -0,0 +1,102 @@ + +package net.wotonomy.web; + +import java.text.DecimalFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +public abstract class WOInput extends WODynamicElement { + + public WOInput() { + super(); + } + + public WOInput(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected abstract String inputType(); + protected abstract Object value(WOContext c); + + protected NSMutableArray additionalAttributes() { + return new NSMutableArray(new Object[]{ + "disabled", "type", "value", "name" + }); + } + + public String inputName(WOContext c) { + String x = (String)valueForProperty("name", c.component()); + if (x != null) + return x; + return c.elementID(); + } + + protected boolean disabled(WOContext c) { + return booleanForProperty("disabled", c.component()); + } + + protected void appendExtras(WOResponse r, WOContext c) { + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(" 0) + r.appendContentString(moreFields); + appendExtras(r, c); + if (disabled(c)) { + r.appendContentString(" DISABLED"); + } + r.appendContentString(">"); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + if (disabled(c)) + return; + Object val = r.formValueForKey(inputName(c)); + WOAssociation va = (WOAssociation)associations.objectForKey("value"); + if (val != null && va != null && va.isValueSettable()) + setValueForProperty("value", val, c.component()); + } + + /** Formats a value as a date or number. Checks for + * numberformat or dateformat associations; if one of them + * exists, the value is formatter using the specified pattern. + * @param value The value to format. + * @return The original object, or a date or number if the + * receiver has a numberformat or dateformat association. + */ + protected Object formattedValue(Object value, WOComponent c) { + //Format the value in case of number + String pattern = (String)valueForProperty("numberformat", c); + if (pattern != null) { + DecimalFormat fmt = new DecimalFormat(pattern); + try { + return fmt.parse(value.toString()); + } catch (ParseException e) { + return value; + } + } + //Format the value in case of date + pattern = (String)valueForProperty("dateformat", c); + if (pattern != null) { + SimpleDateFormat fmt = new SimpleDateFormat(pattern); + try { + return fmt.parse(value.toString()); + } catch (ParseException e) { + return value; + } + } + return value; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java new file mode 100644 index 0000000..eccc489 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMailDelivery.java @@ -0,0 +1,54 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.List; + +/** +* A pure java implementation of WOMailDelivery. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WOMailDelivery +{ + private static WOMailDelivery sharedInstance; + + protected WOMailDelivery () + { + } + + public static WOMailDelivery sharedInstance () + { + if ( sharedInstance == null ) + { +// sharedInstance = new WOMailDelivery(); + } + return sharedInstance; + } + + public abstract String composePlainTextEmail ( + String aSender, List aToList, List aCcList, + String aSubject, String aMessage, boolean sendImmediately ); + public abstract String composeComponentEmail ( + String aSender, List aToList, List aCcList, + String aSubject, WOComponent aComponent, boolean sendImmediately ); + public abstract void sendEmail (String aMailMessage); +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java new file mode 100644 index 0000000..be76be1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOMessage.java @@ -0,0 +1,333 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WOResponse. +* +* @author ezamudio@nasoft.com +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOMessage { + + protected String _contentEncoding = "ISO8859_1"; + protected NSMutableDictionary _headers = new NSMutableDictionary(); + protected NSMutableDictionary _cookies = new NSMutableDictionary(); + private NSMutableDictionary _userInfo = new NSMutableDictionary(); + protected NSMutableData _contentData = new NSMutableData(); + + public WOMessage() { + super(); + } + + /** + * Sets the content encoding for the response. + */ + public void setContentEncoding (String encoding) + { + _contentEncoding = encoding; + } + + /** + * Gets the current content encoding for the response. + */ + public String contentEncoding () { + return _contentEncoding; + } + + /** + * Sets the specified array of values as headers under + * the specified key. + */ + public void setHeaders (NSArray headerArray, String aKey) { + _headers.setObjectForKey( headerArray, aKey ); + } + + /** + * Sets the specified header value for the specified key. + */ + public void setHeader (String aValue, String aKey) { + _headers.setObjectForKey( new NSArray( aValue ), aKey ); + } + + /** + * Returns an array of all the header keys that have been + * set in the response. + */ + public NSArray headerKeys () { + return _headers.allKeys(); + } + + /** + * Returns an array of all the header values for the specified key, + * or null if the key does not exist. + */ + public NSArray headersForKey (String aKey) { + return (NSArray)_headers.objectForKey( aKey ); + } + + /** + * Returns one header value for the specified key. + * Provided as a convenience, since most header keys + * will have a single value. + */ + public String headerForKey (String aKey) { + NSArray values = (NSArray)_headers.objectForKey( aKey ); + if ( values != null && values.count() > 0 ) { + return values.objectAtIndex( 0 ).toString(); + } + return null; + } + + /** + * Sets the content of the response to the bytes represented + * by the specified data object. + */ + public void setContent(NSData aData) { + _contentData.setData( aData ); + setHeader(Integer.toString(aData.length()), "content-length"); + } + + /** + * Retrieves the current content of the response. + */ + public NSData content() { + return _contentData; + } + + /** + * Sets the current user info dictionary. These values + * are for application-specific uses and are available to + * other actions and components in the request-response cycle. + */ + public void setUserInfo (NSDictionary aDict) { + _userInfo = new NSMutableDictionary( aDict ); + } + + /** + * Gets the current user info dictionary. These values + * are for application-specific uses are are available to + * other actions and components in the request-response cycle. + */ + public NSDictionary userInfo () { + return new NSDictionary(_userInfo); + } + + /** + * Appends the bytes in the specified data object to the response. + */ + public void appendContentData (NSData aData) + { + _contentData.appendData( aData ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified byte to the response. + */ + public void appendContentCharacter (char character) { + _contentData.appendByte((byte)character); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string to the response. + * Any special characters will not be escaped. + * The string will be encoded in the current content encoding. + */ + public void appendContentString (String aString) + { + _contentData.appendData( new NSData( aString.getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string containing HTML to the response. + * Any special characters will be escaped appropriately. + * The string will be encoded in the current content encoding. + */ + public void appendContentHTMLString (String aString) + { + _contentData.appendData( + new NSData( stringByEscapingHTMLString( + aString ).getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Appends the specified string containing HTML to the response. + * Any special characters will be escaped appropriately. + * This method escapes tabs and new-line characters as well. + * The string will be encoded in the current content encoding. + */ + public void appendContentHTMLAttributeValue (String aString) + { + _contentData.appendData ( + new NSData( stringByEscapingHTMLAttributeValue( + aString ).getBytes() ) ); + setHeader(Integer.toString(_contentData.length()), "content-length"); + } + + /** + * Adds the specified cookie to the response. + */ + public void addCookie (WOCookie aCookie) + { + _cookies.setObjectForKey( aCookie, aCookie.name() ); + } + + /** + * Removes the specified cookie from the response. + */ + public void removeCookie (WOCookie aCookie) + { + _cookies.removeObjectForKey( aCookie.name() ); + } + + /** + * Returns an array of cookies currently being sent with the response. + * Contains whatever cookies have previously been set in this response. + */ + public NSArray cookies () + { + return _cookies.allValues(); + } + + /** + * Sets the HTTP version header in the response. + */ + public void setHTTPVersion (String aString) + { + setHeader( aString, "Protocol"); + } + + /** + * Gets the current HTTP version header for the response. + * Because servlet responses do not allow read access + * to headers, this method returns null if setHTTPVersion + * has not been called. + */ + public String httpVersion () + { + return headerForKey( "Protocol" ); + } + + /** + * Returns a sting containing the contents of the specified + * string after escaping all special HTML characters. + */ + public static String stringByEscapingHTMLString + (String aString) + { + int len = aString.length(); + StringBuffer result = new StringBuffer(); + char[] buf = new char[ len ]; + aString.getChars( 0, len, buf, 0 ); + for ( int i = 0; i < len; i++ ) + { + if ( buf[i] == '&' ) + { + result.append( "&" ); + } + else + if ( buf[i] == '\\' ) + { + result.append( """ ); + } + else + if ( buf[i] == '<' ) + { + result.append( "<" ); + } + else + if ( buf[i] == '>' ) + { + result.append( ">" ); + } + else + { + result.append( buf[i] ); + } + } + return result.toString(); + } + + /** + * Returns a sting containing the contents of the specified + * string after escaping all special HTML characters. + * This method escapes tabs and new-line characters as well. + */ + public static String stringByEscapingHTMLAttributeValue + (String aString) + { + int len = aString.length(); + StringBuffer result = new StringBuffer(); + char[] buf = new char[ len ]; + aString.getChars( 0, len, buf, 0 ); + for ( int i = 0; i < len; i++ ) + { + if ( buf[i] == '&' ) + { + result.append( "&" ); + } + else + if ( buf[i] == '\\' ) + { + result.append( """ ); + } + else + if ( buf[i] == '<' ) + { + result.append( "<" ); + } + else + if ( buf[i] == '>' ) + { + result.append( ">" ); + } + else + if ( buf[i] == '\t' ) + { + result.append( " " ); + } + else + if ( buf[i] == '\n' ) + { + result.append( " " ); + } + else + if ( buf[i] == '\r' ) + { + result.append( " " ); + } + else + { + result.append( buf[i] ); + } + } + return result.toString(); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java new file mode 100644 index 0000000..40ee618 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOParentElement.java @@ -0,0 +1,178 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.Iterator; +import java.util.List; + +import net.wotonomy.foundation.NSMutableArray; + +/** +* This class represents a parent node in an element tree. +* It has no content in itself, and exists only to forward +* messages to each of its children, in turn. +* Package access only, as it is not in the specification. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +class WOParentElement extends WOElement +{ + NSMutableArray children; + + /** + * Default constructor. + */ + public WOParentElement() + { + children = new NSMutableArray(); + } + + /** + * Returns an element with the specified children. + */ + public WOParentElement( List childElements ) + { + this(); + children.addAll( childElements ); + } + + /** + * Package access only. Called to initialize the component with + * the proper context before the start of the request-response cycle. + * If the context has a current component, that component becomes + * this component's parent. + */ + void ensureAwakeInContext (WOContext aContext) + { + WOElement element; + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + element.ensureAwakeInContext( aContext ); + aContext.popElement(); + } + } + + /** + * Forwards this message to all child elements. + */ + public void takeValuesFromRequest ( + WORequest aRequest, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + element.takeValuesFromRequest( aRequest, aContext ); + aContext.popElement(); + } + + aContext.deleteLastElementIDComponent(); + } + + /** + * Forwards this message to all child elements, + * returning the first non-null result. + */ + public WOActionResults invokeAction ( + WORequest aRequest, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + WOActionResults result = null; + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + result = element.invokeAction( aRequest, aContext ); + aContext.popElement(); + if ( result != null ) break; + } + + aContext.deleteLastElementIDComponent(); + return result; + } + + /** + * Forwards this message to all child elements. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + WOElement element; + +// aContext.incrementLastElementIDComponent(); + aContext.appendZeroElementIDComponent(); + + // for each child element + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + element = (WOElement) it.next(); + aContext.pushElement( element ); + aContext.incrementLastElementIDComponent(); + + // forward the message + element.appendToResponse( + aResponse, aContext ); + + aContext.popElement(); + + } + aContext.deleteLastElementIDComponent(); + } + + public WOResponse generateResponse() + { + WOResponse r = new WOResponse(); + return r; + } + + public String toString() + { + StringBuffer result = new StringBuffer(); + result.append( "[WOParentElement: " ); + // for each child element + Iterator it = children.iterator(); + while ( it.hasNext() ) + { + result.append( "[ " ); + result.append( it.next().toString() ); + result.append( " ]" ); + } + result.append( " ]" ); + return result.toString(); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java new file mode 100644 index 0000000..2d4f16e --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPasswordField.java @@ -0,0 +1,20 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +public class WOPasswordField extends WOTextField { + + public WOPasswordField() { + super(); + } + + public WOPasswordField(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "PASSWORD"; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java new file mode 100644 index 0000000..a73636c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOPopUpButton.java @@ -0,0 +1,135 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCodingAdditions; + +public class WOPopUpButton extends WOInput { + + public WOPopUpButton() { + super(); + } + + public WOPopUpButton(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "SELECT"; + } + + protected int inputSize() { + return 1; + } + + protected NSArray list(WOContext c) { + NSArray l = (NSArray)valueForProperty("list", c.component()); + if (l == null) + l = NSArray.EmptyArray; + return l; + } + + protected void setItem(Object v, WOContext c) { + if (associations.objectForKey("item") == null) + return; + setValueForProperty("item", v, c.component()); + } + protected Object item(WOContext c) { + return valueForProperty("item", c.component()); + } + + protected void setSelection(Object v, WOContext c) { + if (associations.objectForKey("selection") == null) + return; + setValueForProperty("selection", v, c.component()); + } + protected Object selection(WOContext c) { + return valueForProperty("selection", c.component()); + } + + public Object value(WOContext c) { + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(""); + } + + protected void select(Object v, WOContext c) { + if (associations.objectForKey("selection") != null) { + setSelection(v, c); + return; + } + if (associations.objectForKey("item") != null) { + setItem(v,c); + } + } + public void takeValuesFromRequest(WORequest r, WOContext c) { + Object val = r.formValueForKey(inputName(c)); + if (val == null) + return; + NSArray list = list(c); + String valueKey = stringForProperty("value", c.component()); + //If no value binding, just get the index + if (valueKey == null) { + int pos = Integer.parseInt(val.toString()); + val = list.objectAtIndex(pos); + select(val, c); + return; + } + //If value binding is present, lookup the value + java.util.Enumeration numerador = list.objectEnumerator(); + while (numerador.hasMoreElements()) { + Object o = numerador.nextElement(); + if (o instanceof NSKeyValueCodingAdditions) { + Object x = ((NSKeyValueCodingAdditions)o).valueForKeyPath(valueKey); + if (x != null && x.equals(val)) { + select(o, c); + return; + } + } + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java new file mode 100644 index 0000000..386218c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORadioButton.java @@ -0,0 +1,20 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +public class WORadioButton extends WOCheckBox { + + public WORadioButton() { + super(); + } + + public WORadioButton(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "RADIO"; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java new file mode 100644 index 0000000..afa118a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORepetition.java @@ -0,0 +1,179 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import java.util.Collection; +import java.util.Iterator; + +import net.wotonomy.foundation.NSDictionary; + +public class WORepetition extends WODynamicElement { + + protected int count; + protected int index; + protected Object item; + protected Collection list; + protected Iterator iterator; + + protected WORepetition() { + super(); + } + + public WORepetition(String aName, NSDictionary aMap, WOElement template) { + super(aName, aMap, template); + } + + public void setCount(int value) { + count = value; + } + + public int count() { + return count; + } + + public void setIndex(int value) { + index = value; + } + public int index() { + return index; + } + + public void setItem(Object value) { + item = value; + } + public Object item() { + return item; + } + + public void setList(Collection value) { + list = value; + } + public Collection list() { + return list; + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + rootElement.takeValuesFromRequest(r, c); + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null && index >= count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + WOActionResults e = rootElement.invokeAction(r, c); + if (e != null) + { + iterator = null; + return e; + } + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null && index >= count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + return null; + } + + public void appendToResponse(WOResponse r, WOContext c) { + c.appendZeroElementIDComponent(); + pullValuesFromParent(c.component()); + index = 0; + WOAssociation countAsoc = (WOAssociation)associations.objectForKey("count"); + item = getNextItem(); + while (item != null) { + pushValuesToParent(c.component()); + rootElement.appendToResponse(r, c); + index++; + c.incrementLastElementIDComponent(); + if (countAsoc != null || index < count()) + item = null; + else + item = getNextItem(); + } + iterator = null; + c.deleteLastElementIDComponent(); + } + + protected Object getNextItem() { + if ( iterator == null ) + { + if ( list == null ) return null; + iterator = list.iterator(); + } + if ( iterator.hasNext() ) + { + return iterator.next(); + } + return null; + } + + protected void pullValuesFromParent(WOComponent c) { + Object value; + + value = valueForProperty("count", c); + if ( value instanceof Number ) + { + setCount( ((Number)value).intValue() ); + } + else + { + setCount( -1 ); + } + + value = valueForProperty("list", c); + if ( value instanceof Collection ) + { + setList( (Collection) value ); + } + else + { + setList( null ); + } + } + + protected void pushValuesToParent(WOComponent c) { + setValueForProperty("index", new Integer(index), c); + setValueForProperty("item", item, c); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java new file mode 100644 index 0000000..7d71223 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequest.java @@ -0,0 +1,586 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.InputStream; +import java.util.Enumeration; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WORequest. +* This implementation is backed by an HttpServletRequest, +* and thus does not support application-specific subclassing. +* Future implementations may remove this limitation. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WORequest extends WOResponse +{ + HttpServletRequest request; + private WOApplication application; + private NSArray languages; + private String requestHandlerKey; + private String requestHandlerPath; + private String pageName; + private String contextID; + private String senderID; + private String defaultFormValueEncoding; + + /** + * Parameterless constructor which should not be called. + */ + public WORequest () + { + throw new RuntimeException( + "This constructor is not yet supported." ); + } + + /** + * Standard constructor. Method and URL are required. + * HeaderMap is a map of header names to arrays containing + * one or more values. Data is the content of the request. + * UserInfo contains application-defined paramters that get + * passed to all objects involved in dispatching the request. + */ + public WORequest + (String aMethod, String aURL, String aProtocolName, + NSDictionary headerMap, NSData aData, NSDictionary userInfo) + { + throw new RuntimeException( + "This constructor is not yet supported." ); + } + + /** + * The only supported constructor for this implementation. + * This WORequest will wrap the specified servlet request. + */ + public WORequest( HttpServletRequest aRequest, WOApplication anApplication ) + { + request = aRequest; + application = anApplication; + + languages = null; + senderID = null; + pageName = null; + contextID = null; + defaultFormValueEncoding = "ISO8859_1"; + requestHandlerKey = WOApplication.directActionRequestHandlerKey(); + requestHandlerPath = request.getServletPath(); // request.getPathInfo(); + String remainder = requestHandlerPath; + if ( requestHandlerPath == null ) requestHandlerPath = ""; + if ( requestHandlerPath.length() > 0 ) + { + int i; + + // get request handler key + i = requestHandlerPath.indexOf( "/", 1 ); + if ( i != -1 ) + { + requestHandlerKey = requestHandlerPath.substring( 1, i ); + requestHandlerPath = requestHandlerPath.substring( i ); + } + + Enumeration e = requestHandlerPathArray().objectEnumerator(); + // skipping session ID for now + if ( e.hasMoreElements() ) + { + pageName = e.nextElement().toString(); + } + if ( e.hasMoreElements() ) + { + contextID = e.nextElement().toString(); + } + if ( e.hasMoreElements() ) + { + senderID = e.nextElement().toString(); + } + } + } + + /** + * Returns the HTTP method of the request: "GET" or "POST", + * or possibly "PUT". + */ + public String method () + { + return request.getMethod(); + } + + /** + * Returns the Uniform Resource Identifier, which is the part + * of the request URL after the protocol name (after the port + * number) and before the query parameters (before the question mark). + */ + public String uri () + { + return request.getRequestURI(); + } + + /** + * Returns the name of the protocol (presumably HTTP) and the + * version used by the client as sent in the request headers. + */ + public String httpVersion () + { + return request.getProtocol(); + } + + /** + * Returns an array of the header names in this request + * in no particular order. + */ + public NSArray headerKeys () + { + return arrayFromEnumeration( request.getHeaderNames() ); + } + + /** + * Returns an array of the header values for the specified key + * in no particular order. + */ + public NSArray headersForKey (String aString) + { + return arrayFromEnumeration( request.getHeaders( aString ) ); + } + + /** + * Returns one value for the specified header key. + * Provided as a convenience since most headers have + * only one value. Returns null if not found. + */ + public String headerForKey (String aKey) + { + return request.getHeader( aKey ); + } + + /** + * Returns the content of the request, or null if no content. + * The type of the content is determined by the "content-type" header. + * On error, null is returned. + */ + public NSData content () + { + //TODO: This is broken! + NSMutableData data = new NSMutableData(); + try + { + byte[] buf = new byte[ request.getContentLength() ]; + InputStream is = request.getInputStream(); + + // this is so very not efficient + int len; + while ( ( len = is.read( buf ) ) > -1 ) + { + // copies data twice... + data.appendData( new NSData( buf, 0, len ) ); + } + } + catch ( Exception exc ) + { + return null; + } + return data; + } + + /** + * Returns the application-specific userInfo dictionary. + * This implementation returns the servlet attribute map. + */ + public NSDictionary userInfo () + { + //TODO: Test this logic. + Object key, value; + NSMutableDictionary info = new NSMutableDictionary(); + java.util.Enumeration e = request.getAttributeNames(); + while ( e.hasMoreElements() ) + { + key = e.nextElement(); + value = request.getAttribute( e.nextElement().toString() ); + if ( value != null ) + { + info.setObjectForKey( value, key ); + } + } + return info; + } + + /** + * Returns the items in the request handler path parsed + * by the "/" delimiter and put into an array. + */ + public NSArray requestHandlerPathArray () + { + return NSArray.componentsSeparatedByString( requestHandlerPath(), "/" ); + } + + /** + * Returns the client's preferred languages in decreasing + * order of preference. The strings are returned in java + * Locale format, meaning dashes (-) are converted to + * underbars (_): see java.util.Locale.toString(). + */ + public NSArray browserLanguages () + { + if ( languages == null ) + { + languages = arrayFromEnumeration( request.getLocales() ); + } + return languages; + } + + /** + * Returns the portion of the URI specifying the engine within which + * this web application is running. This is important for generating + * URLs to be used in the content of the response. + */ + public String adaptorPrefix () + { + //TODO: Test this logic. + String name = request.getServletPath(); + return name; + +// String uri = request.getRequestURI(); +// int end = uri.indexOf( request.getPathInfo() ); +// return uri.substring( request.getContextPath().length(), end ); + } + + /** + * Returns the application name as specified in the request URI. + * Note that wotonomy web applications do not typically have a .woa + * extension, but the extension will be included for those that do. + */ + public String applicationName () + { + return request.getContextPath(); + } + + /** + * Returns the id of the application instance that is needed to + * service this request. -1 indicates that the request is not + * specific to a particular instance of the application. + * This implementation currently returns -1. + */ + public int applicationNumber () + { + return -1; + } + + /** + * Returns the portion of the URI that specifies which request handler + * should handle the request. + */ + public String requestHandlerKey () + { + return requestHandlerKey; + } + + /** + * Returns the portion of the URI that specifies path information + * for the request, not including the query string. + */ + public String requestHandlerPath () + { + return requestHandlerPath; + } + + /** + * Returns the unique identifier for the sessions associated with + * this request, or null if no session is found. + */ + public String sessionID () + { + HttpSession session = request.getSession( false ); + if ( session == null ) return null; + return session.getId(); + } + + /** + * Returns an array containing all the form keys in the request. + */ + public NSArray formValueKeys () + { + return arrayFromEnumeration( request.getParameterNames() ); + } + + /** + * Returns an array of the values for the specified key in + * no particular order. + */ + public NSArray formValuesForKey (String aKey) + { + NSMutableArray result = new NSMutableArray(); + String[] values = request.getParameterValues( aKey ); + if ( values != null ) + { + for ( int i = 0; i < values.length; i++ ) + { + result.addObject( values[i] ); + } + } + return result; + } + + /** + * Returns one value for the specified key. + * Provided as a convenience since most keys have + * only one value. Returns null if not found. + */ + public Object formValueForKey (String aKey) + { + return request.getParameter( aKey ); + } + + /** + * Returns the value for the specified key, as a String. + */ + public String stringFormValueForKey(String key) { + Object x = formValueForKey(key); + if (x != null) + return x.toString(); + return null; + } + + /** + * Returns a dictionary containing all the key-value + * mappings in the request. + */ + public NSDictionary formValues () + { + NSMutableDictionary result = new NSMutableDictionary (); + java.util.Enumeration e = request.getParameterNames(); + String key; + while ( e.hasMoreElements() ) + { + key = e.nextElement().toString(); + result.setObjectForKey( formValuesForKey( key ), key ); + } + return result; + } + + /** + * Returns whether the request is from a java-based client component. + * This implementation returns false. + */ + public boolean isFromClientComponent () + { + return false; + } + + /** + * Returns an array of cookie values for the specified key in + * no particular order. + */ + public NSArray cookieValuesForKey (String aKey) + { + // TODO: Test this logic. + NSMutableArray result = new NSMutableArray(); + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return result; + + for ( int i = 0; i < cookies.length; i++ ) + { + if ( cookies[i].getName().equals( aKey ) ) + { + result.addObject( cookies[i].getValue() ); + } + } + + return result; + } + + /** + * Returns one value for the specified cookie key. + * Provided as a convenience since most cookies have + * only one value. Returns null if not found. + */ + public String cookieValueForKey (String aKey) + { + // TODO: Test this logic. + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return null; + + for ( int i = 0; i < cookies.length; i++ ) + { + if ( cookies[i].getName().equals( aKey ) ) + { + return cookies[i].getValue(); + } + } + + return null; + } + + /** + * Returns a dictionary of cookie key-value mappings. + */ + public NSDictionary cookieValues () + { + // TODO: Test this logic. + NSMutableDictionary result = new NSMutableDictionary(); + Cookie[] cookies = request.getCookies(); + if ( cookies == null ) return result; + + NSMutableArray value; + for ( int i = 0; i < cookies.length; i++ ) + { + value = (NSMutableArray) result.objectForKey( + cookies[i].getName() ); + if ( value == null ) + { + value = new NSMutableArray(); + result.setObjectForKey( + cookies[i].getValue(), cookies[i].getName() ); + } + value.addObject( cookies[i].getValue() ); + } + + return result; + } + + /** + * Sets the default character encoding. + */ + public void setDefaultFormValueEncoding (String encoding) + { + defaultFormValueEncoding = encoding; + } + + /** + * Returns the default form value encoding ("ISO8859_1"). + */ + public String defaultFormValueEncoding () + { + return defaultFormValueEncoding; + } + + /** + * Sets whether the appropriate encoding scheme for decoding + * the form values will be automatically determined. + */ + public void setFormValueEncodingDetectionEnabled (boolean enabled) + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Gets whether the appropriate encoding scheme for decoding + * the form values is currently automatically determined. + */ + public boolean isFormValueEncodingDetectionEnabled () + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Gets the current method used for decoding form values. + */ + public int formValueEncoding () + { + throw new RuntimeException( "Not yet implemented." ); + } + + /** + * Returns the host name of the server that is the target of this request. + * NOTE: This method is not published in the WORequest specification. + */ + String applicationHost () + { + // FIXME: this should call WOApplication.hostname(); + return request.getServerName(); + } + + /** + * Returns the port of the server that is the target of this request. + * NOTE: This method is not published in the WORequest specification. + */ + int port() + { + // FIXME: this should call WOApplication.port(); + return request.getServerPort(); + } + + /** + * Returns the backing HttpServletRequest. + */ + HttpServletRequest servletRequest () + { + return request; + } + + /** + * Returns the application that was the target of this request. + * This method is not published in the WORequest specification. + */ + WOApplication application () + { + return application; + } + + /** + * This method is not published in the WORequest specification. + * This sender id from the URI, which is the id of the element + * that generated this request. + */ + String senderID () + { + return senderID; + } + + /** + * This method is not published in the WORequest specification. + * This returns the context id from the URI. + */ + String contextID () + { + return contextID; + } + + /** + * This method is not published in the WORequest specification. + */ + String pageName () + { + return pageName; + } + + /** + * Convenience method to populate an NSArray from an Enumeration. + */ + private static NSArray arrayFromEnumeration( java.util.Enumeration e ) + { + NSMutableArray result = new NSMutableArray(); + while ( e.hasMoreElements() ) + { + result.addObject( e.nextElement() ); + } + return result; + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java new file mode 100644 index 0000000..957cf25 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WORequestHandler.java @@ -0,0 +1,68 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import net.wotonomy.foundation.internal.NetworkClassLoader; + +/** +* A pure java implementation that corresponds +* to the abstract class WORequestHandler. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public abstract class WORequestHandler +{ + private NetworkClassLoader loader; + + /** + * Default constructor. + */ + public WORequestHandler() + { + loader = new NetworkClassLoader( WOApplication.application().getClass().getClassLoader() ); + } + + /** + * Dispatches the request and returns the response. + */ + abstract public WOResponse handleRequest (WORequest aRequest); + + /** + * Gets the specified class, loading it if it has not already been + * loaded or if it has been modified. Returns null if not found. + * Because this method is not in the specification, it has only + * package access. + */ + Class loadClass( String aName ) + { + try + { + return loader.loadClass( aName, true ); + } + catch ( ClassNotFoundException cnfe ) + { + return null; + } + } + +// void lock () +// void unlock () +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java new file mode 100644 index 0000000..8428681 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResetButton.java @@ -0,0 +1,50 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; + +/** +* Implements a reset button with dynamic bindings. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOResetButton extends WOInput { + + public WOResetButton() { + super(); + } + + public WOResetButton(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "RESET"; + } + + protected Object value(WOContext c) { + Object v = valueForProperty("value", c.component()); + if (v == null) + return "Reset"; + return v; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java new file mode 100644 index 0000000..c69c9db --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceManager.java @@ -0,0 +1,489 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSKeyValueCoding; +import net.wotonomy.foundation.NSMutableDictionary; +import net.wotonomy.foundation.internal.PropertyListParser; + +/** +* Manages all resources vended by the application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResourceManager +{ + private NSMutableDictionary resourceCache; + private NSMutableDictionary dynamicDataCache; + private NSMutableDictionary stringTableCache; + private Map localeCache; // used for wo-style i18n + private Locale californiaLocale; // used for wo-style i18n + + /** + * Constructor is only accessible to subclasses. + */ + protected WOResourceManager() + { + resourceCache = new NSMutableDictionary(); + dynamicDataCache = new NSMutableDictionary(); + stringTableCache = new NSMutableDictionary(); + localeCache = new HashMap(); + californiaLocale = new Locale( "en", "US" ); + localeCache.put( "en", californiaLocale ); + } + + /** + * Returns the raw data corresponding to the specified resource. + * Any data retrieved by this method will be placed in the + * resource manager's global cache. + */ + public byte[] bytesForResourceNamed(String aFileName, + String aFrameworkName, + NSArray aLanguagesList) + { + String mash = aFileName + aFrameworkName; + if ( aLanguagesList != null ) + { + mash = mash + aLanguagesList.componentsJoinedByString(":"); + } + + byte[] result = (byte[]) resourceCache.objectForKey( mash ); + if ( result == null ) + { + InputStream input = inputStreamForResourceNamed( + aFileName, aFrameworkName, aLanguagesList ); + if ( input != null ) + { + try + { + int c; + ByteArrayOutputStream output = new ByteArrayOutputStream(); + while ( ( c = input.read() ) != -1 ) + { + output.write( c ); + } + output.flush(); + input.close(); + output.close(); + result = output.toByteArray(); + synchronized ( resourceCache ) + { + resourceCache.setObjectForKey( result, mash ); + } + } + catch ( Throwable t ) + { + System.err.println( "WOResourceManager: Error reading bytes: " + aFileName ); + t.printStackTrace(); + } + } + } + return result; + } + + /** + * Returns the content type corresponding to the specified resource. + * This implementation recognizes gif, jpg, png, html, and xml extensions. + * Otherwise, "text/plain" is returned. + */ + public String contentTypeForResourceNamed(String aResourcePath) + { + if ( aResourcePath.endsWith( ".gif" ) ) return "image/gif"; + if ( aResourcePath.endsWith( ".jpg" ) ) return "image/jpeg"; + if ( aResourcePath.endsWith( ".png" ) ) return "image/png"; + if ( aResourcePath.endsWith( ".html" ) ) return "text/html"; + if ( aResourcePath.endsWith( ".xml" ) ) return "text/xml"; + return "text/plain"; + } + + /** + * Returns a url to be used when errors occur while retrieving a resource. + */ + public String errorMessageUrlForResourceNamed(String aResourceName, + String aFrameworkName) + { + if ( aResourceName == null ) aResourceName = "null"; + if ( aFrameworkName == null ) + { + return "/ERROR/NOT_FOUND/app=" + + WOApplication.application().name() + + "/filename=" + aResourceName; + } + else + { + return "/ERROR/NOT_FOUND/framework=" + + aFrameworkName + "/filename=" + aResourceName; + } + + } + + /** + * Clears all cached system-wide resource data. + */ + public void flushDataCache() + { + synchronized ( resourceCache ) + { + resourceCache.removeAllObjects(); + } + synchronized ( dynamicDataCache ) + { + dynamicDataCache.removeAllObjects(); + } + synchronized ( stringTableCache ) + { + stringTableCache.removeAllObjects(); + } + } + + /** + * Returns the file-system path for the specified resource. + * Deprecated and not implemented. + * @deprecated Use inputStreamForResourceNamed instead. + */ + public String pathForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList) + { + throw new RuntimeException( "ResourceManager.pathForResourceNamed: deprecated" ); + } + + /** + * Removes the data from the dynamic data cache for the specified session. + * If aSession is null, the data is removed from the application-wide + * data cache. + */ + public void removeDataForKey(String aKey, + WOSession aSession) + { + if ( aSession != null ) + { + if ( aSession.dynamicDataCache != null ) + { + aSession.dynamicDataCache.removeObjectForKey( aKey ); + } + } + else + { + synchronized ( dynamicDataCache ) + { + dynamicDataCache.removeObjectForKey( aKey ); + } + } + } + + /** + * Sets the data in the dynamic data cache for the specified session. + * If aSession is null, the data is placed in the application-wide + * data cache. If the key is a system-generated key, the data will + * be removed by calling removeData() the next time it is requested. + */ + public void setData(NSData someData, + String key, + String type, + WOSession aSession) + { + if ( aSession != null ) + { + if ( aSession.dynamicDataCache != null ) + { + aSession.dynamicDataCache.setObjectForKey( + new TypedData( type, someData ), key ); + } + } + else + { + synchronized ( dynamicDataCache ) + { + dynamicDataCache.setObjectForKey( + new TypedData( type, someData ), key ); + } + } + } + + /** + * Returns a localized string from a property list for + * a given key. If the key doesn't exist, aDefaultValue + * is returned. + */ + public String stringForKey(String aKey, + String aFileName, + String aDefaultValue, + String aFrameworkName, + NSArray aLanguagesList) + { + + String mash = aFileName + aFrameworkName; + if ( aLanguagesList != null ) + { + mash = mash + aLanguagesList.componentsJoinedByString(":"); + } + + Object table = stringTableCache.objectForKey( mash ); + if ( table == null ) + { + try + { + InputStream input = (InputStream) inputStreamForResourceNamed( + aFileName, aFrameworkName, aLanguagesList); + if ( input != null ) + { + Reader reader = + new BufferedReader(new InputStreamReader(input)); + table = PropertyListParser.propertyListFromReader( reader ); + synchronized ( stringTableCache ) + { + stringTableCache.setObjectForKey( table, mash ); + } + } + } + catch ( IOException ioe ) + { + System.err.println( "WOResourceManager: error reading: " + aFileName ); + ioe.printStackTrace(); + } + catch ( Throwable t ) + { + // could not parse + System.err.println( "WOResourceManager: could not parse: " + aFileName ); + System.err.println( t ); + } + } + + Object result = null; + if ( table != null ) + { + result = NSKeyValueCoding.DefaultImplementation.valueForKey( table, aKey ); + } + if ( result == null ) + { + result = aDefaultValue; + } + else + { + result = result.toString(); + } + + return (String) result; + } + + /** + * Returns a url that invokes the resource manager for the + * specified resource. + */ + public String urlForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList, + WORequest aRequest) + { + StringBuffer buffer = new StringBuffer(); + if ( aFrameworkName == null ) + { + aFrameworkName = "application"; + } + buffer.append( aRequest.applicationName() ); + buffer.append( '/' ); + buffer.append( WOApplication.resourceRequestHandlerKey() ); + if ( !aFrameworkName.startsWith("/") ) + { + buffer.append( '/' ); + } + buffer.append( aFrameworkName ); + buffer.append( '/' ); + buffer.append( aResourceName ); + return buffer.toString(); + } + + /** + * Returns an input for the raw resource. Data returned by + * this method will not be put in the resource manager's global cache. + */ + public InputStream inputStreamForResourceNamed(String aResourceName, + String aFrameworkName, + NSArray aLanguagesList) + { + if ( aResourceName == null ) return null; + InputStream result = null; + + StringBuffer path = new StringBuffer(); + path.append( '/' ); + if ( aFrameworkName != null ) + { + path.append( aFrameworkName ).append( '/' ); + } + + int i = aResourceName.lastIndexOf( "." ); + if ( i != -1 ) + path.append( aResourceName.substring( 0, i ) ); + else + path.append( aResourceName ); + + String location = path.toString(); + if ( aLanguagesList != null ) + { + String language; + Locale locale; + HashSet tried = new HashSet(5); + Enumeration e = aLanguagesList.objectEnumerator(); + while ( e.hasMoreElements() && result == null ) + { + language = e.nextElement().toString(); + + // look for java-style localization + if ( i != -1 ) + { + result = getStream( location + '_' + + language + aResourceName.substring( i ) ); + } + else // no dot extension + { + result = getStream( location + '_' + language ); + } + + // look for wo-style localization + if ( result == null ) + { + locale = (Locale) localeCache.get( language ); + if ( locale == null ) + { + if ( language.length() == 5 ) + { + locale = new Locale( + language.substring( 0, 2 ), + language.substring( 3, 5 ) ); + } + else + { + locale = new Locale( language, "" ); + } + synchronized ( localeCache ) + { + localeCache.put( language, locale ); + } + } + + language = '/'+locale.getDisplayLanguage( californiaLocale )+".lproj"; + if ( !tried.contains( language ) ) + { + if ( aFrameworkName != null ) + { + int j = aFrameworkName.length()+1; + path.insert( j, language ); + result = getStream( path.toString() + aResourceName.substring( i ) ); + path.delete( j, j+language.length() ); + } + else + { + result = getStream( language + path.toString() + aResourceName.substring( i ) ); + } + tried.add( language ); + } + } + } + } + + // look for file in package + if ( result == null ) + { + if ( i != -1 ) + { + result = getStream( path.append( + aResourceName.substring( i ) ).toString() ); + } + else // no dot extension + { + result = getStream( location ); + } + } + + return result; + } + + private static final InputStream getStream( String path ) + { //System.out.println( "getStream: " + path ); + InputStream input = + WOApplication.application().getClass().getResourceAsStream( path ); + if ( input == null ) + { + // in case the local class loader doesn't delegate to its parent + input = ClassLoader.getSystemResourceAsStream( path ); + } + return input; + } + + private static final class TypedData + { + String type; + NSData data; + + public TypedData( String aType, NSData aData ) + { + type = aType; + data = aData; + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.5 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.4 2003/02/28 22:58:57 mpowers + * Added support for wo-style localization (*.lproj). + * + * Revision 1.3 2003/02/21 16:40:24 mpowers + * Now reading port and smtp host from system properties. + * Implemented WOApplication.main. + * + * Revision 1.2 2003/01/28 19:33:52 mpowers + * Implemented the rest of WOResourceManager. + * Implemented support for java-style i18n. + * Components now use the resource manager to load templates. + * + * Revision 1.1 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java new file mode 100644 index 0000000..4d25128 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceRequestHandler.java @@ -0,0 +1,122 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.util.Enumeration; + +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* An implementation of WORequestHandler that +* retrieves resources from the deployed application. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResourceRequestHandler + extends WORequestHandler +{ + private NSMutableDictionary resourceCache; + private WOResourceManager resourceManager; + + public WOResourceRequestHandler() + { + //TODO: should probably have some kind of limit on the cache + resourceCache = new NSMutableDictionary(); + resourceManager = WOApplication.application().resourceManager(); + } + + public WOResponse handleRequest( WORequest aRequest ) + { + WOResponse response = new WOResponse(); + + StringBuffer buf = new StringBuffer(); + + //TODO: this is just to get things working... + String framework = null; + Enumeration e = aRequest.requestHandlerPathArray().objectEnumerator(); + if ( e.hasMoreElements() ) + { + framework = e.nextElement().toString(); + if ( framework.equals( "application" ) ) + { + buf.append('/').append( framework ); + framework = null; + } + } + if ( e.hasMoreElements() ) + { + buf.append( e.nextElement() ); + } + while ( e.hasMoreElements() ) + { + buf.append('/').append( e.nextElement() ); + } + + String resource; + if ( buf.length() > 0 ) + { + resource = buf.toString(); + byte[] data = resourceManager.bytesForResourceNamed( + resource, framework, aRequest.browserLanguages() ); + if ( data != null ) + { + response.setHeader( + resourceManager.contentTypeForResourceNamed( resource ), + "Content-Type" ); + response.setContent( new NSData( data ) ); + return response; + } + } + response.setStatus( 404 ); // not found + return response; + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.6 2003/08/07 00:15:15 chochos + * general cleanup (mostly removing unused imports) + * + * Revision 1.5 2003/01/27 15:08:00 mpowers + * Implemented WOResourceManager, using java resources for now. + * + * Revision 1.4 2003/01/22 23:01:36 mpowers + * Better handling for request handler path. + * Better support for resources with absolute path. + * + * Revision 1.3 2003/01/20 16:18:22 mpowers + * Fixed class loading issues with resource retrieval. + * + * Revision 1.2 2003/01/17 20:58:20 mpowers + * Fixed up WOHyperlink. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java new file mode 100644 index 0000000..862c6dc --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResourceURL.java @@ -0,0 +1,31 @@ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSData; +import net.wotonomy.foundation.NSDictionary; + +public class WOResourceURL extends WODynamicElement { + + public WOResourceURL() { + super(); + } + + public WOResourceURL(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + public void appendToResponse(WOResponse r, WOContext c) { + String fname = stringForProperty("filename", c.component()); + if (fname != null) { + String fwk = stringForProperty("framework", c.component()); + return; + } + NSData data = (NSData)valueForProperty("data", c.component()); + if (data != null) { + String mime = stringForProperty("mimeType", c.component()); + String key = stringForProperty("key", c.component()); + return; + } + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java new file mode 100644 index 0000000..d9fbade --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOResponse.java @@ -0,0 +1,184 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import net.wotonomy.foundation.NSArray; + +/** +* A pure java implementation of WOResponse. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOResponse extends WOMessage + implements WOActionResults +{ + protected static String defaultEncoding; + private static SimpleDateFormat htmlDateFormat; + static + { + defaultEncoding = "ISO8859_1"; + htmlDateFormat = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss z" ); + htmlDateFormat.setTimeZone( + TimeZone.getTimeZone( "GMT" ) ); + } + + private int status; + + /** + * Parameterless constructor which should not be called. + */ + public WOResponse () + { + status = -1; // -1 indicates not yet set + } + + /** + * Sets the status code of the response. + * You should use the constants defined in HttpServletResponse. + */ + public void setStatus (int code) + { + status = code; + } + + /** + * Gets the current status code for the response. + */ + public int status () + { + return status; + } + + /** + * Sets a header in the response to disable client caching. + * (Whether this works depends on the client implementation.) + */ + public void disableClientCaching () + { + String dateString = htmlDateFormat.format( new Date() ); + setHeader( dateString, "Date" ); + setHeader( dateString, "Expires" ); + setHeader( "no-cache", "Pragma" ); + setHeaders( new NSArray( new Object[] { + "private", "no-cache", "max-age = 0" } ), "Cache-Control" ); + //System.out.println( "disableClientCaching: " + dateString ); + } + + /** + * Returns this object. (Implements the WOActionResults interface.) + */ + public WOResponse generateResponse() + { + return this; + } + + /** + * Generates the response using the specified servlet response, + * but does not flush the buffer. The caller is responsible + * for sending the response to the client.

        + * Note that this method is currently responsible for setting + * the content type to "text/html". As far as I can tell, WORequests + * have no way to set the content type and therefore must always + * be of type "text/html". + */ + void generateServletResponse + (javax.servlet.http.HttpServletResponse response) + { + if ( WOApplication.application().isPageRefreshOnBacktrackEnabled() ) + { + disableClientCaching(); + } + + String key; + java.util.Enumeration e, f; + + // set content type: might be overwritten by headers below + response.setContentType( "text/html" ); + + // set status + if ( status != -1 ) + { + response.setStatus( status ); + } + + // set headers + f = _headers.allKeys().objectEnumerator(); + while ( f.hasMoreElements() ) + { + key = f.nextElement().toString(); + e = ((NSArray)_headers.objectForKey( key )).objectEnumerator(); + if ( e.hasMoreElements() ) + { + // overwrite existing header + response.setHeader( key, e.nextElement().toString() ); + } + while ( e.hasMoreElements() ) + { + response.addHeader( key, e.nextElement().toString() ); + } + } + + // set cookies + e = _cookies.allValues().objectEnumerator(); + while ( e.hasMoreElements() ) + { + response.addCookie( (WOCookie) e.nextElement() ); + } + + try + { + // write content + response.getOutputStream().write(_contentData.bytes() ); + if ( status > 299 ) + { + response.sendError( status ); + } + } + catch ( Exception exc ) + { + throw new RuntimeException( + "Error writing response: " + exc ); + } + } + + + /** + * Returns the current default encoding seting. + */ + public static String defaultEncoding () + { + return defaultEncoding; + } + + /** + * Sets the default encoding setting. + */ + public static void setDefaultEncoding (String encoding) + { + defaultEncoding = encoding; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java new file mode 100644 index 0000000..0f2898c --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOServletSessionStore.java @@ -0,0 +1,188 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corp. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; + +import javax.servlet.http.HttpSession; + + +/** +* An implementation of WOSessionStore that stores WOSessions +* inside of HttpSessions, to take advantage of the servlet +* containers built-in distribution capabilities. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOServletSessionStore extends WOSessionStore +{ + private static final String ServletSessionKey = "WOServletSessionStoreKey"; + + /** + * This implementation currently does nothing and returns null, as we rely on the + * servlet container to dispose of the HttpSession which contains our WOSession. + */ + public WOSession removeSessionWithID(String sessionID) + { + return null; + } + + /** + * Returns the WOSession for the specified ID from the store. + * The sessionID parameter is ignored, and the session is removed from + * the store until saveSessionForContext is called. + */ + public WOSession restoreSessionWithID(String sessionID, WORequest aRequest) + { + WOSession result = null; + HttpSession servletSession = aRequest.servletRequest().getSession(); + if ( servletSession != null ) + { + try + { + result = (WOSession) servletSession.getAttribute( ServletSessionKey ); + if ( result != null ) + { + // if the servlet's class loader has changed, we need to reload + if ( result.getClass().getClassLoader() != + WOApplication.application().getClass().getClassLoader() ) + { + throw new ClassCastException( result.getClass().toString() ); + } + } + } + catch ( ClassCastException exc ) + { + // we're having an issue with the container's class loader: + // try serializing and deserializing to see if it is reloaded correctly + try + { + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream output = new ObjectOutputStream( bytes ); + Object o = servletSession.getAttribute( ServletSessionKey ); + output.writeObject( o ); + output.flush(); + output.close(); + + System.out.println( + "WOServletSessionStore: reloaded session with size: " + + bytes.toByteArray().length ); + + ObjectInputStream input = new CustomObjectInputStream( + new ByteArrayInputStream( bytes.toByteArray() ), + WOApplication.application().getClass().getClassLoader() ); + + o = input.readObject(); + input.close(); + + // try it again + result = (WOSession) o; + } + catch ( Exception e ) + { + e.printStackTrace(); + // give up: remove the attribute and allow a new session + servletSession.removeAttribute( ServletSessionKey ); + } + } + + if ( result != null ) + { + servletSession.removeAttribute( ServletSessionKey ); + } + } + //System.out.println( "restoreSessionWithID: " + sessionID + " : " + result ); + return result; + } + + /** + * Places the context's session into the store. + */ + public void saveSessionForContext(WOContext context) + { + //System.out.println( "saveSessionForContext: " + context + " : " + context.session() ); + context.request().servletRequest().getSession( true ).setAttribute( + ServletSessionKey, context.session() ); + } + + /** + * Needed to specify a class loader which may be different that the + * one used to load the ObjectInputStream class. + */ + private static class CustomObjectInputStream extends ObjectInputStream + { + ClassLoader loader; + + public CustomObjectInputStream( + InputStream anInputStream, ClassLoader aClassLoader ) + throws java.io.IOException, java.io.StreamCorruptedException + { + super( anInputStream ); + loader = aClassLoader; + } + + protected Class resolveClass(ObjectStreamClass v) + throws IOException, ClassNotFoundException + { + return loader.loadClass( v.getName() ); + } + } +} + +/* + * $Log$ + * Revision 1.2 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2003/01/22 23:00:32 mpowers + * Now keeping the most recent page around. + * + * Revision 1.6 2003/01/21 17:53:16 mpowers + * Now handling reloading when wotonomy is on the system class path. + * + * Revision 1.5 2003/01/15 19:50:49 mpowers + * Fixed issues with WOSession and Serializable. + * Can now persist sessions between classloaders (hot swap of class impls). + * + * Revision 1.3 2003/01/14 16:05:19 mpowers + * Removed extraneous printlns. + * + * Revision 1.2 2003/01/13 22:25:07 mpowers + * Request-response cycle is working with session and page persistence. + * + * Revision 1.1 2003/01/07 20:48:24 mpowers + * Implemented WOSessionStore and WOServletSessionStore. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java new file mode 100644 index 0000000..e187567 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSession.java @@ -0,0 +1,528 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import net.wotonomy.control.EOEditingContext; +import net.wotonomy.control.KeyValueCodingUtilities; +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDate; +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSKeyValueCodingAdditions; +import net.wotonomy.foundation.NSKeyValueCodingSupport; +import net.wotonomy.foundation.NSMutableArray; +import net.wotonomy.foundation.NSMutableDictionary; + +/** +* A pure java implementation of WOSession. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOSession implements Serializable, NSKeyValueCodingAdditions +{ + //NOTE: need to set this when deserialized and on creation + transient private HttpSession session; + + // the current context + transient private WOContext context; + + // the last requested page: an optimization + transient private WOComponent currentPage; + // the last requested page's context id + transient private String currentContextID; + + //FIXME: transient until ec's implement serializable + private transient EOEditingContext defaultEditingContext; + + private NSMutableDictionary state; + private NSMutableDictionary pages; + private NSMutableDictionary permanentPages; + private NSMutableArray stateStack; + private NSMutableArray pageStack; + private NSMutableArray permanentPageStack; + private boolean terminating; + + // used by WOResourceManager to cache dynamic resources + transient NSMutableDictionary dynamicDataCache; + + public static final String WOSessionDidTimeOutNotification + = "WOSessionDidTimeOutNotification"; + public static final String WOSessionDidRestoreNotification + = "WOSessionDidRestoreNotification"; + public static final String WOSessionDidCreateNotification + = "WOSessionDidCreateNotification"; + + /** + * Default constructor. This is called implicitly by + * subclasses in all cases. + */ + public WOSession () + { + session = null; + state = new NSMutableDictionary(); + pages = new NSMutableDictionary(); + permanentPages = new NSMutableDictionary(); + stateStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + pageStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + permanentPageStack = NSMutableArray.mutableArrayBackedByList( new LinkedList() ); + defaultEditingContext = null; + terminating = false; + } + + /** + * Package method to initialize the backing session. + */ + void setServletSession( HttpSession aSession ) + { + session = aSession; + } + + /** + * Package method to set the current context. + */ + void setContext( WOContext aContext ) + { + context = aContext; + } + + /** + * Returns the id of the current session. If no session + * currently exists, return null. + */ + public String sessionID () + { + if ( session != null ) + { + return session.getId(); + } + return null; + } + + /** + * Sets whether distribution is currently enabled. + * This method is not implemented by this implementation + * as the servlet container manages distribution. + */ + public void setDistributionEnabled (boolean enabled) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether the session is part of a distributed application. + * This implementation always returns false. + */ + public boolean isDistributionEnabled () + { + return false; + } + + /** + * Sets whether session ids should be stored in cookies. + * This method is not implemented in this implementation + * as the servlet container manages sessions with cookies. + */ + public void setStoresIDsInCookies (boolean cookies) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether session ids are currently stored in cookies. + * This implementation always returns true. + */ + public boolean storesIDsInCookies () + { + return true; + } + + /** + * Returns the current expiration date for cookies that store session ids. + */ + public NSDate expirationDateForIDCookies () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Sets whether session ids should be stored in urls. + * This method is not implemented in this implementation + * as the servlet container manages sessions with cookies. + */ + public void setStoresIDsInURLs (boolean urls) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns whether session ids are currently stored in urls. + * This implementation always returns false. + */ + public boolean storesIDsInURLs () + { + return false; + } + + /** + * Returns the current domain for cookies containing session ids. + */ + public String domainForIDCookies () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Terminates this session after the completion of the current response. + */ + public void terminate () + { + terminating = true; + session.invalidate(); + } + + /** + * Returns whether the current session will terminate at the completion + * of the current response. + */ + public boolean isTerminating () + { + return terminating; + } + + /** + * Sets the number of seconds after the last request before + * the session should be terminated. + */ + public void setTimeOut (double timeout) + { + session.setMaxInactiveInterval( (int) timeout ); + } + + /** + * Returns the number of seconds after the last request before + * the session should be terminated. + */ + public double timeOut () + { + return session.getMaxInactiveInterval(); + } + + /** + * Sets the languages for which this session has been localized, + * in order of preference. The application will be responsible for + * localizing the content based on the languages found in this array. + */ + public void setLanguages (NSArray anArray) + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Returns the languages for which this session has been localized, + * in order of preference. The application will be responsible for + * localizing the content based on the languages found in this array. + */ + public NSArray languages () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Stores the specified key-value pair in the session. + */ + public void setObjectForKey (Object anObject, String aKey) + { + state.setObjectForKey( anObject, aKey ); + } + + /** + * Returns the session value associated with the specified key. + */ + public Object objectForKey (String aKey) + { + return state.objectForKey( aKey ); + } + + /** + * Removes the session value associated with the specified key. + */ + public void removeObjectForKey (String aKey) + { + state.removeObjectForKey( aKey ); + } + + /** + * Returns the context for the current request. + */ + public WOContext context () + { + return context; + } + + /** + * Invoked at the beginning of the request-response cycle. + * Override to perform any kind of initialization at the + * start of a request. This implementation does nothing. + */ + public void awake () + { + + } + + /** + * Invoked by the Application to extract user-assigned balues + * and assign them to attributes. This implementation calls + * takeValuesFromRequest on the top-level component. + */ + public void takeValuesFromRequest (WORequest aRequest, WOContext aContext) + { + context().component().takeValuesFromRequest( aRequest, aContext ); + } + + /** + * Invoked by the Application to determine which component is the + * intended recipient of the user's action. This implementation calls + * invokeAction on the top-level component. + */ + public WOActionResults invokeAction (WORequest aRequest, WOContext aContext) + { + return context().component().invokeAction( aRequest, aContext ); + } + + /** + * Invoked by the Application to generate the content of the response. + * This implementation calls appendToResponse on the top-level component. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + context().component().appendToResponse( aResponse, aContext ); + } + + /** + * Invoked at the end of the request-response cycle. + * Override to perform any kind of clean-up at the + * end of a request. This implementation does nothing. + */ + public void sleep () + { + + } + + /** + * Returns a list of pages accessed by this session in order + * of their access and named by calling WOComponent.descriptionForResponse. + */ + public NSArray statistics () + { + throw new RuntimeException( "Not implemented yet." ); + } + + /** + * Puts this page in the session's page cache using the current + * context id as the key. + */ + public void savePage (WOComponent aComponent) + { + currentPage = aComponent; + currentContextID = context.contextID(); + + if ( pages.objectForKey( currentContextID ) == null ) + { + byte[] data = KeyValueCodingUtilities.freeze( + aComponent, defaultEditingContext(), aComponent, true ); + System.out.println( "WOSession.savePage: " + currentContextID + " : " + data.length ); + + pages.setObjectForKey( data, currentContextID ); + pageStack.addObject( currentContextID ); + if ( pageStack.count() > context().application().pageCacheSize() ) + { + String id = pageStack.remove( 0 ).toString(); // removeObjectAtIndex + System.out.println( "WOSession.savePage: removing from cache: " + id ); + pages.removeObjectForKey( id ); + } + } + //System.out.println( "savePage: " + this + " : " + id + " : " + pages ); + } + + /** + * Returns the page in the session's page cache corresponding to + * the specified context id. Any special permanent caches are + * searched before the standard page cache. + */ + public WOComponent restorePageForContextID (String anID) + { + if ( anID == null ) return null; + if ( anID.equals( currentContextID ) ) return currentPage; + + WOComponent result = null; + byte[] data = (byte[]) permanentPages.objectForKey( anID ); + if ( data == null ) data = (byte[]) pages.objectForKey( anID ); + if ( data != null ) + { + System.out.println( "WOSession.restorePageForContextID: " + anID + " : " + data.length ); + result = (WOComponent) KeyValueCodingUtilities.thaw( + data, defaultEditingContext(), + WOApplication.application().getClass().getClassLoader(), true ); + } + //System.out.println( "restorePageForContextID: " + this + " : " + anID + " : " + result + " : " + pages ); + return result; + } + + /** + * Puts this page in the special cache is will not get automatically + * flushed like the session page cache. Use this if the page + * is likely to be around for a while, specifically pages within + * frames. + */ + public void savePageInPermanentCache (WOComponent aComponent) + { + currentPage = aComponent; + currentContextID = context.contextID(); + + if ( permanentPages.objectForKey( currentContextID ) == null ) + { + byte[] data = KeyValueCodingUtilities.freeze( + aComponent, defaultEditingContext(), aComponent, true ); + //System.out.println( "WOSession.savePageInPermanentCache: " + // + currentContextID + " : " + data.length ); + + permanentPages.setObjectForKey( data, currentContextID ); + permanentPageStack.addObject( currentContextID ); + if ( permanentPageStack.count() > context().application().pageCacheSize() ) + { + String id = permanentPageStack.remove( 0 ).toString(); // removeObjectAtIndex + permanentPages.removeObjectForKey( id ); + } + } + } + + /** + * Writes a message to the standard error stream. + */ + public static void logString (String aString) + { + System.err.println( aString ); + } + + /** + * Writes a message to the standard error stream + * if debugging is activated. + */ + public static void debugString (String aString) + { + // TODO: Check to see if debugging is enabled. + System.err.println( aString ); + } + + /** + * Returns the default editing context used by this session. + * Defaults to null. + */ + public EOEditingContext defaultEditingContext () + { + return defaultEditingContext; + } + + /** + * Sets the default editing context used by this session. + */ + public void setDefaultEditingContext (EOEditingContext aContext) + { + defaultEditingContext = aContext; + } + + // interface NSKeyValueCodingAdditions + + public Object valueForKeyPath (String aPath) + { + // currently key value coding support also handles keypaths + return valueForKey( aPath ); + } + + public void takeValueForKeyPath (Object aValue, String aPath) + { + // currently key value coding support also handles keypaths + takeValueForKey( aValue, aPath ); + } + + public NSDictionary valuesForKeys (List aKeyList) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public void takeValuesFromDictionary (Map aValueMap) + { + throw new RuntimeException( "Not implemented yet." ); + } + + public Object valueForKey (String aKey) + { // System.out.println( "valueForKey: " + aKey + "->" + this ); + Object result = objectForKey( aKey ); + if ( result == null ) + result = NSKeyValueCodingSupport.valueForKey( this, aKey ); + return result; + } + + public void takeValueForKey (Object aValue, String aKey) + { // System.out.println( "takeValueForKey: " + aKey + " : " + aValue + "->" + this ); + setObjectForKey( aValue, aKey ); + } + + public Object storedValueForKey (String aKey) + { + Object result = objectForKey( aKey ); + if ( result == null ) + NSKeyValueCodingSupport.storedValueForKey( this, aKey ); + return result; + } + + public void takeStoredValueForKey (Object aValue, String aKey) + { + setObjectForKey( aValue, aKey ); + } + + public Object handleQueryWithUnboundKey (String aKey) + { + return NSKeyValueCodingSupport.handleQueryWithUnboundKey( this, aKey ); + } + + public void handleTakeValueForUnboundKey (Object aValue, String aKey) + { + NSKeyValueCodingSupport.handleTakeValueForUnboundKey( this, aValue, aKey ); + } + + public void unableToSetNullForKey (String aKey) + { + NSKeyValueCodingSupport.unableToSetNullForKey( this, aKey ); + } + + public Object validateTakeValueForKeyPath (Object aValue, String aKey) + { + throw new RuntimeException( "Not implemented yet." ); + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java new file mode 100644 index 0000000..f91a433 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSessionStore.java @@ -0,0 +1,93 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2003 Intersect Software Corp. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +/** +* An abstract class defining the requirements for persisting +* session state across user transactions. Used by WOApplication +* to persist sessions between requests. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +public abstract class WOSessionStore +{ + private static WOSessionStore serverSessionStore = null; + + /** + * Returns the default session store used by WOApplication. + */ + public static WOSessionStore serverSessionStore() + { + if ( serverSessionStore == null ) + { + serverSessionStore = new WOServletSessionStore(); + } + return serverSessionStore; + } + + /** + * Called by WOApplication after the request-response cycle has ended. + * The context's session will again be available for subsequent requests. + */ + public final void checkInSessionForContext(WOContext aContext) + { + saveSessionForContext( aContext ); + } + + /** + * Returns the session with the specified id for the specified request, + * or null if none exist. Subsequent calls for the same id will return + * null until the session is checked in again. + * Called by WOApplication before the request-response cycle starts. + */ + public final WOSession checkOutSessionWithID(String sessionID, WORequest aRequest) + { + return restoreSessionWithID( sessionID, aRequest ); + } + + /** + * Removes the WOSession for the specified ID from the store and returns it. + */ + public abstract WOSession removeSessionWithID(String sessionID); + + /** + * Returns the WOSession for the specified ID from the store. + */ + public abstract WOSession restoreSessionWithID(String sessionID, + WORequest aRequest); + + /** + * Places the context's session into the store. + */ + public abstract void saveSessionForContext(WOContext context); +} + +/* + * $Log$ + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2003/01/07 20:48:29 mpowers + * Implemented WOSessionStore and WOServletSessionStore. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java new file mode 100644 index 0000000..308cd20 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOStaticElement.java @@ -0,0 +1,70 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +/** +* This class represents a static portion of a web page. +* Package access only, as it is not in the specification. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 893 $ +*/ +class WOStaticElement extends WOElement +{ + String content; + + /** + * Default constructor. + */ + public WOStaticElement() + { + content = null; + } + + /** + * Returns a static element representing the specified content. + */ + public WOStaticElement( String aContentString ) + { + this(); + content = aContentString; + } + + /** + * Overridden to append the content string.. + */ + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + aResponse.appendContentString( content ); + } + + + public WOResponse generateResponse() + { + WOResponse r = new WOResponse(); + if (content != null) + { + r.appendContentString(content); + } + return r; + } + + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java new file mode 100644 index 0000000..ef5b771 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOString.java @@ -0,0 +1,136 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Blacksmith, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web; + +import java.text.Format; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSNumberFormatter; +import net.wotonomy.foundation.NSTimestampFormatter; + +/** +* WOString renders a dynamically generated string in the output. +* Bindings are: +*
          +*
        • value: a property returning a value which will be rendered as the +* output. If a formatter is not used, then the value must be convertable +* to a String with toString().
        • +*
        • escapeHTML: a property returning a value convertable to a Boolean +* indicating whether the any html characters in the output should be +* escaped so they are shown as html characters rather than interpreted +* as html.
        • +*
        • formatter: a property returning a Format object that will be +* used to format the value into a String.
        • +*
        • dateformat: a property returning a DateFormat object that will be +* used to format the value into a String.
        • +*
        • numberformat: a property returning a NumberFormat object that will be +* used to format the value into a String. Not yet implemented.
        • +*
        • valueWhenEmpty: a property returning a String that will be used +* in place of an empty or null value. Not yet implemented.
        • +*
        +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOString extends WODynamicElement +{ + protected Object value; + protected boolean escapeHTML; + protected Format formatter; + protected String dateformat; + protected String numberformat; + protected Object valueWhenEmpty; + + /** + * The default constructor. + */ + protected WOString () + { + } + + public WOString ( + String aName, NSDictionary anAssociationMap, WOElement aRootElement) + { + super( aName, anAssociationMap, aRootElement ); + escapeHTML = true; + } + + public void appendToResponse (WOResponse aResponse, WOContext aContext) + { + WOComponent c = aContext.component(); + numberformat = stringForProperty("numberformat", c ); + dateformat = stringForProperty("dateformat", c ); + formatter = (Format) valueForProperty("formatter", c ); + escapeHTML = booleanForProperty("escapeHTML", c ); + value = valueForProperty("value", c ); + valueWhenEmpty = valueForProperty("valueWhenEmpty", c ); + + Object result = value; + if ( result != null ) + { + if ( formatter != null ) + { + try + { + result = formatter.format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + if ( dateformat != null ) + { + try + { + result = new NSTimestampFormatter( dateformat ).format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + if ( numberformat != null ) + { + try + { + result = new NSNumberFormatter( numberformat ).format( result ); + } + catch ( IllegalArgumentException exc ) + { + } + } + } + if ( result == null ) + { + result = valueWhenEmpty; + if ( result == null ) + { + result = "nil"; + } + } + if ( escapeHTML ) + { + aResponse.appendContentHTMLString( result.toString() ); + } + else + { + aResponse.appendContentString( result.toString() ); + } + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java new file mode 100644 index 0000000..05824f3 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSubmitButton.java @@ -0,0 +1,70 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +/** +* Implements a submit button with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOSubmitButton extends WOInput { + + public WOSubmitButton() { + super(); + } + + public WOSubmitButton(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "SUBMIT"; + } + + protected Object value(WOContext c) { + Object v = valueForProperty("value", c.component()); + if (v == null) { + return "Submit"; + } + return v; + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.add("action"); + return a; + } + + public WOActionResults invokeAction(WORequest r, WOContext c) { + if (disabled(c)) + return null; + //It's useless to check the senderID here because it's going to be for the form always. + //So we check if the formValues contain the button's name (which means it was pressed) + if (r.formValueForKey(inputName(c)) != null) { + if (associations.objectForKey("action") != null) + return (WOActionResults)valueForProperty("action", c.component()); + } + return null; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java new file mode 100644 index 0000000..d2e373a --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOSwitchComponent.java @@ -0,0 +1,112 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableDictionary; + +public class WOSwitchComponent extends WODynamicElement { + + private NSMutableDictionary elements; + private String currentName; + private WOElement currentElement; + + protected WOSwitchComponent() + { + super(); + elements = new NSMutableDictionary(); + } + + public WOSwitchComponent(String aName, NSDictionary aMap, WOElement template) + { + super(aName, aMap, template); + elements = new NSMutableDictionary(); + } + + private WOElement getCurrentElement( WOContext c ) + { + String name = stringForProperty( "WOComponentName", c.component() ); + if ( name == null ) return null; + + if ( currentElement != null && name.equals( currentName ) ) return currentElement; + + currentName = name; + currentElement = (WOElement) elements.objectForKey( name ); + if ( currentElement == null ) + { + currentElement = WOApplication.application().pageWithName( name, c ); + if ( currentElement != null ) + { + currentElement.associations = associations; + elements.setObjectForKey( currentElement, name ); + } + } + + return currentElement; + } + + void ensureAwakeInContext (WOContext aContext) + { + if ( aContext != null ) + { + WOElement element = getCurrentElement( aContext ); + if ( element != null ) + { + aContext.pushElement( element ); + element.ensureAwakeInContext( aContext ); + aContext.popElement(); + } + } + } + + public void takeValuesFromRequest(WORequest r, WOContext c) + { + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + element.takeValuesFromRequest( r, c ); + c.popElement(); + } + } + + public WOActionResults invokeAction(WORequest r, WOContext c) + { + WOActionResults result = null; + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + result = element.invokeAction( r, c ); + c.popElement(); + } + return result; + } + + public void appendToResponse(WOResponse r, WOContext c) + { + WOElement element = getCurrentElement( c ); + if ( element != null ) + { + c.pushElement( element ); + element.appendToResponse( r, c ); + c.popElement(); + } + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java new file mode 100644 index 0000000..008fda8 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOText.java @@ -0,0 +1,72 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSArray; +import net.wotonomy.foundation.NSDictionary; + +/** +* Implements a TEXTAREA element, with dynamic bindings. + * @author michael@mpowers.net + * @author $Author: cgruber $ + * @version $Revision: 905 $ + */ +public class WOText extends WOInput { + + public WOText() { + super(); + } + + public WOText(String n, NSDictionary m, WOElement t) { + super(n, m, t); + } + + protected String inputType() { + return "TEXTAREA"; + } + + protected Object value(WOContext c) { + Object fieldValue = valueForProperty("value", c.component()); + if (fieldValue == null) { + fieldValue = ""; + } + return formattedValue(fieldValue, c.component()); + } + + public void takeValuesFromRequest(WORequest r, WOContext c) { + Object val = r.formValueForKey(inputName(c)); + if ( val != null ) + setValueForProperty("value", formattedValue(val, c.component()), c.component()); + } + + public void appendToResponse(WOResponse r, WOContext c) { + r.appendContentString(""); + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java new file mode 100644 index 0000000..4233ad4 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/WOTextField.java @@ -0,0 +1,59 @@ +/* + Wotonomy: OpenStep design patterns for pure Java applications. + Copyright (C) 2000 Blacksmith, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see http://www.gnu.org + */ + +package net.wotonomy.web; + +import net.wotonomy.foundation.NSDictionary; +import net.wotonomy.foundation.NSMutableArray; + +/** +* Implements an INPUT tag of type TEXT, with dynamic bindings. +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class WOTextField extends WOInput { + + public WOTextField() { + super(); + } + + public WOTextField(String aName, NSDictionary assocs, WOElement template) { + super(aName, assocs, template); + } + + protected String inputType() { + return "TEXT"; + } + + protected Object value(WOContext c) { + Object fieldValue = valueForProperty("value", c.component()); + if (fieldValue == null) { + fieldValue = ""; + } + return formattedValue(fieldValue, c.component()); + } + + protected NSMutableArray additionalAttributes() { + NSMutableArray a = super.additionalAttributes(); + a.addObject("dateformat"); + a.addObject("numberformat"); + return a; + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html new file mode 100644 index 0000000..10bef50 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/package.html @@ -0,0 +1,11 @@ + +

        +An implementation of the WebObjects +framework. It tries to be API-compatible to the +maximum extent possible. +

        +

        +This package has dependencies on the foundation +and util packages. +

        + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java new file mode 100644 index 0000000..777d4a1 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/util/BrowserLauncher.java @@ -0,0 +1,665 @@ +//package edu.stanford.ejalbert; +package net.wotonomy.web.util; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/**

        BrowserLauncher is a class that provides one static method, + * openURL, which opens the default web browser for the current user + * of the system to the given URL. It may support other protocols + * depending on the system -- mailto, ftp, etc. -- but that has not + * been rigorously tested and is not guaranteed to work.

        + * + *

        Yes, this is platform-specific code, and yes, it may rely on + * classes on certain platforms that are not part of the standard JDK. + * What we're trying to do, though, is to take something that's + * frequently desirable but inherently platform-specific -- opening a + * default browser -- and allow programmers (you, for example) to do + * so without worrying about dropping into native code or doing + * anything else similarly evil.

        + * + *

        Anyway, this code is completely in Java and will run on all JDK + * 1.1-compliant systems without modification or a need for additional + * libraries. All classes that are required on certain platforms to + * allow this to run are dynamically loaded at runtime via reflection + * and, if not found, will not cause this to do anything other than + * returning an error when opening the browser.

        + * + *

        There are certain system requirements for this class, as it's + * running through Runtime.exec(), which is Java's way of making a + * native system call. Currently, this requires that a Macintosh have + * a Finder which supports the GURL event, which is true for Mac OS + * 8.0 and 8.1 systems that have the Internet Scripting AppleScript + * dictionary installed in the Scripting Additions folder in the + * Extensions folder (which is installed by default as far as I know + * under Mac OS 8.0 and 8.1), and for all Mac OS 8.5 and later + * systems. On Windows, it only runs under Win32 systems (Windows 95, + * 98, and NT 4.0, as well as later versions of all). On other + * systems, this drops back from the inherently platform-sensitive + * concept of a default browser and simply attempts to launch Netscape + * via a shell command.

        + * + *

        This code is Copyright 1999-2001 by Eric Albert + * (ejalbert@cs.stanford.edu) and may be redistributed or modified in + * any form without restrictions as long as the portion of this + * comment from this paragraph through the end of the comment is not + * removed. The author requests that he be notified of any + * application, applet, or other binary that makes use of this code, + * but that's more out of curiosity than anything and is not required. + * This software includes no warranty. The author is not repsonsible + * for any loss of data or functionality or any adverse or unexpected + * effects of using this software.

        + * + * Credits:
        Steven Spencer, JavaWorld magazine (Java + * Tip 66)
        Thanks also to Ron B. Yeh, Eric Shapiro, Ben + * Engber, Paul Teitlebaum, Andrea Cantatore, Larry Barowski, Trevor + * Bedzek, Frank Miedrich, and Ron Rabakukk + * + * + * original author Eric Albert (ejalbert@cs.stanford.edu) + * author's version 1.4b1 (Released June 20, 2001) + * @author Copyright 2001, Intersect Software Corporation + * @version $Revision: 905 $ $Date: 2006-02-18 20:44:03 -0500 (Sat, 18 Feb 2006) $ + */ + +public class BrowserLauncher { + /** The Java virtual machine that we are running on. Actually, in + * most cases we only care about the operating system, but some + * operating systems require us to switch on the VM. */ + private static int jvm; + + /** The browser for the system */ + private static Object browser; + + /** Caches whether any classes, methods, and fields that are not + * part of the JDK and need to be dynamically loaded at runtime + * loaded successfully.

        Note that if this is + * false, openURL() will always return + * an IOException. */ + private static boolean loadedWithoutErrors; + + /** The com.apple.mrj.MRJFileUtils class */ + private static Class mrjFileUtilsClass; + + /** The com.apple.mrj.MRJOSType class */ + private static Class mrjOSTypeClass; + + /** The com.apple.MacOS.AEDesc class */ + private static Class aeDescClass; + + /** The (int) method of com.apple.MacOS.AETarget */ + private static Constructor aeTargetConstructor; + + /** The (int, int, int) method of com.apple.MacOS.AppleEvent */ + private static Constructor appleEventConstructor; + + /** The (String) method of com.apple.MacOS.AEDesc */ + private static Constructor aeDescConstructor; + + /** The findFolder method of com.apple.mrj.MRJFileUtils */ + private static Method findFolder; + + /** The getFileCreator method of com.apple.mrj.MRJFileUtils */ + private static Method getFileCreator; + + /** The getFileType method of com.apple.mrj.MRJFileUtils */ + private static Method getFileType; + + /** The openURL method of com.apple.mrj.MRJFileUtils */ + private static Method openURL; + + /** The makeOSType method of com.apple.MacOS.OSUtils */ + private static Method makeOSType; + + /** The putParameter method of com.apple.MacOS.AppleEvent */ + private static Method putParameter; + + /** The sendNoReply method of com.apple.MacOS.AppleEvent */ + private static Method sendNoReply; + + /** Actually an MRJOSType pointing to the System Folder on a Macintosh */ + private static Object kSystemFolderType; + + /** The keyDirectObject AppleEvent parameter type */ + private static Integer keyDirectObject; + + /** The kAutoGenerateReturnID AppleEvent code */ + private static Integer kAutoGenerateReturnID; + + /** The kAnyTransactionID AppleEvent code */ + private static Integer kAnyTransactionID; + + /** The linkage object required for JDirect 3 on Mac OS X. */ + private static Object linkage; + + /** The framework to reference on Mac OS X */ + private static final String JDirect_MacOSX = "/System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/HIToolbox"; + + /** JVM constant for MRJ 2.0 */ + private static final int MRJ_2_0 = 0; + + /** JVM constant for MRJ 2.1 or later */ + private static final int MRJ_2_1 = 1; + + /** JVM constant for Java on Mac OS X 10.0 (MRJ 3.0) */ + private static final int MRJ_3_0 = 3; + + /** JVM constant for MRJ 3.1 */ + private static final int MRJ_3_1 = 4; + + /** JVM constant for any Windows NT JVM */ + private static final int WINDOWS_NT = 5; + + /** JVM constant for any Windows 9x JVM */ + private static final int WINDOWS_9x = 6; + + /** JVM constant for any other platform */ + private static final int OTHER = -1; + + /** The file type of the Finder on a Macintosh. Hardcoding + * "Finder" would keep non-U.S. English systems from working + * properly. */ + private static final String FINDER_TYPE = "FNDR"; + + /** The creator code of the Finder on a Macintosh, which is needed + * to send AppleEvents to the application. */ + private static final String FINDER_CREATOR = "MACS"; + + /** The name for the AppleEvent type corresponding to a GetURL event. */ + private static final String GURL_EVENT = "GURL"; + + /** The first parameter that needs to be passed into + * Runtime.exec() to open the default web browser on Windows. */ + private static final String FIRST_WINDOWS_PARAMETER = "/c"; + + /** The second parameter for Runtime.exec() on Windows. */ + private static final String SECOND_WINDOWS_PARAMETER = "start"; + + /** The third parameter for Runtime.exec() on Windows. This is a + * "title" parameter that the command line expects. Setting this + * parameter allows URLs containing spaces to work. */ + private static final String THIRD_WINDOWS_PARAMETER = "\"\""; + + /** The shell parameters for Netscape that opens a given URL in an + * already-open copy of Netscape on many command-line systems. */ + private static final String NETSCAPE_REMOTE_PARAMETER = "-remote"; + //private static final String NETSCAPE_OPEN_PARAMETER_START = "'openURL("; + //private static final String NETSCAPE_OPEN_PARAMETER_END = ")'"; + private static final String NETSCAPE_OPEN_PARAMETER_START = "openURL("; + private static final String NETSCAPE_OPEN_PARAMETER_END = ")"; + + /** The message from any exception thrown throughout the + * initialization process. */ + private static String errorMessage; + + /** An initialization block that determines the operating system + * and loads the necessary runtime data. */ + static { + loadedWithoutErrors = true; + String osName = System.getProperty("os.name"); + if (osName.startsWith("Mac OS")) { + String mrjVersion = System.getProperty("mrj.version"); + String majorMRJVersion = mrjVersion.substring(0, 3); + try { + double version = Double.valueOf(majorMRJVersion).doubleValue(); + if (version == 2) { + jvm = MRJ_2_0; + } else if (version >= 2.1 && version < 3) { + // Assume that all 2.x versions of MRJ work the + // same. MRJ 2.1 actually works via + // Runtime.exec() and 2.2 supports that but has an + // openURL() method as well that we currently + // ignore. + jvm = MRJ_2_1; + } else if (version == 3.0) { + jvm = MRJ_3_0; + } else if (version >= 3.1) { + // Assume that all 3.1 and later versions of MRJ + // work the same. + jvm = MRJ_3_1; + } else { + loadedWithoutErrors = false; + errorMessage = "Unsupported MRJ version: " + version; + } + } catch (NumberFormatException nfe) { + loadedWithoutErrors = false; + errorMessage = "Invalid MRJ version: " + mrjVersion; + } + } else if (osName.startsWith("Windows")) { + if (osName.indexOf("9") != -1) { + jvm = WINDOWS_9x; + } else { + jvm = WINDOWS_NT; + } + } else { + jvm = OTHER; + } + + if (loadedWithoutErrors) { // if we haven't hit any errors yet + loadedWithoutErrors = loadClasses(); + } + } + + /** This class should be never be instantiated; this just ensures so. */ + private BrowserLauncher() { } + + /** Called by a static initializer to load any classes, fields, + * and methods required at runtime to locate the user's web + * browser. + * @return true if all intialization succeeded + * false if any portion of the initialization failed */ + private static boolean loadClasses() { + switch (jvm) { + case MRJ_2_0: + try { + Class aeTargetClass = Class.forName("com.apple.MacOS.AETarget"); + Class osUtilsClass = Class.forName("com.apple.MacOS.OSUtils"); + Class appleEventClass = Class.forName + ("com.apple.MacOS.AppleEvent"); + Class aeClass = Class.forName("com.apple.MacOS.ae"); + aeDescClass = Class.forName("com.apple.MacOS.AEDesc"); + + aeTargetConstructor = aeTargetClass.getDeclaredConstructor + (new Class [] { int.class }); + appleEventConstructor = appleEventClass.getDeclaredConstructor + (new Class[] { int.class, int.class, aeTargetClass, + int.class, int.class }); + aeDescConstructor = aeDescClass.getDeclaredConstructor + (new Class[] { String.class }); + + makeOSType = osUtilsClass.getDeclaredMethod + ("makeOSType", new Class [] { String.class }); + putParameter = appleEventClass.getDeclaredMethod + ("putParameter", new Class[] { int.class, aeDescClass }); + sendNoReply = appleEventClass.getDeclaredMethod + ("sendNoReply", new Class[] { }); + + Field keyDirectObjectField = aeClass.getDeclaredField + ("keyDirectObject"); + keyDirectObject = (Integer) keyDirectObjectField.get(null); + Field autoGenerateReturnIDField = appleEventClass + .getDeclaredField("kAutoGenerateReturnID"); + kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField + .get(null); + Field anyTransactionIDField = appleEventClass.getDeclaredField + ("kAnyTransactionID"); + kAnyTransactionID = (Integer) anyTransactionIDField.get(null); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (NoSuchFieldException nsfe) { + errorMessage = nsfe.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_2_1: + try { + mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils"); + mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType"); + Field systemFolderField = mrjFileUtilsClass.getDeclaredField + ("kSystemFolderType"); + kSystemFolderType = systemFolderField.get(null); + findFolder = mrjFileUtilsClass.getDeclaredMethod + ("findFolder", new Class[] { mrjOSTypeClass }); + getFileCreator = mrjFileUtilsClass.getDeclaredMethod + ("getFileCreator", new Class[] { File.class }); + getFileType = mrjFileUtilsClass.getDeclaredMethod + ("getFileType", new Class[] { File.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchFieldException nsfe) { + errorMessage = nsfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (SecurityException se) { + errorMessage = se.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_3_0: + try { + Class linker = Class.forName("com.apple.mrj.jdirect.Linker"); + Constructor constructor = linker.getConstructor + (new Class[]{ Class.class }); + linkage = constructor.newInstance(new Object[] + { BrowserLauncher.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } catch (InvocationTargetException ite) { + errorMessage = ite.getMessage(); + return false; + } catch (InstantiationException ie) { + errorMessage = ie.getMessage(); + return false; + } catch (IllegalAccessException iae) { + errorMessage = iae.getMessage(); + return false; + } + break; + case MRJ_3_1: + try { + mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils"); + openURL = mrjFileUtilsClass.getDeclaredMethod + ("openURL", new Class[] { String.class }); + } catch (ClassNotFoundException cnfe) { + errorMessage = cnfe.getMessage(); + return false; + } catch (NoSuchMethodException nsme) { + errorMessage = nsme.getMessage(); + return false; + } + break; + default: + break; + } + return true; + } + + /** Attempts to locate the default web browser on the local + * system. Caches results so it only locates the browser once * + * for each use of this class per JVM instance. + * @return The browser for the system. Note that this may not be + * what you would consider to be a standard web browser; instead, + * it's the application that gets called to open the default web + * browser. In some cases, this will be a non-String object that + * provides the means of calling the default browser. */ + private static Object locateBrowser() { + if (browser != null) { + return browser; + } + switch (jvm) { + case MRJ_2_0: + try { + Integer finderCreatorCode = (Integer) makeOSType.invoke + (null, new Object[] { FINDER_CREATOR }); + Object aeTarget = aeTargetConstructor.newInstance + (new Object[] { finderCreatorCode }); + Integer gurlType = (Integer) makeOSType.invoke + (null, new Object[] { GURL_EVENT }); + Object appleEvent = appleEventConstructor.newInstance + (new Object[] { gurlType, gurlType, aeTarget, + kAutoGenerateReturnID, kAnyTransactionID }); + // Don't set browser = appleEvent because then the + // next time we call locateBrowser(), we'll get the + // same AppleEvent, to which we'll already have added + // the relevant parameter. Instead, regenerate the + // AppleEvent every time. There's probably a way to + // do this better; if any has any ideas, please let me + // know. + return appleEvent; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InstantiationException ie) { + browser = null; + errorMessage = ie.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getMessage(); + return browser; + } + case MRJ_2_1: + File systemFolder; + try { + systemFolder = (File) findFolder.invoke(null, new Object[] + { kSystemFolderType }); + } catch (IllegalArgumentException iare) { + browser = null; + errorMessage = iare.getMessage(); + return browser; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getTargetException().getClass() + ": " + + ite.getTargetException().getMessage(); + return browser; + } + String[] systemFolderFiles = systemFolder.list(); + // Avoid a FilenameFilter because that can't be stopped mid-list + for(int i = 0; i < systemFolderFiles.length; i++) { + try { + File file = new File(systemFolder, systemFolderFiles[i]); + if (!file.isFile()) { + continue; + } + // We're looking for a file with a creator code of + // 'MACS' and a type of 'FNDR'. Only requiring + // the type results in non-Finder applications + // being picked up on certain Mac OS 9 systems, + // especially German ones, and sending a GURL + // event to those applications results in a logout + // under Multiple Users. + Object fileType = getFileType.invoke + (null, new Object[] { file }); + if (FINDER_TYPE.equals(fileType.toString())) { + Object fileCreator = getFileCreator.invoke + (null, new Object[] { file }); + if (FINDER_CREATOR.equals(fileCreator.toString())) { + browser = file.toString(); // Actually the + // Finder, but that's OK + return browser; + } + } + } catch (IllegalArgumentException iare) { + //WTF? browser = browser; + errorMessage = iare.getMessage(); + return null; + } catch (IllegalAccessException iae) { + browser = null; + errorMessage = iae.getMessage(); + return browser; + } catch (InvocationTargetException ite) { + browser = null; + errorMessage = ite.getTargetException().getClass() + ": " + + ite.getTargetException().getMessage(); + return browser; + } + } + browser = null; + break; + case MRJ_3_0: + case MRJ_3_1: + browser = ""; // Return something non-null + break; + case WINDOWS_NT: + browser = "cmd.exe"; + break; + case WINDOWS_9x: + browser = "command.com"; + break; + case OTHER: + default: + browser = "netscape"; + break; + } + return browser; + } + + /** Attempts to open the default web browser to the given URL. + * @param url The URL to open + * @throws IOException If the web browser could not be located or + * does not run */ + public static void openURL(String url) throws IOException { + if (!loadedWithoutErrors) { + throw new IOException("Exception in finding browser: " + + errorMessage); + } + Object browser = locateBrowser(); + if (browser == null) { + throw new IOException("Unable to locate browser: " + errorMessage); + } + + switch (jvm) { + case MRJ_2_0: + Object aeDesc = null; + try { + aeDesc = aeDescConstructor.newInstance(new Object[] { url }); + putParameter.invoke(browser, new Object[] + { keyDirectObject, aeDesc }); + sendNoReply.invoke(browser, new Object[] { }); + } catch (InvocationTargetException ite) { + throw new IOException("InvocationTargetException while creating" + +" AEDesc: " + ite.getMessage()); + } catch (IllegalAccessException iae) { + throw new IOException("IllegalAccessException while building " + + "AppleEvent: " + iae.getMessage()); + } catch (InstantiationException ie) { + throw new IOException("InstantiationException while creating " + + "AEDesc: " + ie.getMessage()); + } finally { + aeDesc = null; // Encourage it to get disposed if it + // was created + browser = null; // Ditto + } + break; + case MRJ_2_1: + Runtime.getRuntime().exec(new String[] { (String) browser, url } ); + break; + case MRJ_3_0: + int[] instance = new int[1]; + int result = ICStart(instance, 0); + if (result == 0) { + int[] selectionStart = new int[] { 0 }; + byte[] urlBytes = url.getBytes(); + int[] selectionEnd = new int[] { urlBytes.length }; + result = ICLaunchURL(instance[0], new byte[] { 0 }, urlBytes, + urlBytes.length, selectionStart, + selectionEnd); + if (result == 0) { + // Ignore the return value; the URL was launched + // successfully regardless of what happens here. + ICStop(instance); + } else { + throw new IOException("Unable to launch URL: " + result); + } + } else { + throw new IOException("Unable to create an Internet Config " + + "instance: " + result); + } + break; + case MRJ_3_1: + try { + openURL.invoke(null, new Object[] { url }); + } catch (InvocationTargetException ite) { + throw new IOException("InvocationTargetException while calling " + + "openURL: " + ite.getMessage()); + } catch (IllegalAccessException iae) { + throw new IOException("IllegalAccessException while calling " + + "openURL: " + iae.getMessage()); + } + break; + case WINDOWS_NT: + // Add quotes around the URL to allow ampersands and other special + // characters to work. + Process process = Runtime.getRuntime().exec(new String[] + { (String) browser, FIRST_WINDOWS_PARAMETER, + SECOND_WINDOWS_PARAMETER, THIRD_WINDOWS_PARAMETER, + '"' + url + '"' }); + // This avoids a memory leak on some versions of Java on + // Windows. That's hinted at in + // . + try { + process.waitFor(); + process.exitValue(); + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + case WINDOWS_9x: + // Add quotes around the URL to allow ampersands and other special + // characters to work. + // Note: windows 98 doesn't expect the THIRD_WINDOWS_PARAMETER for + // its title. + process = Runtime.getRuntime().exec(new String[] + { (String) browser, FIRST_WINDOWS_PARAMETER, + SECOND_WINDOWS_PARAMETER, '"' + url + '"' }); + // This avoids a memory leak on some versions of Java on + // Windows. That's hinted at in + // . + try { + process.waitFor(); + process.exitValue(); + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + case OTHER: + // Assume that we're on Unix and that Netscape is installed + + // First, attempt to open the URL in a currently running + // session of Netscape + process = Runtime.getRuntime().exec(new String[] + {(String)browser, NETSCAPE_REMOTE_PARAMETER, + NETSCAPE_OPEN_PARAMETER_START + url + + NETSCAPE_OPEN_PARAMETER_END }); + try { + int exitCode = process.waitFor(); + if (exitCode != 0) { // if the command had an error + Runtime.getRuntime().exec(new String[] + { (String) browser, url } ); + } else if(process.getErrorStream() != null) { + // Netscape may not be open, so the command may not have an + // error, it just wouldn't have a process to attach to... + BufferedReader reader = new BufferedReader + (new InputStreamReader(process.getErrorStream())); + String errorStr = reader.readLine(); + + if ( errorStr != null ) { + // Command failed, start up the browser + process = Runtime.getRuntime().exec(new String[] { + (String) browser, url }); + } + } + } catch (InterruptedException ie) { + throw new IOException("InterruptedException while launching " + + "browser: " + ie.getMessage()); + } + break; + default: + // This should never occur, but if it does, we'll try the + // simplest thing possible + Runtime.getRuntime().exec(new String[] { (String) browser, url }); + break; + } + } + + /** Methods required for Mac OS X. The presence of native methods + * does not cause any problems on other platforms. */ + private native static int ICStart(int[] instance, int signature); + private native static int ICStop(int[] instance); + private native static int ICLaunchURL + (int instance, byte[] hint, byte[] data, int len, int[] selectionStart, + int[] selectionEnd); +} + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java new file mode 100644 index 0000000..8da71fe --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoder.java @@ -0,0 +1,114 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.foundation.xml.XMLDecoder; + +import org.xml.sax.SAXException; + +/** +* An implementation of XMLDecoder that reads objects from +* XMLRPC format. +* This implementation is not thread-safe, so a new instances +* should be created to accomodate multiple threads. +*/ +public class XMLRPCDecoder implements XMLDecoder +{ + XMLRPCDecoderHelper helper = new XMLRPCDecoderHelper(); + + /** + * Decodes an object in XML-RPC format from the specified input stream. + * @param anInputStream The input stream from which to read. + * The stream will be read fully. + * @param aDescription A description to accompany error messages + * for the stream, typically a file name. + * @param aURL A URL against which relative references within the + * XML will be resolved. + * @return The object that was constructed from the XML content, + * or null if no object could be constructed. + */ + public Object decode( + InputStream anInputStream, String aDescription, URL aURL ) + { + Object result = null; + + try { + SAXParser sparser = SAXParserFactory.newInstance().newSAXParser(); + sparser.parse(anInputStream,helper,aURL.toExternalForm()); + result = helper.getResult(); + } catch (ParserConfigurationException e) { + throw new WotonomyException("Problem in parser configuration", e ); + } catch (IOException e) { + throw new WotonomyException("IOException thrown while parsing", e ); + } catch (SAXException e) { + throw new WotonomyException("SAXException thrown while parsing", e ); + } + helper.reset(); + return result; + } + + /** + * Decodes an XML-RPC message from the specified input stream. + * Stand-alone values not wrapped in "methodCall" or "param" + * tags will be treated as a response. + * @param anInputStream The input stream from which to read. + * The stream will be read fully. + * @param aReceiver an XMLRPCReceiver that will be invoked with + * the appropriate method: request, response, or fault. + */ + public void decode( + InputStream anInputStream, XMLRPCReceiver aReceiver ) + { + try + { + SAXParser sparser = SAXParserFactory.newInstance().newSAXParser(); + sparser.parse(anInputStream,helper); + + if ( helper.isRequest() ) + { + aReceiver.request( helper.getMethodName(), helper.getParameters() ); + } + else + if ( helper.isFault() ) + { + aReceiver.fault( helper.getFaultCode(), helper.getFaultString() ); + } + else // all else is considered a response + { + aReceiver.response( helper.getResult() ); + } + } catch (ParserConfigurationException e) { + throw new WotonomyException("Problem in parser configuration", e ); + } catch (IOException e) { + throw new WotonomyException("IOException thrown while parsing", e ); + } catch (SAXException e) { + throw new WotonomyException("SAXException thrown while parsing", e ); + } + helper.reset(); + } +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java new file mode 100644 index 0000000..2368672 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCDecoderHelper.java @@ -0,0 +1,534 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Michael Powers + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.ValueConverter; +import net.wotonomy.foundation.internal.WotonomyException; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** +* Used by XMLDecoder to implement the necessary interfaces +* required by the jclark xp parser. +* This class is not thread safe. +*/ +class XMLRPCDecoderHelper extends DefaultHandler +{ + protected final Object nilMarker = new Object(); + protected Stack valueStack; + protected String methodName; + protected int faultCode; + protected String faultString; + protected List parameters; + protected StringBuffer cdataBuffer; + + + + public XMLRPCDecoderHelper() + { + valueStack = new Stack(); + parameters = new LinkedList(); + cdataBuffer = new StringBuffer(); + reset(); + } + + public void reset() + { + valueStack.clear(); + parameters.clear(); + cdataBuffer.setLength( 0 ); + methodName = null; + faultCode = 0; + faultString = null; + } + + public boolean isRequest() + { + return ( methodName != null ); + } + + public boolean isResponse() + { + return ( methodName == null ); + } + + public boolean isFault() + { + // faults are responses + return ( isResponse() ) && ( faultString != null ); + } + + public int getFaultCode() + { + return faultCode; + } + + public String getFaultString() + { + return faultString; + } + + public String getMethodName() + { + return methodName; + } + + public Object[] getParameters() + { + return parameters.toArray(); + } + + public void endDocument() throws SAXException { + // TODO Auto-generated method stub + super.endDocument(); + } + + public void startDocument() throws SAXException { + // TODO Auto-generated method stub + super.startDocument(); + reset(); + } + + public Object getResult() + { + if ( valueStack.empty() ) return null; + Object result = valueStack.peek(); + if ( result == nilMarker ) result = null; + return result; + } + + + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + super.startElement(uri, localName, qName, attributes); + if ( XMLRPCEncoder.VALUE.equals( localName ) ) + { + ValueMarker marker = new ValueMarker(); + String classname = attributes.getValue( uri, XMLRPCEncoder.CLASS ); + if ( classname != null ) + { + try + { + Class c = Class.forName( classname ); + if ( c != null ) + { + marker.setMarkerClass( c ); + } + } + catch ( Exception exc ) + { + System.out.println( "XMLRPCDecoderHelper.startElement: " + + "Can't find class: " + classname ); + } + } + valueStack.push( marker ); + } + } + + public void characters(char[] ch, int start, int length) throws SAXException { + super.characters(ch, start, length); + char[] someChars = new char[((start+length)<=ch.length) ? length : ch.length-start]; + for (int i = 0; i < someChars.length; i++ ) { + someChars[i] = ch[start+i]; + } + cdataBuffer.append(someChars); + } + + public void endElement(String uri, String localName, String qName) throws SAXException { + super.endElement(uri, localName, qName); + + // if any cdata is buffered or if string value + if ( ( XMLRPCEncoder.STRING.equals( localName ) ) + || ( cdataBuffer.length() > 0 ) ) + { + // push value on the stack + valueStack.push( cdataBuffer.toString() ); + cdataBuffer.setLength( 0 ); + } + + if ( XMLRPCEncoder.VALUE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { +// ValueMarker marker = (ValueMarker) valueStack.pop(); + ValueMarker marker = null; + Object markerValue = valueStack.pop(); + if ( markerValue instanceof ValueMarker ) + { + marker = (ValueMarker) markerValue; + } + else + { + throw new WotonomyException( "Expected value marker, found" + + markerValue.getClass() + " : " + markerValue ); + } + +//System.out.println( "getMarkerClass: " + marker.getMarkerClass() + " : " + value ); +//System.out.println( valueStack ); + if ( marker.getMarkerClass() != null ) + { + // apply introspection + if ( value instanceof Map ) + { + Map map = (Map)value; + Map.Entry entry; + value = marker.getMarkerClass().newInstance(); + Iterator it = map.entrySet().iterator(); + Object entryValue; + while( it.hasNext() ) + { + entry = (Map.Entry) it.next(); + entryValue = entry.getValue(); + if ( entryValue == nilMarker ) entryValue = null; + Introspector.set( + value, entry.getKey().toString(), entryValue ); + } + } + if ( ! ( value.getClass().equals( marker.getMarkerClass() ) ) ) + { + Object converted = + ValueConverter.convertObjectToClass( + value, marker.getMarkerClass() ); + if ( converted != null ) + { + value = converted; + } + } + } + } + catch ( Exception exc ) + { + // fall back on unconverted value + } + + valueStack.push( value ); +// System.out.println( "convertedValue: " + value + "("+ value.getClass() +")" ); + } + else + if ( XMLRPCEncoder.MEMBER.equals( localName ) ) // Map.Entry + { + // leave key and value to be handled by struct + } + else + if ( XMLRPCEncoder.STRUCT.equals( localName ) ) // write Entries to map or object + { + // write values to array (reverse the order) + Object value; + Map map = new HashMap(); + while ( ( ! valueStack.empty() ) + && ( ! ( valueStack.peek() instanceof ValueMarker ) ) ) + { + value = valueStack.pop(); + map.put( valueStack.pop(), value ); + } + // push the list on the stack + valueStack.push( map ); + } + else + if ( XMLRPCEncoder.ARRAY.equals( localName ) ) + { +//System.out.println( "ended ARRAY: " + valueStack.size() ); + // write values to array (reverse the order) + Object value; + LinkedList list = new LinkedList(); + while ( ( ! valueStack.empty() ) + && ( ! ( valueStack.peek() instanceof ValueMarker ) ) ) + { + value = valueStack.pop(); + if ( value == nilMarker ) value = null; + list.addFirst( value ); + } + // push the list on the stack + valueStack.push( list ); + } + else + if ( XMLRPCEncoder.INT.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Integer( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.I4.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Integer( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.NIL.equals( localName ) ) + { + valueStack.push( nilMarker ); + } + else + if ( XMLRPCEncoder.DOUBLE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + new Double( value.toString() ) ); + } + catch ( NumberFormatException exc ) + { + throw new WotonomyException( + "Invalid double format: " + value.toString() ); + } + } + else + if ( XMLRPCEncoder.DATE.equals( localName ) ) + { + Object value = valueStack.pop(); + try + { + valueStack.push( + XMLRPCEncoder.DATEFORMAT8601.parseObject( + value.toString() ) ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Invalid date format: " + value ); + } + } + else + if ( XMLRPCEncoder.BOOLEAN.equals( localName ) ) + { + Object value = valueStack.pop(); + if ( XMLRPCEncoder.TRUE.equals( value ) ) + { + valueStack.push( Boolean.TRUE ); + } + else + if ( XMLRPCEncoder.FALSE.equals( value ) ) + { + valueStack.push( Boolean.FALSE ); + } + else + { + throw new WotonomyException( + "Invalid boolean format: " + value ); + } + } + else + if ( XMLRPCEncoder.BASE64.equals( localName ) ) + { + throw new WotonomyException( "Not implemented yet." ); + } + else + if ( XMLRPCEncoder.FAULT.equals( localName ) ) + { + Map faultMap = (Map) valueStack.pop(); + try + { + faultCode = ((Integer) + faultMap.get( XMLRPCEncoder.FAULTCODE )).intValue(); + faultString = (String) faultMap.get( XMLRPCEncoder.FAULTSTRING ); + } + catch ( Exception exc ) + { + throw new WotonomyException( + "Invalid fault format: " + faultMap ); + } + } + else + if ( XMLRPCEncoder.METHODNAME.equals( localName ) ) + { + methodName = (String) valueStack.pop(); + } + else + if ( XMLRPCEncoder.PARAM.equals( localName ) ) + { + //NOTE: this leaves the parameter on the stack + parameters.add( getResult() ); + } + } + + + + public void endPrefixMapping(String prefix) throws SAXException { + // TODO Auto-generated method stub + super.endPrefixMapping(prefix); + } + + public void error(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.error(e); + } + + public void fatalError(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.fatalError(e); + } + + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + // TODO Auto-generated method stub + super.ignorableWhitespace(ch, start, length); + } + + public void notationDecl(String name, String publicId, String systemId) throws SAXException { + // TODO Auto-generated method stub + super.notationDecl(name, publicId, systemId); + } + + public void processingInstruction(String target, String data) throws SAXException { + // TODO Auto-generated method stub + super.processingInstruction(target, data); + } + + public InputSource resolveEntity(String publicId, String systemId) throws SAXException { + // NOTE: Sun accepted an incompatible api difference. The (false) should be tossed by hotspot + try { + if (false) throw new IOException("Fake exception to make it compile in both 1.4 and 1.5"); + return super.resolveEntity(publicId, systemId); + } catch (IOException e) { + throw new SAXException(e.getClass().getName() + " thrown while resolving entity.",e); + } + } + + public void setDocumentLocator(Locator locator) { + // TODO Auto-generated method stub + super.setDocumentLocator(locator); + } + + public void skippedEntity(String name) throws SAXException { + // TODO Auto-generated method stub + super.skippedEntity(name); + } + + + + public void startPrefixMapping(String prefix, String uri) throws SAXException { + // TODO Auto-generated method stub + super.startPrefixMapping(prefix, uri); + } + + public void unparsedEntityDecl(String name, String publicId, String systemId, String notationName) throws SAXException { + // TODO Auto-generated method stub + super.unparsedEntityDecl(name, publicId, systemId, notationName); + } + + public void warning(SAXParseException e) throws SAXException { + // TODO Auto-generated method stub + super.warning(e); + } + + + // marker class + + private class ValueMarker + { + private Class theClass; + + public ValueMarker() + { + theClass = null; + } + + public void setMarkerClass( Class aClass ) + { + theClass = aClass; + } + + public Class getMarkerClass() + { + return theClass; + } + + public String toString() + { + return "[ValueMarker: " + theClass + "]"; + } + } + +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:03 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.7 2003/08/06 23:07:53 chochos + * general code cleanup (mostly, removing unused imports) + * + * Revision 1.6 2001/03/03 15:16:35 mpowers + * Fixed bug in decoding empty strings: string handler did nothing and + * empty cdatas were ignored, so no value was placed on the stack. + * + * Revision 1.5 2001/02/17 16:52:06 mpowers + * Changes in imports to support building with jdk1.1 collections. + * + * Revision 1.4 2001/02/09 15:51:39 mpowers + * Fixed a pernicious bug: I was using the character event entirely + * incorrectly, but the problem only exhibited itself with large data + * and even then only randomly. Now using a string buffer. + * + * Revision 1.3 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + * Revision 1.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:39 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:48 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java new file mode 100644 index 0000000..3a63d45 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCEncoder.java @@ -0,0 +1,526 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.wotonomy.foundation.internal.Introspector; +import net.wotonomy.foundation.internal.WotonomyException; +import net.wotonomy.foundation.xml.XMLEncoder; + +import org.dom4j.Element; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; +import org.dom4j.util.NonLazyElement; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +/** +* An implementation of XMLEncoder that serializes objects +* into XMLRPC format, which is found at http://xmlrpc.com/spec. +* We extend that standard only in that we add a "class" +* attribute to the "value" tag, so that a java-based decoder +* can more closely reconstruct the original java data structure. +* The class attribute can be safely ignored by clients. +* This implementation is not thread-safe, so a new instances +* should be created to accomodate multiple threads. +*/ +public class XMLRPCEncoder implements XMLEncoder +{ + public static final String METHODCALL = "methodCall"; + public static final String METHODNAME = "methodName"; + public static final String METHODRESPONSE = "methodResponse"; + public static final String PARAMS = "params"; + public static final String PARAM = "param"; + public static final String FAULT = "fault"; + public static final String FAULTCODE = "faultCode"; + public static final String FAULTSTRING = "faultString"; + + public static final String VALUE = "value"; + public static final String CLASS = "class"; + + public static final String STRUCT = "struct"; + public static final String MEMBER = "member"; + public static final String NAME = "name"; + + public static final String ARRAY = "array"; + public static final String DATA = "data"; + + public static final String NIL = "nil"; + public static final String INT = "int"; + public static final String I4 = "i4"; + public static final String BOOLEAN = "boolean"; + public static final String STRING = "string"; + public static final String DOUBLE = "double"; + public static final String DATE = "dateTime.iso8601"; + public static final String BASE64 = "base64"; + + public static final String TRUE = "1"; + public static final String FALSE = "0"; + + public static final Format DATEFORMAT8601 = + new SimpleDateFormat( "yyyyMMdd'T'HHmmss" ); + + /** + * Encodes an object to the specified output stream as XML. + * @param anObject The object to be serialized to XML format. + * @param anOutputStream The output stream to which the object + * will be written. + */ + public void encode( Object anObject, OutputStream anOutputStream ) + { + try + { + + //XMLWriter writer = new UTF8XMLWriter( anOutputStream ); + RPCXMLWriter writer = new RPCXMLWriter(anOutputStream,OutputFormat.createCompactFormat()); + writeValueToXMLWriter( anObject, writer ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + } + + /** + * Encodes a method request in XML-RPC format in a "methodCall" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aMethodName The method name to appear in the "methodName" tag. + * @param aParameterArray An array of objects, each of which will be + * encoded as values enclosed in a "param" tag, all of which will be + * enclosed in a "params" tag. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeRequest( + String aMethodName, Object[] aParameterArray, + OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat()); + writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); + writer.startElement( METHODCALL ); + writer.startElement( METHODNAME ); + writer.write( aMethodName ); + writer.endElement( METHODNAME ); + writer.startElement( PARAMS ); + for ( int i = 0; i < aParameterArray.length; i++ ) + { + writer.startElement( PARAM ); + writeValueToXMLWriter( aParameterArray[i], writer ); + writer.endElement( PARAM ); + } + writer.endElement( PARAMS ); + writer.endElement( METHODCALL ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Encodes a method response in XML-RPC format in a "methodResponse" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aResult A object which will be + * encoded as values enclosed in a "param" tag, all of which will be + * enclosed in a "params" tag. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeResponse( + Object aResult, OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); + writer.processingInstruction( "xml", "version=\"1.0\" encoding=\"UTF-8\"" ); + writer.startElement( METHODRESPONSE ); + writer.startElement( PARAMS ); + writer.startElement( PARAM ); + writeValueToXMLWriter( aResult, writer ); + writer.endElement( PARAM ); + writer.endElement( PARAMS ); + writer.endElement( METHODRESPONSE ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Encodes a fault response in XML-RPC format in a "methodResponse" tag, + * and writes the XML to the specified output stream. + * This method only writes XML: the caller is responsible for first + * generating the appropriate header, if any, which should set + * the content type as "text/xml" and the content length as appropriate. + * The caller is also responsible for writing the xml version tag. + * @param aFaultCode An application-defined error code. + * @param aFaultString A human-readable error description. + * @param anOutputStream The stream to which the XML will be written. + */ + public void encodeFault( + int aFaultCode, String aFaultString, OutputStream anOutputStream ) + { + try + { + RPCXMLWriter writer = new RPCXMLWriter( anOutputStream, OutputFormat.createCompactFormat() ); + writer.processingInstruction( "xml", "version=\"1.0\"" ); + writer.startElement( METHODRESPONSE ); + writer.startElement( FAULT ); + writer.startElement( VALUE ); + writer.startElement( STRUCT ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( FAULTCODE ); + writer.endElement( NAME ); + writer.startElement( VALUE ); + writer.startElement( INT ); + writer.write( new Integer( aFaultCode ).toString() ); + writer.endElement( INT ); + writer.endElement( VALUE ); + writer.endElement( MEMBER ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( FAULTSTRING ); + writer.endElement( NAME ); + writer.startElement( VALUE ); + writer.startElement( STRING ); + writer.write( aFaultString ); + writer.endElement( STRING ); + writer.endElement( VALUE ); + writer.endElement( MEMBER ); + + writer.endElement( STRUCT ); + writer.endElement( VALUE ); + writer.endElement( FAULT ); + writer.endElement( METHODRESPONSE ); + writer.flush(); + } + catch ( Exception exc ) + { + throw new WotonomyException( exc ); + } + + //TODO: should this return the content-length? + } + + /** + * Performs the actual writing of the file to XML. + */ + private void writeValueToXMLWriter( + Object anObject, RPCXMLWriter writer ) + { + try + { + + + if ( anObject == null ) + { + writer.startElement( VALUE ); + // write nil for null + Element nill = new NonLazyElement(NIL); + writer.write(nill); + } + else + if ( anObject instanceof Collection ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + // write items in the order we get them from the iterator + + writer.startElement( ARRAY ); + writer.startElement( DATA ); + Iterator it = ((Collection)anObject).iterator(); + while ( it.hasNext() ) + { + writeValueToXMLWriter( it.next(), writer ); + } + writer.endElement( DATA ); + writer.endElement( ARRAY ); + + } + else + if ( anObject instanceof Map ) + { + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + // write items in the order we get them from the iterator + //FIXME: The method-based properties are being ignored! + + Map.Entry entry; + writer.startElement( STRUCT ); + writer.startElement( MEMBER ); + Iterator it = ((Map)anObject).entrySet().iterator(); + while ( it.hasNext() ) + { + entry = (Map.Entry) it.next(); + writer.startElement( NAME ); + writeValueToXMLWriter( entry.getKey(), writer ); + writer.endElement( NAME ); + writeValueToXMLWriter( entry.getValue(), writer ); + } + writer.endElement( MEMBER ); + writer.endElement( STRUCT ); + } + else // not a collection + { + // check for primitive types + if ( anObject instanceof String ) + { + writer.startElement( VALUE ); + + writer.startElement( STRING ); + writer.write( anObject.toString() ); + writer.endElement( STRING ); + } + else + if ( anObject instanceof StringBuffer ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( STRING ); + writer.write( anObject.toString() ); + writer.endElement( STRING ); + } + else + if ( anObject instanceof Number ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + if ( ( anObject instanceof Double ) + || ( anObject instanceof Float ) ) + { + writer.startElement( DOUBLE ); + writer.write( anObject.toString() ); + writer.endElement( DOUBLE ); + } + else + { + writer.startElement( INT ); + writer.write( anObject.toString() ); + writer.endElement( INT ); + } + } + else + if ( anObject instanceof Date ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( DATE ); + writer.write( DATEFORMAT8601.format( anObject ) ); + writer.endElement( DATE ); + } + else + if ( anObject instanceof Boolean ) + { + writer.startElement( BOOLEAN ); + if ( ((Boolean)anObject).booleanValue() ) + { + writer.write( "1" ); + } + else + { + writer.write( "0" ); + } + writer.endElement( BOOLEAN ); + } + else + if ( anObject.getClass().isArray() ) + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + writer.startElement( ARRAY ); + writer.startElement( DATA ); + + int length = Array.getLength( anObject ); + for ( int i = 0; i < length; i++ ) + { + writeValueToXMLWriter( Array.get( anObject, i ), writer ); + } + + writer.endElement( DATA ); + writer.endElement( ARRAY ); + } + else // not primitive or collection, treat as struct + { + // write class so we can restore if possible + AttributesImpl a = new AttributesImpl(); + a.addAttribute(null, CLASS, null, null, anObject.getClass().getName() ); + + writer.startElement( VALUE, a ); + + List readProperties = new ArrayList(); + String[] read = Introspector.getReadPropertiesForObject( anObject ); + for ( int i = 0; i < read.length; i++ ) + { + readProperties.add( read[i] ); + } + + List properties = new ArrayList(); + String[] write = Introspector.getWritePropertiesForObject( anObject ); + for ( int i = 0; i < write.length; i++ ) + { + properties.add( write[i] ); + } + + // only use readable properties + properties.retainAll( readProperties ); + +// if ( properties.size() > 0 ) +// { + String key; + Object value; + Iterator it = properties.iterator(); + writer.startElement( STRUCT ); + while ( it.hasNext() ) + { + key = (String) it.next(); + value = Introspector.get( anObject, key ); + + writer.startElement( MEMBER ); + writer.startElement( NAME ); + writer.write( key ); + writer.endElement( NAME ); + writeValueToXMLWriter( value, writer ); + writer.endElement( MEMBER ); + } + writer.endElement( STRUCT ); +/* + } + else // no properties - write a converted string + { + writer.startElement( STRING ); + Object converted = + ValueConverter.convertObjectToClass( anObject, String.class ); + if ( converted != null ) + { + writer.write( converted.toString() ); + } + else + { + writer.write( anObject.toString() ); + } + writer.endElement( STRING ); + } +*/ + } + } + + writer.endElement( VALUE ); + } + catch ( Exception exc ) + { + System.err.println( "XMLFileSoup.writeValueToXMLWriter: " + exc ); + exc.printStackTrace(); + } + + } +/* + public static void main( String[] argv ) + { + System.out.println( "" ); + XMLRPCEncoder encoder = new XMLRPCEncoder(); + encoder.encodeRequest( "systemObject.test", new Object[] { + new net.wotonomy.test.TestObject(), + new net.wotonomy.test.TestObject(), + new net.wotonomy.test.TestObject() }, System.out ); + System.out.println(); + System.out.println(); + encoder.encodeResponse( new net.wotonomy.test.TestObject(), System.out ); + System.out.println(); + System.out.println(); + encoder.encodeFault( -1, "This is a fault.", System.out ); + System.out.println(); + System.out.println(); + System.out.println( "" ); + } +*/ + + private class RPCXMLWriter extends XMLWriter { + + public RPCXMLWriter(OutputStream arg0, OutputFormat arg1) throws UnsupportedEncodingException { + super(arg0, arg1); + } + + public void endElement(String localname) throws SAXException { + super.endElement(null, localname, null); + } + + public void startElement(String localname) throws SAXException { + this.startElement(localname, null); + } + public void startElement(String localname, Attributes attributes) throws SAXException { + this.startElement(null, localname, null, attributes); + } + + } + +} diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java new file mode 100644 index 0000000..48c9e41 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCReceiver.java @@ -0,0 +1,72 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2000 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +/** +* A call-back interface that receives an XML-RPC transaction message. +* Used by XMLRPCDecoder to return values from a message. +*/ +public interface XMLRPCReceiver +{ + /** + * Receives an XML-RPC request. + * @param aMethodName The method name of the request. + * @param aParameterArray The objects contained in the request, in order. + */ + void request( String aMethodName, Object[] aParameterArray ); + + /** + * Receives an XML-RPC response. + * @param aResult The object contained in the response. + */ + void response( Object aResult ); + + /** + * Receives an XML-RPC fault response. + * @param aFaultCode The fault code contained in the response. + * @param aFaultString The fault string contained in the response. + */ + void fault( int aFaultCode, String aFaultString ); +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.2 2001/02/06 14:34:23 mpowers + * Forgot to rename the package declarations. + * + * Revision 1.1 2001/02/06 14:31:19 mpowers + * Moving XML utilities from util to xml package. + * + * Revision 1.1.1.1 2000/12/21 15:52:44 mpowers + * Contributing wotonomy. + * + * Revision 1.2 2000/12/20 16:25:49 michael + * Added log to all files. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java new file mode 100644 index 0000000..7c1f104 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCSelector.java @@ -0,0 +1,238 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLConnection; + +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* An NSSelector customized to invoke methods with XMLRPC +* when a URL is passed in as the object to the invoke() method. +* The method name and parameters will be marshalled and sent +* as an XMLRPC request to the host specified by the URL.

        +* +* To use this class simply as an XMLRPC client, just call +* invoke() with a URL referencing the XMLRPC server and an +* optional array of parameters. +* +* @author michael@mpowers.net +* @author $Author: cgruber $ +* @version $Revision: 905 $ +*/ +public class XMLRPCSelector extends NSSelector +{ + private boolean copyStream = false; + + /** + * Constructor specifying a method name. + */ + public XMLRPCSelector (String aMethodName) + { + super( aMethodName, EMPTY_CLASS_ARRAY ); + } + + /** + * Constructor specifying a method name and an array of parameter types. + * When accessing XMLRPC servers, invoke() does require that the + * specified objects match the types in the parameter type array. + */ + public XMLRPCSelector (String aMethodName, Class[] aParameterTypeArray) + { + super( aMethodName, aParameterTypeArray ); + } + + /** + * Invokes this selector's method on the specified object + * using the specified parameters. + */ + public Object invoke (Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + if ( anObject instanceof URL ) + { + Receiver receiver = new Receiver(); + byte[] copyOfResponse = null; + try + { + URLConnection cn = ((URL)anObject).openConnection(); + + // set properties + cn.setDoOutput(true); + cn.setDoInput(true); + cn.setRequestProperty( + "content-type","text/xml"); + + // send parameters + OutputStream out = cn.getOutputStream(); + new XMLRPCEncoder().encodeRequest( + name(), parameters, out ); + out.flush(); + out.close(); + + // get response: getInputStream initiates the request + InputStream input = + new BufferedInputStream( cn.getInputStream() ); + if ( copyStream ) + { + ByteArrayOutputStream byteArray = + new ByteArrayOutputStream(); + int b; + while ( ( b = input.read() ) != -1 ) + { + byteArray.write( b ); + } + copyOfResponse = byteArray.toByteArray(); + input = new ByteArrayInputStream( copyOfResponse ); + } + new XMLRPCDecoder().decode( input, receiver ); + } + catch ( FileNotFoundException exc ) + { + throw new WotonomyException( "Server did not return a response." ); + } + catch ( Exception exc ) + { + if ( copyOfResponse != null ) + { + System.out.println( new String( copyOfResponse ) ); + exc.printStackTrace(); + } + throw new InvocationTargetException( exc ); + } + + if ( receiver.faultString == null ) + { + return receiver.result; + } + else + { + throw new InvocationTargetException( + new WotonomyException( + receiver.faultCode + ": " + receiver.faultString ) ); + } + } + + // else: not a URL + return super.invoke( anObject, parameters ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, Object anObject, Object[] parameters) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return new XMLRPCSelector( methodName, parameterTypes ).invoke( anObject, parameters ); + } + + public static Object invoke + (String methodName, Object anObject) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, EMPTY_CLASS_ARRAY, anObject, EMPTY_OBJECT_ARRAY ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object aParameter) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { aParameter } ); + } + + public static Object invoke + (String methodName, Class[] parameterTypes, + Object anObject, Object p1, Object p2) + throws IllegalAccessException, IllegalArgumentException, + InvocationTargetException, NoSuchMethodException + { + return XMLRPCSelector.invoke( + methodName, parameterTypes, anObject, new Object[] { p1, p2 } ); + } + + private class Receiver implements XMLRPCReceiver + { + public Object result; + public int faultCode; + public String faultString; + + public Receiver() + { + result = null; + faultCode = -1; + faultString = null; + } + + public void request( + String aMethodName, Object[] aParameterArray ) + { + throw new WotonomyException( + "Invalid response: Expected response but received request." ); + } + + public void response( + Object aResult ) + { + result = aResult; + faultCode = -1; + faultString = null; + } + + public void fault( + int aFaultCode, String aFaultString) + { + result = null; + faultCode = aFaultCode; + faultString = aFaultString; + } + } + + +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:03 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java new file mode 100644 index 0000000..a9981a4 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/XMLRPCServlet.java @@ -0,0 +1,283 @@ +/* +Wotonomy: OpenStep design patterns for pure Java applications. +Copyright (C) 2001 Intersect Software Corporation + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, see http://www.gnu.org +*/ + +package net.wotonomy.web.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Hashtable; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.wotonomy.foundation.NSSelector; +import net.wotonomy.foundation.internal.WotonomyException; + +/** +* A servlet that can make any java object into an XML-RPC server. +* Simply pass in the object to the constructor and XML-RPC requests +* to this servlet will call the appropriate methods and convert +* the results to an XML-RPC response.

        +* +* Depending on your servlet container, it may be necessary to +* create a simple subclass that creates your handler object +* in its constructor and then calls setHandler().

        +* +* Responses are in the specification's standard response format.

        +* +* Faults are returned if any exception is thrown in the method, +* or if the specified method is not found on the object. +* The fault string is the toString value of the exception, +* and the fault code is the hasCode value of the exception.

        +* +* Remember that this servlet only responds to POSTs, +* per the XML-RPC spec. +*/ +public class XMLRPCServlet extends HttpServlet +{ + protected Object handler; + protected boolean synchronizing; + private Hashtable selectorCache = new Hashtable(); // thread safe + private boolean copyStream = false; + + /** + * Default constructor initializes internal state. + */ + public XMLRPCServlet() + { + handler = null; + synchronizing = false; + } + + /** + * Constructor takes any java object and allows its methods + * to be invoked via XMLRPC requests to this servlet. + * Simply calls setHandler(). + */ + public XMLRPCServlet( Object aHandler ) + { + this(); + setHandler( aHandler ); + } + + /** + * Gets the object whose methods will be invoked to + * handle incoming requests. + */ + public Object getHandler() + { + return handler; + } + + /** + * Sets the object whose methods will be invoked to + * handle incoming requests. + */ + public void setHandler( Object aHandler ) + { + handler = aHandler; + } + + /** + * Gets whether the servlet should synchonize on the + * object before invoking methods on it. + * Defaults to false. + */ + public boolean isSynchronizing() + { + return synchronizing; + } + + /** + * Sets whether the servlet should synchonize on the + * object before invoking methods on it. + * Defaults to false. + */ + public void setSynchronizing( boolean willSynchronize ) + { + synchronizing = willSynchronize; + } + + /** + * Overridden to service the request. + */ + protected void doPost( + HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + if ( getHandler() != null ) + { + InputStream input = req.getInputStream(); + byte[] copyOfRequest = null; + + if ( copyStream ) + { + ByteArrayOutputStream byteArray = + new ByteArrayOutputStream(); + int b; + while ( ( b = input.read() ) != -1 ) + { + byteArray.write( b ); + } + copyOfRequest = byteArray.toByteArray(); + input = new ByteArrayInputStream( copyOfRequest ); + } + + try + { + new XMLRPCDecoder().decode( input, + new Receiver( this, resp ) ); + } + catch ( WotonomyException exc ) + { + if ( copyOfRequest != null ) + { + System.out.println( new String( copyOfRequest ) ); + exc.printStackTrace(); + } + // catches io exceptions thrown in handleRequest. + Throwable t = exc.getWrappedThrowable(); + if ( t instanceof IOException ) + { + throw (IOException)t; + } + throw exc; + } + } + } + + /** + * Called by doPost after parsing an incoming request, + * and is responsible for invoking the specified method + * with the specified parameters on the handler object. + * (This implementation calls getOutputStream() on the response.) + * Override to customize the handling of the request. + */ + protected void handleRequest( String aMethodName, + Object[] aParameterArray, HttpServletResponse aResponse ) + { + OutputStream output = null; + + try + { + output = aResponse.getOutputStream(); + aResponse.setStatus( HttpServletResponse.SC_OK ); // always 200 + aResponse.setContentType( "text/xml" ); + } + catch ( IOException exc ) + { + // caught in doPost + throw new WotonomyException( exc ); + } + + // get the array of types + XMLRPCEncoder encoder = new XMLRPCEncoder(); + Class[] types = new Class[ aParameterArray.length ]; + for ( int i = 0; i < aParameterArray.length; i++ ) + { + types[i] = aParameterArray[i].getClass(); + } + + //TODO: selectors should be cached if possible + + Object handler = getHandler(); + if ( isSynchronizing() ) + { + synchronized ( handler ) + { + execute( encoder, handler, output, + new NSSelector( aMethodName, types ), aParameterArray ); + } + } + else + { + execute( encoder, handler, output, + new NSSelector( aMethodName, types ), aParameterArray ); + } + } + + private void execute( XMLRPCEncoder anEncoder, Object aHandler, + OutputStream output, NSSelector aSelector, Object[] aParameterArray ) + { + try + { + Object result = + aSelector.invoke( aHandler, aParameterArray ); + anEncoder.encodeResponse( result, output ); + } + catch ( Exception exc ) + { + anEncoder.encodeFault( + exc.hashCode(), exc.toString(), output ); + } + } + + private class Receiver implements XMLRPCReceiver + { + XMLRPCServlet controller; + HttpServletResponse response; + + public Receiver( + XMLRPCServlet aController, + HttpServletResponse aResponse ) + { + controller = aController; + response = aResponse; + } + + public void request( + String aMethodName, Object[] aParameterArray ) + { + controller.handleRequest( + aMethodName, aParameterArray, response ); + } + + public void response( + Object aResult ) + { + // does nothing + } + + public void fault( + int aFaultCode, String aFaultString) + { + // does nothing + } + } +} + +/* + * $Log$ + * Revision 1.1 2006/02/19 01:44:02 cgruber + * Add xmlrpc files + * Remove jclark and replace with dom4j and javax.xml.sax stuff + * Re-work dependencies and imports so it all compiles. + * + * Revision 1.1 2006/02/16 13:22:22 cgruber + * Check in all sources in eclipse-friendly maven-enabled packages. + * + * Revision 1.1 2001/02/07 19:24:28 mpowers + * Moved XML classes to separate package. + * + */ + diff --git a/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html new file mode 100644 index 0000000..4303e07 --- /dev/null +++ b/projects/net.wotonomy.web/src/main/java/net/wotonomy/web/xml/package.html @@ -0,0 +1,30 @@ + +

        +The XML object serialization framework, including +an XML-RPC serializer implementation and an easy-to-use +XML-RPC servlet and client. +

        +

        +The primary serialization interfaces are XMLEncoder and XMLDecoder. +

        +

        +The implementation of those interfaces is in XMLRPCEncoder and XMLRPCDecoder. +In addition to serializing java objects to an XML-RPC struct format, these +classes also define methods for generating and parsing XML-RPC requests, +responses, and faults. Other implementations (like SOAP) may follow. +

        +

        +The XMLRPCServlet utilizes the framework to allow you to turn any +java object into an XML-RPC server with one line of code. +

        +

        +XMLRPCSelector turns NSSelector into a XML-RPC client. If you +pass in a URL as the target object, the selector will invoke its +method on the specified XML-RPC server and return the result as +a java object. The selector otherwise works normally. +

        +

        +This package has dependencies on the foundation and util packages. +The servlet has a dependency on javax.servlet.HttpServlet. +

        + diff --git a/projects/net.wotonomy.web/src/test/java/net/wotonomy/web/xml/XMLRPCSelectorTest.java b/projects/net.wotonomy.web/src/test/java/net/wotonomy/web/xml/XMLRPCSelectorTest.java new file mode 100644 index 0000000..989a7ff --- /dev/null +++ b/projects/net.wotonomy.web/src/test/java/net/wotonomy/web/xml/XMLRPCSelectorTest.java @@ -0,0 +1,83 @@ +package net.wotonomy.web.xml; + + +import java.io.Serializable; +import java.net.URL; +import java.util.Date; + +import junit.framework.TestCase; + +public class XMLRPCSelectorTest extends TestCase { + + public XMLRPCSelectorTest(String name) { + super(name); + } + + protected void setUp() throws Exception { + super.setUp(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public static void testFoo( ) { + try + { + // create url for server + URL url = new URL( "http://localhost:8080/xmlrpctest" ); + + // set up selectors + XMLRPCSelector getFullName = + new XMLRPCSelector( "getFullName" ); + XMLRPCSelector getCreateDate = + new XMLRPCSelector( "getCreateDate" ); + XMLRPCSelector setCreateDate = + new XMLRPCSelector( "setCreateDate" ); + XMLRPCSelector getChildList = + new XMLRPCSelector( "getChildList" ); + XMLRPCSelector setChildList = + new XMLRPCSelector( "setChildList" ); + + // fetch the full name + System.out.println( getFullName.invoke( url ) ); + + // fetch the create date + System.out.println( getCreateDate.invoke( url ) ); + // set date to current time + setCreateDate.invoke( url, new java.util.Date() ); + // re-fetch the create date + System.out.println( getCreateDate.invoke( url ) ); + + // fetch the child list + java.util.List childList = (java.util.List) getChildList.invoke( url ); + System.out.println( childList ); + // add a new child + childList.add( new MockSerializableObject(new Long(5),"John","Doe", new Date()) ); + setChildList.invoke( url, childList ); + // re-fetch the child list + System.out.println( getChildList.invoke( url ) ); + } + catch (Exception exc) + { + //exc.printStackTrace(); + } + } + + public static class MockSerializableObject implements Serializable { + public String firstname; + public String lastname; + public Long id; + public Date aDate; + + public MockSerializableObject(Long id, String firstname, String lastname, Date date) { + super(); + aDate = date; + this.firstname = firstname; + this.id = id; + this.lastname = lastname; + } + + } + +} diff --git a/projects/pom.xml b/projects/pom.xml new file mode 100644 index 0000000..8068def --- /dev/null +++ b/projects/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + net.wotonomy + wotonomy-root + 1 + pom + Wotonomy + Root project for wotonomy + http://wotonomy.sourceforge.net + + net.wotonomy.all + + + continuum + + + + + israfil + Israfil Deploy + scp://mikail.israfil.net/var/www/vhosts/www.israfil.net/htdocs/maven2 + + + + + israfil + Israfil Snapshot + scp://mikail.israfil.net/var/www/vhosts/www.israfil.net/htdocs/maven2 + + + + website + scp://cgruber@shell.sourceforge.net/home/groups/w/wo/wotonomy/htdocs + + + + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects + scm:svn:https://svn.sourceforge.net/svnroot/wotonomy/trunk/projects + + -- cgit v1.2.3